251af2c added a `?.` on cursorRef in onGridClick but inadvertently
deleted the closing braces, the trailing reset block, and the
onKeyDown/onKeyUp/onCancel handlers — leaving onGridClick syntactically
unterminated and three undefined references at the emitter.on/off sites.
Restores the deleted block verbatim, keeping the optional-chaining null
guard on cursorRef.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wholesale move of every remaining kind into its own subdirectory under
`packages/nodes/src/`, finishing the registry-driven migration. Each
kind now ships its definition, schema (re-exported from core), and any
of `geometry` / `renderer` / `system` / `floorplan` / `tool` /
`move-tool` / `panel` / `floorplan-move` / `floorplan-affordances` /
`parametrics` / `preview` it needs — no per-kind code remains under
`packages/editor/src/components/tools/` or
`packages/viewer/src/components/renderers/`.
Deleted (replaced by registry-driven equivalents):
- `tools/{ceiling,column,door,fence,item,slab,spawn,wall,window}/...`
(boundary editors, hole editors, placement tools, move tools,
endpoint movers, curve tools, helpers, math libs)
- `ui/helpers/{ceiling,slab,wall}-helper.tsx`
- `ui/panels/{column,door,elevator,item,roof,roof-segment,spawn,
stair,stair-segment,wall,window}-panel.tsx`
- `viewer/src/components/renderers/{building,ceiling,column,door,
elevator,fence,guide,item,level,roof,roof-segment,scan,site,slab,
spawn,stair,stair-segment,wall,window,zone}-renderer.tsx`
- `viewer/src/components/viewer/legacy-system.tsx`
Added under `packages/nodes/src/`:
- `building/`, `column/`, `elevator/`, `guide/`, `level/`, `roof/`,
`roof-segment/`, `scan/`, `shared/`, `site/`, `stair/`,
`stair-segment/` packages with definition + schema + renderer / system
/ floorplan / panel as appropriate.
- New `floorplan-move.ts` for every kind that supports 2D moves
(ceiling, door, item, shelf, slab, window) — single registry-driven
dispatch path via `def.floorplanMoveTarget`.
- New `floorplan-affordances.ts` for kinds with polygon / endpoint
drags (ceiling, fence, slab, wall) — using the shared
`polygon-vertex-affordance` factories.
- New per-kind `panel.tsx` for kinds with custom inspector content
(door, item, shelf, spawn, wall, window).
- New per-kind `tool.tsx` for placement (door, item, shelf, window).
- New per-kind `move-tool.tsx` for kinds with custom 3D move flows
(door, item, slab, window).
Coordinator + manager updates in `packages/editor/`:
- `tool-manager.tsx` resolves tools from the registry only — no
hardcoded type→component map.
- `panel-manager.tsx` resolves inspector panels the same way.
- `placement-{coordinator,strategies,types}.ts` extended with
shelf-surface placement.
- `selection-manager.tsx` adds the registry-selectable fallback.
- `floorplan-panel.tsx`, `floorplan-background-placement.ts`,
`floorplan-render-context.tsx` updated for the registry layer's new
contract (props, affordance dispatch, render context).
Viewer updates:
- `viewer/index.tsx` drops legacy renderer mounts.
- `node-renderer.tsx` resolves by registry only.
- `scene-bvh.tsx`, `use-node-events.ts`, `level-system.tsx`,
`wall-cutout.tsx`, `zone-system.tsx`, `materials.ts` adjusted for
the registry-only world.
Sidebar tree nodes for ceiling / fence / slab / shelf / tree-node
updated to read from the registered nodes instead of the deleted
legacy renderer trees.
Wiki: new `plugin-authoring.md` page, README index updated.
Tests in `packages/nodes/src/index.test.ts` validate every registered
kind has the required shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User pointed at the palette's PNG icons in the bottom toolbar — they
expected the inspector title to use the same artwork, not the iconify
lucide glyphs. Different visual style.
- IconRef gains `{ kind: 'url'; src: string }`. Plain `<img>` render
in ParametricInspector (no next/image — the inspector is
`'use client'`).
- All currently-registered kinds switched to URL refs matching their
palette `iconSrc`:
fence → /icons/fence.png
slab → /icons/floor.png
ceiling → /icons/ceiling.png
wall → /icons/wall.png
spawn → /icons/site.png
shelf → /icons/column.png (placeholder, same as palette)
- Kind-owned panels (slab/ceiling) already pass URL strings to their
own PanelWrapper; unchanged.
Door/window/item will get URL refs when they register at Stage A.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User noticed the auto-derived fence inspector was missing the legacy
panel's Length + Curve sliders, and the section labelling was wrong
(Posts → Structure). Both Length and Curve are awkward for the
parametrics field model:
- **Length** doesn't map to a single node key — it's derived from
`start`/`end`, and editing it moves `end` along the existing
direction.
- **Curve** maps to `curveOffset` but the slider's min/max are
bounded per-node by the chord length, plus updates need
`normalizeWallCurveOffset`.
Adds a `kind: 'custom'` field with a kind-supplied
`component: ComponentType<{ node, onUpdate }>`. The inspector mounts
it and lets the kind own rendering + update logic. `key` becomes a
free-form React key/label since it no longer needs to map to a node
property.
Fence parametrics now mirrors the legacy layout 1:1:
- Style (segmented controls + showInfill toggle).
- Dimensions (Length, Curve, Height, Thickness).
- Structure (Base Height, Top Rail, Post Spacing, Post Size, Ground
Clear, Edge Inset).
Length + Curve live in fence/inspector-editors.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User pointed out two regressions in the auto-derived fence inspector:
- A `color` field rendered for fence — but the legacy `FencePanel`
hid it (fence's color is a leftover schema field that isn't part of
the inspector UX). Dropped from `fenceParametrics`.
- Style + base-style enums rendered as a dropdown, but the legacy
used the inline segmented switcher (Slat/Rail/Privacy +
Grounded/Floating). Added a `display?: 'select' | 'segmented'` hint
to the enum field kind. ParametricInspector renders SegmentedControl
when set; defaults to dropdown otherwise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three flavors of Stage E in one PR:
- **Fence** — fully auto-derived. Adds `kind: 'boolean'` to ParamField
(rendered as ToggleControl), wires `showInfill` through
`def.parametrics`. Legacy `FencePanel` deleted; the auto-derived
`<ParametricInspector>` now drives fence editing entirely.
- **Slab / Ceiling** — kind-owned via `parametrics.customPanel`. The
legacy panels have shape-specific bits (elevation/height presets,
area display, holes list with auto-vs-manual provenance) that don't
fit the auto-derived field model yet. `<ParametricInspector>` learns
to lazy-load and mount `parametrics.customPanel` when present;
legacy `SlabPanel` + `CeilingPanel` files relocate to
`nodes/src/<kind>/panel.tsx` and the legacy copies delete.
When `list` / `computed` / `action` field kinds eventually graduate
to auto-derived support, these custom panels collapse back into
`parametrics.groups`. The plan calls this out under "Custom-behavior
escape hatch" and Foot-gun 5 of the recipe.
Public-surface additions in `@pascal-app/editor`:
- `ActionButton`, `ActionGroup`, `PanelSection`, `SegmentedControl`,
`ToggleControl`, `PanelWrapper` — needed by the kind-owned panels.
Per-kind progress table: fence/slab/ceiling all flip to E ✅.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported the slab/fence mesh jittered/teleported continuously
while dragging (not on commit — during the drag). Root cause: the
move tool wrote TWO conflicting values per grid:move tick:
1. `mesh.position.set(deltaX, 0, deltaZ)` — relative offset, direct
Three.js mutation.
2. `useLiveTransforms.set(id, { position: [originalCenter + deltaX,
0, originalCenter + deltaZ], rotation: 0 })` — absolute world
position of the translated polygon center.
`ParametricNodeRenderer` reads `useLiveTransforms` and binds it via
React: `<group position={liveTransform.position}>`. So every Zustand
notification re-rendered the renderer and reconciled the group's
position back to "originalCenter + delta" (the absolute), overriding
the "delta" the direct mutation had just written. The two systems
fought every frame → visible jitter.
Fix: `useLiveTransforms.position` now holds the SAME delta the direct
mutation uses (`[deltaX, 0, deltaZ]`). React reconciles to the same
value the direct mutation already set — no conflict.
The cursor sphere position stays as the translated polygon center
(it's tracked separately via React state, not `useLiveTransforms`).
Ceiling aligned for consistency, though CeilingRenderer doesn't read
`useLiveTransforms` so the value there has no rendering effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-reported regressions on the 1:1 legacy ports: slab/ceiling moves
were slow (polygon CSG rebuilds per scene.update tick), fence moves
teleported briefly on commit (residual mesh.position offset survived
the geometry rebuild). All three now use the same live-drag pattern
the legacy fence move was designed for:
- During drag, write only to `sceneRegistry.nodes.get(id).position`
+ `useLiveTransforms`. No `scene.update`, no polygon rebuild, no
React re-render of geometry.
- History stays UNPAUSED — scene state isn't changing.
- On commit, a single `scene.update` writes the translated
polygon (or fence start/end + linked-fence cascade). Recorded as one
natural undo step.
- Tools leave `mesh.position` at the drag delta on commit;
GeometrySystem / CeilingSystem reset it to (0,0,0) when they
rebuild the geometry on the next frame. By the time position
clears, the new geometry is in place — no teleport.
Two framework changes enable this:
- `GeometrySystem` (viewer/systems/geometry) now resets
`group.position` + `group.rotation` after every rebuild, matching
the legacy `FenceSystem.updateFenceGeometry` behavior. Tools that
translate the group during live-drag can rely on the reset.
- Legacy `CeilingSystem.updateCeilingGeometry` extends its existing
`position.y` reset to cover X/Z too — previously it left X/Z at the
drag delta after rebuild, double-translating the visual.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each tool is now a direct copy of the legacy implementation, relocated
under @pascal-app/nodes/<kind>/ and dispatched via the registry's
def.affordanceTools. No DragAction abstraction, no clever live-drag
exception, no novel snap pipeline — same code, same UX, same
performance, same history dance.
Ports:
- fence/curve-tool.tsx (legacy CurveFenceTool, 1:1)
- fence/move-tool.tsx (legacy MoveFenceTool, 1:1 — including
the mesh.position + useLiveTransforms
exception that the legacy uses for fence
specifically)
- wall/curve-tool.tsx (legacy CurveWallTool, 1:1)
- slab/move-tool.tsx (legacy MoveSlabTool, 1:1)
- ceiling/move-tool.tsx (legacy MoveCeilingTool, 1:1 — preview
fill + outline overlay preserved)
Drops the obsolete DragAction-based action files
(packages/nodes/src/{fence,wall,slab,ceiling}/actions/{curve,move}.ts)
and their now-empty actions/ directories where applicable. Fence
keeps actions/move-endpoint.ts since that port works.
Editor public surface gains `getWallGridStep` + `snapScalarToGrid`
(transitional exports — Stage F moves them into @pascal-app/nodes).
ToolManager + MoveTool dispatch unchanged: the same legacy-fallback
branches now mount the registry component because the affordances are
declared, but the rendered behavior matches the legacy because the
implementations are copies.
Per-kind progress: fence D ✅ (curve / move-endpoint / move / placement
all kind-owned), slab D ✅, ceiling D ✅, wall D 🟡 (curve only,
endpoint/move/placement still legacy).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-reported regressions made it clear that my D-ports of the curve
and whole-item move tools introduced more friction than they removed.
The legacy CurveFenceTool / CurveWallTool / MoveFenceTool /
MoveSlabTool / MoveCeilingTool ship more polish than the new ports do
right now:
- Legacy curve tools pre-snap the pointer position to the 0.5m grid
BEFORE projecting onto the chord normal, and they Shift-toggle to
a free-place mode. The ports skipped both — finer math, but the
user-visible UX regressed (laggy because the cascade resolver fires
per move, history feels broken near the no-op threshold).
- Legacy whole-item moves use scene.update per tick which keeps
hosted children visually aligned. The live-drag mesh.position port
cleared the offset before the GeometrySystem could rebuild,
producing a one-frame teleport on commit.
Drop the affordance registrations for those tools — the ToolManager /
MoveTool dispatch falls back to the legacy per-kind tools when the
registry doesn't declare the affordance. Stage D progress preserved
for: fence move-endpoint (linked cascade + alt-detach), slab/ceiling
boundary + hole editors, fence placement, slab placement, ceiling
placement.
The kind-owned files (curve-tool.tsx, move-tool.tsx, actions/curve.ts,
actions/move.ts) stay on disk for the next iteration — when they
reach parity with legacy UX, re-add the affordance entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-reported regressions after the previous Stage D move ports:
1. Slab/ceiling moves were sluggish because the actions wrote
scene.update(polygon) every grid:move tick → React re-render +
CSG-with-holes geometry rebuild per frame.
Apply the live-drag exception (same recipe fence move already used):
visual translation via `sceneRegistry.nodes.get(id).position` +
`useLiveTransforms`; scene.polygon is only written on commit. Polygon
center precomputed in ctx, mesh-offset clears on commit/cancel.
2. Cursor sphere sat at the polygon center (offset from the user's
actual cursor by `originalCenter - first_cursor`). Move wrappers
now subscribe to `grid:move` and set `cursorRef.current.position`
directly — no React state, no per-tick reconcile. Cursor lands on
the user's pointer.
3. Fence move had the same React-reconcile-per-tick cost via its
`useLiveTransforms` subscription. Switched to the same direct
ref-mutation pattern.
Adds a "REAL bend" test pinning that one Ctrl-Z after a real curve
drag undoes only the bend, not the create. The previously reported
"first undo does nothing" outcome reproduces only for no-op bends
(drags within `normalizeWallCurveOffset`'s straight-snap threshold,
≈1.5cm on a 3m fence). For visible bends the dance pushes a real
pastState entry and one undo step rolls back the bend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three user-reported regressions, all in the Stage D move/curve ports:
1. **Fence/wall bend cancelled the fence creation on Ctrl-Z.** The
action.commit's `return false` shortcut on "no offset change"
bypassed the dance entirely — pastStates wasn't touched, so the
next Ctrl-Z fell through to whatever preceded activation
(typically the create step). Fix: always run the dance, even on
no-op commits. First Ctrl-Z then absorbs a silent no-op entry,
subsequent presses roll back real prior actions. Same fix applied
to fence move-endpoint, fence move, slab move, ceiling move.
2. **Slab/ceiling move 'maximum update depth exceeded' loop.** The
`useScene` selector in `SlabMoveTool` returned a freshly-allocated
`[sx, sz]` tuple on every call. Zustand's `Object.is` equality
failed each comparison → re-subscribe → re-render → loop. Fix:
subscribe to the stable live-node reference and derive the center
via `useMemo`. Same recipe for fence/ceiling move-tools, including
memoizing the `originalCenter` fallback that was getting a new
array per render.
3. **No grid-snap sfx during move drag.** Action `preview` now tracks
the last snapped pointer on a mutable `lastSnapped` ctx field and
emits `sfx:grid-snap` when it changes between ticks. Matches the
legacy MoveFenceTool's per-tick sound.
Locking test for foot-gun 1 lives in
`packages/core/src/services/single-undo-dance.test.ts`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fourth and final Stage D fence affordance — the placement tool itself.
Unlike the drag affordances (curve / move / endpoint), placement is a
two-click flow with state across grid events, not a single drag-down →
drag-up lifecycle. `DragAction` doesn't fit; the component owns its
own emitter subscriptions directly. The kind exposes it via
`def.tool: () => import('./tool')` and ToolManager's existing
`getRegistryTool()` lookup mounts it (legacy `tools.structure.fence =
FenceTool` falls through when the registry entry is missing).
Adds transitional exports from `@pascal-app/editor` for the helpers
the kind-owned tool needs at module scope: `createFenceOnCurrentLevel`,
`markToolCancelConsumed`, `EDITOR_LAYER`. Stage F cleanup moves these
into `@pascal-app/nodes` once every consumer is registry-driven.
Fence Stage D is now complete. Per-kind progress: A ✅ B ✅ C ✅ D ✅
(four affordances ported — curve, move-endpoint, move, placement).
E (drop legacy panel) and F (cleanup) pending across all kinds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third Stage D affordance port (302 LoC legacy → action + thin wrapper).
The action (`actions/move.ts`) uses the live-drag exception documented
in editor/wiki/architecture/tools.md: visual-only updates via
`sceneRegistry.nodes.get(id).position` + `useLiveTransforms` during
the drag, no scene mutations. Avoids re-rebuilding the fence geometry
(many posts + infill panels) every pointer tick. Commit performs the
single-undo dance — writes final start/end to scene, geometry rebuilds
once, Ctrl-Z reverses the whole drag.
Linked-fence cascade follows the same shape as MoveFenceEndpoint —
any fence in the same parent that shared an endpoint at activation
moves with the drag.
`getRegistryAffordanceTool` extracted to
`tools/shared/affordance-dispatch.ts` so move-tool.tsx and
tool-manager.tsx share the lazy-load helper (no duplicate caches).
MoveTool dispatch gains a generic `affordanceTools.move` check after
the capability-driven `movable` shortcut — fence routes through here;
wall / slab / etc. fall through to their legacy per-kind chain until
their D ports land.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit() just returned true, relying on
createDragSession.terminate() to resumeHistory. But that alone never
captures the drag in zundo's pastStates — the pause window's mutations
are skipped entirely. After commit, Ctrl-Z jumped past the curve *and*
past the prior fence creation.
Adds the same dance now used by move-endpoint: restoreAll →
resumeHistory → re-apply the final draft. Zundo records one undo step
for the whole drag. cancel() becomes a no-op (orchestrator's
restoreAll covers it).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second Stage D affordance port (425 LoC legacy → ~430 split across
action + wrapper). All math (snap, linked-fence cascade, alt-detach,
min-length gate, single-undo dance) lives in the pure
`moveFenceEndpointDragAction`. The React wrapper handles the UI
overlays (cursor sphere, Drag/Detach badge, angle label) and the live
state subscriptions.
The action introduces the single-undo dance pattern for multi-write
commits: `commit()` calls `scene.restoreAll()` → `resumeHistory()` →
re-applies the final draft so zundo records the entire drag as one
undo step. Reusable shape for slab/wall/door endpoint ports.
Transitional exports added to `@pascal-app/editor`'s public surface
(`snapFenceDraftPoint`, `isWallLongEnough`, the segment-angle helpers,
`MovingFenceEndpoint`). Stage F cleanup moves these into
`@pascal-app/nodes` once every consumer is registry-driven.
`getRegistryAffordanceTool` is now generic (`ComponentType<any>`) so
affordances with different prop shapes (`{ node }`, `{ target }`, …)
all dispatch through the same helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy CurveFenceTool ignored grid:click for 150ms after mount —
otherwise the very click that activates a tool (e.g. the floating menu
"curve" button) cascades through the R3F drei <Html> portal into the
grid, fires grid:click on the just-mounted tool, and commits the drag
before any preview move runs. The new useDragAction was missing this
guard, so the Stage D fence curve port "click → place sfx → exit"
without ever letting the user adjust.
Adds `activationGraceMs` (default 150) on useDragAction; ports the
sfx:item-place commit emission into FenceCurveTool so the kind-owned
tool matches legacy UX. Same guard will cover the upcoming Stage D
ports (endpoint move, whole-fence move, placement, plus
slab/ceiling/wall D).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First Stage D port — `CurveFenceTool` (178 LoC legacy) split into a pure
`DragAction` primitive (`packages/nodes/src/fence/actions/curve.ts`) plus
a thin React wrapper (`packages/nodes/src/fence/curve-tool.tsx`) that
feeds it through `useDragAction`. The kind declares the affordance via
`def.affordanceTools.curve`; ToolManager lazy-loads it at runtime when
`useEditor.curvingFence` activates. Falls back to the legacy
`CurveFenceTool` for any kind that hasn't been ported.
The lazy-load dispatch dodges the editor→nodes circular dep (nodes
already depends on editor for `useDragAction` + `CursorSphere`).
Establishes the pattern for the remaining fence affordances
(`MoveFenceEndpoint`, `MoveFence`, placement) and for slab/ceiling/wall
D ports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Parity comparison against deployed prod is now cleaner than juggling
env-var flag toggles locally. Both kinds enter builtinPlugin.nodes
unconditionally; the Phase 0 dispatch shims (<LegacySystem kind="X">
wrappers + NodeRenderer's registry-first branch) handle the cutover.
Files deleted:
- packages/nodes/src/wall/feature-flag.ts
- packages/nodes/src/fence/feature-flag.ts
Files changed:
- packages/nodes/src/index.ts: drops isWallRegistryEnabled /
isFenceRegistryEnabled gates; wallDefinition + fenceDefinition
land directly in builtinPlugin.nodes.
- packages/nodes/src/{wall,fence}/index.ts: drop the flag re-export.
- packages/nodes/src/{wall,fence}/renderer.tsx: drop the one-shot
verification console.info. Same for the system.tsx wrappers.
- packages/viewer/src/components/renderers/{wall,fence}/{wall,fence}-
renderer.tsx: drop the paired [X:legacy] verification logs (no
longer comparing flag-toggled paths).
Net DX: no env var to remember when starting `bun dev:community`. To
A/B test, compare against editor.pascal.app deployed prod.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same shape as wall milestone B — thin renderer + system re-export, no
geometry / floor-plan / tool ports yet (later milestones). Feature flag
NEXT_PUBLIC_USE_REGISTRY_FOR_FENCE gates the dispatch flip.
Files added (packages/nodes/src/fence/):
- schema.ts: re-exports FenceNode from core.
- parametrics.ts: dimensions / posts / style fields for the auto-
inspector. Endpoints + curveOffset edited via tools, not in
parametrics.
- feature-flag.ts: mirrors the wall flag pattern.
- definition.ts: capabilities (snappable + surfaces sides +
selectable + duplicable + deletable), relations (linkedBy
endpoint-match, no hosts, no affectsSpatial — matches legacy),
parametrics, renderer, system, toolHints (Left click / Shift /
Esc — fence has no helper file today so this adds a panel where
there wasn't one). Tool field absent: fence has 4 tools (build,
curve, move, move-endpoint) wired through editor state, not the
registry dispatch — they keep running unchanged.
- renderer.tsx: thin placeholder mesh + markDirty on mount + node
events + DEFAULT_STAIR_MATERIAL (matches legacy material reuse).
Verification log fires once on first mount.
- system.tsx: re-exports the legacy FenceSystem from viewer.
Verification log on mount/unmount confirms the bundle activates.
- index.ts: barrel.
Files changed:
- packages/viewer/src/index.ts: new exports for FenceSystem and
DEFAULT_STAIR_MATERIAL so the @pascal-app/nodes bundle can
compose them without reaching into viewer internals.
- packages/viewer/src/components/renderers/fence/fence-renderer.tsx:
paired one-shot legacy verification log so the dispatch path is
unambiguous from the browser console.
- packages/nodes/src/index.ts: conditional fenceEntries appended to
builtinPlugin.nodes based on isFenceRegistryEnabled. With the flag
off (default), behavior is unchanged; with it on, Phase 0 shims
switch fence to the registry path — legacy <FenceRenderer> and
<LegacySystem kind="fence"><FenceSystem /></LegacySystem> short-
circuit, the bundled system.tsx re-mounts FenceSystem via
RegisteredSystems, and the new renderer takes over the dispatch.
No behavior change with the flag off. With it on, behavior should be
byte-identical (same FenceSystem code, same priority, same geometry
path).
Phase 5 batch order continues with slab / ceiling / door / window /
item / etc. as flagged migrations after fence parity signs off.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>