From 7dd3ffed3fae60639a4eba25c65819089bb1b69a Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Thu, 14 May 2026 11:36:02 -0400 Subject: [PATCH] Wrap legacy systems in LegacySystem + mount RegisteredSystems (shim 3/4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two additions plus a viewer JSX rewire: - legacy-system.tsx: 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: 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) --- .../viewer/src/components/viewer/index.tsx | 90 ++++++++++++++----- .../src/components/viewer/legacy-system.tsx | 20 +++++ .../components/viewer/registered-systems.tsx | 53 +++++++++++ 3 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 packages/viewer/src/components/viewer/legacy-system.tsx create mode 100644 packages/viewer/src/components/viewer/registered-systems.tsx diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx index e1aee9b1..ce56c85e 100644 --- a/packages/viewer/src/components/viewer/index.tsx +++ b/packages/viewer/src/components/viewer/index.tsx @@ -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 = ({ )} {/* Default Systems */} - - - - + + + + + + + + + + + + {/* Core systems */} - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 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). */} + {/* */} - + + + {selectionManager === 'default' && } {(perf || PERF_OVERLAY_ENABLED) && } {children} diff --git a/packages/viewer/src/components/viewer/legacy-system.tsx b/packages/viewer/src/components/viewer/legacy-system.tsx new file mode 100644 index 00000000..a9078663 --- /dev/null +++ b/packages/viewer/src/components/viewer/legacy-system.tsx @@ -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 + * `` and ``) — 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} +} diff --git a/packages/viewer/src/components/viewer/registered-systems.tsx b/packages/viewer/src/components/viewer/registered-systems.tsx new file mode 100644 index 00000000..8d16666d --- /dev/null +++ b/packages/viewer/src/components/viewer/registered-systems.tsx @@ -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, 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 ``. 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 ( + + {entries.map(([kind, def]) => { + const Comp = loadSystem(def) + if (!Comp) return null + return + })} + + ) +}