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

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

职责分离

职责
@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 来管理状态:

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.
Store职责
useScene@pascal-app/core场景数据:节点、根 ID、脏节点 (dirty nodes)、CRUD 操作。通过 Zundo 持久化到 IndexedDB 并支持撤销/重做。
useViewer@pascal-app/viewer查看器状态:当前选择(建筑/楼层/区域 ID)、楼层显示模式(堆叠/爆炸/独立)、相机模式。
useEditorapps/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>) 中,而非嵌套树结构。父子关系通过 parentIdchildren 数组定义。


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 中被标记为脏。系统每帧检查此集合,仅对脏节点重新计算几何体。