Phase 5 Stage C continued: wall, door, window now in registry floor plan

Three remaining kinds at Stage C this session:

wall → C
 - buildWallFloorplan: uses ctx.siblings to gather other walls in the
   level, runs calculateLevelMiters, computes plan footprint via
   getWallPlanFootprint. Same visual output as legacy.
 - getFloorplanWall thickness exaggeration inlined (~25 lines from
   editor/lib/floorplan/walls.ts) to keep nodes/wall self-contained.
 - floorplan-panel.tsx's wallPolygons short-circuits to [] when wall
   is registered.
 - Performance note: recomputes level miter data per wall (O(N²) for N
   walls in a level). Acceptable for typical scenes; ctx.levelData?.
   miters optimization deferred to Stage B's wall design pass.

door → C
 - buildDoorFloorplan: inlines getOpeningFootprint math from
   floorplan-panel.tsx (40 lines, pure math). Uses ctx.parent as the
   wall to compute direction + perpendicular for the cutout footprint.
 - Returns null when parent isn't a wall (orphaned doors during
   placement).

window → C
 - buildWindowFloorplan: same shape as door, glass-blue tint to
   distinguish visually.

Both share the legacy openingsPolygons gating:
 - floorplan-panel.tsx's openingsPolygons useMemo filters per kind so
   a partial migration still works (e.g., if only door registers, only
   doors get skipped). When both registered, returns [] entirely.

Item C intentionally deferred — needs parent-chain transform helpers
(buildFloorplanItemEntry / getItemFloorplanTransform from editor/lib/
floorplan/items.ts) exposed publicly or moved into core. A focused
session is the right place to design that boundary.

Stage B for door / window / wall still pending — each is a focused
session per kind (large geometry math extractions, wall needs ctx.
levelData design).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-15 16:43:53 -04:00
co-authored by Claude Opus 4.7
parent 969b154b08
commit 9bcb25d0aa
7 changed files with 274 additions and 124 deletions
@@ -8486,19 +8486,21 @@ export function FloorplanPanel() {
return [{ fence: displayFence, centerline, markerFrames, path }]
})
}, [fences, movingFloorplanNodeRevision])
const wallPolygons = useMemo(
() =>
walls.map((wall) => {
const floorplanWall = floorplanWallById.get(wall.id) ?? getFloorplanWall(wall)
const polygon = getWallPlanFootprint(floorplanWall, wallMiterData)
return {
points: formatPolygonPoints(polygon),
wall,
polygon,
}
}),
[floorplanWallById, wallMiterData, walls],
)
const wallPolygons = useMemo(() => {
// Wall migrated to def.floorplan (Phase 5 Stage C). When registered,
// FloorplanRegistryLayer renders the mitered wall polygon; this
// legacy path short-circuits. Removed entirely in Phase 6 cleanup.
if (nodeRegistry.has('wall')) return []
return walls.map((wall) => {
const floorplanWall = floorplanWallById.get(wall.id) ?? getFloorplanWall(wall)
const polygon = getWallPlanFootprint(floorplanWall, wallMiterData)
return {
points: formatPolygonPoints(polygon),
wall,
polygon,
}
})
}, [floorplanWallById, wallMiterData, walls])
const displayWallPolygons = useMemo(() => {
if (!(wallEndpointDraft || wallCurveDraft)) {
return wallPolygons
@@ -8547,41 +8549,49 @@ export function FloorplanPanel() {
)
}, [displayWallById, wallCurveDraft, wallEndpointDraft, wallPolygons])
const openingsPolygons = useMemo(
() =>
openings.flatMap((opening) => {
const wall = displayFloorplanWallById.get(opening.parentId as WallNode['id'])
if (!wall) return []
const live = useLiveTransforms.getState().get(opening.id)
const displayOpening =
live &&
(movingNode?.type === 'door' || movingNode?.type === 'window') &&
movingNode.id === opening.id
? {
...opening,
position: [
live.position[0],
opening.position[1],
live.position[2],
] as typeof opening.position,
rotation: [
opening.rotation[0],
live.rotation,
opening.rotation[2],
] as typeof opening.rotation,
}
: opening
const polygon = getOpeningFootprint(wall, displayOpening)
return [
{
opening: displayOpening,
points: formatPolygonPoints(polygon),
polygon,
},
]
}),
[displayFloorplanWallById, movingFloorplanNodeRevision, movingNode, openings],
)
const openingsPolygons = useMemo(() => {
// Doors + windows migrated to def.floorplan (Phase 5 Stage C). When
// both registered, FloorplanRegistryLayer renders each opening's
// polygon via its kind's builder; this legacy path short-circuits
// to avoid double-render. Filter per kind so a partial migration
// would still work.
const doorRegistered = nodeRegistry.has('door')
const windowRegistered = nodeRegistry.has('window')
if (doorRegistered && windowRegistered) return []
return openings.flatMap((opening) => {
if (doorRegistered && opening.type === 'door') return []
if (windowRegistered && opening.type === 'window') return []
const wall = displayFloorplanWallById.get(opening.parentId as WallNode['id'])
if (!wall) return []
const live = useLiveTransforms.getState().get(opening.id)
const displayOpening =
live &&
(movingNode?.type === 'door' || movingNode?.type === 'window') &&
movingNode.id === opening.id
? {
...opening,
position: [
live.position[0],
opening.position[1],
live.position[2],
] as typeof opening.position,
rotation: [
opening.rotation[0],
live.rotation,
opening.rotation[2],
] as typeof opening.rotation,
}
: opening
const polygon = getOpeningFootprint(wall, displayOpening)
return [
{
opening: displayOpening,
points: formatPolygonPoints(polygon),
polygon,
},
]
})
}, [displayFloorplanWallById, movingFloorplanNodeRevision, movingNode, openings])
const slabPolygons = useMemo(() => {
// Slab migrated to def.floorplan (Phase 5 Stage C). When registered,
// FloorplanRegistryLayer renders the slab polygon; this legacy