📌 项目地址:pascalorg/editor | ⭐ 16,310 颗星 | 🔧 TypeScript | 📜 MIT
这篇解读,我们不聊功能清单。我们聊聊这个拥有 16310 个 star 的 TypeScript 项目背后,几个关键的架构决策。这些决策让一个复杂的 3D 编辑器变得可维护,对构建任何有复杂状态和交互的大型应用都有启发。
为什么是单体仓库(Monorepo)?
Pascal Editor 没有把所有东西塞进一个巨大的项目。它用 Turborepo 组织了仓库,结构清晰:
editor-v2/
├── apps/
│ └── editor/ # Next.js 应用
├── packages/
│ ├── core/ # 核心逻辑
│ └── viewer/ # 3D 渲染
这解决了核心问题:3D 场景的核心数据与逻辑,应该和特定的 UI 和交互工具分离。
具体来说,三个部分各司其职:
@pascal-app/core包是心脏。它定义节点数据结构(Schema),用 Zustand 管理场景状态,负责几何生成这类“系统”操作,还提供空间查询和事件总线。它是纯逻辑,不关心画面怎么画。@pascal-app/viewer包是眼睛。它基于 React Three Fiber 实现 3D 渲染,提供合理的默认相机、控制器和后期处理。它读取core包的状态,把场景“看”出来。apps/editor是手和界面。它是 Next.js 应用,在viewer渲染的基础上,叠加了交互工具(比如画墙、选择物体)、编辑器 UI 面板和具体的行为逻辑。
这种分离的好处是,如果只想展示一个预设的 3D 场景,只引入 @pascal-app/core 和 @pascal-app/viewer 就够了,不需要整个编辑器。
状态管理:不是一座大山,而是三座小丘
一个编辑器最大的状态管理挑战是:不同部分关心的状态维度完全不同。硬塞在一个 store 里会变得一团糟。Pascal Editor 为三个包分别建立了独立的 Zustand Store,思路很清晰:
| 存储 | 位置 | 管什么 |
|---|---|---|
useScene |
@pascal-app/core |
场景的“事实”:所有节点(墙、楼板、楼层…)的原始数据、它们的增删改查。状态通过 IndexedDB 持久化,并且用 Zundo 库实现了撤销/重做。 |
useViewer |
@pascal-app/viewer |
场景的“观看”状态:当前选中的是哪栋楼、哪一层?楼层是堆叠显示、爆炸显示还是单独显示?相机是什么模式? |
useEditor |
apps/editor |
编辑器的“工具”状态:当前激活的工具(选择、绘制、移动…)、结构面板是否打开、具体的编辑偏好。 |
代码里的访问方式也体现了这种分层:
// React 组件里订阅特定片段
const nodes = useScene((state) => state.nodes)
const activeTool = useEditor((state) => state.tool)
// 在非 React 代码(如一个独立的处理函数)里直接读取或操作
const node = useScene.getState().nodes[id]
useViewer.getState().setSelection({ levelId: 'level_123' })
这避免了全局状态的混杂。viewer 和 editor 的 Store 都可以从 useScene 读取数据,但 core 的 Store 完全不知道 UI 的存在。这是单向数据流,易于理解和调试。
数据模型的“扁平化”设计
对于 3D 场景,最直观的数据结构可能是嵌套的树(Site 包含 Building,Building 包含 Level…)。但 Pascal Editor 选择了另一种方式:用扁平的字典(Record)存储所有节点。
每个节点的基础结构如下:
BaseNode {
id: string // 如 "wall_abc123",带类型前缀
type: string // "wall", "level", "building" 等
parentId: string | null // 指向父节点的 ID
visible: boolean
camera?: Camera // 可选,保存特定视角
metadata?: JSON // 可选,任意数据
}
节点间的父子关系(比如墙属于哪个楼层)通过 parentId 引用,形成逻辑上的树:
Site
└── Building
└── Level
├── Wall -> Item (门、窗)
├── Slab (楼板)
├── Ceiling -> Item (灯具)
...
这种扁平存储+ID 引用的模型,在编辑器场景下优势明显:
- 查找和更新单个节点极快:直接用 ID 在字典里取,O(1) 复杂度。如果要改一扇门的属性,不需要从 Site 一路向下查找。
- 跨节点关联查询高效:比如“找出所有属于 Level_1 的 Wall”,可以通过遍历所有
parentId是Level_1且type是wall的节点实现,这在空间索引等系统逻辑中很常见。 - 与状态管理库配合更好:Zustand 等库处理这种扁平、更新明确的数据结构非常高效,便于实现精确的响应式更新和撤销/重做。
写在最后
Pascal Editor 的价值,不仅在于它是一个能用的 3D 建筑编辑器。它作为一个开源项目,其源码展示了一套清晰、模块化的复杂应用构建方法。从仓库结构划分责任,到状态管理的精细分层,再到为性能优化的数据模型设计,每一步都有明确的工程考量。
如果你正在用 React Three Fiber 或类似的前端框架构建 3D 工具、数据可视化或复杂交互应用,这个项目的代码结构值得你花时间读一读。它提供的不是具体功能的代码片段,而是一个经过验证的、可靠的骨架。