'use client' import { getScaledDimensions, type AnyNode, ItemNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { Copy, Link, Link2Off, Move, Trash2, X } from 'lucide-react' import Image from 'next/image' import { useCallback, useState } from 'react' import useEditor from '@/store/use-editor' import { NumberInput } from '@/components/ui/primitives/number-input' import { sfxEmitter } from '@/lib/sfx-bus' export function ItemPanel() { 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) // Get the first selected node if it's an item const selectedId = selectedIds[0] const node = selectedId ? (nodes[selectedId as AnyNode['id']] as ItemNode | undefined) : undefined const [uniformScale, setUniformScale] = useState(true) const handleUpdate = useCallback( (updates: Partial) => { if (!selectedId || !node) return updateNode(selectedId as AnyNode['id'], updates) // Mark parent wall as dirty if item is attached to wall if (node.asset.attachTo === 'wall' && node.parentId) { requestAnimationFrame(() => { useScene.getState().dirtyNodes.add(node.parentId as AnyNode['id']) }) } }, [selectedId, node, updateNode], ) const handleClose = useCallback(() => { setSelection({ selectedIds: [] }) }, [setSelection]) const handleMove = useCallback(() => { if (node) { sfxEmitter.emit('sfx:item-pick') setMovingNode(node) // Deselect so the panel closes setSelection({ selectedIds: [] }) } }, [node, setMovingNode, setSelection]) const handleDuplicate = useCallback(() => { if (!node) return sfxEmitter.emit('sfx:item-pick') // Create a proto node (not added to scene) as a carrier for asset/position info. // MoveItemContent detects metadata.isNew and uses draftNode.create() so ghost rendering works correctly. const proto = ItemNode.parse({ position: [...node.position] as [number, number, number], rotation: [...node.rotation] as [number, number, number], name: node.name, asset: node.asset, parentId: node.parentId, side: node.side, metadata: { isNew: true }, }) setMovingNode(proto) setSelection({ selectedIds: [] }) }, [node, setMovingNode, setSelection]) const handleDelete = useCallback(() => { if (!selectedId) return sfxEmitter.emit('sfx:item-delete') deleteNode(selectedId as AnyNode['id']) setSelection({ selectedIds: [] }) }, [selectedId, deleteNode, setSelection]) // Only show if exactly one item is selected if (!node || node.type !== 'item' || selectedIds.length !== 1) return null return (
{/* Header */}

{node.name || node.asset.name}

{/* Content */}
{/* Position */}
{ handleUpdate({ position: [value, node.position[1], node.position[2]] }) }} precision={2} /> { handleUpdate({ position: [node.position[0], value, node.position[2]] }) }} precision={2} /> { handleUpdate({ position: [node.position[0], node.position[1], value] }) }} precision={2} />
{/* Rotation */}
{ const radians = (degrees * Math.PI) / 180 handleUpdate({ rotation: [node.rotation[0], radians, node.rotation[2]] }) }} precision={0} className="flex-1" /> °
{/* Scale */}
{uniformScale ? ( { const v = Math.max(0.01, value) handleUpdate({ scale: [v, v, v] }) }} precision={2} step={0.1} /> ) : (
{ handleUpdate({ scale: [Math.max(0.01, value), node.scale[1], node.scale[2]] }) }} precision={2} step={0.1} /> { handleUpdate({ scale: [node.scale[0], Math.max(0.01, value), node.scale[2]] }) }} precision={2} step={0.1} /> { handleUpdate({ scale: [node.scale[0], node.scale[1], Math.max(0.01, value)] }) }} precision={2} step={0.1} />
)}
{/* Dimensions (effective, read-only) */}
{(() => { const [w, h, d] = getScaledDimensions(node) return `${Math.round(w * 100) / 100}m × ${Math.round(h * 100) / 100}m × ${Math.round(d * 100) / 100}m` })()}
{/* Action Buttons */}
) }