在浏览器端构建功能完备的 3D 建筑编辑器是一项极具挑战性的工程任务。Pascal Editor 作为一个开源的 3D 建筑编辑器项目,采用了 React Three Fiber 作为渲染层,结合 Turborepo monorepo 架构和 Zustand 状态管理,实现了一套清晰的分层架构。本文将从工程实践角度深入分析其核心设计模式,为构建类似的 Web 端 3D 工具提供可落地的参考。

Monorepo 包结构与职责分离

Pascal Editor 采用了 Turborepo 管理 monorepo 结构,整个项目划分为三个核心包,每个包承担独立的职责。@pascal-app/core包负责最底层的节点 schema 定义、场景状态管理、几何生成系统、空间查询以及事件总线。这是整个编辑器的数据引擎,不包含任何渲染相关的代码。@pascal-app/viewer包则专注于 3D 渲染组件,提供基于 React Three Fiber 的渲染能力、默认相机配置和后处理效果。apps/editor包是实际的 Next.js 应用,整合了 UI 组件、各类编辑工具和编辑器特有的行为逻辑。

这种分离设计的核心价值在于清晰的分层边界。viewer 包负责 “只读” 渲染,不感知编辑器的交互模式;editor 包则在其基础上叠加编辑能力。当需要将相同的 3D 渲染能力复用到其他场景(如只读的模型预览器)时,可以直接使用 viewer 包而无需引入编辑器的复杂依赖。这种模式类似于后端开发中的领域驱动设计,通过明确边界来降低系统的耦合度。

扁平化节点模型与层级管理

传统的 3D 场景通常使用嵌套的树形结构来管理对象层级,但 Pascal Editor 采用了扁平化的字典存储方案。所有节点存储在一个Record<id, Node>结构中,父子关系通过每个节点的parentId字段和父节点的children数组来维护。这种设计的优势在于状态更新的原子性和查询的高效性。当修改某个节点的属性时,只需要更新字典中对应的一项,而不需要遍历整棵树去寻找该节点。

节点的类型体系采用了层次化的设计,从 Site(场地)到 Building(建筑)再到 Level(楼层),每一级都有其特定的属性。Level 下的子节点包括 Wall(墙体)、Slab(楼板)、Ceiling(天花)、Roof(屋顶)、Zone(区域)以及 Item(门窗等构件)。每种节点类型都有对应的 Zod schema 进行类型校验,确保数据的一致性。这种模式在前端工程中值得借鉴:通过严格的类型定义来约束业务数据模型,可以在运行时避免大量难以追踪的错误。

节点模型还支持可选的 camera 属性用于保存视角位置,以及 metadata 字段用于存储任意业务数据。例如某些临时节点可以标记{ isTransient: true },在持久化时自动排除。这种灵活的扩展机制使得节点模型能够适应不断演变的业务需求。

三层 Zustand 状态管理

Pascal Editor 定义了三个独立的 Zustand store,分别管理不同域的状态。useScene存储场景的核心数据,包括所有节点、脏节点标记以及 CRUD 操作。这个 store 使用了两个关键的中间件:Persist 中间件将数据持久化到 IndexedDB,支持离线编辑;Temporal 中间件(基于 Zundo)实现了 50 步的撤销重做功能。对于建筑编辑器这类需要频繁修改的应用,撤销重做几乎是必备功能,而将历史栈托管给专门的中间件可以大幅简化业务代码。

useViewer存储查看器相关的状态,如当前选中的 building、level 或 zone ID,层级的显示模式(堆叠、爆炸、单独显示),以及相机模式。这些状态只影响渲染表现,不影响底层数据。useEditor则管理编辑器特有的状态,包括当前激活的工具、结构层的可见性、面板状态等。这三个 store 的职责边界清晰,避免了单一 store 过于膨胀的问题。

Zustand 的另一个设计亮点是其灵活的访问模式。在 React 组件内部可以通过 hook 订阅状态变化,而在组件外部(如事件回调或系统逻辑中)可以通过store.getState()直接访问。这种模式使得状态既可以在响应式场景中使用,也可以在非响应式的系统逻辑中使用,减少了不必要的性能开销。

脏节点模式与系统渲染架构

性能优化是 3D 编辑器的核心挑战之一。Pascal Editor 采用了 “脏节点” 模式来解决这个问题:当节点数据发生变化时,不是立即重新计算几何形状,而是将该节点标记为 “脏”。在每一帧的渲染循环中,系统会检查脏节点集合,只对标记为脏的节点执行几何更新逻辑,更新完成后清除脏标记。

这种延迟计算策略在建筑编辑器中尤为有效。因为墙体、楼板等几何体的生成往往涉及较为复杂的 CSG(构造实体几何)运算,例如墙体需要在门窗位置进行布尔减法。如果每次属性变更都同步触发重新计算,用户在连续拖拽调整参数时会感受到明显的卡顿。通过脏节点模式,系统可以在 16 毫秒的帧预算内批量处理所有待更新对象,保持界面的流畅响应。

系统的几何生成逻辑封装在专门的 “系统” 组件中。WallSystem负责生成墙体几何,支持斜接(miter)和门窗的 CSG 开孔。SlabSystem根据多边形生成楼板,CeilingSystem生成天花,RoofSystem处理屋顶,ItemSystem负责将门窗等构件定位到墙体、楼板或天花上。每一类系统都是独立的渲染逻辑单元,通过注册到脏节点监听来实现按需更新。

Pascal Editor 使用 three-bvh-csg 库处理布尔运算,这是处理建筑开洞的标准方案。对于复杂的建筑模型,CSG 操作的性能可能成为瓶颈,项目中通过脏节点批处理来缓解这一问题。在实际工程中,如果场景规模进一步扩大,还可以考虑引入 Web Workers 将计算密集的几何生成移至后台线程。

事件总线与空间查询

编辑器中不同模块的通信采用了 mitt 事件总线。节点相关的交互事件(如wall:clickitem:enter)会携带完整的事件载荷,包括被点击的节点、点击位置、世界坐标和局部坐标,以及停止传播的方法。这种设计使得工具层可以灵活地监听任意事件而无需直接耦合。

空间查询管理器提供了放置验证的抽象接口。canPlaceOnFloor检查楼板上的位置是否可用,canPlaceOnWall检查墙体表面的位置,getSlabElevationAt计算指定坐标的楼板高程。这些查询被物品放置工具调用,用于实现吸附功能和高程计算。空间查询的抽象意味着底层可以用八叉树、包围盒或其他算法实现,为未来的性能优化留下了空间。

工程化实践与开发体验

Pascal Editor 使用 Bun 作为包管理器,配合 Turborepo 实现高效的增量构建。开发时需要在项目根目录执行bun dev,这样可以同时启动 packages 的 watch 模式和 Next.js 开发服务器。生产构建使用turbo build,会根据包的依赖关系自动进行增量构建。这种工作流在大型前端项目中已经相当成熟,可以显著缩短 CI/CD 时间和开发时的构建等待。

技术栈的选择体现了现代 Web 3D 开发的趋势:React 19 + Next.js 16 提供应用框架,Three.js 的 WebGPU 渲染器支持新一代图形 API,React Three Fiber 和 Drei 提供声明式的 3D 组件,Zod 进行运行时校验。值得关注的是项目已经前瞻性地引入了 WebGPU 支持,虽然目前 WebGPU 的浏览器兼容性仍在推进中,但这为未来的性能提升奠定了基础。

Pascal Editor 的架构为浏览器端 3D 建筑编辑器的开发提供了一个可复用的参考模板。其核心价值不在于某个具体功能的实现,而在于职责边界划分、状态管理模式和性能优化策略的组合。实际项目中可以根据具体需求调整节点类型、扩展系统逻辑,但分层架构和脏节点模式这类设计具有较强的通用性。


资料来源:Pascal Editor GitHub 仓库(https://github.com/pascalorg/editor)