Phase 5 first batch kind: fence migrates to registry behind feature flag

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>
This commit is contained in:
Wassim SAMAD
2026-05-15 11:50:41 -04:00
co-authored by Claude Opus 4.7
parent 713ef5009e
commit 9883f1cdc1
10 changed files with 296 additions and 15 deletions
@@ -1,9 +1,15 @@
import { type FenceNode, useRegistry, useScene } from '@pascal-app/core'
import { useLayoutEffect, useMemo, useRef } from 'react'
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
import type { Mesh } from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
// Phase 5 verification log — see matching `[fence:registry]` log in
// nodes/src/fence/renderer.tsx. Fires once if the legacy path is active
// (flag off or kind not registered). Drop alongside the legacy file at
// Phase 6 cleanup.
let didLogFirstLegacyFenceMount = false
export const FenceRenderer = ({ node }: { node: FenceNode }) => {
const ref = useRef<Mesh>(null!)
const handlers = useNodeEvents(node, 'fence')
@@ -14,6 +20,14 @@ export const FenceRenderer = ({ node }: { node: FenceNode }) => {
useScene.getState().markDirty(node.id)
}, [node.id])
useEffect(() => {
if (didLogFirstLegacyFenceMount) return
didLogFirstLegacyFenceMount = true
console.info(
'[fence:legacy] first legacy FenceRenderer mounted — registry-driven FenceRenderer is NOT in use',
)
}, [])
return (
<mesh
castShadow