diff --git a/packages/editor/src/components/ui/panels/ceiling-panel.tsx b/packages/editor/src/components/ui/panels/ceiling-panel.tsx index 314f2380..31acea62 100644 --- a/packages/editor/src/components/ui/panels/ceiling-panel.tsx +++ b/packages/editor/src/components/ui/panels/ceiling-panel.tsx @@ -3,7 +3,7 @@ import { type AnyNode, type CeilingNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { Edit, Move, Plus, Trash2 } from 'lucide-react' -import { useCallback, useEffect } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { ActionButton, ActionGroup } from '../controls/action-button' @@ -14,7 +14,6 @@ import { PanelWrapper } from './panel-wrapper' export function CeilingPanel() { const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) - const updateNode = useScene((s) => s.updateNode) const editingHole = useEditor((s) => s.editingHole) const setEditingHole = useEditor((s) => s.setEditingHole) const setMovingNode = useEditor((s) => s.setMovingNode) @@ -23,12 +22,17 @@ export function CeilingPanel() { selectedId ? (s.nodes[selectedId as AnyNode['id']] as CeilingNode | undefined) : undefined, ) + // Panel slider-drag fix recipe (plans/editor-node-registry.md): stable + // handler refs so slider drags don't trigger Maximum update depth. + const nodeRef = useRef(node) + nodeRef.current = node + const handleUpdate = useCallback( (updates: Partial) => { if (!selectedId) return - updateNode(selectedId as AnyNode['id'], updates) + useScene.getState().updateNode(selectedId as AnyNode['id'], updates) }, - [selectedId, updateNode], + [selectedId], ) const handleClose = useCallback(() => { diff --git a/packages/nodes/src/ceiling/definition.ts b/packages/nodes/src/ceiling/definition.ts new file mode 100644 index 00000000..afd197d8 --- /dev/null +++ b/packages/nodes/src/ceiling/definition.ts @@ -0,0 +1,80 @@ +import type { NodeDefinition } from '@pascal-app/core' +import { ceilingParametrics } from './parametrics' +import { CeilingNode } from './schema' + +/** + * Ceiling — Phase 5 batch kind, polygon-based. Structurally identical + * to slab but mounted at `height` rather than `elevation`. + * + * Capabilities: + * - **No `movable`**: ceiling move is bespoke via legacy `MoveCeilingTool` + * + the floor-plan boundary / hole editors. Capability-driven dispatch + * keeps the legacy mover (preserves polygon-aware behavior). + * - **`surfaces.top`**: items host on the ceiling at `height`. + * - `selectable`, `duplicable`, `deletable` standard. + * + * Relations: `hosts: ['item']` for ceiling-mounted items (lights, fans). + * `cascadeDelete: 'descendants'` removes hosted items on ceiling delete. + */ +export const ceilingDefinition: NodeDefinition = { + kind: 'ceiling', + schemaVersion: 1, + schema: CeilingNode, + category: 'structure', + + defaults: () => ({ + object: 'node', + parentId: null, + visible: true, + metadata: {}, + children: [], + polygon: [], + holes: [], + holeMetadata: [], + height: 2.5, + autoFromWalls: false, + }), + + capabilities: { + selectable: { hitVolume: 'bbox' }, + surfaces: { + top: { height: (n) => (n as CeilingNode).height }, + }, + duplicable: true, + deletable: true, + }, + + relations: { + hosts: ['item'], + cascadeDelete: 'descendants', + }, + + parametrics: ceilingParametrics, + + renderer: { + kind: 'parametric', + module: () => import('./renderer'), + }, + system: { + module: () => import('./system'), + priority: 4, + }, + + toolHints: [ + { key: 'Left click', label: 'Trace ceiling outline' }, + { key: 'Enter', label: 'Finish ceiling' }, + { key: 'Esc', label: 'Cancel' }, + ], + + presentation: { + label: 'Ceiling', + description: 'A polygon-bounded ceiling surface that hosts ceiling-mounted items.', + icon: { kind: 'iconify', name: 'lucide:square-dashed' }, + paletteSection: 'structure', + paletteOrder: 40, + }, + + mcp: { + description: 'A polygon-bounded ceiling with optional cutout holes.', + }, +} diff --git a/packages/nodes/src/ceiling/index.ts b/packages/nodes/src/ceiling/index.ts new file mode 100644 index 00000000..1c0a3575 --- /dev/null +++ b/packages/nodes/src/ceiling/index.ts @@ -0,0 +1,2 @@ +export { ceilingDefinition } from './definition' +export { CeilingNode } from './schema' diff --git a/packages/nodes/src/ceiling/parametrics.ts b/packages/nodes/src/ceiling/parametrics.ts new file mode 100644 index 00000000..87793ae3 --- /dev/null +++ b/packages/nodes/src/ceiling/parametrics.ts @@ -0,0 +1,16 @@ +import type { ParametricDescriptor } from '@pascal-app/core' +import type { CeilingNode } from './schema' + +/** + * Inspector descriptor for ceiling. Polygon + holes are edited via the + * floor-plan boundary / hole editors — not number inputs. Inspector + * exposes only the per-instance scalar (height). + */ +export const ceilingParametrics: ParametricDescriptor = { + groups: [ + { + label: 'Dimensions', + fields: [{ key: 'height', kind: 'number', unit: 'm', min: 1.5, max: 6, step: 0.05 }], + }, + ], +} diff --git a/packages/nodes/src/ceiling/renderer.tsx b/packages/nodes/src/ceiling/renderer.tsx new file mode 100644 index 00000000..13be02ef --- /dev/null +++ b/packages/nodes/src/ceiling/renderer.tsx @@ -0,0 +1,15 @@ +'use client' + +import { CeilingRenderer } from '@pascal-app/viewer' + +/** + * Wrap-export of the legacy `CeilingRenderer`. + * + * Ceiling's renderer uses TSL shader code for the grid-line pattern + * (~100 lines incl. material setup) — too much to duplicate at Stage A. + * The legacy file stays in viewer; the registry imports it through the + * public export. Phase 5 Stage B/F (per-kind migration stages, see + * plans/editor-node-registry.md) moves the renderer body into this + * folder and deletes the legacy file. + */ +export default CeilingRenderer diff --git a/packages/nodes/src/ceiling/schema.ts b/packages/nodes/src/ceiling/schema.ts new file mode 100644 index 00000000..f562ccaa --- /dev/null +++ b/packages/nodes/src/ceiling/schema.ts @@ -0,0 +1 @@ +export { CeilingNode } from '@pascal-app/core' diff --git a/packages/nodes/src/ceiling/system.tsx b/packages/nodes/src/ceiling/system.tsx new file mode 100644 index 00000000..49dbf3be --- /dev/null +++ b/packages/nodes/src/ceiling/system.tsx @@ -0,0 +1,19 @@ +'use client' + +import { CeilingSystem } from '@pascal-app/viewer' + +/** + * Registry-driven ceiling system bundle. Re-exports the legacy + * `CeilingSystem` so it mounts via `RegisteredSystems` when ceiling is + * registry-driven. `` in viewer/components/ + * viewer/index.tsx short-circuits whenever `nodeRegistry.has('ceiling')` + * is true — same shape wall / fence / slab use. + * + * Future Phase 5+: extract polygon triangulation + hole CSG into a pure + * `buildCeilingGeometry(node)` and migrate to `def.geometry`. + */ +const CeilingSystems = () => { + return +} + +export default CeilingSystems diff --git a/packages/nodes/src/index.ts b/packages/nodes/src/index.ts index 436b3f6f..f00a7004 100644 --- a/packages/nodes/src/index.ts +++ b/packages/nodes/src/index.ts @@ -1,4 +1,5 @@ import type { AnyNodeDefinition, Plugin } from '@pascal-app/core' +import { ceilingDefinition } from './ceiling' import { fenceDefinition } from './fence' import { shelfDefinition } from './shelf' import { slabDefinition } from './slab' @@ -32,9 +33,11 @@ export const builtinPlugin: Plugin = { wallDefinition as unknown as AnyNodeDefinition, fenceDefinition as unknown as AnyNodeDefinition, slabDefinition as unknown as AnyNodeDefinition, + ceilingDefinition as unknown as AnyNodeDefinition, ], } +export { ceilingDefinition } from './ceiling' export { fenceDefinition } from './fence' export { shelfDefinition } from './shelf' export { slabDefinition } from './slab' diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index 0a0c2b21..7ea2af6f 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -2,6 +2,12 @@ // renderers (wall renders doors/windows, slab renders hosted items). // Public so registry-driven kinds can compose children without reaching // into viewer's internal paths. + +// Ceiling internals re-exported for the registry-driven ceiling definition. +// The ceiling renderer uses TSL shaders for a grid pattern — too much code +// to duplicate at Stage A. Wrap-exported here, ported into the nodes +// folder in a later stage. +export { CeilingRenderer } from './components/renderers/ceiling/ceiling-renderer' export { NodeRenderer } from './components/renderers/node-renderer' export { default as Viewer } from './components/viewer' export type { HoverStyle, HoverStyles } from './components/viewer/post-processing' @@ -31,6 +37,7 @@ export { } from './lib/materials' export { mergedOutline } from './lib/merged-outline-node' export { default as useViewer } from './store/use-viewer' +export { CeilingSystem } from './systems/ceiling/ceiling-system' // Fence system follows the wall re-export pattern — composed into the // registry-driven fence definition's `def.system`. Removed in Phase 6 // alongside the legacy fence mount point.