กลับไปหน้า 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

A 3D building editor built with React Three Fiber and 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

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

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 four main runtime packages:

code
editor/
├── apps/
│   └── editor/          # Next.js application
├── packages/
│   ├── core/            # Schemas, scene state, and registry contracts
│   ├── viewer/          # 3D rendering runtime and shared systems
│   ├── editor/          # Editing tools and UI components
│   ├── nodes/           # Built-in node definitions, renderers, and systems
│   └── ui/              # Shared UI components

Separation of Concerns

PackageResponsibility
@pascal-app/coreNode schemas, scene state (Zustand), registry contracts, spatial queries, and event bus
@pascal-app/viewer3D rendering via React Three Fiber, shared render systems, default camera/controls, and post-processing
@pascal-app/editorEditing tools, panels, selection, and direct-manipulation UI
@pascal-app/nodesBuilt-in registry plugin with node definitions, renderers, geometry, and systems
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

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

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

Contributors

Aymeric Rabot Wassim Samad Sudhir


pascalorg/editor | Trendshift