pascalorg/editor

GitHub: pascalorg/editor

一款基于 React Three Fiber 和 WebGPU 构建的开源浏览器端 3D 建筑编辑器,支持在网页中创建、编辑和分享建筑项目。

Stars: 8840 | Forks: 1142

# Pascal 编辑器 一个使用 React Three Fiber 和 WebGPU 构建的 3D 建筑编辑器。 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![npm @pascal-app/core](https://img.shields.io/npm/v/@pascal-app/core?label=%40pascal-app%2Fcore)](https://www.npmjs.com/package/@pascal-app/core) [![npm @pascal-app/viewer](https://img.shields.io/npm/v/@pascal-app/viewer?label=%40pascal-app%2Fviewer)](https://www.npmjs.com/package/@pascal-app/viewer) [![Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white)](https://discord.gg/SaBRA9t2) [![X (Twitter)](https://img.shields.io/badge/follow-%40pascal__app-black?logo=x&logoColor=white)](https://x.com/pascal_app) https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b ## 仓库架构 这是一个包含三个主要包的 Turborepo monorepo: ``` editor-v2/ ├── apps/ │ └── editor/ # Next.js application ├── packages/ │ ├── core/ # Schema definitions, state management, systems │ └── viewer/ # 3D rendering components ``` ### 关注点分离 | 包 | 职责 | |---------|---------------| | **@pascal-app/core** | Node schema,场景状态 (Zustand),系统 (几何生成),空间查询,事件总线 | | **@pascal-app/viewer** | 通过 React Three Fiber 进行 3D 渲染,默认相机/控件,后处理 | | **apps/editor** | UI 组件,工具,自定义行为,编辑器专用系统 | **viewer** 以合理的默认设置渲染场景。**editor** 则通过交互工具、选择管理和编辑功能对其进行扩展。 ### 状态管理 (Stores) 每个包都有自己的 Zustand store 用于管理状态: | Store | 包 | 职责 | |-------|---------|----------------| | `useScene` | `@pascal-app/core` | 场景数据:节点、根 ID、脏节点、CRUD 操作。通过 Zundo 实现撤销/重做,并持久化到 IndexedDB。 | | `useViewer` | `@pascal-app/viewer` | 查看器状态:当前选择(建筑/楼层/区域 ID),楼层显示模式(堆叠/展开/独立),相机模式。 | | `useEditor` | `apps/editor` | 编辑器状态:活动工具,结构图层可见性,面板状态,编辑器特定偏好设置。 | **访问模式:** ``` // Subscribe to state changes (React component) const nodes = useScene((state) => state.nodes) const levelId = useViewer((state) => state.selection.levelId) const activeTool = useEditor((state) => state.tool) // Access state outside React (callbacks, systems) const node = useScene.getState().nodes[id] useViewer.getState().setSelection({ levelId: 'level_123' }) ``` ## 核心概念 ### Nodes Nodes 是描述 3D 场景的数据基元。所有 nodes 都继承自 `BaseNode`: ``` BaseNode { id: string // Auto-generated with type prefix (e.g., "wall_abc123") type: string // Discriminator for type-safe handling parentId: string | null // Parent node reference visible: boolean camera?: Camera // Optional saved camera position metadata?: JSON // Arbitrary metadata (e.g., { isTransient: true }) } ``` **Node 层级结构:** ``` Site └── Building └── Level ├── Wall → Item (doors, windows) ├── Slab ├── Ceiling → Item (lights) ├── Roof ├── Zone ├── Scan (3D reference) └── Guide (2D reference) ``` Nodes 存储在一个**扁平字典** (`Record`) 中,而不是嵌套树。父子关系通过 `parentId` 和 `children` 数组来定义。 ### 场景状态 (Zustand Store) 场景由 `@pascal-app/core` 中的一个 Zustand store 管理: ``` useScene.getState() = { nodes: Record, // All nodes rootNodeIds: string[], // Top-level nodes (sites) dirtyNodes: Set, // Nodes pending system updates createNode(node, parentId), updateNode(id, updates), deleteNode(id), } ``` **中间件:** - **Persist** - 保存到 IndexedDB(排除瞬态节点) - **Temporal** (Zundo) - 支持 50 步历史记录的撤销/重做 ### 场景注册表 注册表将 node ID 映射到其对应的 Three.js 对象,以便快速查找: ``` sceneRegistry = { nodes: Map, // ID → 3D object byType: { wall: Set, item: Set, zone: Set, // ... } } ``` 渲染器使用 `useRegistry` hook 注册它们的 refs: ``` const ref = useRef(null!) useRegistry(node.id, 'wall', ref) ``` 这使得系统可以直接访问 3D 对象,而无需遍历场景图。 ### Node 渲染器 渲染器是为每种 node 类型创建 Three.js 对象的 React 组件: ``` SceneRenderer └── NodeRenderer (dispatches by type) ├── BuildingRenderer ├── LevelRenderer ├── WallRenderer ├── SlabRenderer ├── ZoneRenderer ├── ItemRenderer └── ... ``` **模式:** 1. 渲染器创建一个占位网格/组 2. 使用 `useRegistry` 注册它 3. 系统根据 node 数据更新几何体 示例(简化版): ``` const WallRenderer = ({ node }) => { const ref = useRef(null!) useRegistry(node.id, 'wall', ref) return ( {/* Replaced by WallSystem */} {node.children.map(id => )} ) } ``` ### Systems Systems 是在渲染循环 (`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] // Update geometry, transforms, etc. updateGeometry(obj, node) dirtyNodes.delete(id) } }) ``` ### 脏节点 当一个 node 发生变化时,它会在 `useScene.getState().dirtyNodes` 中被标记为**脏节点**。系统每帧检查此集合,并且只重新计算脏节点的几何体。 ``` // Automatic: createNode, updateNode, deleteNode mark nodes dirty useScene.getState().updateNode(wallId, { thickness: 0.2 }) // → wallId added to dirtyNodes // → WallSystem regenerates geometry next frame // → wallId removed from dirtyNodes ``` **手动标记:** ``` useScene.getState().dirtyNodes.add(wallId) ``` ### 事件总线 组件间的通信使用类型化的事件发射器: ``` // Node events emitter.on('wall:click', (event) => { ... }) emitter.on('item:enter', (event) => { ... }) emitter.on('zone:context-menu', (event) => { ... }) // Grid events (background) emitter.on('grid:click', (event) => { ... }) // Event payload 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** - 创建楼板 ### 选择管理器 编辑器使用带有层级导航的自定义选择管理器: ``` Site → Building → Level → Zone → Items ``` 每个深度级别都有其自己的悬停/点击行为选择策略。 ### 编辑器专用系统 - `ZoneSystem` - 根据楼层模式控制区域可见性 - 带有节点聚焦的自定义相机控件 ## 数据流 ``` User Action (click, drag) ↓ Tool Handler ↓ useScene.createNode() / updateNode() ↓ Node added/updated in store Node marked dirty ↓ React re-renders NodeRenderer useRegistry() registers 3D object ↓ System detects dirty node (useFrame) Updates geometry via sceneRegistry Clears dirty flag ``` ## 技术栈 - **React 19** + **Next.js 16** - **Three.js** (WebGPU 渲染器) - **React Three Fiber** + **Drei** - **Zustand** (状态管理) - **Zod** (schema 验证) - **Zundo** (撤销/重做) - **three-bvh-csg** (布尔几何操作) - **Turborepo** (monorepo 管理) - **Bun** (包管理器) ## 入门指南 ### 开发 从**根目录**运行开发服务器,以为所有包启用热重载: ``` # 安装依赖 bun install # 运行开发服务器 (构建 packages + 以 watch 模式启动编辑器) bun dev # 这将会: # 1. 构建 @pascal-app/core 和 @pascal-app/viewer # 2. 开始监听这两个 packages 的更改 # 3. 启动 Next.js 编辑器 dev server # 打开 http://localhost:3000 ``` **重要提示:** 始终从根目录运行 `bun dev`,以确保包监视器正在运行。这将在你修改 `packages/core/src/` 或 `packages/viewer/src/` 中的文件时启用热重载。 ### 生产环境构建 ``` # 构建所有 packages turbo build # 构建特定 package turbo build --filter=@pascal-app/core ``` ### 发布包 ``` # 构建 packages 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/` | Node 类型定义 | | `packages/core/src/store/use-scene.ts` | 场景状态 store | | `packages/core/src/hooks/scene-registry/` | 3D 对象注册表 | | `packages/core/src/systems/` | 几何体生成系统 | | `packages/viewer/src/components/renderers/` | Node 渲染器 | | `packages/viewer/src/components/viewer/` | 主 Viewer 组件 | | `apps/editor/components/tools/` | 编辑器工具 | | `apps/editor/store/` | 编辑器特定状态 | ## 贡献者 Aymeric Rabot Wassim Samad pascalorg/editor | Trendshift
标签:3D建模, IndexedDB, MIT开源, monorepo, Node Schema, React Three Fiber, Turborepo, UI组件库, WebGPU, Zustand, 三维可视化, 交互工具, 几何生成, 前端工程, 协同编辑, 场景图, 建筑编辑器, 建筑设计, 状态管理, 空间查询, 自动化攻击