Pascal Editor Scene Registry for Fast Three.js Object Lookups: A Deep Dive
The Pascal Editor scene registry eliminates costly Three.js scene traversals by maintaining a constant-time Map (nodes) and type-specific Sets (byType) for instant object lookups.
The pascalorg/editor repository implements a specialized scene registry that bridges the editor's node-based data model with Three.js rendering. This lightweight, in-memory indexing system replaces recursive scene-graph walks with O(1) hash lookups, enabling real-time performance even when managing thousands of architectural meshes.
Core Architecture and Implementation
The registry implementation lives in packages/core/src/hooks/scene-registry/scene-registry.ts and exposes a singleton object that tracks every rendered mesh by its node ID.
The Registry Data Structure
The sceneRegistry object maintains two complementary indexes:
// 📁 packages/core/src/hooks/scene-registry/scene-registry.ts
export const sceneRegistry = {
// Master lookup: ID → Object3D
nodes: new Map<string, THREE.Object3D>(),
// Categorised lookups: Type → Set<ID>
byType: {
site: new Set<string>(),
building: new Set<string>(),
ceiling: new Set<string>(),
level: new Set<string>(),
wall: new Set<string>(),
item: new Set<string>(),
slab: new Set<string>(),
zone: new Set<string>(),
roof: new Set<string>(),
'roof‑segment': new Set<string>(),
scan: new Set<string>(),
guide: new Set<string>(),
window: new Set<string>(),
door: new Set<string>(),
},
/** Remove all entries – called when a scene is unloaded. */
clear() {
this.nodes.clear()
for (const set of Object.values(this.byType)) {
set.clear()
}
},
}
The nodes Map provides immediate access to any Object3D via its string ID, while byType organizes IDs into typed Sets for efficient bulk operations on specific categories like walls, slabs, or roofs.
Automatic Lifecycle with useRegistry
The useRegistry React hook handles registration and cleanup automatically when components mount and unmount:
export function useRegistry(
id: string,
type: keyof typeof sceneRegistry.byType,
ref: React.RefObject<THREE.Object3D>,
) {
useLayoutEffect(() => {
const obj = ref.current
if (!obj) return
// 1️⃣ add to master map
sceneRegistry.nodes.set(id, obj)
// 2️⃣ add to type‑specific set
sceneRegistry.byType[type].add(id)
// 3️⃣ cleanup on unmount
return () => {
sceneRegistry.nodes.delete(id)
sceneRegistry.byType[type].delete(id)
}
}, [id, type, ref])
}
This pattern prevents stale references by ensuring every registered object removes itself from both indexes when its React component unmounts.
Performance Benefits for Three.js Applications
Traditional Three.js selection requires traversing the entire scene graph recursively to find objects by user data or UUID. The Pascal Editor scene registry replaces this O(n) operation with direct Map access:
- Single object retrieval:
sceneRegistry.nodes.get(nodeId)executes in constant time regardless of scene complexity. - Type-filtered iteration:
sceneRegistry.byType.wallcontains only wall IDs, allowing systems to process subsets without filtering irrelevant meshes. - Memory efficiency: The registry stores only string IDs in Sets rather than duplicate object references, minimizing overhead.
This design is essential for interactive features like ray-casting, gizmos, and real-time highlighting that must resolve node IDs to meshes within a single frame budget.
Practical Implementation Examples
Registering a Mesh in a Custom Renderer
Every renderer component in Pascal Editor uses useRegistry to publish its Three.js object:
import { useRef } from 'react'
import { useRegistry } from '@pascal-app/core'
import type * as THREE from 'three'
export function CustomBox({ nodeId }: { nodeId: string }) {
const meshRef = useRef<THREE.Mesh>(null!)
// Register under the “item” bucket
useRegistry(nodeId, 'item', meshRef)
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
)
}
When this component mounts, the mesh enters both sceneRegistry.nodes and sceneRegistry.byType.item. Unmounting automatically triggers cleanup to prevent memory leaks.
Retrieving Objects for Tools and Systems
Systems throughout the editor import the registry directly to access meshes:
import { sceneRegistry } from '@pascal-app/core'
// Find the mesh for a given node id
export function getMesh(nodeId: string): THREE.Object3D | undefined {
return sceneRegistry.nodes.get(nodeId)
}
// Example: move the mesh up by 0.5 m
const mesh = getMesh('wall‑123')
if (mesh) {
mesh.position.y += 0.5
}
The selection manager in packages/viewer/src/components/viewer/selection-manager.tsx uses this pattern to resolve hovered node IDs to their corresponding Object3D instances for visual feedback.
Bulk Operations by Type
The type-specific indexes enable efficient batch processing without scene traversal:
import { sceneRegistry } from '@pascal-app/core'
export function highlightAllWalls() {
sceneRegistry.byType.wall.forEach(wallId => {
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh | undefined
if (mesh) {
mesh.material = new THREE.MeshBasicMaterial({ color: 'yellow' })
}
})
}
The wall cutout system in packages/viewer/src/systems/wall/wall-cutout.tsx iterates sceneRegistry.byType.wall to detect geometric changes and regenerate openings only for wall meshes.
Key Integration Points in the Codebase
The registry serves as the authoritative source for mesh lookups across multiple packages:
packages/viewer/src/systems/wall/wall-cutout.tsx: Iterates the wall Set to process cutout geometry updates.packages/viewer/src/components/viewer/selection-manager.tsx: Resolves user clicks to Three.js objects via the master nodes Map.packages/core/src/systems/wall/wall-system.tsx: Accesses specific wall meshes directly by ID during geometry recomputation.packages/editor/src/components/tools/window/window-tool.tsx: Reads mesh properties like position from the registry to calculate tool interactions.
Summary
- The scene registry in
pascalorg/editormaintains O(1) lookups via a masterMapof node IDs to Three.js objects. - Type-specific Sets in
sceneRegistry.byTypeallow systems to iterate only relevant object categories (walls, slabs, etc.) without scene traversal. - The
useRegistryhook automatically manages object lifecycle, registering on mount and cleaning up on unmount to prevent memory leaks. - Direct imports of
sceneRegistryenable non-React systems like wall cutout processors and selection managers to access meshes instantly. - This architecture replaces recursive Three.js scene walking with hash-based access, maintaining interactive frame rates even with thousands of architectural elements.
Frequently Asked Questions
How does the registry prevent memory leaks when objects are destroyed?
The useRegistry hook returns a cleanup function that executes automatically when the React component unmounts. This function calls sceneRegistry.nodes.delete(id) and sceneRegistry.byType[type].delete(id), ensuring no stale references remain in the Map or Sets. Additionally, the clear() method removes all entries when an entire scene is unloaded.
What node types are supported in the type registry?
The byType object supports architectural categories including wall, slab, roof, window, door, building, level, zone, item, ceiling, site, scan, guide, and roof-segment. Each type maintains its own Set<string> containing only the IDs of objects belonging to that category.
How does this differ from Three.js's built-in scene graph traversal?
Three.js requires recursive traversal of the scene graph using scene.traverse() or scene.getObjectById(), which scales linearly (O(n)) with scene complexity. The Pascal Editor registry provides constant-time (O(1)) access via JavaScript's native Map.get() operation, and type-filtered access without iteration overhead by maintaining pre-sorted Sets.
Can systems outside React components access the registry?
Yes. While useRegistry is designed for React components, any module can import the singleton sceneRegistry object directly from @pascal-app/core to read or modify entries. This allows imperative systems, Web Workers, or utility functions to perform fast lookups without React context.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →