Wrap legacy systems in LegacySystem + mount RegisteredSystems (shim 3/4)

Two additions plus a viewer JSX rewire:

- legacy-system.tsx: <LegacySystem kind="..."> wrapper that renders its
  children only when nodeRegistry.has(kind) is false. Lets one wrapper
  cover all legacy systems for a kind (door has DoorSystem and
  DoorAnimationSystem — both belong to 'door' so they yield together).
- registered-systems.tsx: <RegisteredSystems /> iterates the registry,
  filters entries that contribute a system, sorts by system.priority
  (default 5; e.g. wall mitering at 8 runs after door cuts at 3),
  mounts each via React.lazy. Today empty registry = renders nothing.
- viewer/index.tsx: every existing per-kind system is wrapped in
  LegacySystem. RegisteredSystems is mounted alongside.

With the registry empty (Phase 0), every LegacySystem passes through
unchanged and RegisteredSystems is a no-op — zero behavior change.
Once a kind registers in Phase 2+, its legacy systems yield and its
registry-contributed system runs in their place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-14 11:36:02 -04:00
co-authored by Claude Opus 4.7
parent 0fd7af216c
commit 7dd3ffed3f
3 changed files with 142 additions and 21 deletions
+69 -21
View File
@@ -12,8 +12,8 @@ import { DoorSystem } from '../../systems/door/door-system'
import { ElevatorInteractionSystem } from '../../systems/elevator/elevator-interaction-system'
import { FenceSystem } from '../../systems/fence/fence-system'
import { GuideSystem } from '../../systems/guide/guide-system'
import { ItemLightSystem } from '../../systems/item-light/item-light-system'
import { ItemSystem } from '../../systems/item/item-system'
import { ItemLightSystem } from '../../systems/item-light/item-light-system'
import { LevelSystem } from '../../systems/level/level-system'
import { RoofSystem } from '../../systems/roof/roof-system'
import { ScanSystem } from '../../systems/scan/scan-system'
@@ -27,9 +27,11 @@ import { ZoneSystem } from '../../systems/zone/zone-system'
import { ErrorBoundary } from '../error-boundary'
import { SceneRenderer } from '../renderers/scene-renderer'
import FrameLimiter from './frame-limiter'
import { LegacySystem } from './legacy-system'
import { Lights } from './lights'
import { PerfMonitor } from './perf-monitor'
import PostProcessing, { DEFAULT_HOVER_STYLES, type HoverStyles } from './post-processing'
import { RegisteredSystems } from './registered-systems'
import { SceneBvh } from './scene-bvh'
import { SelectionManager } from './selection-manager'
import { ViewerCamera } from './viewer-camera'
@@ -223,30 +225,76 @@ const Viewer: React.FC<ViewerProps> = ({
)}
{/* Default Systems */}
<LevelSystem />
<GuideSystem />
<ScanSystem />
<WallCutout />
<LegacySystem kind="level">
<LevelSystem />
</LegacySystem>
<LegacySystem kind="guide">
<GuideSystem />
</LegacySystem>
<LegacySystem kind="scan">
<ScanSystem />
</LegacySystem>
<LegacySystem kind="wall">
<WallCutout />
</LegacySystem>
{/* Core systems */}
<CeilingSystem />
<DoorAnimationSystem />
<ElevatorRuntimeSystem />
<ElevatorInteractionSystem />
<ElevatorOpeningSystem />
<WindowAnimationSystem />
<DoorSystem />
<FenceSystem />
<ItemSystem />
<RoofSystem />
<SlabSystem />
<StairSystem />
<WallSystem />
<WindowSystem />
<ZoneSystem />
<LegacySystem kind="ceiling">
<CeilingSystem />
</LegacySystem>
<LegacySystem kind="door">
<DoorAnimationSystem />
</LegacySystem>
<LegacySystem kind="elevator">
<ElevatorRuntimeSystem />
</LegacySystem>
<LegacySystem kind="elevator">
<ElevatorInteractionSystem />
</LegacySystem>
<LegacySystem kind="elevator">
<ElevatorOpeningSystem />
</LegacySystem>
<LegacySystem kind="window">
<WindowAnimationSystem />
</LegacySystem>
<LegacySystem kind="door">
<DoorSystem />
</LegacySystem>
<LegacySystem kind="fence">
<FenceSystem />
</LegacySystem>
<LegacySystem kind="item">
<ItemSystem />
</LegacySystem>
<LegacySystem kind="roof">
<RoofSystem />
</LegacySystem>
<LegacySystem kind="slab">
<SlabSystem />
</LegacySystem>
<LegacySystem kind="stair">
<StairSystem />
</LegacySystem>
<LegacySystem kind="wall">
<WallSystem />
</LegacySystem>
<LegacySystem kind="window">
<WindowSystem />
</LegacySystem>
<LegacySystem kind="zone">
<ZoneSystem />
</LegacySystem>
{/* Mounts systems contributed by registry-backed kinds. Today the
registry is empty so this renders nothing. Once kinds register
(Phase 2+), each kind's registered system runs here and its
legacy counterpart above short-circuits via the LegacySystem
wrapper (which checks nodeRegistry.has). */}
<RegisteredSystems />
<PostProcessing hoverStyles={hoverStyles} />
{/* <DebugRenderer /> */}
<ItemLightSystem />
<LegacySystem kind="item">
<ItemLightSystem />
</LegacySystem>
{selectionManager === 'default' && <SelectionManager />}
{(perf || PERF_OVERLAY_ENABLED) && <PerfMonitor />}
{children}
@@ -0,0 +1,20 @@
'use client'
import { nodeRegistry } from '@pascal-app/core'
import type { ReactNode } from 'react'
/**
* Wraps a legacy per-kind system component so it short-circuits the moment a
* NodeDefinition for the same kind appears in the registry. Lets us migrate
* one kind at a time without editing each legacy system file individually.
*
* Multiple legacy systems can belong to the same kind (e.g. door has both
* `<DoorSystem>` and `<DoorAnimationSystem>`) — wrap them together so they
* yield as a unit when the kind registers.
*
* Removed in Phase 6 alongside the legacy systems themselves.
*/
export function LegacySystem({ kind, children }: { kind: string; children: ReactNode }) {
if (nodeRegistry.has(kind)) return null
return <>{children}</>
}
@@ -0,0 +1,53 @@
'use client'
import { type AnyNodeDefinition, nodeRegistry } from '@pascal-app/core'
import { type ComponentType, lazy, Suspense, useMemo } from 'react'
const DEFAULT_PRIORITY = 5
// Cache lazy components keyed by the module-loader function so React.lazy
// isn't re-invoked across renders.
const lazyCache = new WeakMap<() => Promise<unknown>, ComponentType>()
function loadSystem(def: AnyNodeDefinition): ComponentType | null {
if (!def.system) return null
const cached = lazyCache.get(def.system.module)
if (cached) return cached
const Comp = lazy(def.system.module as () => Promise<{ default: ComponentType }>)
lazyCache.set(def.system.module, Comp)
return Comp
}
/**
* Mounts every registered node kind's system component, ordered by
* `system.priority` (default {@link DEFAULT_PRIORITY}).
*
* Today the registry is empty so this component mounts nothing — coexists
* with legacy `*-System` components in `<Viewer>`. Once kinds register via
* `@pascal-app/nodes`, each kind's registry-driven system takes over and
* its legacy counterpart short-circuits via the `nodeRegistry.has(kind)`
* guard added to each legacy system.
*/
export function RegisteredSystems() {
const entries = useMemo(() => {
return Array.from(nodeRegistry.entries())
.filter(([, def]) => def.system != null)
.sort(([, a], [, b]) => {
const pa = a.system?.priority ?? DEFAULT_PRIORITY
const pb = b.system?.priority ?? DEFAULT_PRIORITY
return pa - pb
})
}, [])
if (entries.length === 0) return null
return (
<Suspense fallback={null}>
{entries.map(([kind, def]) => {
const Comp = loadSystem(def)
if (!Comp) return null
return <Comp key={`registered-system:${kind}`} />
})}
</Suspense>
)
}