pascalorg/editor

GitHub Trending (daily) 工具

摘要

一个使用 React Three Fiber 和 WebGPU 构建的3D建筑编辑器,采用 Turborepo 单体仓库结构,包含 core、viewer 和 editor 包,分别用于模式定义、3D渲染和交互工具。

创建并分享3D建筑项目。
查看原文
查看缓存全文

缓存时间: 2026/05/19 12:34

pascalorg/editor 源码:https://github.com/pascalorg/editor

Pascal 编辑器

使用 React Three Fiber 和 WebGPU 构建的 3D 建筑编辑器。

MIT 许可证

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/editorUI 组件、工具、自定义行为、编辑器特定系统

viewer 使用合理的默认设置渲染场景。editor 通过交互工具、选择管理和编辑功能对其进行扩展。

存储

每个包都有自己用于管理状态的 Zustand store:

Store包职责
useScene@pascal-app/core场景数据:节点、根节点 ID、脏节点、CRUD 操作。持久化到 IndexedDB,通过 Zundo 实现撤销/重做。
useViewer@pascal-app/viewer查看器状态:当前选择(建筑/楼层/区域 ID)、楼层显示模式(堆叠/展开/单独)、相机模式。
useEditorapps/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
    └── ...

模式:

  1. 渲染器创建一个占位网格/组
  2. 使用 useRegistry 注册
  3. 系统根据节点数据更新几何体

简化示例:

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)

Hacker News Top

PolyCSS 是一个 CSS 多边形网格库,能将 3D 模型渲染为真实的 HTML 元素(使用 CSS matrix3d),支持 OBJ/MTL、GLB 和 VOX 格式,可与 React、Vue 或原生 JavaScript 配合使用。

slash-editor

Product Hunt

slash-editor 是一款开源的、Notion 风格的 React 区块编辑器,基于无头架构的 Tiptap v3 内核构建,并通过 shadcn registry 分发,组件直接存放在你的代码仓库中。它提供斜杠菜单、嵌套拖拽、评论、实时协作、表格、Mermaid、Markdown 导入导出以及自带 AI 等功能,全部采用 MIT 协议授权,且没有任何托管服务。

playcanvas/supersplat

GitHub Trending (daily)

SuperSplat 是一款免费、开源的浏览器端编辑器,由 PlayCanvas 基于 Web 技术构建,用于检查、编辑、优化和发布 3D Gaussian Splat。无需安装,可直接在 superspl.at/editor 在线使用。

Online 3D Editor

Product Hunt

Online 3D Editor是一个基于网页的工具,可在浏览器中直接提供Blender风格的3D编辑功能。