From 0fd7af216c9fef5e40322f177cff04e18640816f Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Thu, 14 May 2026 11:32:05 -0400 Subject: [PATCH] Registry-first dispatch in NodeRenderer (shim 2/4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NodeRenderer now checks the registry first. Registered kinds load their renderer module via React.lazy (cached by RendererSource so the Suspense boundary is stable across re-renders). Unregistered kinds fall through to the legacy chain below. Today the registry is empty (Phase 0 builtinPlugin.nodes is []), so every node still hits the legacy chain — no behavior change. The moment a kind registers in @pascal-app/nodes (Phase 2 onward), the registry path takes over without further edits here. GLB / instanced-GLB RendererSource kinds are typed but not yet honored — they get their built-in renderers in Phase 5. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/renderers/node-renderer.tsx | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/viewer/src/components/renderers/node-renderer.tsx b/packages/viewer/src/components/renderers/node-renderer.tsx index f2045fb4..7140c9bc 100644 --- a/packages/viewer/src/components/renderers/node-renderer.tsx +++ b/packages/viewer/src/components/renderers/node-renderer.tsx @@ -1,6 +1,7 @@ 'use client' -import { useScene, type AnyNode } from '@pascal-app/core' +import { type AnyNode, nodeRegistry, type RendererSource, useScene } from '@pascal-app/core' +import { type ComponentType, lazy, Suspense } from 'react' import { BuildingRenderer } from './building/building-renderer' import { CeilingRenderer } from './ceiling/ceiling-renderer' import { ColumnRenderer } from './column/column-renderer' @@ -10,23 +11,60 @@ import { FenceRenderer } from './fence/fence-renderer' import { GuideRenderer } from './guide/guide-renderer' import { ItemRenderer } from './item/item-renderer' import { LevelRenderer } from './level/level-renderer' -import { RoofSegmentRenderer } from './roof-segment/roof-segment-renderer' import { RoofRenderer } from './roof/roof-renderer' +import { RoofSegmentRenderer } from './roof-segment/roof-segment-renderer' import { ScanRenderer } from './scan/scan-renderer' import { SiteRenderer } from './site/site-renderer' import { SlabRenderer } from './slab/slab-renderer' import { SpawnRenderer } from './spawn/spawn-renderer' -import { StairSegmentRenderer } from './stair-segment/stair-segment-renderer' import { StairRenderer } from './stair/stair-renderer' +import { StairSegmentRenderer } from './stair-segment/stair-segment-renderer' import { WallRenderer } from './wall/wall-renderer' import { WindowRenderer } from './window/window-renderer' import { ZoneRenderer } from './zone/zone-renderer' +// Cache lazy components by their RendererSource so React.lazy isn't re-invoked +// on every render — that would create a new Suspense boundary each time. +const lazyCache = new WeakMap, ComponentType<{ node: AnyNode }>>() + +function getRegistryRenderer( + source: RendererSource, +): ComponentType<{ node: AnyNode }> | null { + const cached = lazyCache.get(source) + if (cached) return cached + // GLB / instanced-GLB sources lower onto built-in renderers landed in + // Phase 5 — for now only parametric (lazy module) sources are honored. + if (source.kind !== 'parametric') return null + const Comp = lazy(source.module) as unknown as ComponentType<{ node: AnyNode }> + lazyCache.set(source, Comp) + return Comp +} + +function RegistryRenderer({ node }: { node: AnyNode }) { + const def = nodeRegistry.get(node.type) + if (!def) return null + const Renderer = getRegistryRenderer(def.renderer as RendererSource) + if (!Renderer) return null + return ( + + + + ) +} + export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => { const node = useScene((state) => state.nodes[nodeId]) if (!node) return null + // Registry-first: if a NodeDefinition is registered for this kind (via + // @pascal-app/nodes or a future plugin), it owns the render. Falls through + // to the legacy chain below for kinds not yet migrated. Legacy chain is + // removed in Phase 6 once every kind is registry-backed. + if (nodeRegistry.has(node.type)) { + return + } + return ( <> {node.type === 'site' && }