AI 参与说明(Agent:Codex、Cursor):2026-09-15 Codex 阅读公开源码并运行选定测试,整理技术栈、核心算法与编辑业务流程。依据提交
eacc2a939bec53529809b06f68123df2459b7961(2026-09-14),不将在线产品状态与该提交等同。运行记录:模型gpt-6-astra,reasoning effortmedium,执行入口 Codex Desktop。验证范围为源码、core 构建、选定自动测试与房间识别示例。同日 Cursor Cloud Agent 另写架构长稿(未合并的 draft PR #13),覆盖 monorepo 分包、Zustand + Zundo、registry / Dirty Node systems、WebGPU / TSL 与 CLI 分发。运行记录:模型Claude Opus 4.8,执行入口 Cursor Cloud Agent;reasoning effort 未取得运行记录。2026-09-16 由 Cursor Cloud Agent 将两稿收敛为本文:保留 Codex 的场景几何、MCP 流程与测试核验,并吸收 PR #13 中上述架构材料。运行记录:模型grok-4.6(Cursor 路由标识cursor-grok-4.6-high),提供方 xAI,执行入口 Cursor Cloud。reasoning effort 未取得独立运行记录。未完成整个 Pascal 应用的浏览器端验收。
Pascal Editor 是一个面向建筑场景的参数化三维编辑器。它最值得研究的地方,是把墙、门、楼层、楼板等对象保存为可计算的语义节点,再由渲染与几何系统生成画面;MCP 接入也直接操作这些节点。浏览器与 headless 运行时共用同一份 Scene Graph,而不是给 Agent 另写一套场景逻辑。
可以把项目理解为“建筑领域模型 + 三维编辑工作台 + 本地项目服务 + Agent 操作接口”。 阅读时抓住四个问题:一面墙如何从参数变成 Mesh,关闭房间后哪些数据需要一起变化,声明式 React 树如何与每帧几何重建配合,以及网页和 MCP 如何避免相互覆盖保存。
相关的 Web 3D 基础概念见 Web 3D 基础概念:从 Mesh 到 GLB,工具链协作见 Three.js 与 Blender 入门。
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Scene Graph | 场景图 | 用节点与父子关系表达建筑文档 |
| Node | 节点 | 场景的最小数据单元,如 wall、slab、item |
| Mesh | 网格 | 由顶点和面组成、供渲染器显示的几何对象 |
| CSG | 构造实体几何 | 通过并、交、差等运算组合实体,例如从墙体扣除门洞 |
| Half-edge | 半边 | 把一条边拆成两个方向,用来沿墙边寻找封闭区域 |
| Dirty Node | 脏节点 | 数据已变、几何尚未重建的节点标记 |
| System | 系统 | 每帧扫描 Dirty Node 并重建几何的组件 |
| Registry | 注册表 | 从节点类型或节点 id 到运行时对象的映射 |
| Zundo | —— | Zustand 的撤销中间件;本文场景 store 使用其 temporal wrapper |
| TSL | Three.js Shading Language | 用 JavaScript 组合着色器节点、可编译到 WebGPU / WebGL 的 DSL |
| MCP | 模型上下文协议 | 让外部 Agent 调用创建墙、放置门等结构化工具 |
| SSE | 服务器发送事件 | 服务器通过持续 HTTP 连接向浏览器推送场景变化 |
| Optimistic Locking | 乐观锁 | 保存时检查版本,拒绝用旧数据覆盖新版本 |
| Local-first | 本地优先 | 数据默认存在本地、可离线工作,云同步是可选增强 |
技术栈:Next.js 是宿主,建筑能力集中在包内
下表记录的是该提交的 manifest 声明或根目录 override,不代表各依赖当前最新版本;带 ^ 的范围也不等于锁文件的精确安装版本。
| 层次 | 源码中的技术 | 具体职责 |
|---|---|---|
| 工程 | Bun workspaces、Turborepo、TypeScript、Biome | 组织应用与库、依赖构建、检查和格式化;根目录指定 Bun 1.3.14 |
| Web 宿主 | Next.js 16.3.0 override、React ^19.2.4 | 页面、项目加载、场景 API 与编辑器装配 |
| 三维视图 | Three.js 0.186.0 override、React Three Fiber ^9.5.0、Drei ^10.7.7 | Canvas、相机、对象树和交互辅助 |
| GPU 路径 | WebGPURenderer、TSL、WebGL 2 fallback | 优先 WebGPU,能力探测失败后尝试 WebGL 2 |
| 状态与约束 | Zustand 5、Zundo、Zod 4、mitt | 文档状态、撤销历史、节点校验、编辑事件 |
| 几何处理 | three-bvh-csg、three-mesh-bvh | 墙体开洞、空间加速等;打印导出另有 manifold-3d 依赖 |
| UI | Tailwind CSS 4、Radix UI | 面板、菜单、表单与样式 |
| 本地服务 | MCP SDK、SQLite、Node.js / Bun | 工具调用、项目持久化、事件同步和 CLI runtime |
版本与依赖来源:根 package.json、Web 应用、editor 包、viewer 包。根工程声明 Node.js ≥20.9,但 CLI 要求 ≥22.13,不能用前者替代后者的运行要求。CLI manifest
这不是只包含一个前端应用的仓库。源码分层直接决定了后续功能应放在哪里。The viewer renders the scene; the editor extends it with interactive tools. viewer 可以作为只读运行时单独使用;交互式工具、选择、面板和拖拽叠加在 editor 之上。
flowchart TD
app["apps/editor<br/>Next.js 宿主"]
editor["packages/editor<br/>工具 · 面板 · 选择 · 保存"]
nodes["packages/nodes<br/>内建节点插件"]
viewer["packages/viewer<br/>Canvas · 几何 · 渲染"]
core["packages/core<br/>schema · Scene Graph · store"]
mcp["packages/mcp<br/>SceneBridge · 工具 · SQLite"]
cli["packages/cli<br/>安装 · 启动 · 连接 MCP"]
app --> editor
app --> nodes
editor --> viewer
nodes --> viewer
viewer --> core
nodes --> core
mcp --> core
cli --> mcp
cli -.-> app
| 目录 | 主要责任 | 阅读时关注的问题 |
|---|---|---|
packages/core | schema、Scene Graph、store、领域运算、Registry 契约 | 什么是合法节点,修改会影响什么 |
packages/nodes | 内置节点插件及其 renderer、System、tool、inspector | 一个建筑对象如何把数据、显示和编辑连起来 |
packages/viewer | Canvas、相机、几何 System、渲染系统 | 节点怎样生成与更新画面 |
packages/editor | 工具、面板、选择、拖拽、保存回调、导出 | 用户操作怎样提交到领域模型 |
apps/editor | Next.js 页面与本地场景 API | 项目如何加载、保存、同步 |
packages/mcp | SceneBridge、SceneOperations、工具与存储 | Agent 如何不依赖浏览器执行语义操作 |
packages/cli | 安装与启动 runtime、连接 MCP | 本地服务如何分发、管理和连接 |
core 和 viewer 等包有构建产物入口,但 editor 暴露源码,并带有 Next.js peer dependency。因此,“拆成 package”不等于“已经可以零成本嵌入任意前端框架”。editor 包入口
核心原理一:Scene Graph 保存语义,Mesh 是派生结果
The scene document stores semantic nodes; rendering derives geometry from those nodes. 例如墙保存起点、终点、厚度、高度、材质及与门窗的关系,渲染系统再计算顶点和面。保存项目时,无须把每次生成的墙体 Mesh 当作文档主体。
基础节点包含 id、type、parentId、可见性和 metadata 等字段。id 由带类型前缀的 nanoid 生成,例如 wall_3f9a…。文档主体使用 nodes 字典与 rootNodeIds,节点再通过子节点 ID 建立关系;完整导出还包含 collections、materials、installedPlugins。各类型在 schema/nodes/*.ts 里扩展 BaseNode,再收敛成 Zod discriminated union AnyNode。BaseNode、useScene、SceneBridge
BaseNode.metadata 使用 z.record(z.string(), z.unknown()),而不是递归 z.json()。源码注释给出的实测是:去掉递归 JSON schema 后 WallNode.parse 约快 1.5 倍,并让节点树能使用 Zod 4.5 的 z.compile()。When a schema field is both expensive and rarely constrained, keep it an open record and validate elsewhere.
典型建筑层级如下。这里只画主要节点,实际还支持 roof、stair、item 等更多类型。
flowchart TD
A[Site] --> B[Building]
B --> C[Level]
C --> D[Wall]
D --> E[Door / Window]
C --> F[Slab / Ceiling]
C --> G[Item / Zone]
平面坐标使用 XZ,Y 表示高度,尺寸以米表达。这里应区分楼层排序、楼层基准高度、层高和墙高:Level 的相关字段承担不同职责,不能看到 level 就把它理解成米制高度,也不能默认所有墙总等于层高。Level schema、Wall schema
状态还分成三个用途:useScene 管文档;useViewer 管视图;useEditor 管工具与界面。拖拽过程的临时覆盖值另行管理,避免把每一个鼠标位置都变成正式文档历史。
这套设计的工程收益是:修改墙长、撤销操作、Agent 创建墙和项目保存,都能围绕同一套可校验参数进行。代价是必须处理派生数据的一致性;只更新一面墙的 Mesh,而漏掉房间边界、楼板或门洞,仍会造成文档与画面不同步。
核心原理二:Zustand + Zundo 管文档历史,手势状态不进撤销
README 提到场景 persist 到 IndexedDB,但该提交的 useScene 只包了 Zundo 的 temporal 中间件,并没有 Zustand persist。IndexedDB 的 idb-keyval 主要出现在资产二进制存储中。Scene Graph 的持久化由宿主完成:独立 Web 应用走场景 API 与 SQLite,嵌入时才可能落到 localStorage。这类差异必须以源码为准。场景 store、asset-storage
temporal 的配置有几处工程边界:
- 历史快照上限为 50。
partialize只对nodes/rootNodeIds/collections/materials/installedPlugins建快照,并用语义比较避免无意义的撤销步。- 放置工具会给正在拖动的节点打
metadata.isNew = true;建快照前会剔除这类子树,直到首次 commit 才进入历史。
Preview state and document state are deliberately different stores. 拖拽时的 live overrides 也不写入 Zundo:System 通过 getEffectiveNode() 读取即时值更新 Mesh,指针抬起才提交为一次撤销步。Live gesture state bypasses undo; only committed state enters history.
写入后会给相关节点打 Dirty Node 标记。GuardedDirtySet 只允许“存在脏消费者、且所属插件已启用”的节点进入集合,避免为没有几何 System 的节点做无谓标记。updateNodesAction 还会用 requestAnimationFrame 批量收敛标记。撤销 / 重做后则通过快照 diff 重新标脏受影响的墙、item 与相邻拼角。编辑器 store
核心原理三:墙体建模包含接缝、开洞与增量重建
画墙并非简单地创建一个长方体。入口工具监听网格移动、点击与取消事件,在预览阶段处理吸附与对齐,确认后提交节点。墙体 System 再处理同楼层相邻墙的连接轮廓,以及门窗对子墙体的开洞影响。Wall tool、WallSystem
可按四步理解这条几何链路:
- 根据墙的起终点和厚度求平面轮廓,结合楼层内相邻墙计算转角接缝。
- 把轮廓沿高度方向生成实体。
- 收集门窗等对象的切割几何,使用 CSG subtraction 扣出洞口。
- 将结果更新到对应的 Three.js 对象,并参与后续显示与批处理。
flowchart TD
A[工具提交 Wall 参数] --> B[标记相关节点 Dirty Node]
B --> C[计算墙连接与轮廓]
C --> D[生成墙体几何]
D --> E[减去 Door / Window 切割体]
E --> F[更新 Mesh 与渲染批次]
G[拖拽时的临时覆盖值] --> C
WallSystem 引入 three-bvh-csg 的 Brush、Evaluator 与布尔操作,并缓存同楼层的墙连接信息。拖动门窗时会合并 live overrides,让洞口跟随预览位置,而不是每次都先永久写入文档。
重建也不是每帧遍历重算所有墙。系统消费 Dirty Node 集合,对较大的待处理队列设置每帧数量及时间预算,并让相邻墙更新分批进行。需要注意:时间检查发生在墙体任务之间,单面复杂墙的布尔计算仍可能超出预算;这不构成严格的帧耗时上限。
WebGPU 加速的是显示管线,不意味着所有建模运算都迁到了 GPU。 墙轮廓、关系整理和这里的 CSG 调用仍是 JavaScript 侧的几何工作。复杂建筑的性能分析必须分别观察建模计算、React 状态更新与 GPU 绘制。
核心原理四:封闭房间会触发一次领域级联
当最后一面墙闭合时,用户通常期待房间、楼板、天花板与墙的内外侧一起正确出现。源码通过 space-detection 处理这种跨节点关系,核心是从墙段构建带方向的边,排序连接点的出边,再沿边寻找封闭轮廓。space-detection
这与简单判断“首尾坐标是否相等”不同:连接点可能有多个方向,还需要区分不同区域和墙的朝向。识别结果包括 rooms、spaces 与 wallUpdates,并被后续逻辑用于协调自动生成的 Slab、Ceiling、Zone 等对象。
更关键的是提交边界。项目把本地墙变化引起的相关派生更新纳入同一次 scene commit,并在派生写入阶段控制历史记录,使“一次封闭房间”可以作为一个撤销步骤处理。远端应用图数据时则需要避免再次生成一套派生节点与本地历史。架构说明、对应测试
这说明房间识别并非仅为画一个标签服务,而是参与文档一致性。二次开发若绕过正规节点操作直接写字典,很容易遗漏关系协调、Dirty Node 标记和历史边界。
渲染管线:两张 Registry、Dirty Node Systems、WebGPU 优先
React Three Fiber 支持异步 Canvas gl 初始化;Pascal 使用这一能力等待 renderer 初始化,再交给场景系统使用。R3F Canvas 文档、Viewer 入口
声明式树 + 命令式几何更新
真正把“数据变化”变成“几何更新”的,是两张 Registry 加一套 Dirty Node 循环:
- 节点定义 Registry(
packages/core/src/registry/):类型 → schema、renderer、System、capabilities。插件通过它注册新节点类型。 - 场景对象 Registry
sceneRegistry:节点 id → 具体的THREE.Object3D,外加byType索引。renderer 挂载时用useRegistry登记,卸载时注销。
NodeRenderer 按类型分发。NodeDefinition 的渲染行为可以概括成三选一:自定义 JSX renderer;纯函数 geometry(node, ctx) 交给 GeometrySystem 填充已注册的 group;或每帧 system 由 RegisteredSystems 按 priority 运行。NodeRenderer、registry
墙体是刻意的反例:它没有通用 geometry,而是由 WallRenderer 挂占位 Mesh、再由 priority 为 4 的 WallSystem 独占 CSG 重建。多个 System 用 useFrame 的 priority 排出确定顺序:楼板高程(1)→ 通用几何(2)→ 墙体(4)→ 楼层升降动画(5)→ 场景就绪探测(10)。这种“先地基后上层”的排序,避免同帧内相互依赖的几何读到半成品。
flowchart TD
A[用户操作 / MCP 工具] --> B[useScene 写入文档]
B --> C[标记 Dirty Node]
B --> D[React 重渲染 NodeRenderer]
D --> E[useRegistry 登记 Object3D]
C --> F[System 在 useFrame 中重建几何]
E --> F
F --> G[clearDirty]
若 Object3D 尚未挂载,System 会保持 Dirty Node、下一帧再处理。组件间通信走基于 mitt 的类型化事件总线,事件名形如 wall:click,负载同时带世界坐标与建筑局部坐标。
WebGPU 初始化、帧循环与 TSL
源码会探测 WebGPU adapter 与 device;失败或超时后再探测 WebGL 2,并为 WebGPURenderer 选择相应参数。<Canvas gl={...}> 传入的是异步工厂,并以 canvas 为键缓存 renderer promise,避免 React StrictMode 双调用让两个异步 init 竞争、第二个停在默认 300×150 尺寸。两条路径都不可用时,源码更新 React state 卸载 Canvas、显示不支持界面,而不是让 promise reject 拖垮 R3F。不能依据 README 的 WebGPU 表述推断“不支持 WebGPU 的浏览器一定无法运行”。renderer-capability
Canvas 设置 frameloop="never",帧调度由 FrameLimiter 借助 requestAnimationFrame 和 R3F advance() 接管,默认上限 50 FPS,并处理暂停和 Canvas 被遮挡的情况。这是主动控制更新节奏,并非 R3F 默认连续帧循环。FrameLimiter
后处理使用 TSL,包含 SSGI、降噪、描边等路径;scene、zone、grid、overlay 等层分开处理,避免辅助对象污染场景的深度、法线和光照结果。手绘线稿观感来自屏幕空间 ink edges,不是 cel-shaded 材质。批处理降低绘制成本,但原对象仍需要保留拾取与交互用途。后处理、图层架构
WebGL 2 fallback 不应被解释成与 WebGPU 完全相同的视觉效果。源码包含直接渲染回退,并对后处理初始化错误作处理;其能力检测中也存在以 navigator.gpu 是否存在作为条件的分支。adapter 初始化失败后的具体画面仍应在真实浏览器验证,本次能力测试使用模拟环境,并未替代这种验收。
从用户操作到项目保存:实际走 API 与 SQLite
该提交的主链路需要以实现为准:独立 Web 应用将保存回调接到场景 API,由后端 SceneStore 写 SQLite。SceneLoader、存储工厂
编辑器包保留可替换的宿主契约:有 onSave 时调用宿主回调,没有时才使用 localStorage 保存路径。因此“浏览器独立嵌入”和“仓库中的完整 Next.js 应用”不能混成一种存储行为。use-auto-save
SQLite 驱动按运行时探测:优先 bun:sqlite,否则用 Node 22+ 内置的 node:sqlite(DatabaseSync)。库表包括 scenes、scene_revisions 与 scene_events。写入用事务范围内的版本检查,保存图 JSON、节点数量等元数据,并写 revision 记录;开启 journal_mode=WAL。加载侧使用较宽松的 Zod 校验,避免某个历史脏字段让整份场景打不开。Validate strictly on write, leniently on read. 图数据有默认大小限制,不能把保存函数里的普通 fetch 视为无限制传输。客户端和 API 还包含空图覆盖保护,用来避免加载尚未完成时把已有项目清空。SQLiteSceneStore、scene API
flowchart TD
A[编辑节点与派生对象] --> B[约 1 秒 debounce]
B --> C[宿主 onSave]
C --> D[PUT scene API 携带 If-Match]
D --> E{版本是否匹配}
E -->|否| F[409 冲突提示]
E -->|是| G[SQLite 保存图与 revision]
G --> H[返回新版本]
H --> I[客户端更新已知版本]
版本冲突出现后,当前客户端呈现冲突状态;源码并没有在这里实现 CRDT 式自动合并。网页收到 SSE 新版本后会应用服务器的图,并短时间抑制重复保存,避免把刚收到的远端变化立刻回写。这是一套版本化同步流程,不能直接称为完整多人协同编辑。SceneLoader
MCP 业务流程:外部 Agent 调工具,项目执行确定性操作
Pascal 的 MCP 层提供语义工具,例如创建墙、添加门窗、创建房间、放置物品和检查场景。自然语言规划由外部 Agent 完成;工具层负责参数校验和具体操作,不需要在每个工具里重新调用一个语言模型。server、工具目录
关键点是:SceneBridge 没有另写一套场景逻辑,而是直接驱动 @pascal-app/core 那个本为浏览器写的 Zustand store。为了让该 store 在 Node 里加载,MCP 侧只 polyfill 了 requestAnimationFrame,因为 updateNodesAction 与 temporal 订阅在模块加载期会用到它;没有 IndexedDB shim,持久化在 MCP 层替换成 SQLite,而不是在 core 里加运行时分支。SceneBridge
以 create_wall 为例:输入指定父 Level 和墙参数,工具校验节点类型及参数,构造 WallNode,经 SceneOperations / SceneBridge 调用核心 store,然后尝试发布当前场景快照。create-wall
完整同步链路是:
- Agent 通过 stdio 或 Streamable HTTP 调用 MCP 工具。
- 工具修改无浏览器 renderer 的 Scene Graph。
- 场景已绑定且存储支持事件时,按预期版本保存当前图。
- 服务追加 scene event,浏览器通过 SSE 接收。
- 浏览器更新
useScene,由自己的几何与渲染系统生成画面。
Agents edit the same Scene Graph the browser editor uses—no renderer, no React, no WebGPU required on the server. 工具修改内存成功与持久化成功是两个阶段。未绑定场景或存储不支持事件时,代码返回 persistence warning;版本冲突和保存失败则转换成 MCP error。不能仅看输出有一个 wallId 就认定项目一定已落盘,也不能推断保存失败会自动撤销此前的内存操作。live-sync
本地 CLI 负责服务 runtime 的安装、启动与连接;MCP 服务默认面向本地使用,HTTP 路径还包含认证及访问约束。SceneBridge 复用了核心 store,本地服务的 active scene 与多个传输会话之间并不能据此推导出独立的多租户隔离。CLI 源码、HTTP transport
CLI 分发:瘦包 + 校验过的 runtime 下载
npx @pascal-app/cli editor 背后是刻意做瘦的 npm 包:它只带 CLI 与打包好的 MCP service。Web 编辑器 runtime 在首次启动时按版本下载,并用 SHA-256 校验;下载的是 Next.js standalone server 包(apps/editor 以 output: 'standalone' 构建),再由 CLI 启动完整本地服务(含 scene API 与 SSE)。runtime-download
产物用确定性 tar 打包(路径排序、mtime 归零),因此钉在 CLI 里的摘要在不同构建间可核对。进程管理会选择无冲突的 loopback 端口,并用 /api/health 比对 instanceId 与版本,避免端口被其他进程占用后误判。MCP service 启动时生成本地 token 文件;连接器从磁盘读取后把 stdio 桥到本地 HTTP MCP。本文不展开 token 文件路径或权限细节。
插件化已经深入架构,但迁移尚未完全统一
节点 Registry 让一个节点类型描述 schema、默认值、能力、关系、renderer、geometry、System、tool 和 inspector。内建节点和第三方插件走同一套 Plugin API:id、apiVersion 与 nodes;tools / panels / systems 声明在各自的 NodeDefinition 内部,而不是 Plugin 顶层特权字段。宿主在挂载 Viewer 前 loadPlugin(builtinPlugin)。Wall 定义
不过,Wall 定义仍包装既有专用 renderer 与 System,schema 也保留从 core 导出的兼容路径。墙接缝需要楼层范围的信息,无法仅靠一个完全孤立的单节点 geometry 函数解决。当前项目是逐步统一中的插件架构,不能把所有节点都描述成同一套纯函数流水线。Wall schema
添加新节点时,除了 renderer,还需检查持久化、关系协调、选择拖拽、撤销、MCP 与导出。某个对象能显示出来,只证明其中一段链路已经接通。
导出与产品边界
| 能力 | 该提交可核验的实现 | 边界 |
|---|---|---|
| Scene Graph JSON | 导出节点及相关文档数据 | 用于保留参数与继续编辑,和最终 Mesh 交换不同 |
| 浏览器 GLB | editor 包调用 GLTFExporter,含导出对象整理 | 依赖浏览器侧几何准备,不保证覆盖所有异步节点情形 |
MCP export_glb | 返回 status: not_implemented 且 isError: true | 注册了工具名不代表已经能生成 GLB |
| 本地持久化 | SQLite SceneStore 与版本检查 | 类型里出现其他 backend 名称不等于已有对应实现 |
| 同步 | 场景版本、事件和 SSE | 本文未验证生产多人并发及远端服务部署 |
来源:SceneBridge、浏览器 GLB 导出、MCP GLB 工具、存储工厂。
仓库采用 MIT License。这里分析的是公开代码中的建模与本地运行链路;仓库存在远端服务或插件连接配置,不足以证明公开源码包含线上产品的所有账户、商业服务与资产后台。LICENSE
源码复现与实际验证
以下命令用于固定研究对象,不依赖之后的默认分支变化。工程指定 Bun 1.3.14;本次实际测试环境使用 Bun 1.3.2。安装使用 --ignore-scripts,仅用于这次源码测试,不代表完整应用安装流程已验收。
git clone https://github.com/pascalorg/editor.git
cd editor
git checkout eacc2a939bec53529809b06f68123df2459b7961
bun install --frozen-lockfile --ignore-scripts
bun run --cwd packages/core build
bun test packages/core/src/lib/space-detection.test.ts packages/core/src/lib/space-detection-reconciliation.test.ts packages/core/src/store/history-control.test.ts packages/viewer/src/lib/renderer-capability.test.tsx packages/mcp/src/bridge/scene-bridge.test.ts packages/mcp/src/storage/sqlite-scene-store.test.ts packages/mcp/src/operations/scene-operations.test.ts
必须先构建 core:MCP 包的跨包导入指向其导出入口;直接运行测试而缺少 dist,会出现 Cannot find module '@pascal-app/core/schema'。本次首次运行遇到该问题,按工程构建入口补齐后重跑通过。
还可以在仓库根目录保存下面的 inspect-room.ts,执行 bun inspect-room.ts。它直接复用项目算法,检查一个 4 × 3 米矩形的封闭识别;不需要启动浏览器。
import { WallNode } from './packages/core/src/schema'
import { detectSpacesForLevel } from './packages/core/src/lib/space-detection'
const corners: [number, number][] = [[0, 0], [4, 0], [4, 3], [0, 3]]
const walls = corners.map((start, i) => WallNode.parse({
start,
end: corners[(i + 1) % 4],
height: 2.5,
}))
const result = detectSpacesForLevel('level_demo', walls)
console.log(JSON.stringify({
rooms: result.rooms.length,
spaces: result.spaces.length,
wallUpdates: result.wallUpdates.length,
}))
实际输出为 {"rooms":1,"spaces":1,"wallUpdates":4}。这个示例只验证闭合区域识别,不证明自动楼板、门洞外观或建筑尺寸精度;相关派生协调另由所选测试覆盖。
本次验证结果:core 构建通过;上述 7 个文件共 175 项测试通过,0 项失败(530 次断言);矩形识别示例输出符合预期。测试覆盖历史暂停控制、房间协调、renderer 能力回退、SceneBridge、SQLite 保存和 SceneOperations。未运行整个仓库全部测试,未启动 Pascal 完整应用,也未进行真实 GPU 性能、复杂模型及生产并发测试。
阅读源码时最有价值的切入顺序
先读 BaseNode、WallNode 与 useScene,明确文档真值;然后沿 Wall tool → WallSystem 观察一次参数变化如何产生 Mesh;再读 space-detection 理解派生对象与撤销边界;接着看 sceneRegistry 与 GeometrySystem / WallSystem 的 Dirty Node 循环;最后串起 SceneLoader → scene API → SceneStore 和 MCP 的 create-wall → live-sync。
这条顺序能把项目从“很多 Three.js 文件”还原为完整业务系统。它的可复用经验是:把领域参数和渲染结果分开,让人工与自动化共享操作语义,再把派生更新、历史和持久化边界明确下来。需要继续审视的地方,则是专用 System 与插件迁移的并存、复杂几何的单次计算成本,以及版本冲突后的用户恢复流程。
相关阅读:OpenGemCutting 架构调研。两者都把参数化文档与三维显示分离,但 OpenGemCutting 的主线是 CUT 工序,Pascal 的主线是建筑节点及其相互关系,不能直接套用同一套领域模型。