Registry-first dispatch in NodeRenderer (shim 2/4)
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
4c3feea052
commit
0fd7af216c
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'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 { BuildingRenderer } from './building/building-renderer'
|
||||||
import { CeilingRenderer } from './ceiling/ceiling-renderer'
|
import { CeilingRenderer } from './ceiling/ceiling-renderer'
|
||||||
import { ColumnRenderer } from './column/column-renderer'
|
import { ColumnRenderer } from './column/column-renderer'
|
||||||
@@ -10,23 +11,60 @@ import { FenceRenderer } from './fence/fence-renderer'
|
|||||||
import { GuideRenderer } from './guide/guide-renderer'
|
import { GuideRenderer } from './guide/guide-renderer'
|
||||||
import { ItemRenderer } from './item/item-renderer'
|
import { ItemRenderer } from './item/item-renderer'
|
||||||
import { LevelRenderer } from './level/level-renderer'
|
import { LevelRenderer } from './level/level-renderer'
|
||||||
import { RoofSegmentRenderer } from './roof-segment/roof-segment-renderer'
|
|
||||||
import { RoofRenderer } from './roof/roof-renderer'
|
import { RoofRenderer } from './roof/roof-renderer'
|
||||||
|
import { RoofSegmentRenderer } from './roof-segment/roof-segment-renderer'
|
||||||
import { ScanRenderer } from './scan/scan-renderer'
|
import { ScanRenderer } from './scan/scan-renderer'
|
||||||
import { SiteRenderer } from './site/site-renderer'
|
import { SiteRenderer } from './site/site-renderer'
|
||||||
import { SlabRenderer } from './slab/slab-renderer'
|
import { SlabRenderer } from './slab/slab-renderer'
|
||||||
import { SpawnRenderer } from './spawn/spawn-renderer'
|
import { SpawnRenderer } from './spawn/spawn-renderer'
|
||||||
import { StairSegmentRenderer } from './stair-segment/stair-segment-renderer'
|
|
||||||
import { StairRenderer } from './stair/stair-renderer'
|
import { StairRenderer } from './stair/stair-renderer'
|
||||||
|
import { StairSegmentRenderer } from './stair-segment/stair-segment-renderer'
|
||||||
import { WallRenderer } from './wall/wall-renderer'
|
import { WallRenderer } from './wall/wall-renderer'
|
||||||
import { WindowRenderer } from './window/window-renderer'
|
import { WindowRenderer } from './window/window-renderer'
|
||||||
import { ZoneRenderer } from './zone/zone-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<RendererSource<AnyNode>, ComponentType<{ node: AnyNode }>>()
|
||||||
|
|
||||||
|
function getRegistryRenderer(
|
||||||
|
source: RendererSource<AnyNode>,
|
||||||
|
): 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<AnyNode>)
|
||||||
|
if (!Renderer) return null
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<Renderer node={node} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => {
|
export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => {
|
||||||
const node = useScene((state) => state.nodes[nodeId])
|
const node = useScene((state) => state.nodes[nodeId])
|
||||||
|
|
||||||
if (!node) return null
|
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 <RegistryRenderer node={node} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{node.type === 'site' && <SiteRenderer node={node} />}
|
{node.type === 'site' && <SiteRenderer node={node} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user