Phase 5 Stage D ceiling: port placement + move + boundary/hole editors

Replicates the slab Stage D recipe for ceiling: four affordances
routed through the registry — def.tool for the placement flow,
def.affordanceTools for boundary edit / hole edit / whole-ceiling move.

Ceiling-specific bits preserved:
- Placement tool keeps the dual-cursor + vertical TSL-gradient
  connector + ground-shadow lines (1:1 with legacy).
- Move tool wrapper renders the translucent preview fill + outline
  overlay so the user sees the destination before clicking.

ToolManager mount sites for CeilingBoundaryEditor / CeilingHoleEditor
now route through `getRegistryAffordanceTool` with legacy fallback.

Per-kind progress: ceiling A  B (n/a, def.renderer escape hatch
preserved) C  D ; E + F pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-15 18:31:40 -04:00
co-authored by Claude Opus 4.7
parent b2e5d84986
commit de3efa1b53
7 changed files with 733 additions and 6 deletions
@@ -0,0 +1,47 @@
'use client'
import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'
import { PolygonEditor } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useCallback } from 'react'
/**
* Phase 5 Stage D — ceiling boundary editor (registry-driven).
*
* Thin wrapper around the shared `<PolygonEditor>` (same shape as
* slab's boundary-editor). Activates when a ceiling is selected in
* structure/select mode and no hole edit is in progress.
*/
export const CeilingBoundaryEditor: React.FC<{ ceilingId: CeilingNode['id'] }> = ({
ceilingId,
}) => {
const ceilingNode = useScene((s) => s.nodes[ceilingId])
const updateNode = useScene((s) => s.updateNode)
const setSelection = useViewer((s) => s.setSelection)
const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null
const handlePolygonChange = useCallback(
(newPolygon: Array<[number, number]>) => {
updateNode(ceilingId, { polygon: newPolygon })
setSelection({ selectedIds: [ceilingId] })
},
[ceilingId, updateNode, setSelection],
)
if (!ceiling?.polygon || ceiling.polygon.length < 3) return null
return (
<PolygonEditor
allowEdgeMove
color="#d4d4d4"
levelId={resolveLevelId(ceiling, useScene.getState().nodes)}
minVertices={3}
onPolygonChange={handlePolygonChange}
polygon={ceiling.polygon}
surfaceHeight={ceiling.height ?? 2.5}
/>
)
}
export default CeilingBoundaryEditor