在浏览器环境中构建一款功能完整的 3D 建筑编辑器,长期以来面临着渲染性能与交互响应之间的平衡难题。Pascal Editor 作为近期在 GitHub Trending 上获得关注的开源项目,展示了一套基于 React Three Fiber 和 WebGPU 的工程化解决方案。该项目采用 Turborepo 单体仓库架构,将核心渲染逻辑、场景状态管理与编辑器交互层进行清晰分离,形成了一套可维护、可扩展的技术架构。对于计划在 Web 端构建类似应用的开发者而言,其设计思路与实现细节具有较高的参考价值。
仓库架构与职责边界
Pascal Editor 的代码组织遵循明确的关注点分离原则,整个项目划分为三个核心包,每个包承担独立的职责范围。这种分层设计使得渲染逻辑与编辑业务逻辑得以解耦,为后续的功能迭代和技术选型调整预留了充足空间。
@pascal-app/core 是整个编辑器的基础层,负责定义场景数据的结构规范、状态管理以及几何生成系统。该包包含节点(Node)的 Zod schema 定义、场景状态的 Zustand 存储实现、几何生成系统的核心逻辑、空间查询工具以及事件总线。@pascal-app/viewer 则专注于 3D 渲染本身,基于 React Three Fiber 构建,提供默认的相机控制、场景渲染管线以及后处理效果。apps/editor 作为应用层,集成了所有编辑器特有的交互逻辑,包括各类工具(选择工具、墙体绘制工具、区域创建工具等)、选择管理机制以及 UI 组件。
这种分层架构的核心优势在于 viewer 包可以独立复用于纯展示场景,而 editor 包则通过继承和扩展 viewer 的能力来实现完整的编辑功能。当需要将渲染能力嵌入到其他应用(如产品配置器或可视化看板)时,只需引入 @pascal-app/viewer 即可,无需携带完整的编辑器负担。
场景状态管理的分层设计
3D 编辑器的状态管理需要在数据一致性与性能之间找到平衡点。Pascal Editor 采用了多 Zustand store 分层策略,为不同域的状态提供独立的存储空间。
useScene 存储于 @pascal-app/core 包中,管理所有场景数据的核心状态。该存储包含节点字典(Record<id, AnyNode>)、根节点 ID 列表、待处理脏节点集合以及标准的 CRUD 操作方法。场景数据以扁平化字典形式存储,而非嵌套的树形结构,节点间的父子关系通过 parentId 字段和 children 数组维护。这种设计使得状态序列化和反序列化变得简单直接,同时避免了深层嵌套带来的更新开销。useScene 还集成了两个重要的中间件:Persist 中间件负责将场景数据持久化至 IndexedDB,确保页面刷新后数据不丢失;Temporal 中间件(基于 Zundo)提供 50 步的撤销与重做能力。
useViewer 存储管理渲染器相关的视图状态,包括当前选中的建筑、楼层和区域 ID,楼层的显示模式(堆叠、爆炸、单独显示)以及相机模式。useEditor 存储则专注于编辑器特有的 UI 状态,如当前激活的工具、图层可见性配置和面板展开状态。这种分离设计使得状态订阅更加精确,React 组件仅需订阅其真正关心的状态片段,避免不必要的重渲染。
在状态访问模式上,项目同时支持组件内的响应式订阅和组件外的直接访问。组件内通过选择器函数订阅状态变化,而对于事件回调等非 React 上下文中需要修改状态的场景,则通过 useScene.getState () 等方式直接获取和修改状态,这种混合模式在保证响应式更新的同时,也为性能敏感操作提供了直接访问的通道。
节点系统与脏节点机制
节点是 Pascal Editor 描述 3D 场景的基本原语,所有节点类型都继承自 BaseNode 接口,包含自动生成的 ID(含类型前缀,如 "wall_abc123")、类型标识符、父节点引用、可见性标记以及可选的相机位置和元数据。节点的类型层次从 Site 开始向下延伸,依次为 Building、Level,然后是 Wall、Slab、Ceiling、Roof、Zone、Scan 和 Guide 等元素类型。
脏节点(dirty nodes)机制是该编辑器性能优化的关键设计。当节点数据发生变化时,节点 ID 会被添加到 useScene.getState ().dirtyNodes 集合中,而不是立即触发几何重建。专门的系统(System)组件会在每一帧的渲染循环中检查该集合,仅对标记为脏的节点执行几何计算。这种延迟处理策略避免了每次状态变更都触发全量重算,显著降低了渲染帧时间的波动。
项目中的核心系统包括 WallSystem(负责墙体几何生成,包含转角处理和门窗 CSG 布尔运算)、SlabSystem(从多边形生成楼板几何)、CeilingSystem(生成天花板几何)、RoofSystem(生成屋顶几何)以及 ItemSystem(将家具等元素定位到墙面、天花板或楼板上)。每个系统都是独立的 React 组件,通过 useFrame 钩子接入渲染循环,实现几何计算的按需执行。
场景注册表与快速查找
为了在系统层能够快速定位与节点对应的 Three.js 对象,项目引入了场景注册表(sceneRegistry)机制。该注册表维护两个核心数据结构:节点 ID 到 Object3D 实例的 Map 映射,以及按类型索引的 ID 集合。渲染器组件通过 useRegistry 钩子将其创建的 3D 对象注册到全局注册表中,使系统层可以直接通过节点 ID 获取到对应的 Three.js 对象引用,无需遍历整个场景图。
这种设计将数据的逻辑表示(节点)与渲染的物理表现(Object3D)进行了有效桥接。当 WallSystem 需要更新某个墙体的几何形状时,它首先通过 sceneRegistry.nodes.get (wallId) 获取到对应的 Mesh 对象,然后直接操作其 geometry 属性完成几何重建,整个过程无需经过 React 的重渲染流程。
渲染管线与交互设计
在渲染层面,项目使用 React Three Fiber 作为 React 与 Three.js 的粘合层,配合 Drei 提供的高级抽象简化开发。渲染器采用组件化的树形结构:SceneRenderer 作为根组件,内部通过 NodeRenderer 根据节点类型分发到具体的渲染器实现(如 BuildingRenderer、WallRenderer、SlabRenderer 等)。每个渲染器负责创建对应类型的 Mesh 或 Group,并使用 useRegistry 将其注册到场景注册表中。
编辑器的交互设计围绕工具(Tool)展开,不同的工具对应不同的用户输入处理逻辑。当前支持的核心工具包括 SelectTool(选择与变换)、WallTool(墙体绘制)、ZoneTool(区域创建)、ItemTool(家具布置)和 SlabTool(楼板创建)。工具通过事件总线(基于 mitt 的事件发射器)与系统其他部分通信,事件载荷包含节点信息、点击位置、本地坐标以及法线方向等完整上下文。
空间网格管理器(spatialGridManager)为放置操作提供碰撞检测和位置验证能力。其核心方法包括 canPlaceOnFloor(检查楼板上的位置是否可用)、canPlaceOnWall(检查墙面上的位置是否可用)以及 getSlabElevationAt(获取指定位置的楼板高程)。这些接口被 ItemTool 等工具调用,确保元素放置的合法性并自动计算合理的 elevations 值。
工程化选型与构建策略
项目的技术栈选型体现了对开发效率与运行性能的平衡考量。React 19 配合 Next.js 16 提供现代的 React 能力和服务端渲染支持;Three.js 的 WebGPU 渲染器为未来硬件加速做好准备;React Three Fiber 和 Drei 大幅降低了 Three.js 的使用门槛;Zustand 以极简的 API 提供了强大的状态管理能力;Zod 用于运行时类型验证,确保节点数据的合法性;three-bvh-csg 提供了高效的布尔几何运算能力,这对处理门窗开洞等场景至关重要;Turborepo 解决了单体仓库中的构建缓存和任务编排问题;Bun 作为包管理器提供了极快的安装和执行速度。
在开发流程上,项目要求始终从根目录执行 bun dev 命令,该命令会同时启动包的 watch 模式和 Next.js 开发服务器,确保对 core 和 viewer 包的修改能够自动触发整个项目的热更新。这种工作流设计使得在 monorepo 环境下进行多包协同开发变得顺畅自然。
Pascal Editor 的架构设计展示了构建浏览器端 3D 建筑编辑器的一套可行路径。通过将场景数据、渲染表现和交互逻辑进行清晰分离,结合脏节点按需更新和场景注册表快速查找等优化策略,该项目在保持代码可维护性的同时,也为实时交互场景下的性能表现提供了保障。
资料来源:Pascal Editor GitHub 仓库 (https://github.com/pascalorg/editor)