'use client' import { type AnyNode, type AnyNodeId, type RoofSegmentNode, RoofSegmentNode as RoofSegmentNodeSchema, type RoofType, useScene, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { Copy, Move, Trash2 } from 'lucide-react' import { useCallback } from 'react' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { ActionButton, ActionGroup } from '../controls/action-button' import { PanelSection } from '../controls/panel-section' import { SegmentedControl } from '../controls/segmented-control' import { SliderControl } from '../controls/slider-control' import { PanelWrapper } from './panel-wrapper' const ROOF_TYPE_OPTIONS: { label: string; value: RoofType }[] = [ { label: 'Hip', value: 'hip' }, { label: 'Gable', value: 'gable' }, { label: 'Shed', value: 'shed' }, { label: 'Flat', value: 'flat' }, ] const ROOF_TYPE_OPTIONS_2: { label: string; value: RoofType }[] = [ { label: 'Gambrel', value: 'gambrel' }, { label: 'Dutch', value: 'dutch' }, { label: 'Mansard', value: 'mansard' }, ] export function RoofSegmentPanel() { const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const updateNode = useScene((s) => s.updateNode) const setMovingNode = useEditor((s) => s.setMovingNode) const node = useScene((s) => selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofSegmentNode | undefined) : undefined, ) const handleUpdate = useCallback( (updates: Partial) => { if (!selectedId) return updateNode(selectedId as AnyNode['id'], updates) }, [selectedId, updateNode], ) const handleClose = useCallback(() => { setSelection({ selectedIds: [] }) }, [setSelection]) const handleBack = useCallback(() => { if (node?.parentId) { setSelection({ selectedIds: [node.parentId] }) } }, [node?.parentId, setSelection]) const handleDuplicate = useCallback(() => { if (!node?.parentId) return sfxEmitter.emit('sfx:item-pick') let duplicateInfo = structuredClone(node) as any delete duplicateInfo.id duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true } // Offset slightly so it's visible duplicateInfo.position = [ duplicateInfo.position[0] + 1, duplicateInfo.position[1], duplicateInfo.position[2] + 1, ] try { const duplicate = RoofSegmentNodeSchema.parse(duplicateInfo) useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId) setSelection({ selectedIds: [] }) setMovingNode(duplicate) } catch (e) { console.error('Failed to duplicate roof segment', e) } }, [node, setSelection, setMovingNode]) const handleMove = useCallback(() => { if (node) { sfxEmitter.emit('sfx:item-pick') setMovingNode(node) setSelection({ selectedIds: [] }) } }, [node, setMovingNode, setSelection]) const handleDelete = useCallback(() => { if (!(selectedId && node)) return sfxEmitter.emit('sfx:item-delete') const parentId = node.parentId useScene.getState().deleteNode(selectedId as AnyNodeId) if (parentId) { useScene.getState().dirtyNodes.add(parentId as AnyNodeId) setSelection({ selectedIds: [parentId] }) } else { setSelection({ selectedIds: [] }) } }, [selectedId, node, setSelection]) if (!(node && node.type === 'roof-segment' && selectedId)) return null return ( handleUpdate({ roofType: v })} options={ROOF_TYPE_OPTIONS} value={node.roofType} /> handleUpdate({ roofType: v })} options={ROOF_TYPE_OPTIONS_2} value={node.roofType} /> handleUpdate({ width: v })} precision={2} step={0.5} unit="m" value={Math.round(node.width * 100) / 100} /> handleUpdate({ depth: v })} precision={2} step={0.5} unit="m" value={Math.round(node.depth * 100) / 100} /> handleUpdate({ wallHeight: v })} precision={2} step={0.1} unit="m" value={Math.round(node.wallHeight * 100) / 100} /> handleUpdate({ roofHeight: v })} precision={2} step={0.1} unit="m" value={Math.round(node.roofHeight * 100) / 100} /> handleUpdate({ wallThickness: v })} precision={2} step={0.05} unit="m" value={Math.round(node.wallThickness * 100) / 100} /> handleUpdate({ deckThickness: v })} precision={2} step={0.01} unit="m" value={Math.round(node.deckThickness * 100) / 100} /> handleUpdate({ overhang: v })} precision={2} step={0.05} unit="m" value={Math.round(node.overhang * 100) / 100} /> handleUpdate({ shingleThickness: v })} precision={2} step={0.01} unit="m" value={Math.round(node.shingleThickness * 100) / 100} /> { const pos = [...node.position] as [number, number, number] pos[0] = v handleUpdate({ position: pos }) }} precision={2} step={0.05} unit="m" value={Math.round(node.position[0] * 100) / 100} /> { const pos = [...node.position] as [number, number, number] pos[1] = v handleUpdate({ position: pos }) }} precision={2} step={0.05} unit="m" value={Math.round(node.position[1] * 100) / 100} /> { const pos = [...node.position] as [number, number, number] pos[2] = v handleUpdate({ position: pos }) }} precision={2} step={0.05} unit="m" value={Math.round(node.position[2] * 100) / 100} /> { handleUpdate({ rotation: (degrees * Math.PI) / 180 }) }} precision={0} step={1} unit="°" value={Math.round((node.rotation * 180) / Math.PI)} />
{ sfxEmitter.emit('sfx:item-rotate') handleUpdate({ rotation: node.rotation - Math.PI / 4 }) }} /> { sfxEmitter.emit('sfx:item-rotate') handleUpdate({ rotation: node.rotation + Math.PI / 4 }) }} />
} label="Move" onClick={handleMove} /> } label="Duplicate" onClick={handleDuplicate} /> } label="Delete" onClick={handleDelete} />
) }