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.wall contains 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:

Summary

  • The scene registry in pascalorg/editor maintains O(1) lookups via a master Map of node IDs to Three.js objects.
  • Type-specific Sets in sceneRegistry.byType allow systems to iterate only relevant object categories (walls, slabs, etc.) without scene traversal.
  • The useRegistry hook automatically manages object lifecycle, registering on mount and cleaning up on unmount to prevent memory leaks.
  • Direct imports of sceneRegistry enable 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →