From 4c3feea0524243b837969ee6d6a584e918b74ea2 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Thu, 14 May 2026 11:30:55 -0400 Subject: [PATCH] Proxy-back sceneRegistry.byType for registry/plugin kinds (shim 1/4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit byType was a hardcoded object keyed by the built-in node kinds. With the registry, kinds can come from @pascal-app/nodes (or future plugins) — so byType now wraps a Map via a Proxy that auto-creates an empty Set the first time any kind is touched. Built-in kinds are still pre-seeded at module init so the fast path (no Proxy trap) is preserved. clear() iterates the backing Map. useRegistry's `type` parameter widens from `keyof typeof byType` to `KnownNodeKind | (string & {})` — preserves autocomplete for built-ins while accepting plugin-supplied kinds. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../hooks/scene-registry/scene-registry.ts | 103 ++++++++++++------ 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/packages/core/src/hooks/scene-registry/scene-registry.ts b/packages/core/src/hooks/scene-registry/scene-registry.ts index ab1a1a89..c9f4832c 100644 --- a/packages/core/src/hooks/scene-registry/scene-registry.ts +++ b/packages/core/src/hooks/scene-registry/scene-registry.ts @@ -3,49 +3,85 @@ import { useLayoutEffect } from 'react' import type * as THREE from 'three' +const KNOWN_NODE_KINDS = [ + 'site', + 'building', + 'ceiling', + 'column', + 'elevator', + 'level', + 'wall', + 'fence', + 'item', + 'slab', + 'spawn', + 'zone', + 'roof', + 'roof-segment', + 'stair', + 'stair-segment', + 'scan', + 'guide', + 'window', + 'door', +] as const + +type KnownNodeKind = (typeof KNOWN_NODE_KINDS)[number] +// Allow registry-registered (plugin) kinds while keeping autocomplete for built-ins. +type NodeKind = KnownNodeKind | (string & {}) + +type ByTypeShape = Record> & Record> + +const byTypeStore = new Map>( + KNOWN_NODE_KINDS.map((k) => [k, new Set()]), +) + +// Auto-creates a Set the first time an unknown kind is accessed. This is what +// lets registry-registered (and future plugin-contributed) kinds participate +// in `byType` without being hardcoded here. +const byTypeProxy = new Proxy({} as ByTypeShape, { + get(_target, key) { + if (typeof key !== 'string') return undefined + let set = byTypeStore.get(key) + if (!set) { + set = new Set() + byTypeStore.set(key, set) + } + return set + }, + ownKeys() { + return Array.from(byTypeStore.keys()) + }, + has(_target, key) { + return typeof key === 'string' && byTypeStore.has(key) + }, + getOwnPropertyDescriptor(_target, key) { + if (typeof key !== 'string') return undefined + const set = byTypeStore.get(key) + if (!set) return undefined + return { configurable: true, enumerable: true, value: set, writable: false } + }, +}) + export const sceneRegistry = { // Master lookup: ID -> Object3D nodes: new Map(), - // Categorized lookups: Type -> Set of IDs - // Using a Set is faster for adding/deleting than an Array - byType: { - site: new Set(), - building: new Set(), - ceiling: new Set(), - column: new Set(), - elevator: new Set(), - level: new Set(), - wall: new Set(), - fence: new Set(), - item: new Set(), - slab: new Set(), - spawn: new Set(), - zone: new Set(), - roof: new Set(), - 'roof-segment': new Set(), - stair: new Set(), - 'stair-segment': new Set(), - scan: new Set(), - guide: new Set(), - window: new Set(), - door: new Set(), - }, + // Categorized lookups: Kind -> Set of IDs. + // Backed by a Proxy so registry-registered kinds get a Set on first touch, + // while built-in kinds remain present from module init for fast paths. + byType: byTypeProxy, /** Remove all entries. Call when unloading a scene to prevent stale 3D refs. */ clear() { this.nodes.clear() - for (const set of Object.values(this.byType)) { + for (const set of byTypeStore.values()) { set.clear() } }, } -export function useRegistry( - id: string, - type: keyof typeof sceneRegistry.byType, - ref: React.RefObject, -) { +export function useRegistry(id: string, type: NodeKind, ref: React.RefObject) { useLayoutEffect(() => { const obj = ref.current if (!obj) return @@ -53,13 +89,14 @@ export function useRegistry( // 1. Add to master map sceneRegistry.nodes.set(id, obj) - // 2. Add to type-specific set - sceneRegistry.byType[type].add(id) + // 2. Add to type-specific set — Proxy auto-creates on first access so the + // assertion is safe; TS just can't see through the Proxy. + sceneRegistry.byType[type]!.add(id) // 4. Cleanup when component unmounts return () => { sceneRegistry.nodes.delete(id) - sceneRegistry.byType[type].delete(id) + sceneRegistry.byType[type]!.delete(id) } }, [id, type, ref]) }