Drop wall + fence feature flags: register unconditionally; remove verification logs

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>
This commit is contained in:
Wassim SAMAD
2026-05-15 11:59:09 -04:00
co-authored by Claude Opus 4.7
parent 9883f1cdc1
commit 6a4de8cff5
11 changed files with 20 additions and 147 deletions
-13
View File
@@ -1,13 +0,0 @@
/**
* Feature flag for the registry-driven fence.
*
* Same pattern as wall (Phase 3) and spawn (Phase 2): with the flag on,
* `fenceDefinition` is appended to `builtinPlugin.nodes` and the Phase 0
* dispatch shims hand fence over to the registry. With the flag off,
* the legacy fence paths run unchanged.
*
* Drops the moment Phase 5 fence parity is signed off.
*/
export const isFenceRegistryEnabled = (): boolean => {
return process.env.NEXT_PUBLIC_USE_REGISTRY_FOR_FENCE === 'true'
}
-1
View File
@@ -1,3 +1,2 @@
export { fenceDefinition } from './definition' export { fenceDefinition } from './definition'
export { isFenceRegistryEnabled } from './feature-flag'
export { FenceNode } from './schema' export { FenceNode } from './schema'
+1 -11
View File
@@ -2,7 +2,7 @@
import { type FenceNode, useRegistry, useScene } from '@pascal-app/core' import { type FenceNode, useRegistry, useScene } from '@pascal-app/core'
import { DEFAULT_STAIR_MATERIAL, useNodeEvents } from '@pascal-app/viewer' import { DEFAULT_STAIR_MATERIAL, useNodeEvents } from '@pascal-app/viewer'
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react' import { useLayoutEffect, useMemo, useRef } from 'react'
import type { Mesh } from 'three' import type { Mesh } from 'three'
/** /**
@@ -17,8 +17,6 @@ import type { Mesh } from 'three'
* Material is `DEFAULT_STAIR_MATERIAL` (legacy reuse; fence and stairs * Material is `DEFAULT_STAIR_MATERIAL` (legacy reuse; fence and stairs
* share the wood-tone preset). * share the wood-tone preset).
*/ */
let didLogFirstRegistryFenceMount = false
const FenceRenderer = ({ node }: { node: FenceNode }) => { const FenceRenderer = ({ node }: { node: FenceNode }) => {
const ref = useRef<Mesh>(null!) const ref = useRef<Mesh>(null!)
const handlers = useNodeEvents(node, 'fence') const handlers = useNodeEvents(node, 'fence')
@@ -29,14 +27,6 @@ const FenceRenderer = ({ node }: { node: FenceNode }) => {
useScene.getState().markDirty(node.id) useScene.getState().markDirty(node.id)
}, [node.id]) }, [node.id])
useEffect(() => {
if (didLogFirstRegistryFenceMount) return
didLogFirstRegistryFenceMount = true
console.info(
'[fence:registry] first registry-driven FenceRenderer mounted — legacy FenceRenderer is NOT in use',
)
}, [])
return ( return (
<mesh <mesh
castShadow castShadow
+1 -10
View File
@@ -1,7 +1,6 @@
'use client' 'use client'
import { FenceSystem } from '@pascal-app/viewer' import { FenceSystem } from '@pascal-app/viewer'
import { useEffect } from 'react'
/** /**
* Registry-driven fence system bundle. * Registry-driven fence system bundle.
@@ -10,8 +9,7 @@ import { useEffect } from 'react'
* via `RegisteredSystems` when fence is registry-driven. The legacy * via `RegisteredSystems` when fence is registry-driven. The legacy
* `<LegacySystem kind="fence">` wrapper around `<FenceSystem />` in * `<LegacySystem kind="fence">` wrapper around `<FenceSystem />` in
* `viewer/components/viewer/index.tsx` short-circuits whenever * `viewer/components/viewer/index.tsx` short-circuits whenever
* `nodeRegistry.has('fence')` is true — same pattern wall used in * `nodeRegistry.has('fence')` is true — same pattern wall uses.
* milestone B.
* *
* Phase 6 deletes the legacy mount point; until then this bundle is the * 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. * single mount surface for fence's per-frame work when registry-driven.
@@ -22,13 +20,6 @@ import { useEffect } from 'react'
* the rebuild loop and this bundle can be deleted. * the rebuild loop and this bundle can be deleted.
*/ */
const FenceSystems = () => { const FenceSystems = () => {
useEffect(() => {
console.info('[fence:registry] system bundle mounted — registry path active')
return () => {
console.info('[fence:registry] system bundle unmounted')
}
}, [])
return <FenceSystem /> return <FenceSystem />
} }
+10 -24
View File
@@ -1,8 +1,8 @@
import type { AnyNodeDefinition, Plugin } from '@pascal-app/core' import type { AnyNodeDefinition, Plugin } from '@pascal-app/core'
import { fenceDefinition, isFenceRegistryEnabled } from './fence' import { fenceDefinition } from './fence'
import { shelfDefinition } from './shelf' import { shelfDefinition } from './shelf'
import { spawnDefinition } from './spawn' import { spawnDefinition } from './spawn'
import { isWallRegistryEnabled, wallDefinition } from './wall' import { wallDefinition } from './wall'
/** /**
* Built-in plugin bundling every node kind shipped with the Pascal editor. * Built-in plugin bundling every node kind shipped with the Pascal editor.
@@ -15,35 +15,21 @@ import { isWallRegistryEnabled, wallDefinition } from './wall'
* `loadPlugin` call path. This is intentional: the API is stress-tested * `loadPlugin` call path. This is intentional: the API is stress-tested
* by built-ins before any third-party plugin lands. * by built-ins before any third-party plugin lands.
* *
* Status by kind: * All four current kinds are registered unconditionally. Parity is
* - **shelf**: brand-new kind, registry-driven, no legacy. Registered * verified by comparing against deployed production rather than an
* unconditionally. * in-app env-var flag toggle. Legacy paths still exist in `viewer/` and
* - **spawn**: migrated to the registry path during Phase 2. Legacy * `editor/` for kinds undergoing migration; they short-circuit via the
* SpawnRenderer / SpawnTool files still present in viewer/editor but * Phase 0 `<LegacySystem>` wrapper + `NodeRenderer`'s registry-first
* short-circuited by the Phase 0 shims. Registered unconditionally. * dispatch. Phase 6 deletes the legacy paths.
* - **wall**: registry-driven behind `NEXT_PUBLIC_USE_REGISTRY_FOR_WALL`.
* Phase 3 stress test; flag drops when fixture parity signs off.
* - **fence**: registry-driven behind `NEXT_PUBLIC_USE_REGISTRY_FOR_FENCE`.
* First Phase 5 batch-migration kind. Same shape as wall (thin
* renderer + system re-export); pure geometry / floor-plan / tool
* affordance ports as later milestones.
*/ */
const wallEntries: AnyNodeDefinition[] = isWallRegistryEnabled()
? [wallDefinition as unknown as AnyNodeDefinition]
: []
const fenceEntries: AnyNodeDefinition[] = isFenceRegistryEnabled()
? [fenceDefinition as unknown as AnyNodeDefinition]
: []
export const builtinPlugin: Plugin = { export const builtinPlugin: Plugin = {
id: 'pascal:core', id: 'pascal:core',
apiVersion: 1, apiVersion: 1,
nodes: [ nodes: [
shelfDefinition as unknown as AnyNodeDefinition, shelfDefinition as unknown as AnyNodeDefinition,
spawnDefinition as unknown as AnyNodeDefinition, spawnDefinition as unknown as AnyNodeDefinition,
...wallEntries, wallDefinition as unknown as AnyNodeDefinition,
...fenceEntries, fenceDefinition as unknown as AnyNodeDefinition,
], ],
} }
-23
View File
@@ -1,23 +0,0 @@
/**
* Feature flag for the registry-driven wall.
*
* Wall is the Phase 3 stress test — its scope is large enough (5 affordances,
* miter cascade, host re-anchor, slab/zone dirty propagation, undo
* correctness) that flipping it on uncontrolled would risk silently
* regressing every wall scene in production.
*
* Pattern mirrors `NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN` used during the
* Phase 2 spawn migration: when ON, `wallDefinition` is appended to
* `builtinPlugin.nodes` and the Phase 0 dispatch shims take over wall
* rendering / tooling. When OFF, the legacy wall paths run unchanged.
*
* Literal `process.env.NEXT_PUBLIC_USE_REGISTRY_FOR_WALL` access is required
* — Next.js only inline-substitutes literal env reads, so `process.env[name]`
* with a variable would always read undefined in client bundles.
*
* Drop this file the moment Phase 3 parity is signed off and wall is
* registered unconditionally.
*/
export const isWallRegistryEnabled = (): boolean => {
return process.env.NEXT_PUBLIC_USE_REGISTRY_FOR_WALL === 'true'
}
-1
View File
@@ -1,3 +1,2 @@
export { wallDefinition } from './definition' export { wallDefinition } from './definition'
export { isWallRegistryEnabled } from './feature-flag'
export { WallNode } from './schema' export { WallNode } from './schema'
+1 -16
View File
@@ -14,8 +14,7 @@ import { BufferGeometry, Float32BufferAttribute, type Mesh } from 'three'
* items) inside the wall's local frame. * items) inside the wall's local frame.
* *
* Behaviorally identical to the legacy `WallRenderer` in * Behaviorally identical to the legacy `WallRenderer` in
* `@pascal-app/viewer/components/renderers/wall/wall-renderer.tsx` — same * `@pascal-app/viewer/components/renderers/wall/wall-renderer.tsx`.
* placeholder geometry, same material lookup, same dirty-mark on mount.
* Phase 6 deletes the legacy file; until then both coexist and the Phase 0 * Phase 6 deletes the legacy file; until then both coexist and the Phase 0
* shims pick which one renders based on `nodeRegistry.has('wall')`. * shims pick which one renders based on `nodeRegistry.has('wall')`.
* *
@@ -25,12 +24,6 @@ import { BufferGeometry, Float32BufferAttribute, type Mesh } from 'three'
* That decision lands in a later milestone; for now the system retains * That decision lands in a later milestone; for now the system retains
* ownership of the rebuild loop. * ownership of the rebuild loop.
*/ */
// Phase 3 verification — set once when the first registry-driven wall
// renderer mounts. Stops the log firing per wall (a scene with 200 walls
// would spam the console). Drop alongside the feature flag at Phase 3
// sign-off.
let didLogFirstRegistryWallMount = false
function createEmptyWallGeometry(): BufferGeometry { function createEmptyWallGeometry(): BufferGeometry {
const geometry = new BufferGeometry() const geometry = new BufferGeometry()
geometry.setAttribute('position', new Float32BufferAttribute([], 3)) geometry.setAttribute('position', new Float32BufferAttribute([], 3))
@@ -55,14 +48,6 @@ const WallRenderer = ({ node }: { node: WallNode }) => {
useScene.getState().markDirty(node.id) useScene.getState().markDirty(node.id)
}, [node.id]) }, [node.id])
useEffect(() => {
if (didLogFirstRegistryWallMount) return
didLogFirstRegistryWallMount = true
console.info(
'[wall:registry] first registry-driven WallRenderer mounted — legacy WallRenderer is NOT in use',
)
}, [])
useEffect(() => { useEffect(() => {
return () => { return () => {
placeholderGeometry.dispose() placeholderGeometry.dispose()
+6 -19
View File
@@ -1,12 +1,11 @@
'use client' 'use client'
import { WallCutout, WallSystem } from '@pascal-app/viewer' import { WallCutout, WallSystem } from '@pascal-app/viewer'
import { useEffect } from 'react'
/** /**
* Registry-driven wall system bundle. * Registry-driven wall system bundle.
* *
* Wall has two per-frame concerns that need to mount when the kind is * Wall has two per-frame concerns that mount when the kind is
* registry-driven: * registry-driven:
* *
* - **`WallSystem`** — reads `dirtyNodes`, batches by level, runs * - **`WallSystem`** — reads `dirtyNodes`, batches by level, runs
@@ -17,28 +16,16 @@ import { useEffect } from 'react'
* - **`WallCutout`** — cutaway-mode hide/show logic based on camera * - **`WallCutout`** — cutaway-mode hide/show logic based on camera
* direction and `frontSide` / `backSide` interior/exterior tags. * direction and `frontSide` / `backSide` interior/exterior tags.
* *
* Both already live in `@pascal-app/viewer` and are wrapped in * Both live in `@pascal-app/viewer` and are wrapped in
* `<LegacySystem kind="wall">` at the legacy mount point. When the * `<LegacySystem kind="wall">` at the legacy mount point. With wall
* `wall` kind appears in `nodeRegistry`, those wrappers short-circuit and * registered, those wrappers short-circuit and this bundle takes over
* this bundle takes over the mount via `RegisteredSystems`. The * the mount via `RegisteredSystems`. The components themselves are
* components themselves are unchanged — no logic duplication during * unchanged — no logic duplication.
* Phase 3.
* *
* Phase 6 deletes the legacy `<LegacySystem kind="wall">` wrappers; until * Phase 6 deletes the legacy `<LegacySystem kind="wall">` wrappers; until
* then this file is the single mount surface for wall's per-frame work. * then this file is the single mount surface for wall's per-frame work.
*/ */
const WallSystems = () => { const WallSystems = () => {
// Phase 3 verification log — confirms the registry took over the wall
// dispatch. Fires once when RegisteredSystems lazy-loads this bundle (so
// exactly once per viewer mount when NEXT_PUBLIC_USE_REGISTRY_FOR_WALL=true).
// Drop alongside the feature flag at Phase 3 sign-off.
useEffect(() => {
console.info('[wall:registry] system bundle mounted — registry path active')
return () => {
console.info('[wall:registry] system bundle unmounted')
}
}, [])
return ( return (
<> <>
<WallSystem /> <WallSystem />
@@ -1,15 +1,9 @@
import { type FenceNode, useRegistry, useScene } from '@pascal-app/core' import { type FenceNode, useRegistry, useScene } from '@pascal-app/core'
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react' import { useLayoutEffect, useMemo, useRef } from 'react'
import type { Mesh } from 'three' import type { Mesh } from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events' import { useNodeEvents } from '../../../hooks/use-node-events'
import { DEFAULT_STAIR_MATERIAL } from '../../../lib/materials' 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 }) => { export const FenceRenderer = ({ node }: { node: FenceNode }) => {
const ref = useRef<Mesh>(null!) const ref = useRef<Mesh>(null!)
const handlers = useNodeEvents(node, 'fence') const handlers = useNodeEvents(node, 'fence')
@@ -20,14 +14,6 @@ export const FenceRenderer = ({ node }: { node: FenceNode }) => {
useScene.getState().markDirty(node.id) useScene.getState().markDirty(node.id)
}, [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 ( return (
<mesh <mesh
castShadow castShadow
@@ -5,12 +5,6 @@ import { useNodeEvents } from '../../../hooks/use-node-events'
import { getVisibleWallMaterials } from '../../../systems/wall/wall-materials' import { getVisibleWallMaterials } from '../../../systems/wall/wall-materials'
import { NodeRenderer } from '../node-renderer' import { NodeRenderer } from '../node-renderer'
// Phase 3 verification — see the matching `[wall:registry]` log in
// nodes/src/wall/renderer.tsx. When this fires, wall is on the legacy
// path; the registry-driven WallRenderer is not in use. Drop alongside
// the legacy file at Phase 6.
let didLogFirstLegacyWallMount = false
function createEmptyWallGeometry() { function createEmptyWallGeometry() {
const geometry = new BufferGeometry() const geometry = new BufferGeometry()
geometry.setAttribute('position', new Float32BufferAttribute([], 3)) geometry.setAttribute('position', new Float32BufferAttribute([], 3))
@@ -35,14 +29,6 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
useScene.getState().markDirty(node.id) useScene.getState().markDirty(node.id)
}, [node.id]) }, [node.id])
useEffect(() => {
if (didLogFirstLegacyWallMount) return
didLogFirstLegacyWallMount = true
console.info(
'[wall:legacy] first legacy WallRenderer mounted — registry-driven WallRenderer is NOT in use',
)
}, [])
useEffect(() => { useEffect(() => {
return () => { return () => {
placeholderGeometry.dispose() placeholderGeometry.dispose()