Pascal 0a46a9deb4
sync: comprehensive monorepo → editor parity (2D/3D decoupling, UX polish, crash fixes)
Squash merge of 3 commits:
1. useLiveTransforms store for 2D/3D decoupling + floorplan overhaul + Sentry crash fixes
2. Comprehensive 59-file sync bringing editor to full monorepo parity (selection highlights, delete tool, furnish/zone modes, keyboard shortcuts, all panels)
3. Missing files fix (materials.ts, merged-outline-node.ts, type fix)

75 files changed, ~6K additions.
2026-04-07 19:21:10 -04:00

124 lines
3.3 KiB
TypeScript
Executable File

'use client'
import {
type AnyNode,
type AnyNodeId,
type MaterialSchema,
useScene,
type WallNode,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useCallback } from 'react'
import { MaterialPicker } from '../controls/material-picker'
import { PanelSection } from '../controls/panel-section'
import { SliderControl } from '../controls/slider-control'
import { PanelWrapper } from './panel-wrapper'
export function WallPanel() {
const selectedIds = useViewer((s) => s.selection.selectedIds)
const setSelection = useViewer((s) => s.setSelection)
const nodes = useScene((s) => s.nodes)
const updateNode = useScene((s) => s.updateNode)
const selectedId = selectedIds[0]
const node = selectedId ? (nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined
const handleUpdate = useCallback(
(updates: Partial<WallNode>) => {
if (!selectedId) return
updateNode(selectedId as AnyNode['id'], updates)
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
},
[selectedId, updateNode],
)
const handleUpdateLength = useCallback(
(newLength: number) => {
if (!node || newLength <= 0) return
const dx = node.end[0] - node.start[0]
const dz = node.end[1] - node.start[1]
const currentLength = Math.sqrt(dx * dx + dz * dz)
if (currentLength === 0) return
const dirX = dx / currentLength
const dirZ = dz / currentLength
const newEnd: [number, number] = [
node.start[0] + dirX * newLength,
node.start[1] + dirZ * newLength,
]
handleUpdate({ end: newEnd })
},
[node, handleUpdate],
)
const handleMaterialChange = useCallback(
(material: MaterialSchema) => {
handleUpdate({ material })
},
[handleUpdate],
)
const handleClose = useCallback(() => {
setSelection({ selectedIds: [] })
}, [setSelection])
if (!node || node.type !== 'wall' || selectedIds.length !== 1) return null
const dx = node.end[0] - node.start[0]
const dz = node.end[1] - node.start[1]
const length = Math.sqrt(dx * dx + dz * dz)
const height = node.height ?? 2.5
const thickness = node.thickness ?? 0.1
return (
<PanelWrapper
icon="/icons/wall.png"
onClose={handleClose}
title={node.name || 'Wall'}
width={280}
>
<PanelSection title="Dimensions">
<SliderControl
label="Length"
max={20}
min={0.1}
onChange={handleUpdateLength}
precision={2}
step={0.01}
unit="m"
value={length}
/>
<SliderControl
label="Height"
max={6}
min={0.1}
onChange={(v) => handleUpdate({ height: Math.max(0.1, v) })}
precision={2}
step={0.1}
unit="m"
value={Math.round(height * 100) / 100}
/>
<SliderControl
label="Thickness"
max={1}
min={0.05}
onChange={(v) => handleUpdate({ thickness: Math.max(0.05, v) })}
precision={3}
step={0.01}
unit="m"
value={Math.round(thickness * 1000) / 1000}
/>
</PanelSection>
<PanelSection title="Material">
<MaterialPicker onChange={handleMaterialChange} value={node.material} />
</PanelSection>
</PanelWrapper>
)
}