'use client' import { type AnyNode, type AnyNodeId, getActiveRoofHeight, type RoofSegmentNode, TurbineVentNode as TurbineVentSchema, useLiveNodeOverrides, useScene, } from '@pascal-app/core' import { ActionButton, ActionGroup, PanelSection, PanelWrapper, SegmentedControl, SliderControl, triggerSFX, useEditor, } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { Copy, Move, Pause, Play, Trash2 } from 'lucide-react' import { useCallback, useRef } from 'react' import type { TurbineVentNode } from './schema' // Speed restored when resuming a paused turbine that has no remembered // speed yet (matches the schema default). const DEFAULT_SPIN_SPEED = 0.8 /** * Inspector panel for a placed turbine vent. Exposes style + dimensions + * spin speed plus Move / Duplicate / Delete wired into the same * ghost-preview drag flow the placement tool uses (see `./move-tool.tsx`). * Mirrors the box-vent panel. */ export default function TurbineVentPanel() { const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const updateNode = useScene((s) => s.updateNode) const deleteNode = useScene((s) => s.deleteNode) const setMovingNode = useEditor((s) => s.setMovingNode) const storeNode = useScene((s) => selectedId ? (s.nodes[selectedId as AnyNode['id']] as TurbineVentNode | undefined) : undefined, ) const overrides = useLiveNodeOverrides((s) => selectedId ? (s.get(selectedId as AnyNodeId) as Partial | undefined) : undefined, ) const node: TurbineVentNode | undefined = storeNode && overrides ? ({ ...storeNode, ...overrides } as TurbineVentNode) : storeNode const segment = useScene((s) => node?.roofSegmentId ? (s.nodes[node.roofSegmentId as AnyNodeId] as RoofSegmentNode | undefined) : undefined, ) const previewProp = useCallback( (updates: Partial) => { if (!selectedId) return useLiveNodeOverrides.getState().set(selectedId as AnyNodeId, updates) }, [selectedId], ) const commitProp = useCallback( (updates: Partial) => { if (!selectedId) return updateNode(selectedId as AnyNode['id'], updates) useLiveNodeOverrides.getState().clear(selectedId as AnyNodeId) }, [selectedId, updateNode], ) const handleUpdate = commitProp const handleClose = useCallback(() => { setSelection({ selectedIds: [] }) }, [setSelection]) const handleBack = useCallback(() => { if (node?.roofSegmentId) { setSelection({ selectedIds: [node.roofSegmentId as AnyNode['id']] }) } }, [node?.roofSegmentId, setSelection]) const handleMove = useCallback(() => { if (!node) return triggerSFX('sfx:item-pick') setMovingNode(node as never) setSelection({ selectedIds: [] }) }, [node, setMovingNode, setSelection]) const handleDuplicate = useCallback(() => { if (!node) return triggerSFX('sfx:item-pick') const parentId = node.roofSegmentId as AnyNodeId | undefined if (!parentId) return const state = useScene.getState() const meta = typeof node.metadata === 'object' && node.metadata !== null ? (node.metadata as Record) : {} const cloneInput = { ...node, id: undefined, metadata: { ...meta, isNew: true }, } as Record const cloned = TurbineVentSchema.parse(cloneInput) as TurbineVentNode state.createNode(cloned, parentId) state.dirtyNodes.add(parentId) setMovingNode(cloned as never) setSelection({ selectedIds: [] }) }, [node, setMovingNode, setSelection]) const handleDelete = useCallback(() => { if (!(selectedId && node)) return triggerSFX('sfx:item-delete') const segmentId = node.roofSegmentId if (segmentId) { const state = useScene.getState() const seg = state.nodes[segmentId as AnyNodeId] as RoofSegmentNode | undefined if (seg) { state.updateNode(segmentId as AnyNode['id'], { children: (seg.children ?? []).filter((id) => id !== selectedId), }) } } deleteNode(selectedId as AnyNodeId) if (segmentId) { useScene.getState().dirtyNodes.add(segmentId as AnyNodeId) setSelection({ selectedIds: [segmentId as AnyNode['id']] }) } else { setSelection({ selectedIds: [] }) } }, [selectedId, node, deleteNode, setSelection]) // Play / pause the idle spin. Pausing remembers the current speed so // Play restores it (falling back to the default if it was never running). const lastSpeedRef = useRef(DEFAULT_SPIN_SPEED) const isSpinning = (node?.spinSpeed ?? 0) > 0 const handleToggleSpin = useCallback(() => { if (!node) return const current = node.spinSpeed ?? 0 if (current > 0) { lastSpeedRef.current = current handleUpdate({ spinSpeed: 0 }) } else { handleUpdate({ spinSpeed: lastSpeedRef.current > 0 ? lastSpeedRef.current : DEFAULT_SPIN_SPEED, }) } }, [node, handleUpdate]) if (!(node && node.type === 'turbine-vent' && selectedId)) return null return ( handleUpdate({ style: v as TurbineVentNode['style'] })} options={[ { label: 'Globe', value: 'globe' }, { label: 'Cylinder', value: 'cylinder' }, ]} value={node.style ?? 'globe'} /> previewProp({ diameter: v })} onCommit={(v) => handleUpdate({ diameter: v })} precision={2} restoreOnCommit={false} step={0.01} unit="m" value={Math.round(node.diameter * 100) / 100} /> previewProp({ height: v })} onCommit={(v) => handleUpdate({ height: v })} precision={2} restoreOnCommit={false} step={0.01} unit="m" value={Math.round(node.height * 100) / 100} /> previewProp({ neckHeight: v })} onCommit={(v) => handleUpdate({ neckHeight: v })} precision={2} restoreOnCommit={false} step={0.01} unit="m" value={Math.round((node.neckHeight ?? 0.09) * 100) / 100} /> previewProp({ vaneCount: Math.round(v) })} onCommit={(v) => handleUpdate({ vaneCount: Math.round(v) })} precision={0} restoreOnCommit={false} step={1} unit="" value={Math.round(node.vaneCount ?? 20)} /> : } label={isSpinning ? 'Pause' : 'Play'} onClick={handleToggleSpin} /> previewProp({ spinSpeed: v })} onCommit={(v) => handleUpdate({ spinSpeed: v })} precision={1} restoreOnCommit={false} step={0.1} unit="rad/s" value={Math.round((node.spinSpeed ?? 0) * 10) / 10} /> previewProp({ position: [v, node.position[1] ?? 0, node.position[2] ?? 0] }) } onCommit={(v) => handleUpdate({ position: [v, node.position[1] ?? 0, node.position[2] ?? 0] }) } precision={2} restoreOnCommit={false} step={0.05} unit="m" value={Math.round((node.position[0] ?? 0) * 100) / 100} /> previewProp({ position: [node.position[0] ?? 0, v, node.position[2] ?? 0] }) } onCommit={(v) => handleUpdate({ position: [node.position[0] ?? 0, v, node.position[2] ?? 0] }) } precision={2} restoreOnCommit={false} step={0.05} unit="m" value={Math.round((node.position[1] ?? 0) * 100) / 100} /> previewProp({ position: [node.position[0] ?? 0, node.position[1] ?? 0, v] }) } onCommit={(v) => handleUpdate({ position: [node.position[0] ?? 0, node.position[1] ?? 0, v] }) } precision={2} restoreOnCommit={false} step={0.05} unit="m" value={Math.round((node.position[2] ?? 0) * 100) / 100} /> previewProp({ rotation: (deg * Math.PI) / 180 })} onCommit={(deg) => handleUpdate({ rotation: (deg * Math.PI) / 180 })} precision={0} restoreOnCommit={false} step={1} unit="°" value={Math.round(((node.rotation ?? 0) * 180) / Math.PI)} /> } label="Move" onClick={handleMove} /> } label="Duplicate" onClick={handleDuplicate} /> } label="Delete" onClick={handleDelete} /> ) }