'use client' import { type AnyNode, type AnyNodeId, getClampedWallCurveOffset, getMaxWallCurveOffset, getWallCurveLength, normalizeWallCurveOffset, useLiveNodeOverrides, useScene, type WallNode, } from '@pascal-app/core' import { ActionButton, ActionGroup, PanelSection, PanelWrapper, SliderControl, triggerSFX, useEditor, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Spline } from 'lucide-react' import { useCallback, useMemo, useRef } from 'react' export default function WallPanel() { const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const setCurvingWall = useEditor((s) => s.setCurvingWall) const sceneNode = useScene((s) => selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined, ) // Live override published by the 2D drag handlers (side-arrows / // corner dots / curve handle). Merged on top of the scene node so // the sliders read the live `start` / `end` / `curveOffset` during // a drag without zustand being touched until commit. const liveOverride = useLiveNodeOverrides((s) => selectedId ? s.get(selectedId as AnyNodeId) : undefined, ) const node = useMemo(() => { if (!sceneNode) return undefined if (!liveOverride || Object.keys(liveOverride).length === 0) return sceneNode return { ...sceneNode, ...liveOverride } as WallNode }, [sceneNode, liveOverride]) // Boolean selector — re-renders only when this specific wall's child // composition crosses the "has a door/window/wall-item" threshold. const hasWallChildrenBlockingCurve = useScene((s) => { if (!node) return false return (node.children ?? []).some((childId) => { const child = s.nodes[childId as AnyNodeId] if (!child) return false if (child.type === 'door' || child.type === 'window') return true if (child.type === 'item') { const attachTo = child.asset?.attachTo return attachTo === 'wall' || attachTo === 'wall-side' } return false }) }) // Mirror the latest node into a ref so the slider handlers below have // stable identities across re-renders. Without this, every store tick // (one per pointermove during a slider drag) rebuilt the handler // refs, destabilising SliderControl's pointer-capture listeners and // combining with float drift in `getWallCurveLength` produced a // "Maximum update depth exceeded" cascade. Same fix in fence-panel.tsx. const nodeRef = useRef(node) nodeRef.current = node const handleUpdate = useCallback( (updates: Partial) => { if (!selectedId) return useScene.getState().updateNode(selectedId as AnyNode['id'], updates) }, [selectedId], ) const handleUpdateLength = useCallback( (newLength: number) => { const n = nodeRef.current if (!n || newLength <= 0) return const dx = n.end[0] - n.start[0] const dz = n.end[1] - n.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] = [ n.start[0] + dirX * newLength, n.start[1] + dirZ * newLength, ] handleUpdate({ end: newEnd }) }, [handleUpdate], ) const handleClose = useCallback(() => { setSelection({ selectedIds: [] }) }, [setSelection]) const handleCurve = useCallback(() => { if (!node) return triggerSFX('sfx:item-pick') setCurvingWall(node) setSelection({ selectedIds: [] }) }, [node, setCurvingWall, setSelection]) if (!(node && node.type === 'wall' && selectedId)) return null const dx = node.end[0] - node.start[0] const dz = node.end[1] - node.start[1] const length = getWallCurveLength(node) const height = node.height ?? 2.5 const thickness = node.thickness ?? 0.1 const curveOffset = getClampedWallCurveOffset(node) const maxCurveOffset = getMaxWallCurveOffset(node) return ( handleUpdate({ height: Math.max(0.1, v) })} precision={2} step={0.1} unit="m" value={Math.round(height * 100) / 100} /> handleUpdate({ thickness: Math.max(0.05, v) })} precision={3} step={0.01} unit="m" value={Math.round(thickness * 1000) / 1000} /> {!hasWallChildrenBlockingCurve && ( handleUpdate({ curveOffset: normalizeWallCurveOffset(node, v) })} precision={2} step={0.1} unit="m" value={Math.round(curveOffset * 100) / 100} /> )} {!hasWallChildrenBlockingCurve && ( } label="Curve" onClick={handleCurve} /> )} ) }