Phase 5 batch kind: ceiling migrates to registry (always-on)
Structurally identical to slab. Stage A migration: registers the kind, wraps the legacy renderer + system, applies the panel slider-drag fix recipe. Files added (packages/nodes/src/ceiling/): - schema.ts: re-exports CeilingNode from core. - parametrics.ts: height slider only. Polygon + holes via floor-plan editors. - definition.ts: capabilities (no `movable`, `surfaces.top` mapped to `height`), relations (hosts: ['item'] for ceiling-mounted lights / fans, cascadeDelete: 'descendants'), toolHints, parametrics. - renderer.tsx: wrap-export of legacy CeilingRenderer. The legacy renderer uses TSL shader code for grid-line patterns (~100 lines); not worth duplicating at Stage A. Per-stage migration plan in plans/editor-node-registry.md moves the renderer body into this folder at Stage B/F. - system.tsx: re-exports legacy CeilingSystem. - index.ts: barrel. Files changed: - packages/viewer/src/index.ts: new public exports for CeilingRenderer + CeilingSystem. - packages/nodes/src/index.ts: appends ceilingDefinition. - packages/editor/src/components/ui/panels/ceiling-panel.tsx: panel slider-drag fix recipe applied (nodeRef pattern, useScene.getState() inside handler, drop subscribed updateNode dep) so the height slider doesn't trigger the same cascade fence + wall + slab fixed. Phase 5 progress: shelf ✅, spawn ✅, wall ✅, fence ✅, slab ✅, ceiling ✅. Six kinds on the registry. Door / window / item / stair / roof / zone follow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
4891f681f3
commit
2dd50fa5be
@@ -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<CeilingNode>) => {
|
||||
if (!selectedId) return
|
||||
updateNode(selectedId as AnyNode['id'], updates)
|
||||
useScene.getState().updateNode(selectedId as AnyNode['id'], updates)
|
||||
},
|
||||
[selectedId, updateNode],
|
||||
[selectedId],
|
||||
)
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
|
||||
@@ -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<typeof CeilingNode> = {
|
||||
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.',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ceilingDefinition } from './definition'
|
||||
export { CeilingNode } from './schema'
|
||||
@@ -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<CeilingNode> = {
|
||||
groups: [
|
||||
{
|
||||
label: 'Dimensions',
|
||||
fields: [{ key: 'height', kind: 'number', unit: 'm', min: 1.5, max: 6, step: 0.05 }],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
export { CeilingNode } from '@pascal-app/core'
|
||||
@@ -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. `<LegacySystem kind="ceiling">` 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 <CeilingSystem />
|
||||
}
|
||||
|
||||
export default CeilingSystems
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user