pascalorg/editor
GitHub: pascalorg/editor
一款基于 React Three Fiber 和 WebGPU 构建的开源浏览器端 3D 建筑编辑器,支持在网页中创建、编辑和分享建筑项目。
Stars: 8840 | Forks: 1142
# Pascal 编辑器
一个使用 React Three Fiber 和 WebGPU 构建的 3D 建筑编辑器。
[](LICENSE)
[](https://www.npmjs.com/package/@pascal-app/core)
[](https://www.npmjs.com/package/@pascal-app/viewer)
[](https://discord.gg/SaBRA9t2)
[](https://x.com/pascal_app)
https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b
## 仓库架构
这是一个包含三个主要包的 Turborepo monorepo:
```
editor-v2/
├── apps/
│ └── editor/ # Next.js application
├── packages/
│ ├── core/ # Schema definitions, state management, systems
│ └── viewer/ # 3D rendering components
```
### 关注点分离
| 包 | 职责 |
|---------|---------------|
| **@pascal-app/core** | Node schema,场景状态 (Zustand),系统 (几何生成),空间查询,事件总线 |
| **@pascal-app/viewer** | 通过 React Three Fiber 进行 3D 渲染,默认相机/控件,后处理 |
| **apps/editor** | UI 组件,工具,自定义行为,编辑器专用系统 |
**viewer** 以合理的默认设置渲染场景。**editor** 则通过交互工具、选择管理和编辑功能对其进行扩展。
### 状态管理 (Stores)
每个包都有自己的 Zustand store 用于管理状态:
| Store | 包 | 职责 |
|-------|---------|----------------|
| `useScene` | `@pascal-app/core` | 场景数据:节点、根 ID、脏节点、CRUD 操作。通过 Zundo 实现撤销/重做,并持久化到 IndexedDB。 |
| `useViewer` | `@pascal-app/viewer` | 查看器状态:当前选择(建筑/楼层/区域 ID),楼层显示模式(堆叠/展开/独立),相机模式。 |
| `useEditor` | `apps/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 是描述 3D 场景的数据基元。所有 nodes 都继承自 `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 层级结构:**
```
Site
└── Building
└── Level
├── Wall → Item (doors, windows)
├── Slab
├── Ceiling → Item (lights)
├── Roof
├── Zone
├── Scan (3D reference)
└── Guide (2D reference)
```
Nodes 存储在一个**扁平字典** (`Record`) 中,而不是嵌套树。父子关系通过 `parentId` 和 `children` 数组来定义。
### 场景状态 (Zustand Store)
场景由 `@pascal-app/core` 中的一个 Zustand store 管理:
```
useScene.getState() = {
nodes: Record, // All nodes
rootNodeIds: string[], // Top-level nodes (sites)
dirtyNodes: Set, // Nodes pending system updates
createNode(node, parentId),
updateNode(id, updates),
deleteNode(id),
}
```
**中间件:**
- **Persist** - 保存到 IndexedDB(排除瞬态节点)
- **Temporal** (Zundo) - 支持 50 步历史记录的撤销/重做
### 场景注册表
注册表将 node ID 映射到其对应的 Three.js 对象,以便快速查找:
```
sceneRegistry = {
nodes: Map, // ID → 3D object
byType: {
wall: Set,
item: Set,
zone: Set,
// ...
}
}
```
渲染器使用 `useRegistry` hook 注册它们的 refs:
```
const ref = useRef(null!)
useRegistry(node.id, 'wall', ref)
```
这使得系统可以直接访问 3D 对象,而无需遍历场景图。
### Node 渲染器
渲染器是为每种 node 类型创建 Three.js 对象的 React 组件:
```
SceneRenderer
└── NodeRenderer (dispatches by type)
├── BuildingRenderer
├── LevelRenderer
├── WallRenderer
├── SlabRenderer
├── ZoneRenderer
├── ItemRenderer
└── ...
```
**模式:**
1. 渲染器创建一个占位网格/组
2. 使用 `useRegistry` 注册它
3. 系统根据 node 数据更新几何体
示例(简化版):
```
const WallRenderer = ({ node }) => {
const ref = useRef(null!)
useRegistry(node.id, 'wall', ref)
return (
{/* Replaced by WallSystem */}
{node.children.map(id => )}
)
}
```
### Systems
Systems 是在渲染循环 (`useFrame`) 中运行的 React 组件,用于更新几何体和变换。它们处理由 store 标记的**脏节点**。
**核心系统 (在 `@pascal-app/core` 中):**
| 系统 | 职责 |
|--------|---------------|
| `WallSystem` | 生成带有斜接和用于门/窗的 CSG 切割的墙体几何体 |
| `SlabSystem` | 根据多边形生成楼板几何体 |
| `CeilingSystem` | 生成天花板几何体 |
| `RoofSystem` | 生成屋顶几何体 |
| `ItemSystem` | 将物件放置在墙体、天花板或地板上(楼板标高) |
**查看器系统 (在 `@pascal-app/viewer` 中):**
| 系统 | 职责 |
|--------|---------------|
| `LevelSystem` | 处理楼层可见性和垂直定位(堆叠/展开/独立模式) |
| `ScanSystem` | 控制 3D 扫描的可见性 |
| `GuideSystem` | 控制引导图像的可见性 |
**处理模式:**
```
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)
}
})
```
### 脏节点
当一个 node 发生变化时,它会在 `useScene.getState().dirtyNodes` 中被标记为**脏节点**。系统每帧检查此集合,并且只重新计算脏节点的几何体。
```
// 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
```
**手动标记:**
```
useScene.getState().dirtyNodes.add(wallId)
```
### 事件总线
组件间的通信使用类型化的事件发射器:
```
// 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
}
```
### 空间网格管理器
处理碰撞检测和放置验证:
```
spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation)
spatialGridManager.canPlaceOnWall(wallId, t, height, dimensions)
spatialGridManager.getSlabElevationAt(levelId, x, z)
```
供物件放置工具用于验证位置和计算楼板标高。
## 编辑器架构
编辑器通过以下方式扩展了查看器:
### 工具
工具通过工具栏激活,并处理特定操作的用户输入:
- **SelectTool** - 选择和操作
- **WallTool** - 绘制墙体
- **ZoneTool** - 创建区域
- **ItemTool** - 放置家具/固定设施
- **SlabTool** - 创建楼板
### 选择管理器
编辑器使用带有层级导航的自定义选择管理器:
```
Site → Building → Level → Zone → Items
```
每个深度级别都有其自己的悬停/点击行为选择策略。
### 编辑器专用系统
- `ZoneSystem` - 根据楼层模式控制区域可见性
- 带有节点聚焦的自定义相机控件
## 数据流
```
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
```
## 技术栈
- **React 19** + **Next.js 16**
- **Three.js** (WebGPU 渲染器)
- **React Three Fiber** + **Drei**
- **Zustand** (状态管理)
- **Zod** (schema 验证)
- **Zundo** (撤销/重做)
- **three-bvh-csg** (布尔几何操作)
- **Turborepo** (monorepo 管理)
- **Bun** (包管理器)
## 入门指南
### 开发
从**根目录**运行开发服务器,以为所有包启用热重载:
```
# 安装依赖
bun install
# 运行开发服务器 (构建 packages + 以 watch 模式启动编辑器)
bun dev
# 这将会:
# 1. 构建 @pascal-app/core 和 @pascal-app/viewer
# 2. 开始监听这两个 packages 的更改
# 3. 启动 Next.js 编辑器 dev server
# 打开 http://localhost:3000
```
**重要提示:** 始终从根目录运行 `bun dev`,以确保包监视器正在运行。这将在你修改 `packages/core/src/` 或 `packages/viewer/src/` 中的文件时启用热重载。
### 生产环境构建
```
# 构建所有 packages
turbo build
# 构建特定 package
turbo build --filter=@pascal-app/core
```
### 发布包
```
# 构建 packages
turbo build --filter=@pascal-app/core --filter=@pascal-app/viewer
# 发布到 npm
npm publish --workspace=@pascal-app/core --access public
npm publish --workspace=@pascal-app/viewer --access public
```
## 关键文件
| 路径 | 描述 |
|------|-------------|
| `packages/core/src/schema/` | Node 类型定义 |
| `packages/core/src/store/use-scene.ts` | 场景状态 store |
| `packages/core/src/hooks/scene-registry/` | 3D 对象注册表 |
| `packages/core/src/systems/` | 几何体生成系统 |
| `packages/viewer/src/components/renderers/` | Node 渲染器 |
| `packages/viewer/src/components/viewer/` | 主 Viewer 组件 |
| `apps/editor/components/tools/` | 编辑器工具 |
| `apps/editor/store/` | 编辑器特定状态 |
## 贡献者

标签:3D建模, IndexedDB, MIT开源, monorepo, Node Schema, React Three Fiber, Turborepo, UI组件库, WebGPU, Zustand, 三维可视化, 交互工具, 几何生成, 前端工程, 协同编辑, 场景图, 建筑编辑器, 建筑设计, 状态管理, 空间查询, 自动化攻击