pascalorg/editor
摘要
一个使用 React Three Fiber 和 WebGPU 构建的3D建筑编辑器,采用 Turborepo 单体仓库结构,包含 core、viewer 和 editor 包,分别用于模式定义、3D渲染和交互工具。
查看缓存全文
缓存时间: 2026/05/19 12:34
pascalorg/editor 源码:https://github.com/pascalorg/editor
Pascal 编辑器
使用 React Three Fiber 和 WebGPU 构建的 3D 建筑编辑器。
npm @pascal-app/core (https://www.npmjs.com/package/@pascal-app/core) npm @pascal-app/viewer (https://www.npmjs.com/package/@pascal-app/viewer) Discord (https://discord.gg/SaBRA9t2) X (Twitter) (https://x.com/pascal_app)
https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b
仓库架构
这是一个 Turborepo 单仓库,包含三个主要包:
editor-v2/
├── apps/
│ └── editor/ # Next.js 应用
├── packages/
│ ├── core/ # schema 定义、状态管理、系统
│ └── viewer/ # 3D 渲染组件
关注点分离
| 包 | 职责 |
|---|---|
| @pascal-app/core | 节点 schema、场景状态(Zustand)、系统(几何体生成)、空间查询、事件总线 |
| @pascal-app/viewer | 通过 React Three Fiber 进行 3D 渲染、默认相机/控制器、后期处理 |
| apps/editor | UI 组件、工具、自定义行为、编辑器特定系统 |
viewer 使用合理的默认设置渲染场景。editor 通过交互工具、选择管理和编辑功能对其进行扩展。
存储
每个包都有自己用于管理状态的 Zustand store:
| Store | 包 | 职责 |
|---|---|---|
useScene | @pascal-app/core | 场景数据:节点、根节点 ID、脏节点、CRUD 操作。持久化到 IndexedDB,通过 Zundo 实现撤销/重做。 |
useViewer | @pascal-app/viewer | 查看器状态:当前选择(建筑/楼层/区域 ID)、楼层显示模式(堆叠/展开/单独)、相机模式。 |
useEditor | apps/editor | 编辑器状态:当前工具、结构层可见性、面板状态、编辑器特定偏好。 |
访问模式:
// 在 React 组件中订阅状态变化
const nodes = useScene((state) => state.nodes)
const levelId = useViewer((state) => state.selection.levelId)
const activeTool = useEditor((state) => state.tool)
// 在 React 外部访问状态(回调、系统)
const node = useScene.getState().nodes[id]
useViewer.getState().setSelection({ levelId: 'level_123' })
核心概念
节点
节点是描述 3D 场景的数据基元。所有节点都扩展自 BaseNode:
BaseNode {
id: string // 自动生成,带类型前缀(例如 "wall_abc123")
type: string // 用于类型安全处理的判别器
parentId: string | null // 父节点引用
visible: boolean
camera?: Camera // 可选的已保存相机位置
metadata?: JSON // 任意元数据(例如 { isTransient: true })
}
节点层级:
场地
└── 建筑
└── 楼层
├── 墙 → 构件(门、窗)
├── 楼板
├── 天花板 → 构件(灯具)
├── 屋顶
├── 区域
├── 扫描(3D 参考)
└── 引导(2D 参考)
节点存储在扁平字典(Record)中,而不是嵌套树。父子关系通过 parentId 和 children 数组定义。
场景状态(Zustand Store)
场景由 @pascal-app/core 中的 Zustand store 管理:
useScene.getState() = {
nodes: Record<string, AnyNode>, // 所有节点
rootNodeIds: string[], // 顶层节点(场地)
dirtyNodes: Set<string>, // 等待系统更新的节点
createNode(node, parentId),
updateNode(id, updates),
deleteNode(id),
}
中间件:
- Persist - 保存到 IndexedDB(排除瞬态节点)
- Temporal (Zundo) - 撤销/重做,最多 50 步历史记录
场景注册表
注册表将节点 ID 映射到其 Three.js 对象,以实现快速查找:
sceneRegistry = {
nodes: Map<string, THREE.Object3D>, // ID → 3D 对象
byType: {
wall: Set<THREE.Object3D>,
item: Set<THREE.Object3D>,
zone: Set<THREE.Object3D>,
// ...
}
}
渲染器使用 useRegistry hook 注册其 ref:
const ref = useRef<THREE.Mesh>(null!)
useRegistry(node.id, 'wall', ref)
这使得系统可以直接访问 3D 对象,而无需遍历场景图。
节点渲染器
渲染器是 React 组件,为每种节点类型创建 Three.js 对象:
SceneRenderer
└── NodeRenderer(根据类型分发)
├── BuildingRenderer
├── LevelRenderer
├── WallRenderer
├── SlabRenderer
├── ZoneRenderer
├── ItemRenderer
└── ...
模式:
- 渲染器创建一个占位网格/组
- 使用
useRegistry注册 - 系统根据节点数据更新几何体
简化示例:
const WallRenderer = ({ node }: { node: WallNode }) => {
const ref = useRef<THREE.Mesh>(null!)
useRegistry(node.id, 'wall', ref)
return (
<mesh ref={ref}>
{/* 由 WallSystem 替换 */}
<boxGeometry args={[1, 1, 1]} />
{node.children.map(id => <NodeRenderer key={id} id={id} />)}
</mesh>
)
}
系统
系统是在渲染循环(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]
// 更新几何体、变换等
updateGeometry(obj, node)
dirtyNodes.delete(id)
}
})
脏节点
当节点发生变化时,它会被标记为 useScene.getState().dirtyNodes 中的脏节点。系统每帧检查此集合,仅重新计算脏节点的几何体。
// 自动:createNode、updateNode、deleteNode 将节点标记为脏
useScene.getState().updateNode(wallId, { thickness: 0.2 })
// → wallId 被添加到 dirtyNodes
// → WallSystem 在下一帧重新生成几何体
// → wallId 从 dirtyNodes 中移除
手动标记:
useScene.getState().dirtyNodes.add(wallId)
事件总线
组件间通信使用类型化的事件发射器(mitt):
// 节点事件
emitter.on('wall:click', (event) => { ... })
emitter.on('item:enter', (event) => { ... })
emitter.on('zone:context-menu', (event) => { ... })
// 网格事件(背景)
emitter.on('grid:click', (event) => { ... })
// 事件载荷
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 - 创建楼板
选择管理器
编辑器使用自定义选择管理器,具有层级导航:
场地 → 建筑 → 楼层 → 区域 → 构件
每个深度级别都有自己的悬停/点击行为选择策略。
编辑器特定系统
ZoneSystem- 根据楼层模式控制区域可见性- 带有节点聚焦的自定义相机控制
数据流
用户操作(点击、拖拽)
↓
工具处理程序
↓
useScene.createNode() / updateNode()
↓
节点被添加/更新到 store
节点被标记为脏
↓
React 重新渲染
NodeRenderer
useRegistry() 注册 3D 对象
↓
系统检测到脏节点(useFrame)
通过 sceneRegistry 更新几何体
清除脏标志
技术栈
- React 19 + Next.js 16
- Three.js (WebGPU 渲染器)
- React Three Fiber + Drei
- Zustand (状态管理)
- Zod (schema 验证)
- Zundo (撤销/重做)
- three-bvh-csg (布尔几何体运算)
- Turborepo (单仓库管理)
- Bun (包管理器)
入门指南
开发
从根目录运行开发服务器,以启用所有包的热重载:
# 安装依赖
bun install
# 运行开发服务器(构建包 + 以监听模式启动编辑器)
bun dev
# 此操作将:
# 1. 构建 @pascal-app/core 和 @pascal-app/viewer
# 2. 开始监听这两个包的更改
# 3. 启动 Next.js 编辑器开发服务器
# 打开 http://localhost:3000
重要: 始终从根目录运行 bun dev,以确保包监听器正在运行。这样,当您编辑 packages/core/src/ 或 packages/viewer/src/ 中的文件时,可以启用热重载。
构建生产版本
# 构建所有包
turbo build
# 构建特定包
turbo build --filter=@pascal-app/core
发布包
# 构建包
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/ | 节点类型定义(Zod schemas) |
packages/core/src/store/use-scene.ts | 场景状态 store |
packages/core/src/hooks/scene-registry/ | 3D 对象注册表 |
packages/core/src/systems/ | 几何体生成系统 |
packages/viewer/src/components/renderers/ | 节点渲染器 |
packages/viewer/src/components/viewer/ | 主查看器组件 |
apps/editor/components/tools/ | 编辑器工具 |
apps/editor/store/ | 编辑器特定状态 |
贡献者
相似文章
Show HN:一个CSS 3D引擎(无需WebGL)
PolyCSS 是一个 CSS 多边形网格库,能将 3D 模型渲染为真实的 HTML 元素(使用 CSS matrix3d),支持 OBJ/MTL、GLB 和 VOX 格式,可与 React、Vue 或原生 JavaScript 配合使用。
slash-editor
slash-editor 是一款开源的、Notion 风格的 React 区块编辑器,基于无头架构的 Tiptap v3 内核构建,并通过 shadcn registry 分发,组件直接存放在你的代码仓库中。它提供斜杠菜单、嵌套拖拽、评论、实时协作、表格、Mermaid、Markdown 导入导出以及自带 AI 等功能,全部采用 MIT 协议授权,且没有任何托管服务。
playcanvas/supersplat
SuperSplat 是一款免费、开源的浏览器端编辑器,由 PlayCanvas 基于 Web 技术构建,用于检查、编辑、优化和发布 3D Gaussian Splat。无需安装,可直接在 superspl.at/editor 在线使用。
Online 3D Editor
Online 3D Editor是一个基于网页的工具,可在浏览器中直接提供Blender风格的3D编辑功能。
@servasyy_ai: 3D开发者多年来苦练的物理演算,仅凭一句话就能搞定。 从英语指令生成复杂的3D粒子效果,并能立即输出React或three.js代码的免费工具问世。 数学与物理引擎的壁垒消失,即时将创意转化为实体的时代到来。 体验
一款免费工具发布,能够根据英语指令生成复杂的3D粒子效果,并直接输出React或three.js代码,降低了3D物理演算的门槛。