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' && }