'use client' import { type AnyNode, type AnyNodeId, WindowNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { Copy, FlipHorizontal2, Move, Trash2, X } from 'lucide-react' import Image from 'next/image' import { useCallback } from 'react' import { sfxEmitter } from '@/lib/sfx-bus' import useEditor from '@/store/use-editor' import { NumberInput } from '@/components/ui/primitives/number-input' import { Switch } from '@/components/ui/primitives/switch' export function WindowPanel() { 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 deleteNode = useScene((s) => s.deleteNode) const setMovingNode = useEditor((s) => s.setMovingNode) const selectedId = selectedIds[0] const node = selectedId ? (nodes[selectedId as AnyNode['id']] as WindowNode | undefined) : undefined const handleUpdate = useCallback( (updates: Partial) => { if (!selectedId) return updateNode(selectedId as AnyNode['id'], updates) useScene.getState().dirtyNodes.add(selectedId as AnyNodeId) }, [selectedId, updateNode], ) const handleClose = useCallback(() => { setSelection({ selectedIds: [] }) }, [setSelection]) const handleFlip = useCallback(() => { if (!node) return handleUpdate({ side: node.side === 'front' ? 'back' : 'front', rotation: [node.rotation[0], node.rotation[1] + Math.PI, node.rotation[2]], }) }, [node, handleUpdate]) const handleMove = useCallback(() => { if (!node) return sfxEmitter.emit('sfx:item-pick') setMovingNode(node) setSelection({ selectedIds: [] }) }, [node, setMovingNode, setSelection]) const handleDelete = useCallback(() => { if (!selectedId || !node) return sfxEmitter.emit('sfx:item-delete') deleteNode(selectedId as AnyNode['id']) if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId) setSelection({ selectedIds: [] }) }, [selectedId, node, deleteNode, setSelection]) const handleDuplicate = useCallback(() => { if (!node || !node.parentId) return sfxEmitter.emit('sfx:item-pick') useScene.temporal.getState().pause() const duplicate = WindowNode.parse({ position: [...node.position] as [number, number, number], rotation: [...node.rotation] as [number, number, number], side: node.side, wallId: node.wallId, parentId: node.parentId, width: node.width, height: node.height, frameThickness: node.frameThickness, frameDepth: node.frameDepth, columnRatios: [...node.columnRatios], rowRatios: [...node.rowRatios], columnDividerThickness: node.columnDividerThickness, rowDividerThickness: node.rowDividerThickness, sill: node.sill, sillDepth: node.sillDepth, sillThickness: node.sillThickness, metadata: { isNew: true }, }) useScene.getState().createNode(duplicate, node.parentId as AnyNodeId) setMovingNode(duplicate) setSelection({ selectedIds: [] }) }, [node, setMovingNode, setSelection]) if (!node || node.type !== 'window' || selectedIds.length !== 1) return null const numCols = node.columnRatios.length const numRows = node.rowRatios.length // Normalized ratios (always sum to 1 for display) const colSum = node.columnRatios.reduce((a, b) => a + b, 0) const rowSum = node.rowRatios.reduce((a, b) => a + b, 0) const normCols = node.columnRatios.map(r => r / colSum) const normRows = node.rowRatios.map(r => r / rowSum) const setColumnRatio = (index: number, newVal: number) => { const clamped = Math.max(0.05, Math.min(0.95, newVal)) const neighborIdx = index < numCols - 1 ? index + 1 : index - 1 const delta = clamped - normCols[index]! const neighborVal = Math.max(0.05, normCols[neighborIdx]! - delta) const newRatios = normCols.map((v, i) => { if (i === index) return clamped if (i === neighborIdx) return neighborVal return v }) handleUpdate({ columnRatios: newRatios }) } const setRowRatio = (index: number, newVal: number) => { const clamped = Math.max(0.05, Math.min(0.95, newVal)) const neighborIdx = index < numRows - 1 ? index + 1 : index - 1 const delta = clamped - normRows[index]! const neighborVal = Math.max(0.05, normRows[neighborIdx]! - delta) const newRatios = normRows.map((v, i) => { if (i === index) return clamped if (i === neighborIdx) return neighborVal return v }) handleUpdate({ rowRatios: newRatios }) } return (
{/* Header */}

{node.name || `Window (${node.width}×${node.height}m)`}

{/* Content */}
{/* Position */}
handleUpdate({ position: [v, node.position[1], node.position[2]] })} precision={2} /> handleUpdate({ position: [node.position[0], v, node.position[2]] })} precision={2} />
{/* Dimensions */}
handleUpdate({ width: v })} min={0.2} precision={2} className="flex-1" /> m
handleUpdate({ height: v })} min={0.2} precision={2} className="flex-1" /> m
{/* Frame */}
handleUpdate({ frameThickness: v })} min={0.01} precision={3} step={0.01} className="flex-1" /> m
handleUpdate({ frameDepth: v })} min={0.01} precision={3} step={0.01} className="flex-1" /> m
{/* Grid */}
{ const n = Math.max(1, Math.min(8, Math.round(v))) handleUpdate({ columnRatios: Array(n).fill(1 / n) }) }} min={1} max={8} precision={0} step={1} /> { const n = Math.max(1, Math.min(8, Math.round(v))) handleUpdate({ rowRatios: Array(n).fill(1 / n) }) }} min={1} max={8} precision={0} step={1} />
{/* Column ratios */} {numCols > 1 && (
Column widths {normCols.map((ratio, i) => (
setColumnRatio(i, v / 100)} min={5} max={95} precision={1} step={1} className="flex-1" /> %
))}
handleUpdate({ columnDividerThickness: v })} min={0.005} precision={3} step={0.01} className="flex-1" /> m
)} {/* Row ratios */} {numRows > 1 && (
Row heights {normRows.map((ratio, i) => (
setRowRatio(i, v / 100)} min={5} max={95} precision={1} step={1} className="flex-1" /> %
))}
handleUpdate({ rowDividerThickness: v })} min={0.005} precision={3} step={0.01} className="flex-1" /> m
)}
{/* Sill */}
handleUpdate({ sill: checked })} />
{node.sill && (
handleUpdate({ sillDepth: v })} min={0.01} precision={3} step={0.01} className="flex-1" /> m
handleUpdate({ sillThickness: v })} min={0.005} precision={3} step={0.01} className="flex-1" /> m
)}
{/* Action Buttons */}
) }