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
https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b
การใช้งานแพ็กเกจที่เผยแพร่
รันไทม์ของ viewer และ node definitions ที่มาพร้อมเครื่องมือจะถูกแยกเป็นแพ็กเกจต่างหาก ติดตั้งชุด viewer ที่มาพร้อมเครื่องมือทั้งหมด จากนั้นโหลดปลั๊กอินที่มาพร้อมเครื่องมือหนึ่งครั้งก่อนที่จะ mount <Viewer>:
npm install @pascal-app/core @pascal-app/viewer @pascal-app/editor @pascal-app/nodesimport { loadPlugin } from '@pascal-app/core'
import { builtinPlugin } from '@pascal-app/nodes'
await loadPlugin(builtinPlugin) ดู @pascal-app/viewer quick start สำหรับตัวอย่าง React
สถาปัตยกรรมของ Repository
นี่คือ Turborepo monorepo ที่มีแพ็กเกจรันไทม์หลักสี่แพ็กเกจ:
editor/
├── apps/
│ └── editor/ # แอปพลิเคชัน Next.js
├── packages/
│ ├── core/ # Schemas, สถานะฉาก, และ registry contracts
│ ├── viewer/ # รันไทม์การเรนเดอร์ 3D และระบบที่ใช้ร่วมกัน
│ ├── editor/ # เครื่องมือแก้ไขและส่วนประกอบ UI
│ ├── nodes/ # Node definitions, renderers, และระบบที่มาพร้อมเครื่องมือ
│ └── ui/ # ส่วนประกอบ UI ที่ใช้ร่วมกันการแยกความรับผิดชอบ
| แพ็กเกจ | ความรับผิดชอบ |
|---|---|
| @pascal-app/core | Node 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), โหมดกล้อง |
useEditor | apps/editor | สถานะ editor: เครื่องมือที่ใช้งานอยู่, การมองเห็นเลเยอร์โครงสร้าง, สถานะแผงควบคุม, การตั้งค่าเฉพาะ editor |
รูปแบบการเข้าถึง:
// 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:
BaseNode {
id: string // สร้างอัตโนมัติพร้อมคำนำหน้าประเภท (เช่น "wall_abc123")
type: string // ตัวแยกประเภทสำหรับการจัดการแบบ type-safe
parentId: string | null // การอ้างอิงถึง node แม่
visible: boolean
camera?: Camera // ตำแหน่งกล้องที่บันทึกไว้ (ไม่บังคับ)
metadata?: JSON // เมตาดาต้าตามอำเภอใจ (เช่น { isTransient: true })
}ลำดับชั้นของ Node:
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:
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 เพื่อการค้นหาที่รวดเร็ว:
sceneRegistry = {
nodes: Map<id, Object3D>, // ID → ออบเจกต์ 3D
byType: {
wall: Set<id>,
item: Set<id>,
zone: Set<id>,
// ...
}
} Renderers จะลงทะเบียน refs ของตนโดยใช้ hook useRegistry:
const ref = useRef<Mesh>(null!)
useRegistry(node.id, 'wall', ref)สิ่งนี้ช่วยให้ระบบสามารถเข้าถึงออบเจกต์ 3D ได้โดยตรงโดยไม่ต้องสำรวจ scene graph
Node Renderers
Renderers คือส่วนประกอบ React ที่สร้างออบเจกต์ Three.js สำหรับแต่ละประเภทของ node:
SceneRenderer
└── NodeRenderer (dispatches by type)
├── BuildingRenderer
├── LevelRenderer
├── WallRenderer
├── SlabRenderer
├── ZoneRenderer
├── ItemRenderer
└── ...รูปแบบ:
- 1Renderer สร้าง mesh/group ตัวยึด
- 2ลงทะเบียนกับ
useRegistry - 3ระบบอัปเดต geometry ตามข้อมูล node
ตัวอย่าง (แบบง่าย):
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 |
รูปแบบการประมวลผล:
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 เท่านั้น
// อัตโนมัติ: createNode, updateNode, deleteNode ทำเครื่องหมาย nodes ว่า dirty
useScene.getState().updateNode(wallId, { thickness: 0.2 })
// → wallId ถูกเพิ่มไปยัง dirtyNodes
// → WallSystem สร้าง geometry ใหม่ในเฟรมถัดไป
// → wallId ถูกลบออกจาก dirtyNodesการทำเครื่องหมายด้วยตนเอง:
useScene.getState().dirtyNodes.add(wallId)Event Bus
การสื่อสารระหว่างส่วนประกอบใช้ typed event emitter (mitt):
// 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
จัดการการตรวจจับการชนและการตรวจสอบตำแหน่ง:
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)
เอดิเตอร์ใช้ตัวจัดการการเลือกแบบกำหนดเองพร้อมการนำทางแบบลำดับชั้น:
Site → Building → Level → Zone → Itemsแต่ละระดับความลึกมีกลยุทธ์การเลือกของตัวเองสำหรับพฤติกรรมการโฮเวอร์/คลิก
ระบบเฉพาะของเอดิเตอร์
ZoneSystem- ควบคุมการมองเห็นโซนตามโหมดระดับ- การควบคุมกล้องแบบกำหนดเองพร้อมการโฟกัสโหนด
การไหลของข้อมูล (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การสร้างปลั๊กอิน
เอดิเตอร์สามารถขยายได้: ปลั๊กอินจะจัดส่งชนิดของโหนด (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 สำหรับทุกแพ็กเกจ:
# 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
# Build all packages
turbo build
# Build specific package
turbo build --filter=@pascal-app/coreการเผยแพร่แพ็กเกจ
# 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.ts | Scene 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/ | สถานะเฉพาะของเอดิเตอร์ |
ผู้ร่วมให้ข้อมูล
เอกสารโปรเจกต์
อ่านเอกสารต้นฉบับ
README วิธีติดตั้ง วิธีใช้งาน และข้อกำหนดจาก repository ต้นฉบับ
Pascal Editor
A 3D building editor built with React Three Fiber and WebGPU.
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>:
npm install @pascal-app/core @pascal-app/viewer @pascal-app/editor @pascal-app/nodesimport { 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:
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 componentsSeparation 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/editor | Editing tools, panels, selection, and direct-manipulation UI |
| @pascal-app/nodes | Built-in registry plugin with node definitions, renderers, geometry, and systems |
| 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:
// 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:
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:
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:
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:
- 1Renderer creates a placeholder mesh/group
- 2Registers it with
useRegistry - 3Systems 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:
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.
// 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 dirtyNodesManual 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:
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 → ItemsEach 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 flagBuilding 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
Pluginshape, 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:3002Important: 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/corePublishing 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 publicKey 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 |
Contributors
