3D 建筑编辑器 Pascal Editor:IFC 模型转换必然有损

2026-08-05

本文基于 Pascal Editor 仓库 commit 64dca3d(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/pascalorg/editor 最新代码与文档为准。

这条转换链路的产物不是原模型的等价表示,而是一份”够编辑器画出来”的近似,所有对不上的部分要么被兜底常量顶替,要么被塞进一个渲染器根本不读的字段里。 你要是把它当成 IFC 的无损导入用,第一次拿它去核对面积或构件数量就会翻车。

先把两边说清楚。IFC(Industry Foundation Classes)是建筑行业交换模型的开放文件格式,一栋楼的墙、板、门窗、梁柱、材料分层、属性集都写在里面,各家软件导出的写法差异极大。Pascal Editor 是一个跑在浏览器里的开源 3D 建筑编辑器(和 Pascal 编程语言、压强单位帕斯卡都没有关系),它的场景是一棵参数化节点树:一面墙不是一堆三角面,而是”起点、终点、厚度、高度”这四个数。转换器要做的,就是把前者的自由几何压成后者的参数。压缩必然丢东西,问题只在于丢的是哪些。

站内 AI 翻译工具怎么选 讨论的是自然语言之间的转换取舍,结构化输出不稳定API 返回结构变了怎么办 讨论的是模型与接口输出的结构漂移;本篇讨论的是另一类问题——两套都确定、都有 schema 的数据模型之间做映射,损耗从哪来、怎么量化。

一、这块代码解决什么问题

仓库 README 这样定位这个转换器:把 IFC 建筑模型转成 Pascal 场景图 JSON,并在真实的 @pascal-app/viewer 里预览结果,输出用于预览和迭代,不用于生产。它同时挂了一段醒目的 early alpha 声明,列了已知短板:元素错位或缺失、读不到几何的墙退回固定高度、部分构件整类跳过、还有一些元素类型没映射。

我把这段自述当成这篇的起点,而不是结论。因为”有损”这个说法太笼统,工程上真正需要的是一张损耗清单:哪些字段有兜底值、兜底值是多少、哪些信息被搬去了别处、哪些直接消失。这些答案全在两个文件里,任何人都能自己翻。

代码结构上,纯逻辑和界面是分开的。packages/ifc-converter 是纯转换逻辑,不碰 DOM 和 React,入口函数是 convertIfcToPascal,接收 IFC 字节流和一个进度回调;apps/ifc-converter 是网页外壳,拖放、示例选择、元素搜索、三维预览、JSON 下载都在这层。底层的 IFC 解析交给 web-ifc(一个 WebAssembly 版的 IFC 解析器),所以整个转换在你自己的浏览器里跑完,模型文件不经过任何服务端。

组成部分它负责什么仓库位置你什么时候会碰到它
convertIfcToPascal解析 IFC、建空间层级、抽几何、回填属性packages/ifc-converter/src/index.ts想知道某个字段是怎么算出来的
simplifyConvertedSceneGraph转换收尾的清理与合并packages/ifc-converter/src/cleanup.ts发现构件数量对不上源模型
网页外壳拖放、筛选、预览、下载 JSONapps/ifc-converter/components/IfcConverter.tsx要改交互或看它怎么调用转换
示例清单十个样例文件的元数据与取用地址apps/ifc-converter/lib/test-files.ts排查样例加载失败、想关掉出网
wasm 拷贝脚本把 web-ifc 的 wasm 放进 public/apps/ifc-converter/scripts/copy-web-ifc-wasm.mjs解析器初始化失败时
节点 schema 与默认值墙的默认高度、厚度等常量packages/core/src/systems/wall/wall-footprint.ts想确认兜底值到底是多少

二、一次转换里发生了什么

按代码顺序走一遍,你就知道损耗都发生在哪一步。

单位归一。 getLengthUnitFactor 去 IFCPROJECT 里找长度单位,认 METRE/METER 加十六种词头前缀,认 FOOT/FEET(0.3048)和 INCH(0.0254),也认换算型单位里的换算系数。这里有个安静的风险:任何一步读不到,函数直接返回 1,也就是”按米处理”。一个用毫米建模却没把单位声明写对的文件,会得到一栋放大一千倍的楼,而且没有任何报错。

原点归一。 它拿第一个 IFCSITE 的放置矩阵解出世界原点,作为整个场景的偏移量,把带地理坐标的模型拉回坐标原点附近。副作用是:地理参考本身没有进节点,只留在原始 IFC 里。

空间层级。 先扫 IFCRELAGGREGATES 和 IFCRELCONTAINEDINSPATIALSTRUCTURE 两类关系,建出”谁是谁的父级”的映射,再依次处理场地、建筑、楼层。楼层标高优先从放置链算,算不出才退回属性里的 Elevation。

几何提取。 墙是最花力气的一类。它先找名为 Axis 的表示项拿到轴线折线,取首末两点当墙的起终点;拿不到轴线,就退回放置原点加上剖面的 X 向尺寸推一段长度。高度和厚度从拉伸体(把一个平面轮廓沿某方向推出来形成的实体)的深度与剖面 XDim/YDim 读;还读不到,就用 measureWallLocalExtents 把这面墙的网格投影到它自己的轴线坐标系里量范围——注释里写得很明白,世界坐标系下的轴对齐包围盒会把斜墙的长度和厚度搅在一起,所以必须在墙自身的坐标系里量。三层都失败,才落到 DEFAULT_WALL_HEIGHT(2.5 米)和 DEFAULT_WALL_THICKNESS(0.1 米)。还有一条更狠的:连终点都定不下来的墙,直接跳过,这面墙在产物里就不存在了。

属性回填。 建完节点之后,它再扫三类关系把信息补回来:IFCRELDEFINESBYPROPERTIES 取属性集和数量集写进 metadata.properties(属性集是 IFC 里挂在构件上的一组键值,比如防火等级、是否承重;数量集是长度、面积、体积这类算量用的数值),IFCRELASSOCIATESMATERIAL 取材料名与材料分层写进 metadata.materialmetadata.materialLayers,IFCRELDEFINESBYTYPE 取类型名写进 metadata.typeName。注意这一步只认已经建出节点的元素——前面被跳过的构件,属性也跟着一起没了。

收尾简化。 最后调 simplifyConvertedSceneGraph,删掉长度小于 0.08 米且没有子节点的碎墙,把同一父级下共线、同高、间隙够小的墙段合并成一段,再去掉重复的门窗洞口。

三、清点一下过桥时掉了什么

这是本篇的正题。按”掉得多严重”排一下。

整类不进场的构件。 梁被完整跳过,代码里只是遍历一遍数个数,然后打一行警告,理由是目标端还没有梁这个节点类型。另一批跳过得更值得警惕:家具、通用构件代理、栏杆、面层、幕墙、板件、杆件、基础这八类 IFC 实体,全部归到”需要目录资产才能落地”的一类里,同样只计数不建节点。README 里写的是”家具等物件”,但代码里这一类实际吞掉的包括幕墙和栏杆——对一栋玻璃幕墙为主的楼来说,这不是配角丢失,是立面没了。

降维成占位的构件。 楼梯只留一个包围盒(能把物体整个装进去的最小方盒子),塞在 metadata.boundingBox 里,因为目标端的楼梯节点是参数化的(踏步、踢面、段),转换器还没法把 IFC 楼梯映射过去。屋顶同理,只有一个平面多边形加一个高度进 metadata,而目标端的屋顶是由若干屋面段拼出来的。场地更直白:属性线多边形直接写死成编辑器默认的 30×30 方框,注释里挂着待办,说以后要从场地地址或建筑轮廓推。

字段级的挪位与丢失。 楼板(建筑里的水平承重构件,也就是楼面/地面那层)的厚度在目标 schema 里没有对应字段,被移进了 metadata;窗台高度同理,窗节点当前没有这个字段,值只能留档。墙的正反面做法固定写成未知。这些”救下来”的值有个共同特征:metadata 在 schema 里是一个松散的 JSON 值,转换器往里写一套固定形状,但渲染器并不读它。换句话说,它们在产物里可查,在画面上不存在。

门窗定位的近似。 正常路径是走 IFCRELVOIDSELEMENT(谁在谁身上开了洞)加 IFCRELFILLSELEMENT(哪扇门填了这个洞)。有些导出软件只写前者不写后者,这时转换器改用最近墙投影:把门窗的世界位置投到每面墙的轴线上,取一米以内最近的那面当宿主,超过一米的就挂回它的空间容器、位置归零。这里还有一处很实的工程细节——候选墙里优先选”装得下”这扇门的,因为某些模型会在墙角留下短碎墙段,纯按距离选会把 0.7 米宽的门吸到 0.4 米长的碎墙上,随后在墙上挖洞的实体布尔运算(用一个形体从另一个形体上减掉体积的几何运算)会溢出,把整面墙挖坏。门窗尺寸读不到时也有兜底:门 0.9×2.1 米,窗 1.0×1.2 米,且门底默认贴着墙底。

简化带来的对不上。 合并规则是同父级、方向角相差不超过一个一度的分桶、两段墙偏离同一条直线的容差(沿墙的法线方向量,也就是垂直于墙面那个方向)在 0.06 到 0.14 米之间按厚度取、高差不超过 0.35 米、间隙不超过 1.25 米或重叠超过一半。这条规则对”一面墙被导出成十段”的文件很有用,但对”设计上就分段”的文件是破坏性的:按材质或防火分区刻意拆开的墙会被并成一条,保留的是子节点最多、其次最长的那段,其余段连同它们的全局标识与属性集一起消失。所以产物里的墙数量和源模型对不上是预期行为,不是缺陷。

四、边界与代价:它明确不管什么

它不做几何保真。 目标节点体系是参数化的,凡是不能被”轴线加厚度加高度”或”多边形加标高”描述的形体,要么降级要么出局。异形墙、变截面构件、复杂屋面在这条链路上没有活路。想要保面的场景,这条路本身就选错了。

它不做双向同步。 产物是一份下载下来的 JSON,网页端给的出口只有下载和复制到剪贴板两个。全局标识虽然留在 metadata 里,但没有任何回写机制;你在编辑器里改完,改动回不到 IFC。把它当成建模的起点可以,当成协同的中间格式不行。

它不管转换参数的可调性。 转换选项里有坐标轴交换、拉伸深度是否当高度、剖面尺寸是否对调三个开关,还预置了两套组合。但网页外壳调用转换时只传了数据和进度回调,没传选项,走的全是默认值。也就是说,遇到坐标轴约定不同的模型,你在页面上没有开关可拨,只能直接用这个包写脚本。

它和这个项目的 MCP 服务器之间没有通路。 这个编辑器还带一套 MCP 服务器,能让 AI 助手直接建墙、开门、放家具。但在我读到的这版代码里,MCP 那个包的源码里一次都没提过 IFC——转换产物要进编辑器,还是得手动下载再加载。别指望”让 Agent 帮我导入这个 IFC”能自动走通。

它会往你机器上写东西、也会出网。 这两件事要分开看。IFC 解析这一段是纯本地的:wasm 从站点根目录加载,文件在浏览器里读完就转,不上传。但示例文件不是——大号样例默认从一个公开只读的对象存储桶按文件名取,仓库里只提交了四个小样例,其余六个走网络。这个基址可以用 NEXT_PUBLIC_IFC_EXAMPLES_BASE_URL 覆盖,设成空串就只显示本地那四个。真正需要当心的是 MCP 那一侧:通过它保存的场景落在本地 SQLite 数据库文件里(默认在用户目录下的 ~/.pascal/data/pascal.db,可用 PASCAL_DATA_DIRPASCAL_DB_PATH 改),Agent 调用的建模工具是直接改这个库的,改完还会写一条事件流让打开的浏览器页面跟着更新。它默认走标准输入输出;开 HTTP 时默认只绑回环地址,要绑非回环地址必须提供 PASCAL_MCP_HTTP_TOKEN。这个门槛是对的,但你得知道它意味着什么:暴露端口等于把”改你本地场景库”的权限开给了网络另一头。这类判断可以对照 MCP 的安全边界怎么划给 Agent 划改动边界的约定 一起看。

五、上手与避坑清单

别照包的 README 去解构返回值。 那份 README 写的返回形状是节点、根节点列表加统计三项,但函数实际只返回前两项,统计信息是打到控制台的。踩坑原因是文档和代码各自演进;避法是以 packages/ifc-converter/src/index.ts 结尾的 return 为准,需要统计就自己按节点类型数一遍——网页外壳就是这么干的。

先看控制台再看画面。 跳过的梁和跳过的构件都只在控制台留警告和计数,界面上不会提示。踩坑原因是画面看着挺完整,你不会主动怀疑少了东西;避法是转换完先翻日志,把跳过数和源模型里的构件数对一遍,缺口大就说明这个文件不适合走这条路。

拿到产物先验单位和尺度。 单位识别失败会静默按米处理。踩坑原因是它不抛错,只是所有数字整体差一个量级;避法是转换后随手点一面墙,看厚度是不是零点几米这个量级,不对就回头查源文件的单位声明。

别用产物做工程量统计。 碎墙会被删、共线墙会被并、整类构件会被跳过。踩坑原因是产物看起来是一份结构化 JSON,很容易被下游当成可信数据源;避法是把它定位成”可编辑的起点”,任何面积、数量、材料统计都回原始 IFC 去算。

大文件先看警告再点。 示例清单里给几十兆的样例挂了明确提示,说渲染时可能拖慢甚至崩掉浏览器。踩坑原因是转换本身能跑完,卡住的是预览渲染;避法是大模型先用包在脚本里转出 JSON,别在页面上直接渲染。

改代码前先看目录里的约定文件。 这个转换器应用目录下同时放着 AGENTS.mdCLAUDE.md,仓库根目录另有三份面向不同助手的约定文件,架构说明则集中在 wiki/architecture 的二十份 md 里(一份索引加十九篇分主题)。踩坑原因是让助手直接改代码时它不知道这些约定;避法是把对应约定文件先喂给它。

收个尾

判断这条链路能不能用在你手上,问三个问题就够:你的模型里主要构件是不是墙板门窗柱(是梁、幕墙、栏杆为主就别走);你要的是可编辑的起点还是可信的数据(要后者就回原文件);你能不能接受构件与源模型不再一一对应(简化合并是默认开的)。

要继续往下读,顺序建议是:先看 packages/ifc-converter/src/index.tsconvertIfcToPascal 的主流程和几处待办注释,那是作者自己标出来的缺口;再看 cleanup.ts 开头那几个常量,它们定义了简化的激进程度,也是你最可能想调的地方;最后看 apps/ifc-converter/components/IfcConverter.tsx 里调用转换的那几行,确认它到底传了什么参数。这三处看完,你对这份产物该信几分,心里会有个准数。

本篇属于一个把开源3D 建筑编辑器 Pascal Editor逐层拆开讲的系列,整体地图见 Pascal Editor 是什么:浏览器里的开源 3D 建筑编辑器,与它自带的建模 MCP 服务器;沿着这条线往下,还可以看 三维建筑编辑器 Pascal Editor 的平面图模式与图纸导出链路开源 3D 建筑编辑器 Pascal Editor:为什么要给 Agent 单独写一份使用指南

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。