กลับไปหน้า Tools

GetNotes Tools

pascalorg/editor

Tool นี้คืออะไร

Pascal Editor เป็นเครื่องมือแก้ไขโมเดล 3 มิติที่สร้างด้วย React Three Fiber และ WebGPU เหมาะสำหรับนักพัฒนาที่ต้องการสร้างแอปพลิเคชันแก้ไขโมเดล 3 มิติ โดยมีประโยชน์ในการจัดการสถานะฉาก การเรนเดอร์ 3 มิติ และเครื่องมือแก้ไขแบบโต้ตอบ

ข้อมูลโปรเจกต์

ดาว

19.1K

Forks

2.6K

License

MIT

อัปเดต GitHub ล่าสุด

28 ก.ค. 2569

เพิ่มใน GetNotes

30 ก.ค. 2569

Repository

pascalorg/editor

เหมาะกับอาชีพ

Ecosystem

TypeScript

แปลและเรียบเรียงโดย AI

เนื้อหาฉบับภาษาไทย

ใช้อ่านเพื่อทำความเข้าใจเบื้องต้น โปรดตรวจสอบรายละเอียดสำคัญกับเอกสารต้นฉบับด้านล่าง

Pascal Editor

เครื่องมือแก้ไขโมเดล 3 มิติที่สร้างด้วย React Three Fiber และ WebGPU

MIT License npm @pascal-app/core npm @pascal-app/viewer Discord X (Twitter)

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

การใช้งานแพ็กเกจที่เผยแพร่

รันไทม์ของ viewer และ node definitions ที่มาพร้อมเครื่องมือจะถูกแยกเป็นแพ็กเกจต่างหาก ติดตั้งชุด viewer ที่มาพร้อมเครื่องมือทั้งหมด จากนั้นโหลดปลั๊กอินที่มาพร้อมเครื่องมือหนึ่งครั้งก่อนที่จะ mount <Viewer>:

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

await loadPlugin(builtinPlugin)

ดู @pascal-app/viewer quick start สำหรับตัวอย่าง React

สถาปัตยกรรมของ Repository

นี่คือ Turborepo monorepo ที่มีแพ็กเกจรันไทม์หลักสี่แพ็กเกจ:

code
editor/
├── apps/
│   └── editor/          # แอปพลิเคชัน Next.js
├── packages/
│   ├── core/            # Schemas, สถานะฉาก, และ registry contracts
│   ├── viewer/          # รันไทม์การเรนเดอร์ 3D และระบบที่ใช้ร่วมกัน
│   ├── editor/          # เครื่องมือแก้ไขและส่วนประกอบ UI
│   ├── nodes/           # Node definitions, renderers, และระบบที่มาพร้อมเครื่องมือ
│   └── ui/              # ส่วนประกอบ UI ที่ใช้ร่วมกัน

การแยกความรับผิดชอบ

แพ็กเกจความรับผิดชอบ
@pascal-app/coreNode schemas, สถานะฉาก (Zustand), registry contracts, การสอบถามเชิงพื้นที่, และ event bus
@pascal-app/viewerการเรนเดอร์ 3D ผ่าน React Three Fiber, ระบบเรนเดอร์ที่ใช้ร่วมกัน, กล้อง/การควบคุมเริ่มต้น, และ post-processing
@pascal-app/editorเครื่องมือแก้ไข, แผงควบคุม, การเลือก, และ UI สำหรับการจัดการโดยตรง
@pascal-app/nodesปลั๊กอิน registry ที่มาพร้อมเครื่องมือพร้อม node definitions, renderers, geometry, และระบบ
apps/editorโฮสต์ Next.js แบบสแตนด์อโลนสำหรับแพ็กเกจ editor

viewer จะเรนเดอร์ฉากด้วยค่าเริ่มต้นที่เหมาะสม editor จะขยายความสามารถด้วยเครื่องมือแบบโต้ตอบ การจัดการการเลือก และความสามารถในการแก้ไข

Stores

แต่ละแพ็กเกจมี Zustand store ของตัวเองสำหรับจัดการสถานะ:

Storeแพ็กเกจความรับผิดชอบ
useScene@pascal-app/coreข้อมูลฉาก: nodes, root IDs, dirty nodes, การดำเนินการ CRUD บันทึกไปยัง IndexedDB พร้อม undo/redo ผ่าน Zundo
useViewer@pascal-app/viewerสถานะ viewer: การเลือกปัจจุบัน (building/level/zone IDs), โหมดการแสดงผลระดับ (stacked/exploded/solo), โหมดกล้อง
useEditorapps/editorสถานะ editor: เครื่องมือที่ใช้งานอยู่, การมองเห็นเลเยอร์โครงสร้าง, สถานะแผงควบคุม, การตั้งค่าเฉพาะ editor

รูปแบบการเข้าถึง:

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' })

แนวคิดหลัก

Nodes

Nodes คือข้อมูลพื้นฐานที่อธิบายฉาก 3 มิติ Nodes ทั้งหมดจะขยาย BaseNode:

typescript
BaseNode {
  id: string              // สร้างอัตโนมัติพร้อมคำนำหน้าประเภท (เช่น "wall_abc123")
  type: string            // ตัวแยกประเภทสำหรับการจัดการแบบ type-safe
  parentId: string | null // การอ้างอิงถึง node แม่
  visible: boolean
  camera?: Camera         // ตำแหน่งกล้องที่บันทึกไว้ (ไม่บังคับ)
  metadata?: JSON         // เมตาดาต้าตามอำเภอใจ (เช่น { isTransient: true })
}

ลำดับชั้นของ Node:

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

Nodes ถูกเก็บไว้ใน flat dictionary (Record<id, Node>) ไม่ใช่ tree แบบซ้อนกัน ความสัมพันธ์แบบพ่อ-ลูกถูกกำหนดผ่าน parentId และอาร์เรย์ children


สถานะฉาก (Zustand Store)

ฉากถูกจัดการโดย Zustand store ใน @pascal-app/core:

typescript
useScene.getState() = {
  nodes: Record<id, AnyNode>,  // Nodes ทั้งหมด
  rootNodeIds: string[],       // Nodes ระดับบนสุด (sites)
  dirtyNodes: Set<string>,     // Nodes ที่รอการอัปเดตระบบ

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

Middleware:

  • Persist - บันทึกไปยัง IndexedDB (ไม่รวม transient nodes)
  • Temporal (Zundo) - Undo/redo พร้อมประวัติ 50 ขั้นตอน

Scene Registry

registry จะแมป ID ของ node กับออบเจกต์ Three.js เพื่อการค้นหาที่รวดเร็ว:

typescript
sceneRegistry = {
  nodes: Map<id, Object3D>,    // ID → ออบเจกต์ 3D
  byType: {
    wall: Set<id>,
    item: Set<id>,
    zone: Set<id>,
    // ...
  }
}

Renderers จะลงทะเบียน refs ของตนโดยใช้ hook useRegistry:

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

สิ่งนี้ช่วยให้ระบบสามารถเข้าถึงออบเจกต์ 3D ได้โดยตรงโดยไม่ต้องสำรวจ scene graph


Node Renderers

Renderers คือส่วนประกอบ React ที่สร้างออบเจกต์ Three.js สำหรับแต่ละประเภทของ node:

code
SceneRenderer
└── NodeRenderer (dispatches by type)
    ├── BuildingRenderer
    ├── LevelRenderer
    ├── WallRenderer
    ├── SlabRenderer
    ├── ZoneRenderer
    ├── ItemRenderer
    └── ...

รูปแบบ:

  1. 1Renderer สร้าง mesh/group ตัวยึด
  2. 2ลงทะเบียนกับ useRegistry
  3. 3ระบบอัปเดต geometry ตามข้อมูล node

ตัวอย่าง (แบบง่าย):

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

  return (
    <mesh ref={ref}>
      <boxGeometry args={[0, 0, 0]} />  {/* ถูกแทนที่โดย WallSystem */}
      <meshStandardMaterial />
      {node.children.map(id => <NodeRenderer key={id} nodeId={id} />)}
    </mesh>
  )
}

Systems

Systems คือส่วนประกอบ React ที่ทำงานใน render loop (useFrame) เพื่ออัปเดต geometry และ transforms พวกมันประมวลผล dirty nodes ที่ถูกทำเครื่องหมายโดย store

Core Systems (ใน @pascal-app/core):

Systemความรับผิดชอบ
WallSystemสร้าง geometry ของผนังด้วย mitering และ CSG cutouts สำหรับประตู/หน้าต่าง
SlabSystemสร้าง geometry ของพื้นจาก polygons
CeilingSystemสร้าง geometry ของเพดาน
RoofSystemสร้าง geometry ของหลังคา
ItemSystemจัดตำแหน่ง items บนผนัง เพดาน หรือพื้น (slab elevation)

Viewer Systems (ใน @pascal-app/viewer):

Systemความรับผิดชอบ
LevelSystemจัดการการมองเห็นระดับและการจัดตำแหน่งแนวตั้ง (โหมด stacked/exploded/solo)
ScanSystemควบคุมการมองเห็นการสแกน 3D
GuideSystemควบคุมการมองเห็นภาพ guide

รูปแบบการประมวลผล:

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

    // อัปเดต geometry, transforms, ฯลฯ
    updateGeometry(obj, node)

    dirtyNodes.delete(id)
  }
})

Dirty Nodes

เมื่อ node เปลี่ยนแปลง มันจะถูกทำเครื่องหมายเป็น dirty ใน useScene.getState().dirtyNodes Systems จะตรวจสอบชุดนี้ในแต่ละเฟรมและจะคำนวณ geometry ใหม่เฉพาะสำหรับ dirty nodes เท่านั้น

typescript
// อัตโนมัติ: createNode, updateNode, deleteNode ทำเครื่องหมาย nodes ว่า dirty
useScene.getState().updateNode(wallId, { thickness: 0.2 })
// → wallId ถูกเพิ่มไปยัง dirtyNodes
// → WallSystem สร้าง geometry ใหม่ในเฟรมถัดไป
// → wallId ถูกลบออกจาก dirtyNodes

การทำเครื่องหมายด้วยตนเอง:

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

Event Bus

การสื่อสารระหว่างส่วนประกอบใช้ typed event emitter (mitt):

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

// Grid events (พื้นหลัง)
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

จัดการการตรวจจับการชนและการตรวจสอบตำแหน่ง:

typescript
spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation)
spatialGridManager.canPlaceOnWall(wallId, t, height, dimensions)
spatialGridManager.getSlabElevationAt(levelId, x, z)

ใช้โดยเครื่องมือจัดวาง item เพื่อตรวจสอบตำแหน่งและคำนวณระดับความสูงของ slab


สถาปัตยกรรม Editor

เอดิเตอร์ขยายความสามารถของวิวเวอร์ด้วย:

เครื่องมือ

เครื่องมือจะถูกเปิดใช้งานผ่านแถบเครื่องมือและจัดการอินพุตของผู้ใช้สำหรับการดำเนินการเฉพาะ:

  • SelectTool - การเลือกและการจัดการ
  • WallTool - วาดผนัง
  • ZoneTool - สร้างโซน
  • ItemTool - วางเฟอร์นิเจอร์/อุปกรณ์ติดตั้ง
  • SlabTool - สร้างแผ่นพื้น

ตัวจัดการการเลือก (Selection Manager)

เอดิเตอร์ใช้ตัวจัดการการเลือกแบบกำหนดเองพร้อมการนำทางแบบลำดับชั้น:

code
Site → Building → Level → Zone → Items

แต่ละระดับความลึกมีกลยุทธ์การเลือกของตัวเองสำหรับพฤติกรรมการโฮเวอร์/คลิก

ระบบเฉพาะของเอดิเตอร์

  • ZoneSystem - ควบคุมการมองเห็นโซนตามโหมดระดับ
  • การควบคุมกล้องแบบกำหนดเองพร้อมการโฟกัสโหนด

การไหลของข้อมูล (Data Flow)

code
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

การสร้างปลั๊กอิน

เอดิเตอร์สามารถขยายได้: ปลั๊กอินจะจัดส่งชนิดของโหนด (schema, การเรนเดอร์ 3D/2D, เครื่องมือจัดวาง, inspector parametrics) และแผงด้านซ้ายผ่าน Plugin manifest เดียวกันกับที่บิลต์อินใช้ — ไม่มี API ภายในแยกต่างหาก

  • คู่มือสำหรับนักพัฒนาสร้างปลั๊กอิน: รูปแบบของ Plugin, การมีส่วนร่วมของแผง, การค้นพบ, วงจรชีวิต และสิ่งที่อยู่ใน/นอก v1
  • ตัวอย่างการทำงานpascalorg/plugin-trees: ปลั๊กอินแบบสแตนด์อโลนที่มีต้นไม้, ดอกไม้, หญ้าแบบโปรซีเดอรัล และแผงพรีเซ็ต โคลนไปใช้เป็นจุดเริ่มต้นได้

เทคโนโลยีที่ใช้ (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)

การเริ่มต้นใช้งาน

การพัฒนา

รันเซิร์ฟเวอร์สำหรับการพัฒนาจาก root directory เพื่อเปิดใช้งาน hot reload สำหรับทุกแพ็กเกจ:

bash
# 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

สำคัญ: ควรรัน bun dev จาก root directory เสมอ เพื่อให้แน่ใจว่าตัวเฝ้าระวังแพ็กเกจกำลังทำงานอยู่ ซึ่งจะเปิดใช้งาน hot reload เมื่อคุณแก้ไขไฟล์ใน packages/core/src/ หรือ packages/viewer/src/

การสร้างสำหรับ Production

bash
# Build all packages
turbo build

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

การเผยแพร่แพ็กเกจ

bash
# 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

ไฟล์สำคัญ

Pathคำอธิบาย
packages/core/src/schema/คำจำกัดความชนิดของโหนด (Zod schemas)
packages/core/src/store/use-scene.tsScene state store
packages/core/src/hooks/scene-registry/3D object registry
packages/core/src/systems/ระบบสร้าง Geometry
packages/viewer/src/components/renderers/ตัวเรนเดอร์โหนด
packages/viewer/src/components/viewer/คอมโพเนนต์ Viewer หลัก
apps/editor/components/tools/เครื่องมือเอดิเตอร์
apps/editor/store/สถานะเฉพาะของเอดิเตอร์

ผู้ร่วมให้ข้อมูล

Aymeric Rabot Wassim Samad Sudhir


pascalorg/editor | Trendshift

เอกสารโปรเจกต์

อ่านเอกสารต้นฉบับ

README วิธีติดตั้ง วิธีใช้งาน และข้อกำหนดจาก repository ต้นฉบับ

ดูไฟล์บน GitHub

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 npm @pascal-app/viewer npm @pascal-app/cli Discord X (Twitter)

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:

bash
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. The npm package holds the CLI and that MCP service; the web editor runtime is downloaded once per version on the first command that starts the editor and verified against a digest published inside the package. Configure an agent to launch pascal mcp connect, which needs neither the editor process nor that download. Install the pascal command with npm install --global @pascal-app/cli. See Run Pascal locally for pnpm/Bun commands, project management, MCP setup, updates, storage paths, and troubleshooting.

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 with skills

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

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

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

text
/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. Its plugin root is this repository's skills/ directory, so an install copies only the skill bundles and their plugin metadata rather than the repository.

The plugin bundles two servers: the local pascal connector above and a hosted pascal-hosted server for https://editor.pascal.app/api/mcp, which prompts for an optional Pascal API key at enable time and stores it in the OS keychain. Leave the key empty to run local-only.

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:

bash
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:

bash
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 extension shipped inside those packages as the @pascal-app/core/capture and @pascal-app/viewer/capture subpaths:

bash
npm install @pascal-app/core @pascal-app/viewer @pascal-app/editor @pascal-app/nodes
typescript
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:

code
editor/
├── apps/
│   └── editor/          # Next.js application
├── packages/
│   ├── core/            # Schemas, scene state, registry contracts, capture contracts
│   ├── viewer/          # 3D rendering runtime, shared systems, capture runtime
│   ├── 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

PackageResponsibility
@pascal-app/coreNode schemas, scene state (Zustand), registry contracts, spatial queries, and event bus. core/capture adds versioned capture manifests, normalized streams, and transport-neutral static/live sources
@pascal-app/viewer3D rendering via React Three Fiber, shared render systems, default camera/controls, and post-processing. viewer/capture adds the capture runtime and reference model, device-motion, point-cloud, and surface-mesh layers
@pascal-app/editorEditing tools, panels, selection, and direct-manipulation UI
@pascal-app/nodesBuilt-in registry plugin with node definitions, renderers, geometry, and systems
@pascal-app/cliInstalls and manages a versioned standalone editor runtime and persistent local data
@pascal-app/mcpExposes scene tools, resources, prompts, and local storage to MCP-compatible AI hosts
apps/editorStandalone 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:

StorePackageResponsibility
useScene@pascal-app/coreScene data: nodes, root IDs, dirty nodes, CRUD operations. Persisted to IndexedDB with undo/redo via Zundo.
useViewer@pascal-app/viewerViewer state: current selection (building/level/zone IDs), level display mode (stacked/exploded/solo), camera mode.
useEditorapps/editorEditor 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:

code
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<id, AnyNode>,  // All nodes
  rootNodeIds: string[],       // Top-level nodes (sites)
  dirtyNodes: Set<string>,     // 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:

tsx
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:

code
SceneRenderer
└── NodeRenderer (dispatches by type)
    ├── BuildingRenderer
    ├── LevelRenderer
    ├── WallRenderer
    ├── SlabRenderer
    ├── ZoneRenderer
    ├── ItemRenderer
    └── ...

Pattern:

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

Example (simplified):

tsx
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):

SystemResponsibility
WallSystemGenerates wall geometry with mitering and CSG cutouts for doors/windows
SlabSystemGenerates floor geometry from polygons
CeilingSystemGenerates ceiling geometry
RoofSystemGenerates roof geometry
ItemSystemPositions items on walls, ceilings, or floors (slab elevation)

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

SystemResponsibility
LevelSystemHandles level visibility and vertical positioning (stacked/exploded/solo modes)
ScanSystemControls 3D scan visibility
GuideSystemControls 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:

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

Event Bus

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

typescript
// 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:

code
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

code
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 guideCreate a plugin: the Plugin shape, panel contributions, discovery, lifecycle, and what's in/out of v1.
  • Worked examplepascalorg/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:

bash
# 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

bash
# Build all packages
turbo build

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

Publishing Packages

Releases run from .github/workflows/release.yml (workflow_dispatch, with package, bump, and dry-run inputs). The workflow bumps versions, rewrites the internal @pascal-app/* ranges, builds, publishes in dependency order (coreviewereditornodesmcpifc-convertercli), then commits the release and pushes one tag per package. A dry run validates the builds without touching the registry.


Key Files

PathDescription
packages/core/src/schema/Node type definitions (Zod schemas)
packages/core/src/store/use-scene.tsScene 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.


Contributors

Aymeric Rabot Wassim Samad Sudhir


pascalorg/editor | Trendshift