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
@@ -1,34 +1,21 @@
|
||||
import type { NodeDefinition } from '@pascal-app/core'
|
||||
import { buildFenceFloorplan } from './floorplan'
|
||||
import { buildFenceGeometry } from './geometry'
|
||||
import { fenceParametrics } from './parametrics'
|
||||
import { FenceNode } from './schema'
|
||||
|
||||
/**
|
||||
* Fence — the first Phase 5 batch-migration kind.
|
||||
* Fence — Phase 5 batch kind. Stage B complete: `def.geometry` drives
|
||||
* the rebuild via the generic `<GeometrySystem>`; `<ParametricNodeRenderer>`
|
||||
* mounts the empty group. No per-kind renderer or system file.
|
||||
*
|
||||
* What this definition encodes:
|
||||
* - **Capabilities**: snappable (other walls/fences/items snap to it),
|
||||
* surfaces (front + back faces host items), selectable, duplicable,
|
||||
* deletable. No `movable` capability — fence move is bespoke
|
||||
* endpoint-drag, same shape as wall (handled by legacy MoveFenceTool
|
||||
* until the affordance port).
|
||||
* - **Relations**: `linkedBy: 'endpoint-match'` for fence-corner cascade.
|
||||
* No `hosts` field — doors/windows don't mount on fences. `affectsSpatial`
|
||||
* omitted: moving a fence doesn't dirty slabs/zones in the legacy
|
||||
* behavior, so the registry stays parity-equivalent until we
|
||||
* explicitly add the cascade (separate decision).
|
||||
* - **Parametrics**: dimensions, posts, style — see `./parametrics.ts`.
|
||||
* - **toolHints**: placement panel hints for the fence-build tool.
|
||||
* - **Renderer + system**: thin placeholder mesh + re-export of the
|
||||
* legacy `FenceSystem`. Same shape as wall milestone B; future Phase 5+
|
||||
* extracts the pure geometry function and migrates to `def.geometry`.
|
||||
* Capabilities:
|
||||
* - **No `movable`**: fence move is bespoke endpoint-drag. Capability-
|
||||
* driven dispatch keeps the legacy MoveFenceTool until the
|
||||
* affordance port (Stage D).
|
||||
* - `surfaces.sides`, `selectable`, `duplicable`, `deletable` standard.
|
||||
*
|
||||
* Tool field stays absent: fence has 4 separate tools (build, curve,
|
||||
* move, move-endpoint) wired through editor state, not the registry
|
||||
* tool dispatch. They keep running unchanged until the affordance port.
|
||||
*
|
||||
* Migration is gated by `feature-flag.ts`
|
||||
* (env: `NEXT_PUBLIC_USE_REGISTRY_FOR_FENCE`). See
|
||||
* `plans/editor-node-registry.md#phase-5` for the batch order.
|
||||
* Relations: `linkedBy: 'endpoint-match'` for corner cascade.
|
||||
*/
|
||||
export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
|
||||
kind: 'fence',
|
||||
@@ -71,16 +58,15 @@ export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
|
||||
|
||||
parametrics: fenceParametrics,
|
||||
|
||||
renderer: {
|
||||
kind: 'parametric',
|
||||
module: () => import('./renderer'),
|
||||
},
|
||||
system: {
|
||||
module: () => import('./system'),
|
||||
// Same frame priority as the legacy FenceSystem (4 — runs after door/
|
||||
// window animations at 2-3, before zone/level systems at 6+).
|
||||
priority: 4,
|
||||
},
|
||||
// Stage B: pure geometry function. Generic <GeometrySystem> rebuilds
|
||||
// on dirtyNodes; <ParametricNodeRenderer> mounts the empty group.
|
||||
// `renderer` + `system` fields dropped along with their files.
|
||||
geometry: buildFenceGeometry,
|
||||
// Stage C: floor-plan rendering. FloorplanRegistryLayer iterates kinds
|
||||
// with `floorplan` set and renders via FloorplanGeometryRenderer.
|
||||
// Legacy `floorplanFenceEntries` short-circuits to [] when fence is
|
||||
// registered (see floorplan-panel.tsx).
|
||||
floorplan: buildFenceFloorplan,
|
||||
|
||||
toolHints: [
|
||||
{ key: 'Left click', label: 'Set fence start / end' },
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { FloorplanGeometry, FloorplanPoint } from '@pascal-app/core'
|
||||
import { isCurvedWall, sampleWallCenterline } from '@pascal-app/core'
|
||||
import type { FenceNode } from './schema'
|
||||
|
||||
/**
|
||||
* Stage C floor-plan builder for fence. Draws the fence centerline as
|
||||
* a polyline; thickness becomes the stroke width.
|
||||
*
|
||||
* Curved fences sample the centerline at 24 segments — same density the
|
||||
* legacy `floorplanFenceEntries` useMemo uses, so straight + curved
|
||||
* fences look comparable to the legacy rendering.
|
||||
*
|
||||
* Visual nuances the legacy ships (side hatching to indicate thickness
|
||||
* direction, post markers along the centerline) are deferred — Phase 5
|
||||
* Stage D will revisit if real visual parity is needed.
|
||||
*/
|
||||
export function buildFenceFloorplan(node: FenceNode): FloorplanGeometry {
|
||||
const points: FloorplanPoint[] = isCurvedWall(node)
|
||||
? sampleWallCenterline(node, 24).map((p) => [p.x, p.y] as FloorplanPoint)
|
||||
: [
|
||||
[node.start[0], node.start[1]],
|
||||
[node.end[0], node.end[1]],
|
||||
]
|
||||
|
||||
return {
|
||||
kind: 'polyline',
|
||||
points,
|
||||
stroke: node.color || '#475569',
|
||||
strokeWidth: Math.max(node.thickness, 0.05),
|
||||
opacity: 0.9,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { DEFAULT_STAIR_MATERIAL, generateFenceGeometry } from '@pascal-app/viewer'
|
||||
import { Group, Mesh } from 'three'
|
||||
import type { FenceNode } from './schema'
|
||||
|
||||
/**
|
||||
* Stage B builder for fence. Reuses the legacy `generateFenceGeometry`
|
||||
* (pure function from viewer that returns a merged BufferGeometry of
|
||||
* posts + base + top rail + curve spans) and wraps it in a Mesh-in-Group
|
||||
* shape the generic `<GeometrySystem>` expects.
|
||||
*
|
||||
* Material is a single shared reference — fences look the same regardless
|
||||
* of instance, so we don't clone per node. If per-fence material
|
||||
* customization lands later (color picker on the panel maps to a real
|
||||
* material), this becomes a per-node lookup.
|
||||
*
|
||||
* Phase 6 cleanup moves the 280 lines of geometry math out of the
|
||||
* legacy `viewer/src/systems/fence/fence-system.tsx` into this folder
|
||||
* once the legacy system file is deleted. Until then `generateFenceGeometry`
|
||||
* is publicly re-exported from viewer.
|
||||
*/
|
||||
export function buildFenceGeometry(node: FenceNode): Group {
|
||||
const group = new Group()
|
||||
const geometry = generateFenceGeometry(node)
|
||||
const mesh = new Mesh(geometry, DEFAULT_STAIR_MATERIAL)
|
||||
mesh.castShadow = true
|
||||
mesh.receiveShadow = true
|
||||
group.add(mesh)
|
||||
return group
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { type FenceNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import { DEFAULT_STAIR_MATERIAL, useNodeEvents } from '@pascal-app/viewer'
|
||||
import { useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
|
||||
/**
|
||||
* Thin fence renderer — registers an empty mesh, marks the node dirty so
|
||||
* `FenceSystem` (re-exported via `./system`) fills the geometry next
|
||||
* frame, and wires pointer events through `useNodeEvents`.
|
||||
*
|
||||
* Behaviorally identical to the legacy `FenceRenderer` in
|
||||
* `@pascal-app/viewer/components/renderers/fence/fence-renderer.tsx`.
|
||||
* Phase 0 shims pick which one mounts based on `nodeRegistry.has('fence')`.
|
||||
*
|
||||
* Material is `DEFAULT_STAIR_MATERIAL` (legacy reuse; fence and stairs
|
||||
* share the wood-tone preset).
|
||||
*/
|
||||
const FenceRenderer = ({ node }: { node: FenceNode }) => {
|
||||
const ref = useRef<Mesh>(null!)
|
||||
const handlers = useNodeEvents(node, 'fence')
|
||||
const material = useMemo(() => DEFAULT_STAIR_MATERIAL, [])
|
||||
|
||||
useRegistry(node.id, 'fence', ref)
|
||||
useLayoutEffect(() => {
|
||||
useScene.getState().markDirty(node.id)
|
||||
}, [node.id])
|
||||
|
||||
return (
|
||||
<mesh
|
||||
castShadow
|
||||
material={material}
|
||||
receiveShadow
|
||||
ref={ref}
|
||||
visible={node.visible}
|
||||
{...handlers}
|
||||
>
|
||||
<boxGeometry args={[0, 0, 0]} />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
export default FenceRenderer
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { FenceSystem } from '@pascal-app/viewer'
|
||||
|
||||
/**
|
||||
* Registry-driven fence system bundle.
|
||||
*
|
||||
* Wraps the legacy `FenceSystem` (re-exported from viewer) so it mounts
|
||||
* via `RegisteredSystems` when fence is registry-driven. The legacy
|
||||
* `<LegacySystem kind="fence">` wrapper around `<FenceSystem />` in
|
||||
* `viewer/components/viewer/index.tsx` short-circuits whenever
|
||||
* `nodeRegistry.has('fence')` is true — same pattern wall uses.
|
||||
*
|
||||
* Phase 6 deletes the legacy mount point; until then this bundle is the
|
||||
* single mount surface for fence's per-frame work when registry-driven.
|
||||
*
|
||||
* Future Phase 5+ work: extract fence geometry out of `FenceSystem`'s
|
||||
* useFrame body into a pure `buildFenceGeometry(node, ctx)` and migrate
|
||||
* to `def.geometry`. The generic `<GeometrySystem>` will then handle
|
||||
* the rebuild loop and this bundle can be deleted.
|
||||
*/
|
||||
const FenceSystems = () => {
|
||||
return <FenceSystem />
|
||||
}
|
||||
|
||||
export default FenceSystems
|
||||
Reference in New Issue
Block a user