editor
集腋成裘,聚沙成塔。几秒钟虽然不长,却构成永恒长河中的伟大时代。——弗莱彻
Pascal Editor
有些编辑器喜欢写字,有些编辑器喜欢画画,而 Pascal Editor 更像一个戴着安全帽的建筑师:他不在纸上涂涂改改,他直接把你拽进三维空间里,抬手就是一堵墙,转身就是一块楼板,眨眼之间,一个 Site、一栋 Building、好几层 Level 就搭起来了——而且他跑在 React Three Fiber 与 WebGPU 的快车道上,动作利落,手感顺滑。
一句话就把他的人设钉死在门口:
A 3D building editor built with React Three Fiber and WebGPU.
视频入口也在 README 里(那是他给你递来的“看我施工”的短片):
https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b
住址(Homepage)
他把自己的工作室地址挂得很显眼:
这是一个 Turborepo 的“城市规划”:三大街区,各司其职
Pascal Editor 不把自己塞进一个大杂烩仓库里,他更像一个讲究分区管理的城市规划师——把自己做成了一个 Turborepo monorepo,核心结构清清楚楚:
1 | editor-v2/ |
它的世界里有三个主角:
- @pascal-app/core:管“规则、数据与秩序”
- @pascal-app/viewer:管“呈现、镜头与观感”
- apps/editor:管“工具、交互与编辑体验”
Separation of Concerns:谁负责什么,它写得明明白白
| Package | Responsibility |
|---|---|
| @pascal-app/core | Node schemas, scene state (Zustand), systems (geometry generation), spatial queries, event bus |
| @pascal-app/viewer | 3D rendering via React Three Fiber, default camera/controls, post-processing |
| apps/editor | UI components, tools, custom behaviors, editor-specific systems |
它甚至把关系说得很拟人:
- viewer:负责把场景“好好渲染出来”,给出 sensible defaults
- editor:在 viewer 的基础上“长出双手”,提供交互工具、选择管理与编辑能力
三个 Store:三个大脑,彼此配合
Pascal Editor 把状态管理拆得很干净:每个包都有自己的 Zustand store,各管一块地盘。
| Store | Package | Responsibility |
|---|---|---|
useScene |
@pascal-app/core |
Scene data: nodes, root IDs, dirty nodes, CRUD operations. Persisted to IndexedDB with undo/redo via Zundo. |
useViewer |
@pascal-app/viewer |
Viewer state: current selection (building/level/zone IDs), level display mode (stacked/exploded/solo), camera mode. |
useEditor |
apps/editor |
Editor state: active tool, structure layer visibility, panel states, editor-specific preferences. |
Access patterns:在 React 里订阅,在 React 外直接取
1 | // Subscribe to state changes (React component) |
这段代码就像他在说:
“想看我现在的状态?你可以坐在观众席(React)订阅,也可以走进后台(systems/callbacks)直接问我本人。”
Core Concepts:他是怎么把“建筑”变成“数据 + 系统 + 渲染”的
Nodes:3D 场景的原子单位
在 Pascal Editor 的世界里,一切都是 Node。所有 Node 都继承自 BaseNode:
1 | BaseNode { |
他给 Node 的身份感很强:
每个 Node 都有 id、type、parentId,能被看见(visible),还能把相机位置“记在心里”(camera),甚至可以藏点小秘密(metadata)。
Node Hierarchy:一座建筑是怎样长出来的
1 | Site |
但他又很务实:
Nodes 并不是一个嵌套树,而是一个 flat dictionary:Record<id, Node>
父子关系靠 parentId 与 children 数组来维系。
这像极了一个“施工总控台”:表格里每个工位都能查到,关系清清楚楚,不用到处爬树找节点。
Scene State(Zustand Store):场景的记忆与行动力
@pascal-app/core 用 Zustand 管场景:
1 | useScene.getState() = { |
并且它装了两层“记忆增强”中间件:
- Persist:保存到 IndexedDB(排除 transient nodes)
- Temporal(Zundo):undo/redo,50 步历史
也就是说:
他不仅会搭房子,还会记得你刚才怎么搭的;你后悔了,他能带你回到 50 步之前。
Scene Registry:不想在场景树里迷路?我直接给你地图
Registry 把 node id 映射到 Three.js 的 Object3D:
1 | sceneRegistry = { |
渲染器用 useRegistry 把自己的 ref 登记进去:
1 | const ref = useRef<Mesh>(null!) |
这就像他在说:
“别在场景图里慢慢找,我把每个物体的‘身份证’和‘位置’都登记了,系统要用,直接拿。”
Node Renderers:每一种 Node,都有自己的“造型师”
渲染结构大概是这样:
1 | SceneRenderer |
Pattern 非常明确:
- Renderer 先造一个 placeholder mesh/group
- 用
useRegistry注册 - 系统(Systems)根据 node 数据更新几何
简化版示例(WallRenderer):
1 | const WallRenderer = ({ node }) => { |
这个设计很像“先搭脚手架,再浇筑混凝土”:
渲染器先把结构摆好,真正的几何细节交给系统在渲染循环里生成与替换。
Systems:它们是“施工队”,在每一帧里把脏活累活干完
Systems 是在 render loop(useFrame)里跑的 React 组件,会处理 store 里标记为 dirty 的节点。
Core Systems(@pascal-app/core)
| System | Responsibility |
|---|---|
WallSystem |
Generates wall geometry with mitering and CSG cutouts for doors/windows |
SlabSystem |
Generates floor geometry from polygons |
CeilingSystem |
Generates ceiling geometry |
RoofSystem |
Generates roof geometry |
ItemSystem |
Positions items on walls, ceilings, or floors (slab elevation) |
Viewer Systems(@pascal-app/viewer)
| System | Responsibility |
|---|---|
LevelSystem |
Handles level visibility and vertical positioning (stacked/exploded/solo modes) |
ScanSystem |
Controls 3D scan visibility |
GuideSystem |
Controls guide image visibility |
Processing Pattern:只处理 dirty nodes
1 | useFrame(() => { |
它像一个超高效的队长:
“谁变了我就修谁,没变的别打扰我——每一帧都只干必须干的事。”
Dirty Nodes:哪里变了,我就把哪里点亮
当 Node 变化时,它会被标记 dirty,下一帧系统会重新算它的几何,然后清除标记。
1 | // Automatic: createNode, updateNode, deleteNode mark nodes dirty |
你也能手动标记:
1 | useScene.getState().dirtyNodes.add(wallId) |
Event Bus:组件之间不喊话,用“广播站”
Inter-component communication 用 typed event emitter(mitt):
1 | // Node events |
它就像一个很有礼貌的城市:
你点墙,墙发广播;你滑过物件,物件发广播;需要阻止冒泡,就 stopPropagation()。
Spatial Grid Manager:放不放得下,撞不撞得上,它来判
它负责 collision detection 与 placement validation:
1 | spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation) |
这个角色像一个严谨的监理:
“家具能不能摆这?尺寸会不会穿模?墙上挂得稳不稳?地面高度在哪?别猜,我来算。”
Editor Architecture:Editor 给 Viewer 装上“工具手套”
Editor 在 viewer 之上,主要带来三件大事:
Tools:工具栏里的“施工模式切换器”
- SelectTool - Selection and manipulation
- WallTool - Draw walls
- ZoneTool - Create zones
- ItemTool - Place furniture/fixtures
- SlabTool - Create floor slabs
它像一个工具箱,里面每一把工具都有自己的脾气:
选中、画墙、划分区域、摆放物件、铺楼板……你点谁,谁就接管你的鼠标与输入。
Selection Manager:选择也要讲层级礼仪
选择管理是分层的:
1 | Site → Building → Level → Zone → Items |
每一层都有自己的 hover/click 策略——像是在说:
“你要选东西,先看你站在哪一层;在不同的深度,选择���语义也不同。”
Editor-Specific Systems:它也有自己的后台队伍
ZoneSystem:根据 level mode 控制 zone visibility- 自定义相机控制(node focusing)
Data Flow:从你手指一动到几何更新,整套流水线像滚轮一样转起来
README 直接画了流程:
1 | User Action (click, drag) |
这是一条极其“工业化”的链路:
你操作 → 工具处理 → 数据变更 → 标记 dirty → 渲染器更新骨架 → 系统更新几何 → 清理 dirty。
Technology Stack:它的骨骼与肌肉都写在墙上
仓库里给出了两套(主 README 与 apps/editor README)略有差异的 stack 描述,但核心一致:它是一套围绕 React / Next / Three / WebGPU / Zustand 的“3D 编辑器工程体系”。
主 README 写的是:
- React 19 + Next.js 16
- Three.js (WebGPU renderer)
- React Three Fiber + Drei
- Zustand
- Zod
- Zundo
- three-bvh-csg
- Turborepo
- Bun
apps/editor README 里还写到:
- React 19 + Next.js 15
- three-bvh-csg
- (Getting Started 例子用 pnpm)
Getting Started:把他叫到本地开工
开发(推荐从根目录启动,热更新能照顾到所有包)
主 README 的开发方式(Bun):
1 | # Install dependencies |
它还反复强调一个习惯(像个认真的工头在喊):
Always run
bun devfrom the root directory … hot reload …packages/core/src/orpackages/viewer/src/.
apps/editor README 也给了 pnpm 的启动方式:
1 | # Install dependencies |
生产构建(Turborepo 开始发力)
1 | # Build all packages |
发布 packages(把 core/viewer 推到 npm)
1 | # Build packages |
Key Files:想快速熟悉代码,从这些入口钻进去
| Path | Description |
|---|---|
packages/core/src/schema/ |
Node type definitions (Zod schemas) |
packages/core/src/store/use-scene.ts |
Scene state store |
packages/core/src/hooks/scene-registry/ |
3D object registry |
packages/core/src/systems/ |
Geometry generation systems |
packages/viewer/src/components/renderers/ |
Node renderers |
packages/viewer/src/components/viewer/ |
Main Viewer component |
apps/editor/components/tools/ |
Editor tools |
apps/editor/store/ |
Editor-specific state |
这些路径就像这位“3D 建筑师”递给你的工地图纸:
你想看规则去 schema,你想看数据去 store,你想看渲染去 renderers,你想看工具去 tools。
@pascal-app/core:这位“地基工程师”也可以单独雇佣
仓库里 packages/core/README.md 把 core 单独介绍为:
Core library for Pascal 3D building editor.
Installation
1 | npm install @pascal-app/core |
Peer Dependencies
1 | npm install react three @react-three/fiber @react-three/drei |
What’s Included(core 的“职责清单”)
- Node Schemas(Zod)
- Scene State(Zustand + IndexedDB persist + undo/redo)
- Systems(walls/floors/ceilings/roofs)
- Scene Registry(id → Object3D)
- Spatial Grid(碰撞与放置校验)
- Event Bus(typed emitter)
- Asset Storage(IndexedDB file storage)
Usage:用 core 直接造一堵墙
1 | import { useScene, WallNode, ItemNode } from '@pascal-app/core' |
Node Types:core 里都有哪些“建筑积木”
SiteNodeBuildingNodeLevelNodeWallNodeSlabNodeCeilingNodeRoofNodeZoneNodeItemNodeScanNodeGuideNode
Systems:core 里的“几何施工队”
WallSystemSlabSystemCeilingSystemRoofSystemItemSystem
License
MIT
