pascalorg / editor
pascalorg / editor
Pascal Editor
A 3D building editor built with React Three Fiber and WebGPU. Pascal Editor 是一个基于 React Three Fiber 和 WebGPU 构建的 3D 建筑编辑器。
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>:
查看器运行时(Runtime)和内置节点定义是独立的包。请安装完整的内置查看器套件,并在挂载 <Viewer> 之前加载一次内置插件:
npm install @pascal-app/core @pascal-app/viewer @pascal-app/editor @pascal-app/nodes
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.
请参阅 @pascal-app/viewer 的快速入门以获取 React 示例。
Repository Architecture
This is a Turborepo monorepo with four main runtime packages: 这是一个采用 Turborepo 管理的 Monorepo,包含四个主要的运行时包:
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
| 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 |
职责分离
| 包 | 职责 |
|---|---|
@pascal-app/core | 节点模式、场景状态 (Zustand)、注册表契约、空间查询和事件总线 |
@pascal-app/viewer | 通过 React Three Fiber 进行 3D 渲染、共享渲染系统、默认相机/控制器及后期处理 |
@pascal-app/editor | 编辑工具、面板、选择功能及直接操作 UI |
@pascal-app/nodes | 包含节点定义、渲染器、几何体和系统的内置注册表插件 |
apps/editor | 编辑器包的独立 Next.js 宿主 |
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: 每个包都有其独立的 Zustand Store 来管理状态:
| 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. |
| Store | 包 | 职责 |
|---|---|---|
useScene | @pascal-app/core | 场景数据:节点、根 ID、脏节点 (dirty nodes)、CRUD 操作。通过 Zundo 持久化到 IndexedDB 并支持撤销/重做。 |
useViewer | @pascal-app/viewer | 查看器状态:当前选择(建筑/楼层/区域 ID)、楼层显示模式(堆叠/爆炸/独立)、相机模式。 |
useEditor | apps/editor | 编辑器状态:当前工具、结构层可见性、面板状态、编辑器特定偏好设置。 |
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:
核心概念:节点
节点是描述 3D 场景的数据原语。所有节点都继承自 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.
节点存储在扁平的字典 (Record<id, Node>) 中,而非嵌套树结构。父子关系通过 parentId 和 children 数组定义。
Scene State (Zustand Store)
The scene is managed by a Zustand store in @pascal-app/core:
场景状态 (Zustand Store)
场景由 @pascal-app/core 中的 Zustand Store 管理:
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
- 中间件:
- Persist - 保存到 IndexedDB(排除瞬态节点)
- Temporal (Zundo) - 50 步历史记录的撤销/重做
Scene Registry
The registry maps node IDs to their Three.js objects for fast lookup: 场景注册表 注册表将节点 ID 映射到其 Three.js 对象,以便快速查找:
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:
渲染器使用 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. 这允许系统直接访问 3D 对象,而无需遍历场景图。
Node Renderers
Renderers are React components that create Three.js objects for each node type: 节点渲染器 渲染器是为每种节点类型创建 Three.js 对象的 React 组件:
- Pattern: Renderer creates a placeholder mesh/group, registers it with
useRegistry, and systems update geometry based on node data. - 模式: 渲染器创建占位符网格/组,通过
useRegistry注册,系统根据节点数据更新几何体。
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.
系统
系统是在渲染循环 (useFrame) 中运行的 React 组件,用于更新几何体和变换。它们处理由 Store 标记的“脏节点”。
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.
脏节点:
当节点发生变化时,它会在 useScene.getState().dirtyNodes 中被标记为脏。系统每帧检查此集合,仅对脏节点重新计算几何体。