Pascal Editor

An open-source, local-first 3D building editor built with React Three Fiber and WebGPU. Run it in the browser or from the CLI, and connect AI agents through MCP.

MIT License [![npm

@pascal-app/core](https://img.shields.io/npm/v/@pascal-app/core?label=%40pascal-app%2Fcore)](https://www.npmjs.com/package/@pascal-app/core) [![npm

@pascal-app/viewer](https://img.shields.io/npm/v/@pascal-app/viewer?label=%40pascal-app%2Fviewer)](https://www.npmjs.com/package/@pascal-app/viewer)

npm @pascal-app/cli

Discord [![X

(Twitter)](https://img.shields.io/badge/follow-%40pascal__app-black?logo=x&logoColor=white)](https://x.com/pascal_app)

https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b

Run the Editor Locally

Node.js 22.13 or newer can create a persistent local Pascal installation without cloning this repository:

npx @pascal-app/cli editor

The CLI starts the editor and an authenticated MCP service in the background, selects collision-free loopback ports, and keeps projects in

~/.pascal/data/pascal.db. Configure an agent to launch pascal mcp connect. See [Run Pascal

locally](https://editor.pascal.app/docs/developers/local-editor) for pnpm/Bun commands, project management, MCP setup, updates, storage paths, and

troubleshooting. The npm release is the older runtime described below; use the verified GitHub preview when a task needs the new read-only furniture

candidate check.

Candidate-enabled CLI preview

The npm beta tag currently resolves to @pascal-app/[email protected], which predates the read-only furniture candidate input in this repository. To

use that capability before the next npm release, install the verified GitHub prerelease built from commit aa653f2f523f81f361ac20cb42b745faf7e46844:

```bash PASCAL_PREVIEW_VERSION='1.0.0-beta.1.agent-skills.0' PASCAL_PREVIEW_PREFIX="${XDG_DATA_HOME:-$HOME/.local/share}/pascal-preview"

PASCAL_PREVIEW_DOWNLOAD="$(mktemp -d)" cd "$PASCAL_PREVIEW_DOWNLOAD"

curl --fail --location --remote-name \

"https://github.com/pascalorg/editor/releases/download/cli-v1.0.0-beta.1-agent-skills.0/pascal-app-cli-${PASCAL_PREVIEW_VERSION}.tgz" curl --fail

--location --remote-name \ "https://github.com/pascalorg/editor/releases/download/cli-v1.0.0-beta.1-agent-skills.0/SHA256SUMS.txt"

macOS

shasum -a 256 -c SHA256SUMS.txt

Linux: use sha256sum -c SHA256SUMS.txt instead.

npm install --global --prefix "$PASCAL_PREVIEW_PREFIX" --ignore-scripts \ "./pascal-app-cli-${PASCAL_PREVIEW_VERSION}.tgz" export

PATH="$PASCAL_PREVIEW_PREFIX/bin:$PATH" pascal --version pascal update --version "$PASCAL_PREVIEW_VERSION" pascal editor --no-open ```

The expected archive SHA-256 is 814ffa8c6f6a5fced73bf909c616d9a78feff18fd61fd0b4b7d65e74fad5a33d. The same-version update command installs and

activates this CLI's bundled runtime, restarting an older running service when necessary. Keep an existing PASCAL_HOME unchanged so stored projects

remain in the same data directory; pascal editor alone reuses any healthy service, including an older one. Keep the preview prefix on the agent

host's PATH before using the Claude plugin-provided connector, running pascal mcp setup claude or pascal mcp setup codex for another

installation path, or configuring pascal mcp connect manually. This GitHub prerelease is not an npm version.

Use one active agent client per local CLI service. The standalone local HTTP runtime shares active scene state between clients; use separate

PASCAL_HOME directories and service processes when independent concurrent work is required.

Agent skills

Install Pascal's public agent workflows from this repository with skills.sh:

npx skills add pascalorg/editor \
  --skill pascal-3d \
  --skill furniture-fit

Claude Code users can install the same canonical skill source as a plugin:

/plugin marketplace add pascalorg/editor
/plugin install pascal-agent-skills@pascal

The Claude plugin also supplies the local pascal mcp connect server. Install and start the Pascal CLI first, and keep pascal on the PATH used to

launch Claude Code. This local connector needs no Pascal account or API key and does not upload projects automatically.

Claude Code 2.1.258 loads both the user-scoped pascal server created by pascal mcp setup claude and the plugin-provided server. Remove the manual

entry before reloading or restarting Claude Code so only the plugin owns the connection lifecycle:

claude mcp remove --scope user pascal

Use /mcp to remove or disable any project- or local-scoped Pascal connection too. Leaving both connections active violates the

one-active-agent-client-per-local-service requirement. When the intended project is hosted in a Pascal account or organization, disable the

plugin-provided local server in /mcp and configure the hosted endpoint from the skill setup guide instead.

Codex users can install the same plugin from the repository marketplace:

codex plugin marketplace add pascalorg/editor
codex plugin add pascal-agent-skills@pascal

OpenClaw installation becomes available after the skills are published under Pascal's ClawHub publisher. See skills/README.md for

the owner-qualified install and verification commands.

pascal-3d covers safe local or hosted MCP setup and verified scene work.

furniture-fit produces a bounded, evidence-based footprint assessment without claiming unsupported height, swing,

or delivery checks. See skills/README.md for package details and validation.

The skills inspect the connected MCP tool schemas before using optional fields. A capability present in this repository may be absent from an older

installed or hosted release; the agent should report the narrower supported result instead of assuming source-only inputs are available.

These workflows require a connected Pascal MCP server for their tool-backed actions. An OpenAI directory submission must therefore use With MCP

and submit the production hosted MCP endpoint together with the skills. The repository package does not prove that the endpoint, OAuth flow, reviewer

credentials, domain verification, or portal scan is ready for review.

MCP Registry

server.json is Pascal's manifest for the official MCP Registry. Its version tracks the hosted MCP implementation independently of the

npm package version. Pull requests validate the manifest and production endpoint. A Pascal organization owner publishes an approved version from

main with the official registry publisher.

Using Published Packages

The viewer runtime and built-in node definitions are separate packages. Install the full built-in viewer set, then load the built-in plugin once

before mounting <Viewer>. Capture sessions are an optional transport-neutral extension:

npm install @pascal-app/core @pascal-app/viewer @pascal-app/editor @pascal-app/nodes
npm install @pascal-app/capture-protocol @pascal-app/capture-viewer
import { loadPlugin } from '@pascal-app/core'
import { builtinPlugin } from '@pascal-app/nodes'

await loadPlugin(builtinPlugin)

See the @pascal-app/viewer quick start for a React example.

Repository Architecture

This is a Turborepo monorepo with the reusable editor packages, the standalone app, and the CLI that distributes it:

``` editor/ ├── apps/ │ └── editor/ # Next.js application ├── packages/ │ ├── core/ # Schemas, scene state, and registry contracts │ ├── viewer/ # 3D

rendering runtime and shared systems │ ├── capture-protocol/ # Static/live capture-session contracts │ ├── capture-viewer/ # Capture source runtime

and reference renderers │ ├── editor/ # Editing tools and UI components │ ├── nodes/ # Built-in node definitions, renderers, and systems │ ├── cli/ #

Persistent local editor installer and process manager │ ├── mcp/ # Model Context Protocol server and scene storage │ └── ui/ # Shared UI components


### Separation of Concerns

| Package | Responsibility | |---------|---------------| | **@pascal-app/core** | Node schemas, scene state (Zustand), registry contracts, spatial

queries, and event bus | | **@pascal-app/viewer** | 3D rendering via React Three Fiber, shared render systems, default camera/controls, and

post-processing | | **@pascal-app/capture-protocol** | Versioned capture manifests, normalized streams, and transport-neutral static/live sources | |

**@pascal-app/capture-viewer** | Viewer child runtime and reference model, device-motion, and point-cloud layers | | **@pascal-app/editor** | Editing

tools, panels, selection, and direct-manipulation UI | | **@pascal-app/nodes** | Built-in registry plugin with node definitions, renderers, geometry,

and systems | | **@pascal-app/cli** | Installs and manages a versioned standalone editor runtime and persistent local data | | **@pascal-app/mcp** |

Exposes scene tools, resources, prompts, and local storage to MCP-compatible AI hosts | | **apps/editor** | Standalone Next.js host for the editor

packages |

The **viewer** renders the scene with sensible defaults. The **editor** extends it with interactive tools, selection management, and editing capabilities.

### Stores

Each package has its own Zustand store for managing state:

| Store | Package | Responsibility | |-------|---------|----------------| | `useScene` | `@pascal-app/core` | Scene data: nodes, root IDs, dirty

nodes, CRUD operations. Persisted to IndexedDB with undo/redo via Zundo. | | `useViewer` | `@pascal-app/viewer` | Viewer state: current selection

(building/level/zone IDs), level display mode (stacked/exploded/solo), camera mode. | | `useEditor` | `apps/editor` | Editor state: active tool,

structure layer visibility, panel states, editor-specific preferences. |

**Access patterns:**

```typescript // Subscribe to state changes (React component) const nodes = useScene((state) => state.nodes) const levelId = useViewer((state) =>

state.selection.levelId) const activeTool = useEditor((state) => state.tool)

// Access state outside React (callbacks, systems)
const node = useScene.getState().nodes[id]
useViewer.getState().setSelection({ levelId: 'level_123' })

Core Concepts

Nodes

Nodes are the data primitives that describe the 3D scene. All nodes extend BaseNode:

```typescript BaseNode { id: string // Auto-generated with type prefix (e.g., "wall_abc123") type: string // Discriminator for type-safe handling

parentId: string | null // Parent node reference visible: boolean camera?: Camera // Optional saved camera position metadata?: JSON // Arbitrary

metadata (e.g., { isTransient: true }) } ```

Node Hierarchy:

``` Site └── Building └── Level ├── Wall → Item (doors, windows) ├── Slab ├── Ceiling → Item (lights) ├── Roof ├── Zone ├── Scan (3D reference) └──

Guide (2D reference) ```

Nodes are stored in a flat dictionary (Record<id, Node>), not a nested tree. Parent-child relationships are defined via parentId and children arrays.


Scene State (Zustand Store)

The scene is managed by a Zustand store in @pascal-app/core:

```typescript useScene.getState() = { nodes: Record, // All nodes rootNodeIds: string[], // Top-level nodes (sites) dirtyNodes:

Set, // Nodes pending system updates

createNode(node, parentId), updateNode(id, updates), deleteNode(id), }


**Middleware:**
- **Persist** - Saves to IndexedDB (excludes transient nodes)
- **Temporal** (Zundo) - Undo/redo with 50-step history

---

### Scene Registry

The registry maps node IDs to their Three.js objects for fast lookup:

```typescript
sceneRegistry = {
  nodes: Map<id, Object3D>,    // ID → 3D object
  byType: {
    wall: Set<id>,
    item: Set<id>,
    zone: Set<id>,
    // ...
  }
}

Renderers register their refs using the useRegistry hook:

const ref = useRef<Mesh>(null!)
useRegistry(node.id, 'wall', ref)

This allows systems to access 3D objects directly without traversing the scene graph.


Node Renderers

Renderers are React components that create Three.js objects for each node type:

``` SceneRenderer └── NodeRenderer (dispatches by type) ├── BuildingRenderer ├── LevelRenderer ├── WallRenderer ├── SlabRenderer ├── ZoneRenderer ├──

ItemRenderer └── ... ```

Pattern: 1. Renderer creates a placeholder mesh/group 2. Registers it with useRegistry 3. Systems update geometry based on node data

Example (simplified):

const WallRenderer = ({ node }) => {
  const ref = useRef<Mesh>(null!)
  useRegistry(node.id, 'wall', ref)

return ( <mesh ref={ref}> <boxGeometry args={[0, 0, 0]} /> {/* Replaced by WallSystem */} <meshStandardMaterial /> {node.children.map(id =>

<NodeRenderer key={id} nodeId={id} />)} </mesh> ) } ```

---

### Systems

Systems are React components that run in the render loop (`useFrame`) to update geometry and transforms. They process **dirty nodes** marked by the store.

**Core Systems (in `@pascal-app/core`):**

| System | Responsibility | |--------|---------------| | `WallSystem` | Generates wall geometry with mitering and CSG cutouts for doors/windows | |

`SlabSystem` | Generates floor geometry from polygons | | `CeilingSystem` | Generates ceiling geometry | | `RoofSystem` | Generates roof geometry | |

`ItemSystem` | Positions items on walls, ceilings, or floors (slab elevation) |

**Viewer Systems (in `@pascal-app/viewer`):**

| System | Responsibility | |--------|---------------| | `LevelSystem` | Handles level visibility and vertical positioning (stacked/exploded/solo

modes) | | `ScanSystem` | Controls 3D scan visibility | | `GuideSystem` | Controls guide image visibility |

**Processing Pattern:**
```typescript
useFrame(() => {
  for (const id of dirtyNodes) {
    const obj = sceneRegistry.nodes.get(id)
    const node = useScene.getState().nodes[id]

// Update geometry, transforms, etc.
    updateGeometry(obj, node)

dirtyNodes.delete(id)
  }
})

Dirty Nodes

When a node changes, it's marked as dirty in useScene.getState().dirtyNodes. Systems check this set each frame and only recompute geometry for dirty nodes.

```typescript // Automatic: createNode, updateNode, deleteNode mark nodes dirty useScene.getState().updateNode(wallId, { thickness: 0.2 }) // → wallId

added to dirtyNodes // → WallSystem regenerates geometry next frame // → wallId removed from dirtyNodes ```

Manual marking:

useScene.getState().dirtyNodes.add(wallId)

Event Bus

Inter-component communication uses a typed event emitter (mitt):

// Node events
emitter.on('wall:click', (event) => { ... })
emitter.on('item:enter', (event) => { ... })
emitter.on('zone:context-menu', (event) => { ... })

// Grid events (background)
emitter.on('grid:click', (event) => { ... })

// Event payload
NodeEvent {
  node: AnyNode
  position: [x, y, z]
  localPosition: [x, y, z]
  normal?: [x, y, z]
  stopPropagation: () => void
}

Spatial Grid Manager

Handles collision detection and placement validation:

```typescript spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation) spatialGridManager.canPlaceOnWall(wallId, t, height,

dimensions) spatialGridManager.getSlabElevationAt(levelId, x, z) ```

Used by item placement tools to validate positions and calculate slab elevations.


Editor Architecture

The editor extends the viewer with:

Tools

Tools are activated via the toolbar and handle user input for specific operations:

  • SelectTool - Selection and manipulation
  • WallTool - Draw walls
  • ZoneTool - Create zones
  • ItemTool - Place furniture/fixtures
  • SlabTool - Create floor slabs

Selection Manager

The editor uses a custom selection manager with hierarchical navigation:

Site → Building → Level → Zone → Items

Each depth level has its own selection strategy for hover/click behavior.

Editor-Specific Systems

  • ZoneSystem - Controls zone visibility based on level mode
  • Custom camera controls with node focusing

Data Flow

``` User Action (click, drag) ↓ Tool Handler ↓ useScene.createNode() / updateNode() ↓ Node added/updated in store Node marked dirty ↓ React re-renders

NodeRenderer useRegistry() registers 3D object ↓ System detects dirty node (useFrame) Updates geometry via sceneRegistry Clears dirty flag ```


Building a Plugin

The editor is extensible: a plugin ships node kinds (schema, 3D/2D rendering, placement tools, inspector parametrics) and left-rail panels through the

same Plugin manifest the built-ins use — there is no separate internal API.

  • Developer guide — Create a plugin: the Plugin shape, panel contributions, discovery,

lifecycle, and what's in/out of v1. - Worked example — pascalorg/plugin-trees: a standalone plugin

with procedural trees, flowers, grass, and a presets panel. Clone it as a starting point.


Technology Stack

  • React 19 + Next.js 16 - Three.js (WebGPU renderer) - React Three Fiber + Drei - Zustand (state management) - Zod (schema

validation) - Zundo (undo/redo) - three-bvh-csg (Boolean geometry operations) - Turborepo (monorepo management) - Bun (package

manager)


Getting Started

Development

Run the development server from the root directory to enable hot reload for all packages:

# Install dependencies
bun install

# Run development server (builds packages + starts editor with watch mode)
bun dev

# This will:
# 1. Build @pascal-app/core and @pascal-app/viewer
# 2. Start watching both packages for changes
# 3. Start the Next.js editor dev server
# Open http://localhost:3002

Important: Always run bun dev from the root directory to ensure the package watchers are running. This enables hot reload when you edit files in packages/core/src/ or packages/viewer/src/.

Building for Production

# Build all packages
turbo build

# Build specific package
turbo build --filter=@pascal-app/core

Publishing Packages

# Build packages
turbo build --filter=@pascal-app/core --filter=@pascal-app/viewer

# Publish to npm
npm publish --workspace=@pascal-app/core --access public
npm publish --workspace=@pascal-app/viewer --access public

Key Files

| Path | Description | |------|-------------| | packages/core/src/schema/ | Node type definitions (Zod schemas) | |

packages/core/src/store/use-scene.ts | Scene state store | | packages/core/src/hooks/scene-registry/ | 3D object registry | |

packages/core/src/systems/ | Geometry generation systems | | packages/viewer/src/components/renderers/ | Node renderers | |

packages/viewer/src/components/viewer/ | Main Viewer component | | apps/editor/components/tools/ | Editor tools | | apps/editor/store/ |

Editor-specific state |


Contributing

Bug fixes, features, docs and ideas are all welcome. Start with CONTRIBUTING.md for setup, code style and the PR flow.

  • New node kinds and sidebar panels ship as plugins rather than edits to the built-ins —

pascalorg/plugin-trees is a worked example - Questions and ideas go to

Discussions; reproducible bugs go to Issues -

Participation is covered by our Code of Conduct - Security problems go to SECURITY.md, not a public issue


Contributors

<img src="https://avatars.githubusercontent.com/u/4444492?v=4" width="60" height="60" alt="Aymeric Rabot"

style="border-radius:50%"> <img src="https://avatars.githubusercontent.com/u/6551176?v=4" width="60"

height="60" alt="Wassim Samad" style="border-radius:50%"> <img

src="https://avatars.githubusercontent.com/sudhir9297?v=4" width="60" height="60" alt="Sudhir" style="border-radius:50%">


pascalorg/editor | Trendshift