# Creating Custom Renderers for New Node Types in Pascal Editor

> Learn to create custom renderers for new node types in Pascal Editor. Define Zod schemas, register them, implement React components, and update the NodeRenderer dispatcher for enhanced functionality.

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

---

**To create a custom renderer for a new node type in Pascal Editor, define a Zod schema in the core package, register it in the `AnyNode` discriminated union, implement a React‑Three‑Fiber component that calls `useRegistry`, and add a conditional branch to the central `NodeRenderer` dispatcher.**

Pascal Editor enforces a strict separation between **data** (the core schema) and **presentation** (the viewer). When you need to extend the scene graph with custom entities—such as lights, sensors, or procedural geometry—you must modify three specific layers in the `pascalorg/editor` repository. Because the viewer package never imports from the editor app, changes made following this pattern automatically work in both the full editing interface and the read‑only preview route.

## Step 1: Define the Node Schema in the Core Package

Every node type begins with a Zod schema that extends `BaseNode`. Create a new file under `packages/core/src/schema/nodes/` that defines the node’s discriminant key and properties.

```typescript
// packages/core/src/schema/nodes/light.ts
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'

export const LightNode = BaseNode.extend({
  id: objectId('light'),
  type: nodeType('light'),
  position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
  color: z.string().default('#ffffff'),
  intensity: z.number().default(1),
}).describe(
  dedent`
    Light node – a point light placed in the scene.
    - position: XYZ coordinates
    - color: CSS hex string
    - intensity: light strength
  `
)

export type LightNode = z.infer<typeof LightNode>

```

Next, expose the new schema to the rest of the application by appending it to the `AnyNode` discriminated union in [`types.ts`](https://github.com/pascalorg/editor/blob/main/types.ts). This union drives TypeScript inference throughout the editor and viewer.

```typescript
// packages/core/src/schema/types.ts
import { LightNode } from './nodes/light'

export const AnyNode = z.discriminatedUnion('type', [
  SiteNode,
  BuildingNode,
  WallNode,
  // … existing nodes …
  LightNode,  // ← new entry
])

```

The `type` field serves as the discriminant that allows the viewer to narrow union members without manual type casting.

## Step 2: Implement the React‑Three‑Fiber Renderer

The viewer renders nodes via React‑Three‑Fiber components located in `packages/viewer/src/components/renderers/`. Each renderer must register its Three.js object with the global registry and forward pointer events to support selection and hover states.

```tsx
// packages/viewer/src/components/renderers/light/light-renderer.tsx
import { useRegistry, type LightNode } from '@pascal-app/core'
import { useMemo, useRef } from 'react'
import { PointLight } from '@react-three/drei'
import { useNodeEvents } from '../../../hooks/use-node-events'

export const LightRenderer = ({ node }: { node: LightNode }) => {
  const ref = useRef<PointLight>(null!)
  
  // Register the object for systemic lookup by ID
  useRegistry(node.id, 'light', ref)
  
  // Inject selection / hover handlers
  const handlers = useNodeEvents(node, 'light')
  
  // Memoize to prevent unnecessary re‑renders
  const lightProps = useMemo(
    () => ({
      position: node.position,
      color: node.color,
      intensity: node.intensity,
    }),
    [node.position, node.color, node.intensity]
  )

  return <PointLight ref={ref} {...handlers} {...lightProps} />
}

```

**`useRegistry(node.id, 'light', ref)`** stores a reference to the Three.js object so external systems—such as exporters or physics engines—can retrieve it by ID, mirroring the pattern seen in `WallRenderer`. **`useNodeEvents(node, 'light')`** returns event handlers that wire the object into the editor’s interaction layer, enabling standard behaviors like click‑to‑select and hover highlighting.

## Step 3: Register the Renderer in the Node Dispatcher

The `NodeRenderer` component acts as the central switchboard that maps node types to their corresponding React components. Import your new renderer and add a conditional branch that matches the discriminant key defined in the schema.

```tsx
// packages/viewer/src/components/renderers/node-renderer.tsx
import { LightRenderer } from './light/light-renderer'

export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => {
  const node = useScene((state) => state.nodes[nodeId])
  if (!node) return null

  return (
    <>
      {node.type === 'wall' && <WallRenderer node={node} />}
      {node.type === 'window' && <WindowRenderer node={node} />}
      {node.type === 'light' && <LightRenderer node={node} />}  // ← new branch
    </>
  )
}

```

Because `AnyNode` is a discriminated union, TypeScript automatically narrows the `node` type inside each conditional block, ensuring type safety without explicit casting.

## Optional: Add Editor UI Controls

If the full editor UI must expose editable properties for the new node, create a panel component under `packages/editor/src/components/ui/panels/`. Follow the pattern established by existing panels (e.g., [`wall-panel.tsx`](https://github.com/pascalorg/editor/blob/main/wall-panel.tsx)): read the node via `useScene`, render form inputs, and call `markDirty` after mutations to trigger viewer updates.

## Summary

- **Define the schema** in `packages/core/src/schema/nodes/<type>.ts` using `BaseNode.extend`, then append the exported type to the `AnyNode` union in [`packages/core/src/schema/types.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/schema/types.ts).
- **Build the renderer** in `packages/viewer/src/components/renderers/<type>/<type>-renderer.tsx`, utilizing `useRegistry` for object lookup and `useNodeEvents` for interaction handling.
- **Expose the renderer** in [`packages/viewer/src/components/renderers/node-renderer.tsx`](https://github.com/pascalorg/editor/blob/main/packages/viewer/src/components/renderers/node-renderer.tsx) by adding a conditional check against the node’s `type` discriminant.
- **Maintain isolation** between the core schema and viewer to ensure the new node renders identically in the editor and preview modes.

## Frequently Asked Questions

### What is the purpose of `useRegistry` in Pascal Editor?

`useRegistry` stores a reference to a Three.js object in a global, ID‑indexed map. According to the `pascalorg/editor` source code, systems such as the selection manager, raycaster, and export utilities query this registry to retrieve objects by their stable node ID rather than by React component instance.

### Why does Pascal Editor use a discriminated union for node types?

The `AnyNode` discriminated union—defined in [`packages/core/src/schema/types.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/schema/types.ts)—uses a shared `type` field to enable TypeScript’s exhaustive narrowing. This pattern guarantees that the `NodeRenderer` switch statement handles every possible node variant, catching missing cases at compile time and eliminating the need for runtime type guards or unsafe casting.

### Can I use custom Three.js geometry in my node renderer?

Yes. The renderer is a standard React‑Three‑Fiber component, so you may return any valid Three.js primitive, custom `BufferGeometry`, or imported GLTF model. Ensure you still invoke `useRegistry` with a ref to the root mesh or group so that pointer events and external lookups function correctly.

### How do I make my new node type selectable in the editor?

Selection support requires two steps: first, attach the spread `handlers` from `useNodeEvents(node, '<type>')` to your root Three.js element in the renderer; second, ensure the node schema includes the standard `selectable` property inherited from `BaseNode`. The editor’s input system automatically recognizes objects that combine registry entries with event handler props.