Proxy-back sceneRegistry.byType for registry/plugin kinds (shim 1/4)

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) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-14 11:30:55 -04:00
co-authored by Claude Opus 4.7
parent 4d5226ce10
commit 4c3feea052
@@ -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<KnownNodeKind, Set<string>> & Record<string, Set<string>>
const byTypeStore = new Map<string, Set<string>>(
KNOWN_NODE_KINDS.map((k) => [k, new Set<string>()]),
)
// 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<string>()
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<string, THREE.Object3D>(),
// Categorized lookups: Type -> Set of IDs
// Using a Set is faster for adding/deleting than an Array
byType: {
site: new Set<string>(),
building: new Set<string>(),
ceiling: new Set<string>(),
column: new Set<string>(),
elevator: new Set<string>(),
level: new Set<string>(),
wall: new Set<string>(),
fence: new Set<string>(),
item: new Set<string>(),
slab: new Set<string>(),
spawn: new Set<string>(),
zone: new Set<string>(),
roof: new Set<string>(),
'roof-segment': new Set<string>(),
stair: new Set<string>(),
'stair-segment': new Set<string>(),
scan: new Set<string>(),
guide: new Set<string>(),
window: new Set<string>(),
door: new Set<string>(),
},
// 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<THREE.Object3D>,
) {
export function useRegistry(id: string, type: NodeKind, ref: React.RefObject<THREE.Object3D>) {
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])
}