'use client' import { type AnyNode, type AnyNodeId, type BuildingNode, type CeilingNode, type ColumnNode, type DoorNode, type FenceNode, type ItemNode, type RoofNode, type RoofSegmentNode, type SlabNode, type StairNode, type StairSegmentNode, useScene, type WallNode, type WindowNode, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useCallback, useEffect, useState } from 'react' import { useIsMobile } from '../../../hooks/use-mobile' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { CeilingPanel } from './ceiling-panel' import { ColumnPanel } from './column-panel' import { DoorPanel } from './door-panel' import { FencePanel } from './fence-panel' import { ItemPanel } from './item-panel' import { MobilePanelSheet } from './mobile-panel-sheet' import { MobileSelectionBar } from './mobile-selection-bar' import { getNodeDisplay } from './node-display' import { PaintPanel } from './paint-panel' import { ReferencePanel } from './reference-panel' import { RoofPanel } from './roof-panel' import { RoofSegmentPanel } from './roof-segment-panel' import { SlabPanel } from './slab-panel' import { SpawnPanel } from './spawn-panel' import { StairPanel } from './stair-panel' import { StairSegmentPanel } from './stair-segment-panel' import { WallPanel } from './wall-panel' import { WindowPanel } from './window-panel' type MovableNode = | ItemNode | WindowNode | DoorNode | CeilingNode | ColumnNode | SlabNode | WallNode | FenceNode | RoofNode | RoofSegmentNode | StairNode | StairSegmentNode | BuildingNode const MOVABLE_TYPES = new Set([ 'item', 'window', 'door', 'ceiling', 'column', 'slab', 'wall', 'fence', 'roof', 'roof-segment', 'stair', 'stair-segment', 'building', ]) function isMovableNode(node: AnyNode | null): node is MovableNode { return !!node && MOVABLE_TYPES.has(node.type) } function panelForType(type: string | null) { if (!type) return null switch (type) { case 'item': return case 'roof': return case 'roof-segment': return case 'stair': return case 'stair-segment': return case 'slab': return case 'ceiling': return case 'column': return case 'wall': return case 'fence': return case 'door': return case 'window': return default: return null } } function MobilePanelLayer({ node, panel, isReference, }: { node: AnyNode | null panel: React.ReactNode isReference: boolean }) { const setSelection = useViewer((s) => s.setSelection) const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId) const setMovingNode = useEditor((s) => s.setMovingNode) const deleteNode = useScene((s) => s.deleteNode) const [isSheetOpen, setIsSheetOpen] = useState(false) // Reset sheet open state when the selection changes / clears const selectionKey = node?.id ?? (isReference ? 'reference' : null) useEffect(() => { setIsSheetOpen(false) }, [selectionKey]) const clearSelection = useCallback(() => { setSelection({ selectedIds: [] }) setSelectedReferenceId(null) }, [setSelection, setSelectedReferenceId]) const handleMove = useCallback(() => { if (!isMovableNode(node)) return sfxEmitter.emit('sfx:item-pick') setMovingNode(node) clearSelection() }, [node, setMovingNode, clearSelection]) const handleDuplicate = useCallback(() => { if (!isMovableNode(node)) return sfxEmitter.emit('sfx:item-pick') const cloned = structuredClone(node) as MovableNode & { id?: AnyNodeId } delete (cloned as { id?: AnyNodeId }).id const prevMeta = cloned.metadata && typeof cloned.metadata === 'object' && !Array.isArray(cloned.metadata) ? (cloned.metadata as Record) : {} cloned.metadata = { ...prevMeta, isNew: true } setMovingNode(cloned as MovableNode) clearSelection() }, [node, setMovingNode, clearSelection]) const handleDelete = useCallback(() => { if (!node) return sfxEmitter.emit('sfx:item-delete') deleteNode(node.id) clearSelection() }, [node, deleteNode, clearSelection]) if (!(node || isReference)) return null const display = getNodeDisplay(node) return ( <> {node && ( setIsSheetOpen((v) => !v)} onMove={handleMove} /> )} setIsSheetOpen(false)} open={isSheetOpen} title={display.label} > {panel} ) } export function PanelManager() { const isMobile = useIsMobile() const selectedIds = useViewer((s) => s.selection.selectedIds) const selectedReferenceId = useEditor((s) => s.selectedReferenceId) const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen) const mode = useEditor((s) => s.mode) const activePaintMaterial = useEditor((s) => s.activePaintMaterial) // Only subscribe to the *type* of the single-selected node — string primitive // so we don't re-render on unrelated scene mutations. const selectedNodeType = useScene((s) => { if (selectedIds.length !== 1) return null const id = selectedIds[0] return id ? (s.nodes[id as AnyNodeId]?.type ?? null) : null }) const selectedNode = useScene((s) => { if (selectedIds.length !== 1) return null const id = selectedIds[0] return id ? (s.nodes[id as AnyNodeId] ?? null) : null }) if (isMobile) { if (selectedReferenceId) { return } /> } return ( ) } // Show reference panel if a reference is selected if (selectedReferenceId) { return } if ( isPaintPanelOpen && mode === 'material-paint' && activePaintMaterial?.material?.properties && !activePaintMaterial.materialPreset ) { return } // Show appropriate panel based on selected node type if (selectedNodeType) { switch (selectedNodeType) { case 'item': return case 'roof': return case 'roof-segment': return case 'stair': return case 'stair-segment': return case 'slab': return case 'spawn': return case 'ceiling': return case 'column': return case 'wall': return case 'fence': return case 'door': return case 'window': return } } return null }