Phase 5 depth-first: spawn C, fence B+C, slab B+C, ceiling C
Depth-first session: drive registered kinds through Stage B (pure
def.geometry, drop system re-export) and Stage C (def.floorplan,
short-circuit legacy inline rendering in floorplan-panel.tsx).
spawn → C
- buildSpawnFloorplan wired on definition (was written but deferred
to avoid double-render).
- floorplan-panel.tsx's floorplanSpawnEntries useMemo short-circuits
to [] when nodeRegistry.has('spawn').
fence → B
- generateFenceGeometry exported from viewer; buildFenceGeometry
wraps it in a Group+Mesh with DEFAULT_STAIR_MATERIAL.
- def.geometry set; renderer + system fields dropped.
- Deleted nodes/src/fence/{renderer.tsx,system.tsx}.
fence → C
- buildFenceFloorplan: polyline along centerline (sampled for curved
fences via sampleWallCenterline from core). Stroke width = node.thickness.
- floorplan-panel.tsx's floorplanFenceEntries short-circuits.
slab → B
- generateSlabGeometry exported from viewer; buildSlabGeometry wraps
it in a Group+Mesh + cached material (preset / custom / default
pattern preserved from legacy renderer).
- def.geometry set; renderer + system fields dropped.
- Deleted nodes/src/slab/{renderer.tsx,system.tsx}.
slab → C
- buildSlabFloorplan: SVG path with outer polygon + hole subpaths
(uses getRenderableSlabPolygon from core for wall-clipping parity).
- floorplan-panel.tsx's slabPolygons short-circuits.
ceiling → B INTENTIONALLY SKIPPED
- Ceiling renderer renders React children (hosted items) + uses TSL
shader materials + named meshes that other systems poke
(getObjectByName('ceiling-grid')). Pure def.geometry can't preserve
that. Ceiling keeps def.renderer (the custom escape hatch) — same
pattern item uses. Documented in ceiling/definition.ts.
ceiling → C
- buildCeilingFloorplan: dashed-outline path with hole subpaths
(visually distinct from slab since ceilings are above).
- floorplan-panel.tsx's ceilingPolygons short-circuits.
Per-kind progress after this session:
- shelf: B ✅ C ✅ (Stage E since brand-new)
- spawn: A ✅ C ✅
- wall: A ✅ (B blocked on ctx.levelData design)
- fence: A ✅ B ✅ C ✅
- slab: A ✅ B ✅ C ✅
- ceiling: A ✅ C ✅ (B intentionally not applicable)
- door / window / item: A ✅ (B+C pending in future sessions)
Known test issue: `bun test` in packages/nodes fails to load
`three-bvh-csg` through the viewer's transitive imports (UMD/ESM
mismatch in Bun's test runner). The Next.js editor build works fine
because it bundles differently. Fix requires either dynamic imports
(breaks sync def.geometry contract) or test env config — deferred.
Other tests (schema, geometry, parity) pass.
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
df07f7bcb2
commit
969b154b08
@@ -8440,49 +8440,52 @@ export function FloorplanPanel() {
|
||||
|
||||
return hasPreviewWalls ? nextFloorplanWallById : floorplanWallById
|
||||
}, [displayWallById, floorplanWallById, wallCurveDraft, wallEndpointDraft])
|
||||
const floorplanFenceEntries = useMemo(
|
||||
() =>
|
||||
fences.flatMap((fence) => {
|
||||
const live = useLiveTransforms.getState().get(fence.id)
|
||||
const fenceCenterX = (fence.start[0] + fence.end[0]) / 2
|
||||
const fenceCenterZ = (fence.start[1] + fence.end[1]) / 2
|
||||
const displayFence = live
|
||||
? {
|
||||
...fence,
|
||||
start: [
|
||||
fence.start[0] + (live.position[0] - fenceCenterX),
|
||||
fence.start[1] + (live.position[2] - fenceCenterZ),
|
||||
] as typeof fence.start,
|
||||
end: [
|
||||
fence.end[0] + (live.position[0] - fenceCenterX),
|
||||
fence.end[1] + (live.position[2] - fenceCenterZ),
|
||||
] as typeof fence.end,
|
||||
}
|
||||
: fence
|
||||
const centerline = isCurvedWall(displayFence)
|
||||
? sampleWallCenterline(displayFence, 24)
|
||||
: [
|
||||
{ x: displayFence.start[0], y: displayFence.start[1] },
|
||||
{ x: displayFence.end[0], y: displayFence.end[1] },
|
||||
]
|
||||
const path = buildSvgPolylinePath(centerline)
|
||||
if (!path) {
|
||||
return []
|
||||
}
|
||||
|
||||
const markerFrames = getFloorplanFenceMarkerTs(displayFence).map((t) => {
|
||||
const frame = getWallCurveFrameAt(displayFence, t)
|
||||
|
||||
return {
|
||||
angleDeg: (Math.atan2(frame.tangent.y, frame.tangent.x) * 180) / Math.PI,
|
||||
point: frame.point,
|
||||
const floorplanFenceEntries = useMemo(() => {
|
||||
// Fence migrated to def.floorplan (Phase 5 Stage C). When registered,
|
||||
// FloorplanRegistryLayer renders the fence polyline; this legacy
|
||||
// path short-circuits to avoid double-render. Removed entirely in
|
||||
// Phase 6 cleanup.
|
||||
if (nodeRegistry.has('fence')) return []
|
||||
return fences.flatMap((fence) => {
|
||||
const live = useLiveTransforms.getState().get(fence.id)
|
||||
const fenceCenterX = (fence.start[0] + fence.end[0]) / 2
|
||||
const fenceCenterZ = (fence.start[1] + fence.end[1]) / 2
|
||||
const displayFence = live
|
||||
? {
|
||||
...fence,
|
||||
start: [
|
||||
fence.start[0] + (live.position[0] - fenceCenterX),
|
||||
fence.start[1] + (live.position[2] - fenceCenterZ),
|
||||
] as typeof fence.start,
|
||||
end: [
|
||||
fence.end[0] + (live.position[0] - fenceCenterX),
|
||||
fence.end[1] + (live.position[2] - fenceCenterZ),
|
||||
] as typeof fence.end,
|
||||
}
|
||||
})
|
||||
: fence
|
||||
const centerline = isCurvedWall(displayFence)
|
||||
? sampleWallCenterline(displayFence, 24)
|
||||
: [
|
||||
{ x: displayFence.start[0], y: displayFence.start[1] },
|
||||
{ x: displayFence.end[0], y: displayFence.end[1] },
|
||||
]
|
||||
const path = buildSvgPolylinePath(centerline)
|
||||
if (!path) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{ fence: displayFence, centerline, markerFrames, path }]
|
||||
}),
|
||||
[fences, movingFloorplanNodeRevision],
|
||||
)
|
||||
const markerFrames = getFloorplanFenceMarkerTs(displayFence).map((t) => {
|
||||
const frame = getWallCurveFrameAt(displayFence, t)
|
||||
|
||||
return {
|
||||
angleDeg: (Math.atan2(frame.tangent.y, frame.tangent.x) * 180) / Math.PI,
|
||||
point: frame.point,
|
||||
}
|
||||
})
|
||||
|
||||
return [{ fence: displayFence, centerline, markerFrames, path }]
|
||||
})
|
||||
}, [fences, movingFloorplanNodeRevision])
|
||||
const wallPolygons = useMemo(
|
||||
() =>
|
||||
walls.map((wall) => {
|
||||
@@ -8579,33 +8582,36 @@ export function FloorplanPanel() {
|
||||
}),
|
||||
[displayFloorplanWallById, movingFloorplanNodeRevision, movingNode, openings],
|
||||
)
|
||||
const slabPolygons = useMemo(
|
||||
() =>
|
||||
slabs.flatMap((slab) => {
|
||||
const polygon = toFloorplanPolygon(slab.polygon)
|
||||
if (polygon.length < 3) {
|
||||
return []
|
||||
}
|
||||
const slabPolygons = useMemo(() => {
|
||||
// Slab migrated to def.floorplan (Phase 5 Stage C). When registered,
|
||||
// FloorplanRegistryLayer renders the slab polygon; this legacy
|
||||
// path short-circuits to avoid double-render. Removed entirely in
|
||||
// Phase 6 cleanup.
|
||||
if (nodeRegistry.has('slab')) return []
|
||||
return slabs.flatMap((slab) => {
|
||||
const polygon = toFloorplanPolygon(slab.polygon)
|
||||
if (polygon.length < 3) {
|
||||
return []
|
||||
}
|
||||
|
||||
const holes = (slab.holes ?? [])
|
||||
.map((hole) => toFloorplanPolygon(hole))
|
||||
.filter((hole) => hole.length >= 3)
|
||||
const visualPolygon = toFloorplanPolygon(getRenderableSlabPolygon(slab))
|
||||
const visualHoles = holes
|
||||
const holes = (slab.holes ?? [])
|
||||
.map((hole) => toFloorplanPolygon(hole))
|
||||
.filter((hole) => hole.length >= 3)
|
||||
const visualPolygon = toFloorplanPolygon(getRenderableSlabPolygon(slab))
|
||||
const visualHoles = holes
|
||||
|
||||
return [
|
||||
{
|
||||
slab,
|
||||
polygon,
|
||||
holes,
|
||||
visualPolygon,
|
||||
visualHoles,
|
||||
path: formatPolygonPath(visualPolygon, visualHoles),
|
||||
},
|
||||
]
|
||||
}),
|
||||
[slabs],
|
||||
)
|
||||
return [
|
||||
{
|
||||
slab,
|
||||
polygon,
|
||||
holes,
|
||||
visualPolygon,
|
||||
visualHoles,
|
||||
path: formatPolygonPath(visualPolygon, visualHoles),
|
||||
},
|
||||
]
|
||||
})
|
||||
}, [slabs])
|
||||
const displaySlabPolygons = useMemo(() => {
|
||||
if (!(slabBoundaryDraft || slabHoleBoundaryDraft || slabHoleMoveDraft)) {
|
||||
return slabPolygons
|
||||
@@ -8662,29 +8668,31 @@ export function FloorplanPanel() {
|
||||
return nextEntry
|
||||
})
|
||||
}, [slabBoundaryDraft, slabHoleBoundaryDraft, slabHoleMoveDraft, slabPolygons])
|
||||
const ceilingPolygons = useMemo(
|
||||
() =>
|
||||
ceilings.flatMap((ceiling) => {
|
||||
const polygon = toFloorplanPolygon(ceiling.polygon)
|
||||
if (polygon.length < 3) {
|
||||
return []
|
||||
}
|
||||
const ceilingPolygons = useMemo(() => {
|
||||
// Ceiling migrated to def.floorplan (Phase 5 Stage C). When registered,
|
||||
// FloorplanRegistryLayer renders the ceiling polygon; this legacy
|
||||
// path short-circuits. Removed entirely in Phase 6 cleanup.
|
||||
if (nodeRegistry.has('ceiling')) return []
|
||||
return ceilings.flatMap((ceiling) => {
|
||||
const polygon = toFloorplanPolygon(ceiling.polygon)
|
||||
if (polygon.length < 3) {
|
||||
return []
|
||||
}
|
||||
|
||||
const holes = (ceiling.holes ?? [])
|
||||
.map((hole) => toFloorplanPolygon(hole))
|
||||
.filter((hole) => hole.length >= 3)
|
||||
const holes = (ceiling.holes ?? [])
|
||||
.map((hole) => toFloorplanPolygon(hole))
|
||||
.filter((hole) => hole.length >= 3)
|
||||
|
||||
return [
|
||||
{
|
||||
ceiling,
|
||||
polygon,
|
||||
holes,
|
||||
path: formatPolygonPath(polygon, holes),
|
||||
},
|
||||
]
|
||||
}),
|
||||
[ceilings],
|
||||
)
|
||||
return [
|
||||
{
|
||||
ceiling,
|
||||
polygon,
|
||||
holes,
|
||||
path: formatPolygonPath(polygon, holes),
|
||||
},
|
||||
]
|
||||
})
|
||||
}, [ceilings])
|
||||
const displayCeilingPolygons = useMemo(() => {
|
||||
if (!(ceilingBoundaryDraft || ceilingHoleBoundaryDraft || ceilingHoleMoveDraft)) {
|
||||
return ceilingPolygons
|
||||
@@ -8802,24 +8810,28 @@ export function FloorplanPanel() {
|
||||
),
|
||||
[levelDescendantNodes],
|
||||
)
|
||||
const floorplanSpawnEntries = useMemo<FloorplanSpawnEntry[]>(
|
||||
() =>
|
||||
spawns
|
||||
.filter((spawn) => spawn.visible !== false)
|
||||
.map((spawn) => {
|
||||
const live = useLiveTransforms.getState().get(spawn.id)
|
||||
|
||||
return {
|
||||
spawn,
|
||||
position: {
|
||||
x: live?.position[0] ?? spawn.position[0],
|
||||
y: live?.position[2] ?? spawn.position[2],
|
||||
},
|
||||
rotation: live?.rotation ?? spawn.rotation,
|
||||
}
|
||||
}),
|
||||
[movingFloorplanNodeRevision, spawns],
|
||||
)
|
||||
const floorplanSpawnEntries = useMemo<FloorplanSpawnEntry[]>(() => {
|
||||
// Spawn migrated to the registry-driven floor-plan layer (Phase 5
|
||||
// Stage C). When registered, FloorplanRegistryLayer renders the
|
||||
// spawn marker via def.floorplan; FloorplanRegistryActionMenu
|
||||
// handles select / move / delete. Returning [] here skips the
|
||||
// legacy rendering + action menu paths to avoid double-render.
|
||||
// Removed entirely in Phase 6 cleanup.
|
||||
if (nodeRegistry.has('spawn')) return []
|
||||
return spawns
|
||||
.filter((spawn) => spawn.visible !== false)
|
||||
.map((spawn) => {
|
||||
const live = useLiveTransforms.getState().get(spawn.id)
|
||||
return {
|
||||
spawn,
|
||||
position: {
|
||||
x: live?.position[0] ?? spawn.position[0],
|
||||
y: live?.position[2] ?? spawn.position[2],
|
||||
},
|
||||
rotation: live?.rotation ?? spawn.rotation,
|
||||
}
|
||||
})
|
||||
}, [movingFloorplanNodeRevision, spawns])
|
||||
const floorplanItemEntries = useMemo(() => {
|
||||
const transformCache = new Map<string, SharedFloorplanNodeTransform | null>()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user