# How to Register Node Renderers with the Pascal Editor Scene Registry

> Learn to register node renderers with the Pascal Editor scene registry using the useRegistry hook for fast lookups and automatic cleanup. Optimize your editor's performance.

- Repository: [Pascal/editor](https://github.com/pascalorg/editor)
- Tags: how-to-guide
- Published: 2026-03-25

---

**Pascal Editor uses a centralized `sceneRegistry` object and the `useRegistry` React hook to synchronously register every node renderer's root `THREE.Object3D` before the first paint, enabling fast lookups by ID or type and automatic cleanup on unmount.**

The **pascalorg/editor** repository separates rendering logic from scene management by maintaining a type-aware registry of all visual nodes. By registering node renderers with the Pascal Editor scene registry, systems like selection, export, and level-stacking can reliably access the underlying Three.js objects without traversing the React component tree. This pattern ensures deterministic lookup performance and memory-safe unmounting across the entire application.

## Understanding the Scene Registry Architecture

The core registry is implemented as a singleton in [`packages/core/src/hooks/scene-registry/scene-registry.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/hooks/scene-registry/scene-registry.ts). It exposes a `sceneRegistry` object with two complementary data structures: a master `Map` for ID-based lookups and typed `Set` collections for category-based queries.

```ts
// packages/core/src/hooks/scene-registry/scene-registry.ts
export const sceneRegistry = {
  nodes: new Map<string, THREE.Object3D>(),
  byType: {
    wall: new Set<string>(),
    window: new Set<string>(),
    slab: new Set<string>(),
    // … one Set per node type
  }
}

```

The `nodes` map stores the root `THREE.Object3D` for every registered node, keyed by its unique ID. The `byType` record groups these IDs into sets according to their semantic type (wall, window, etc.), allowing systems to quickly iterate over specific categories without filtering the entire scene.

## The useRegistry Hook: Synchronous Registration

Renderers consume the registry through the **`useRegistry`** hook, which guarantees that the 3-D object is discoverable before the browser paints the first frame. The hook uses `useLayoutEffect` to execute registration synchronously after the DOM mutation but before the visual update.

```ts
// packages/core/src/hooks/scene-registry/scene-registry.ts
export function useRegistry(
  id: string,
  type: keyof typeof sceneRegistry.byType,
  ref: React.RefObject<THREE.Object3D>
) {
  useLayoutEffect(() => {
    const obj = ref.current
    if (!obj) return
    
    sceneRegistry.nodes.set(id, obj)      // master lookup
    sceneRegistry.byType[type].add(id)    // typed lookup
    
    return () => {
      sceneRegistry.nodes.delete(id)      // cleanup on unmount
      sceneRegistry.byType[type].delete(id)
    }
  }, [id, type, ref])
}

```

When the component unmounts, the effect’s cleanup function removes the ID from both the master map and the typed set. This prevents stale references that would otherwise break systems attempting to query wall heights, selection bounds, or export geometry.

## Implementing a Wall Node Renderer

Each renderer is responsible for creating a React ref attached to its root mesh or group, then passing that ref to `useRegistry` along with the node ID and type. In [`packages/viewer/src/components/renderers/wall/wall-renderer.tsx`](https://github.com/pascalorg/editor/blob/main/packages/viewer/src/components/renderers/wall/wall-renderer.tsx), the **WallRenderer** registers the wall’s mesh so that the **WallSystem** can later replace its geometry while the registry maintains the correct object reference.

```tsx
// packages/viewer/src/components/renderers/wall/wall-renderer.tsx
import { useRegistry, useScene, type WallNode } from '@pascal-app/core'
import { useRef, useLayoutEffect } from 'react'
import type { Mesh } from 'three'

export const WallRenderer = ({ node }: { node: WallNode }) => {
  const ref = useRef<Mesh>(null!)
  
  // Register the wall mesh with the scene registry
  useRegistry(node.id, 'wall', ref)

  // Mark dirty so WallSystem rebuilds geometry
  useLayoutEffect(() => useScene.getState().markDirty(node.id), [node.id])

  return (
    <mesh ref={ref} visible={node.visible}>
      {/* geometry will be replaced by WallSystem */}
      <boxGeometry args={[0, 0, 0]} />
    </mesh>
  )
}

```

The registration must target the **outermost group** that represents the whole node. If a renderer creates multiple meshes, it should register the parent group so that systems receive a single consistent reference for transformations and queries.

## Registering Window and Specialized Renderers

The same pattern applies to windows, doors, and custom nodes. The **WindowRenderer** in [`packages/viewer/src/components/renderers/window/window-renderer.tsx`](https://github.com/pascalorg/editor/blob/main/packages/viewer/src/components/renderers/window/window-renderer.tsx) registers with type `'window'`, allowing the scene registry to distinguish it from walls when calculating level heights or collision bounds.

```tsx
// packages/viewer/src/components/renderers/window/window-renderer.tsx
import { useRegistry, type WindowNode } from '@pascal-app/core'
import { useRef } from 'react'
import type { Mesh } from 'three'

export const WindowRenderer = ({ node }: { node: WindowNode }) => {
  const ref = useRef<Mesh>(null!)
  useRegistry(node.id, 'window', ref)

  const isTransient = !!(node.metadata as Record<string, unknown> | null)?.isTransient

  return (
    <mesh ref={ref} position={node.position} rotation={node.rotation} visible={node.visible}>
      <boxGeometry args={[0, 0, 0]} />
      <meshStandardMaterial color="#d1d5db" />
    </mesh>
  )
}

```

Because the registry tracks transient state through the same ID-based mechanism, systems can optionally filter nodes by metadata without breaking the core lookup contract.

## Querying Registered Objects from Systems

Systems consume the registry to read world positions, dimensions, or visibility states without coupling to React component internals. The **LevelSystem** in [`packages/viewer/src/systems/level/level-utils.ts`](https://github.com/pascalorg/editor/blob/main/packages/viewer/src/systems/level/level-utils.ts) queries `sceneRegistry.nodes` to determine the vertical position of walls when calculating level heights.

```ts
// packages/viewer/src/systems/level/level-utils.ts
import { sceneRegistry, type WallNode } from '@pascal-app/core'

const meshY = sceneRegistry.nodes.get(childId as string)?.position.y ?? 0
const top = meshY + ((child as WallNode).height ?? DEFAULT_LEVEL_HEIGHT)

```

This direct access pattern eliminates the need for prop drilling or context providers, keeping geometry calculations in pure TypeScript logic while the React layer handles only visual representation.

## Memory Management and Scene Teardown

To prevent memory leaks when unloading projects, [`packages/editor/src/lib/scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/lib/scene.ts) invokes **`sceneRegistry.clear()`**, which wipes the `nodes` map and clears every typed set. This ensures that no orphaned `Object3D` references persist between sessions.

- **One registration per node ID**: If a renderer attempts to register multiple objects under the same ID, later registrations overwrite earlier ones, so always register the single root group.
- **Automatic cleanup**: The `useRegistry` hook handles unmounting, but explicit `clear()` is required for full scene resets.

The same “register → cleanup” contract is mirrored in [`packages/editor/src/store/use-command-registry.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/store/use-command-registry.ts), demonstrating a consistent resource management pattern throughout the Pascal Editor codebase.

## Summary

- The **scene registry** in [`packages/core/src/hooks/scene-registry/scene-registry.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/hooks/scene-registry/scene-registry.ts) maintains a `Map` of all node objects and typed `Set` collections for fast categorical lookups.
- The **`useRegistry`** hook uses `useLayoutEffect` to register `THREE.Object3D` references synchronously before painting and automatically removes them on unmount.
- Renderers in `packages/viewer/src/components/renderers/` call `useRegistry(id, type, ref)` to expose their root mesh to systems like **LevelSystem**.
- Systems query the registry via `sceneRegistry.nodes.get(id)` to read positions and dimensions without React overhead.
- Calling `sceneRegistry.clear()` in [`packages/editor/src/lib/scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/lib/scene.ts) frees all references when a scene is unloaded, preventing memory leaks.

## Frequently Asked Questions

### What is the difference between the `nodes` map and the `byType` sets in the registry?

The `nodes` property is a `Map<string, THREE.Object3D>` that provides O(1) lookup of any node's 3-D object by its unique ID. The `byType` property is a record of `Set<string>` instances that group node IDs by their semantic category (wall, window, slab, etc.). Systems use `nodes` for direct object access and `byType` to iterate over specific categories without scanning the entire scene.

### Why does Pascal Editor use `useLayoutEffect` instead of `useEffect` for registration?

`useLayoutEffect` fires synchronously after all DOM mutations but before the browser paints the screen. This guarantees that the `THREE.Object3D` is registered in `sceneRegistry.nodes` before any system attempts to query it during the same render cycle, preventing race conditions in selection or level-calculation logic that runs immediately after mounting.

### How should I register a renderer that uses multiple meshes for a single node?

You should register only the **outermost parent group** that contains all child meshes. Create a single `RefObject` for the root group, attach that ref to the `<group>` element in JSX, and pass it to `useRegistry`. Systems expect one `Object3D` per node ID; registering child meshes individually would create ambiguous references and break height calculations or bounding-box queries.

### When is the registry cleared during the application lifecycle?

The registry is cleared when a scene is explicitly unloaded or the editor resets, via the `sceneRegistry.clear()` method called in [`packages/editor/src/lib/scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/lib/scene.ts). This wipes both the `nodes` map and all `byType` sets, ensuring no stale `Object3D` references persist when loading a new project. Individual entries are removed automatically when their renderer unmounts thanks to the cleanup function inside `useRegistry`.