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
@@ -12,8 +12,8 @@ import { DoorSystem } from '../../systems/door/door-system'
import { ElevatorInteractionSystem } from '../../systems/elevator/elevator-interaction-system' import { ElevatorInteractionSystem } from '../../systems/elevator/elevator-interaction-system'
import { FenceSystem } from '../../systems/fence/fence-system' import { FenceSystem } from '../../systems/fence/fence-system'
import { GuideSystem } from '../../systems/guide/guide-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 { ItemSystem } from '../../systems/item/item-system'
import { ItemLightSystem } from '../../systems/item-light/item-light-system'
import { LevelSystem } from '../../systems/level/level-system' import { LevelSystem } from '../../systems/level/level-system'
import { RoofSystem } from '../../systems/roof/roof-system' import { RoofSystem } from '../../systems/roof/roof-system'
import { ScanSystem } from '../../systems/scan/scan-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 { ErrorBoundary } from '../error-boundary'
import { SceneRenderer } from '../renderers/scene-renderer' import { SceneRenderer } from '../renderers/scene-renderer'
import FrameLimiter from './frame-limiter' import FrameLimiter from './frame-limiter'
import { LegacySystem } from './legacy-system'
import { Lights } from './lights' import { Lights } from './lights'
import { PerfMonitor } from './perf-monitor' import { PerfMonitor } from './perf-monitor'
import PostProcessing, { DEFAULT_HOVER_STYLES, type HoverStyles } from './post-processing' import PostProcessing, { DEFAULT_HOVER_STYLES, type HoverStyles } from './post-processing'
import { RegisteredSystems } from './registered-systems'
import { SceneBvh } from './scene-bvh' import { SceneBvh } from './scene-bvh'
import { SelectionManager } from './selection-manager' import { SelectionManager } from './selection-manager'
import { ViewerCamera } from './viewer-camera' import { ViewerCamera } from './viewer-camera'
@@ -223,30 +225,76 @@ const Viewer: React.FC<ViewerProps> = ({
)} )}
{/* Default Systems */} {/* Default Systems */}
<LegacySystem kind="level">
<LevelSystem /> <LevelSystem />
</LegacySystem>
<LegacySystem kind="guide">
<GuideSystem /> <GuideSystem />
</LegacySystem>
<LegacySystem kind="scan">
<ScanSystem /> <ScanSystem />
</LegacySystem>
<LegacySystem kind="wall">
<WallCutout /> <WallCutout />
</LegacySystem>
{/* Core systems */} {/* Core systems */}
<LegacySystem kind="ceiling">
<CeilingSystem /> <CeilingSystem />
</LegacySystem>
<LegacySystem kind="door">
<DoorAnimationSystem /> <DoorAnimationSystem />
</LegacySystem>
<LegacySystem kind="elevator">
<ElevatorRuntimeSystem /> <ElevatorRuntimeSystem />
</LegacySystem>
<LegacySystem kind="elevator">
<ElevatorInteractionSystem /> <ElevatorInteractionSystem />
</LegacySystem>
<LegacySystem kind="elevator">
<ElevatorOpeningSystem /> <ElevatorOpeningSystem />
</LegacySystem>
<LegacySystem kind="window">
<WindowAnimationSystem /> <WindowAnimationSystem />
</LegacySystem>
<LegacySystem kind="door">
<DoorSystem /> <DoorSystem />
</LegacySystem>
<LegacySystem kind="fence">
<FenceSystem /> <FenceSystem />
</LegacySystem>
<LegacySystem kind="item">
<ItemSystem /> <ItemSystem />
</LegacySystem>
<LegacySystem kind="roof">
<RoofSystem /> <RoofSystem />
</LegacySystem>
<LegacySystem kind="slab">
<SlabSystem /> <SlabSystem />
</LegacySystem>
<LegacySystem kind="stair">
<StairSystem /> <StairSystem />
</LegacySystem>
<LegacySystem kind="wall">
<WallSystem /> <WallSystem />
</LegacySystem>
<LegacySystem kind="window">
<WindowSystem /> <WindowSystem />
</LegacySystem>
<LegacySystem kind="zone">
<ZoneSystem /> <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} /> <PostProcessing hoverStyles={hoverStyles} />
{/* <DebugRenderer /> */} {/* <DebugRenderer /> */}
<LegacySystem kind="item">
<ItemLightSystem /> <ItemLightSystem />
</LegacySystem>
{selectionManager === 'default' && <SelectionManager />} {selectionManager === 'default' && <SelectionManager />}
{(perf || PERF_OVERLAY_ENABLED) && <PerfMonitor />} {(perf || PERF_OVERLAY_ENABLED) && <PerfMonitor />}
{children} {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>
)
}