在很多团队里,“BIM 装企落地”最后变成了“给业主看一个 3D 效果图”——模型很好看,一到施工就断档。我们的自研家装云编辑器从立项起就确定了一个原则:三维可视化只是结果,参数化驱动施工才是核心价值。
墙面为什么是这个厚度,地砖为什么从这个方向起铺,吊顶跌级为什么悬挑 320mm 而不是 300mm,这些必须由规则计算出来,而不是建模师“随手画出来”。前几篇讲了编辑器的整体架构、户型解析和软装体系,这篇集中拆解硬装部分最重的一块:墙、地、顶的参数化施工,包括规则设计、代码结构、校验逻辑以及我们在实际项目中踩过的坑。
如果你正在自研家装 BIM 工具,或者准备把现有 3D 编辑器往“施工可落地”的方向演进,这篇文章应该能给你一个相对完整的参考系。
1. 这篇文章真正要解决的问题
传统的家装设计软件通常把墙、地、顶当作“显示层”处理。设计师画完户型,程序自动把地台、吊顶、墙面装饰面生成出来,至于这些几何模型能不能加工、安装顺序是否合理、材料用量是否准确,往往不关心。
这套逻辑在“效果图设计”场景下没问题,但一旦进入施工落地,就会集中暴露三个问题:
第一,模型和施工数据脱节。设计师在编辑器里看到的每一面墙,到了施工队手里只有一张平面图。墙体厚度、材质层顺序、预留洞口位置、梁底净高这些关键参数全部丢失,工人只能靠现场经验判断,误差几乎不可避免。
第二,改造一处,处处连锁。墙体参数化最麻烦的不是“生成一面墙”,而是“改一处尺寸后,关联的门窗、踢脚线、水电点位、墙饰面全都需要联动更新”。如果模型之间没有约束关系,每一次方案修改都是手工返工。
第三,BIM 落地到施工缺少“翻译层”。设计端的几何模型和施工端的工艺节点之间,不是一个简单的文件导出能解决的。墙面完成面到结构面之间有多少厚度,决定了门的尺寸是否需要加宽;地面找平层厚度决定了门洞预留高度够不够。这些工艺参数必须内置到编辑器规则里。
本文要讲的,就是我们如何用一套“参数化规则引擎 + 三维几何求解”的方案,把墙、地、顶从“画出来的模型”变成“算出来的模型”,并输出施工端真正需要的图纸、清单和加工数据。
阅读这篇文章,你不需要完整掌握 BIM 理论,但如果你承担过装修软件或云编辑器相关的架构设计,会发现我们讨论的很多坑都是共通的。
2. 核心概念:硬装 BIM、参数化施工与云编辑器边界
先厘清几个容易被混为一谈的概念。
2.1 硬装 BIM 和“翻模”的区别
很多团队做 BIM 的做法是:先在 CAD 里画好图纸,再导入三维软件“翻”出一个模型。这个模型本质上是可视化用的,不是计算用的。
我们定义的硬装 BIM 是从规则生成模型的过程:
- 设计师只在编辑器里定义“层高 2800mm、客厅开间 3900mm、墙体厚度 200mm、地暖找平厚度 70mm”;
- 程序根据这些基础参数,自动算出墙体高度、吊顶标高、地面完成面高度;
- 每一个几何面都带有材质、工艺、施工顺序等属性。
用一句话概括:传统建模是“画什么有什么”,参数化施工是“改参数,模型和工程量自动跟着变”。
2.2 参数化施工的层级
我们落地时把参数化施工分成三层,避免一开始就陷入具体算法而丢失整体结构:
| 层级 | 内容 | 典型例子 |
|---|---|---|
| 参数层 | 基础条件输入 | 层高、开间、进深、墙体厚度、地面做法厚度 |
| 规则层 | 工艺约束与联动规则 | 完成面高度推算、洞口过梁生成、排砖起始点规则 |
| 几何层 | 三维求解结果 | 实体墙体、吊顶网格、地面砖格、材质坐标 |
这个分层带来一个直接好处:设计师修改的是参数层,规则层负责解释参数是否合理,几何层负责生成可渲染、可加工的结果。
2.3 云编辑器和单机建模软件的边界
云编辑器不是把 Three.js 搬进浏览器就完了。它真正的差异化在于:
- 多人协作:设计师改墙体参数,水电工程师立刻看到点位联动变化;
- 规则集中管控:企业可以把工艺工法模板下发到云端,所有项目共用一套规则;
- 设备无关:设计师不需要装重型软件,打开浏览器就能建模、出清单。
但云编辑器也有限制,比如复杂几何运算在浏览器端跑会有性能瓶颈,所以我们在架构上把“高频交互的几何生成”放前端、“重量级规则校验和清单计算”放后端。这个边界划分,后面第 4 节详细讲。
从材料看,“bim模型文件下载”“bim模型导入到unreal”等搜索热度一直不低。这说明很多用户的真实诉求不只是看图,而是拿到模型文件去做二次加工,比如导入游戏引擎做场景漫游,或者给工厂做数控加工。因此,编辑器的导出能力必须一开始就设计好,而不是后期补丁式地增加一个“导出 OBJ”按钮。
3. 云编辑器整体架构与数据流
我们的编辑器整体架构可以分成四层:交互层、规则引擎层、几何求解层、数据服务层。
交互层(前端) 规则引擎层 几何求解层 数据服务层 户型/墙/地/顶操作 -> 参数校验 + 联动规则 -> 三维几何生成 -> 模型存储/清单/图纸 ^ | | | | v v v 用户自定义参数 工艺约束表 + 模板 网格/实体/UV 任务队列/消息推送3.1 交互层:只做“输入意图”,不做“几何结果”
交互层原则很简单:用户操作永远只修改参数,不直接修改三角网格。
比如用户拖拽一面墙的长度,前端只更新这条墙的 start/end 坐标和长度参数,然后调用规则引擎。规则引擎返回新的墙体和关联模型状态,前端再更新 Three.js 场景。
这么做的好处是撤销/重做非常容易——只需要记录参数快照,不需要记录网格顶点。坏处是复杂操作会带来一定的等待延迟,所以我们在前端做了乐观更新,用户拖动时先用旧的三角网格做视觉效果,后台计算完成后精准替换。
3.2 规则引擎层:工艺约束的核心
规则引擎是整篇文章的主角。它由三张基础表组成:
- 参数表:定义墙、地、顶可输入的参数项,如厚度、高度、找平层厚度、材质层数;
- 约束表:定义参数之间的函数关系,如“完成面高度 = 结构面高度 - 找平厚度 - 瓷砖厚度”;
- 联动表:定义修改某个实体后,哪些关联实体需要重新计算,比如墙厚变化后,门联窗的宽度是否需要调整。
实际运行中,我们用一套类似 JSON Schema 的描述来注册规则,这样工艺人员可以不用写代码就能增加新的工艺约束。
{ "ruleId": "wall-finished-height", "description": "墙体完成面高度等于层高减去地面完成面厚度", "inputs": ["storeyHeight", "floorFinishThickness"], "output": "wallFinishedHeight", "formula": "storeyHeight - floorFinishThickness" }3.3 几何求解层:把规则变成可渲染的实体
几何求解层接收规则引擎输出的参数,生成三维几何。这里我们没有直接使用 Three.js 自带几何体拼装,而是自己做了一套轻量级 BSP(二叉空间分割)变体,用于墙体洞口开洞、墙角自动连接这类布尔运算。
浏览器端跑完整 BSP 性能不够,所以我们的策略是:
- 常用墙体、楼板、吊顶使用参数化网格生成,直接按规则顶点坐标生成,不走布尔;
- 只有在门窗洞口开洞、墙面造型切分时才使用求交运算,且尽量使用简化几何。
3.4 数据服务层:全生命周期 BIM 的基础
所有参数和生成的几何最终都要落库。我们使用 JSON + GeoJSON 混合格式存储:JSON 存参数和业务属性,GeoJSON 存轮廓和空间关系。每次保存同时保留参数快照和网格快照,方便后续做版本对比。
从“全生命周期 bim 深度信息模型开发”这个热词来看,行业现在越来越关注模型能不能贯通设计、施工、运维。我们在编辑器里做的每一次规则计算,实际上都是在为这个目标积累结构化数据,而不是让信息停留在“一个好看的 3D 文件”层面。
4. 墙地顶参数化设计的核心原理
墙体、地面、顶面,三块看似独立,实际施工时是一套互相咬合的系统。设计参数化规则时,不能把三个模型分开做,必须统一考虑完成面基准线。
4.1 统一完成面基准:一切尺寸的锚点
装修设计和土建设计最大的不同,是装修有一个“完成面”概念。
我们设定房间地面对应的基准面为floorFinishLevel,它等于结构楼板标高加上找平层、地暖层、瓷砖/地板厚度之和。顶面对应的基准面为ceilingFinishLevel,它等于楼板底标高减去吊顶降板高度。墙面完成面则根据每个房间的饰面做法单独计算。
这三个完成面确定了整个房间的“净空”尺寸。设计师在编辑器里放家具、定衣柜高度、留插座位置,都是基于完成面来算,而不是基于结构面来算。这一步如果能算准,后面很多安装冲突都能提前暴露。
4.2 墙体参数化:从“一条线”到“多层构造体”
墙体不是一个简单的长方体,它由多条构造层组成。从结构面到完成面,通常包括:
- 基层墙体(砌体或轻钢龙骨墙体)
- 找平层(水泥砂浆或石膏)
- 饰面层(乳胶漆、瓷砖、木饰面等)
每一层都有厚度、材料、工艺要求。我们在墙的数据结构里设计了一个layers数组,墙体最终的三角网格由这些层按厚度方向叠加生成。
这样做的价值在施工图阶段体现得非常明显:工人可以随时查看某一面墙的剖面,知道该在哪里收口、哪里留缝、哪里加基层板。
4.3 地面参数化:排砖不是贴图
地面参数化的难点不是“铺一块平面”,而是排砖方案。瓷砖规格、起始点、缝隙宽度、波打线、过门石之间的几何关系,全部需要算法计算。
我们的排砖规则包含了几个常见模式:
- 起始点规则:从入口门洞中心开始起铺,还是从房间中心开始铺;
- 缝隙规则:留缝宽度、错缝率(如工字铺、三七铺);
- 边界规则:靠边砖的最小宽度,比如小于 100mm 时,需要把起始点偏移半块砖来避免窄条;
- 波打线:将房间轮廓向内偏移固定宽度,生成围合区域。
这些规则直接决定了瓷砖用量,而用量又直接进入施工清单。如果不做排砖计算,清单里的砖数只能按面积估算,落地后必然出现补砖浪费。
4.4 顶面参数化:跌级与设备点位
吊顶参数化比其他两项更依赖“设备协同”。中央空调出风口、新风风口、筒灯、检修口,需要在吊顶模型上开孔定位,且设备之间的间距要满足安装规范。
我们的吊顶参数化方案使用“网格区域 + 特征线”的方式:设计师先画吊顶标高线,程序自动生成跌级侧面;再在顶面上放置设备族,程序自动做开洞和加固提示。
这套机制避免了最常见的返工:工人施工到一半发现空调出风口位置和吊顶龙骨冲突,只能现场改方案。
5. 核心工程实现:墙体参数化完整示例
概念讲完,开始写代码。这里以墙体参数化为例,展示从参数定义到几何生成的全过程。代码使用 TypeScript 编写,直接运行于浏览器端规则引擎。
5.1 墙体分段数据模型
墙体在编辑器中不是一整块长方体和开洞组成的,而是一系列的“段 + 洞口”结构。首先定义基础类型:
// src/parametric/WallTypes.ts export interface Vector3 { x: number; y: number; z: number; } export interface WallLayer { id: string; name: string; // 材料名称 thickness: number; // 厚度 mm order: number; // 层顺序,从结构面到完成面 } export interface WallSegment { id: string; start: Vector3; // 墙段起点(轴线点) end: Vector3; // 墙段终点(轴线点) height: number; // 墙高 mm baseOffset: number; // 底部偏移(相对于完成面) layers: WallLayer[]; // 多层构造 } export interface WallOpenings { wallId: string; type: 'door' | 'window' | 'pass'; start: number; // 沿墙轴线的起始偏移 mm width: number; // 洞口宽度 mm height: number; // 洞口高度 mm sillHeight: number; // 离地高度 mm(门一般为 0) }5.2 规则引擎:从参数到墙体实体
我们用一个WallSolver类来求解墙体几何。它的核心不是直接生成网格,而是先生成“墙体轮廓体”,再由轮廓体去生成网格。
// src/parametric/WallSolver.ts import { WallSegment, WallOpenings, Vector3 } from './WallTypes'; export interface WallCreateConfig { start: Vector3; end: Vector3; layerHeights: number[]; wallHeight: number; baseOffset: number; } export class WallSolver { /** * 从轴线生成墙体构造层轮廓 * 这里返回的是每层构造对应的矩形面集合, * 后续由 MeshBuilder 生成三角网格。 */ generateLayerProfile(segment: WallSegment): LayerProfile[] { const dx = segment.end.x - segment.start.x; const dy = segment.end.y - segment.start.y; const length = Math.sqrt(dx * dx + dy * dy); // 轴线单位方向 const ux = dx / length; const uy = dy / length; // 法线方向(逆时针) const nx = -uy; const ny = ux; const profiles: LayerProfile[] = []; let accThickness = 0; // 按照 layer 顺序,逐个生成每层构造体 for (const layer of segment.layers.sort((a, b) => a.order - b.order)) { const x1 = segment.start.x + nx * accThickness; const y1 = segment.start.y + ny * accThickness; const x2 = segment.start.x + nx * (accThickness + layer.thickness); const y2 = segment.start.y + ny * (accThickness + layer.thickness); profiles.push({ layerId: layer.id, materialName: layer.name, bottom: segment.baseOffset, top: segment.baseOffset + segment.height, length: length, leftX: x1, leftY: y1, rightX: x2, rightY: y2, }); accThickness += layer.thickness; } return profiles; } } export interface LayerProfile { layerId: string; materialName: string; bottom: number; top: number; length: number; leftX: number; leftY: number; rightX: number; rightY: number; }这段代码做的事可以理解为:把墙上任意一点沿法线方向逐步“加厚”,每一层记录自己的净轮廓。后续无论是渲染、剖面图还是碰撞检测,都直接基于LayerProfile而不是依赖三角网格。
5.3 洞口自动开洞与过梁生成
洞口处理是最容易出 Bug 的地方。传统做法是直接做布尔减运算,但浏览器端布尔运算在大墙面下性能很差,且容易出现破面。
我们的方案是:先生成完整墙体,再对洞口区域做“模型替换”——把完整墙拆成几块规则几何体,中间留出洞口位置,洞口上方自动生成过梁。
// src/parametric/WallOpeningSolver.ts import { WallSegment, WallOpenings, Vector3 } from './WallTypes'; export function splitWallWithOpenings( segment: WallSegment, openings: WallOpenings[] ): SplitWallResult[] { const length = segmentLength(segment); const pieces: SplitWallResult[] = []; // 将所有洞口按轴线偏移排序 const sortedOpenings = openings .filter(o => o.wallId === segment.id) .sort((a, b) => a.start - b.start); let cursor = 0; for (const opening of sortedOpenings) { // 洞口前的整墙段 if (opening.start - cursor > 1) { pieces.push(createPiece(segment, cursor, opening.start, null)); } // 洞口本身:需要生成过梁 pieces.push(createPiece(segment, opening.start, opening.start + opening.width, { type: opening.type, openingHeight: opening.height, sillHeight: opening.sillHeight, })); cursor = opening.start + opening.width; } // 最后一段剩余墙体 if (length - cursor > 1) { pieces.push(createPiece(segment, cursor, length, null)); } return pieces; } function createPiece( segment: WallSegment, startOffset: number, endOffset: number, opening: { type: string; openingHeight: number; sillHeight: number; } | null ): SplitWallResult { // 根据偏移量计算该段的局部墙体几何 // 如果有洞口,则会在该段内生成洞口框架以及过梁 return { startOffset, endOffset, opening, geometry: null, // 实际生成由 MeshBuilder 完成 }; } interface SplitWallResult { startOffset: number; endOffset: number; opening: { type: string; openingHeight: number; sillHeight: number; } | null; geometry: unknown; } function segmentLength(segment: WallSegment): number { const dx = segment.end.x - segment.start.x; const dy = segment.end.y - segment.start.y; return Math.sqrt(dx * dx + dy * dy); }这样把“布尔减运算”变成了“切段重组”,既绕开了浏览器端 BSP 的性能问题,又让每一段墙体都保留了洞口参数,后续生成施工图时可以直接标注。
5.4 如何运行和验证
在浏览器环境中,我们可以直接调用WallSolver生成一个测试墙体:
npm run dev然后在项目里写一个临时测试脚本:
// scripts/test-wall.ts import { WallSolver } from '../src/parametric/WallSolver'; const solver = new WallSolver(); const profile = solver.generateLayerProfile({ id: 'wall-001', start: { x: 0, y: 0, z: 0 }, end: { x: 3000, y: 0, z: 0 }, height: 2600, baseOffset: 0, layers: [ { id: 'l1', name: '水泥砂浆找平', thickness: 20, order: 1 }, { id: 'l2', name: '加气混凝土砌块', thickness: 200, order: 2 }, { id: 'l3', name: '水泥砂浆找平', thickness: 20, order: 3 }, ], }); console.log('生成构造层数:', profile.length); console.log('总厚度:', profile.reduce((sum, p) => sum + p.rightX - p.leftX, 0));预期输出结果:生成 3 层构造,总厚度 240mm。如果输出与预期不符,优先检查layers的排序逻辑和accThickness累加是否在正确位置。
6. 地面与顶面参数化实现与收口规则
墙体解决了竖向空间,地面的排砖与顶面的吊顶则负责水平空间。这两个模块的算法更“工程化”,但实现思路同样遵循“先规则,后几何”。
6.1 地面区域识别与排砖算法
地面排砖的第一步是识别房间的可铺贴区域。我们通过户型的墙线数据,将房间轮廓转成多边形,再根据内边距去掉踢脚线和波打线区域,剩下的才是主铺区。
// src/parametric/FloorLayoutSolver.ts export interface Tile { x: number; y: number; width: number; height: number; // 该砖是否被裁剪 isCut: boolean; } export interface FloorRegion { boundary: Array<{ x: number; y: number }>; tileWidth: number; tileHeight: number; seam: number; startPoint: { x: number; y: number }; minEdgeTile: number; } /** * 生成铺砖格网。 * 以起铺点为原点,按瓷砖规格 + 缝隙宽度迭代生成。 */ export function generateTiles(region: FloorRegion): Tile[] { const tiles: Tile[] = []; const { boundary, tileWidth, tileHeight, seam, startPoint, minEdgeTile } = region; // 计算边界矩形 const minX = Math.min(...boundary.map(p => p.x)); const maxX = Math.max(...boundary.map(p => p.x)); const minY = Math.min(...boundary.map(p => p.y)); const maxY = Math.max(...boundary.map(p => p.y)); const stepX = tileWidth + seam; const stepY = tileHeight + seam; // 从起铺点开始,向左下和向右上两个方向扩展 for (let gy = Math.floor((minY - startPoint.y) / stepY) - 1; gy <= Math.ceil((maxY - startPoint.y) / stepY) + 1; gy++) { for (let gx = Math.floor((minX - startPoint.x) / stepX) - 1; gx <= Math.ceil((maxX - startPoint.x) / stepX) + 1; gx++) { const x = startPoint.x + gx * stepX; const y = startPoint.y + gy * stepY; // 简化处理:这里用 AABB 判断是否与房间相交, // 实际项目需用多边形裁剪判断瓷砖是否落在房间内。 const tile: Tile = { x, y, width: tileWidth, height: tileHeight, isCut: x < minX || x + tileWidth > maxX || y < minY || y + tileHeight > maxY, }; tiles.push(tile); } } // 窄边处理:如果边缘砖小于 minEdgeTile,整体平移半块砖 const edges = tiles.filter(t => t.isCut); const hasNarrowEdge = edges.some( t => t.width < minEdgeTile || t.height < minEdgeTile ); if (hasNarrowEdge) { // 调整起铺点后重新生成 return generateTiles({ ...region, startPoint: { x: startPoint.x - tileWidth / 2, y: startPoint.y - tileHeight / 2, }, }); } return tiles; }这套算法的优点是规则直白,适合 2D 排砖。实际项目中瓷砖还需要做纹理旋转、连纹拼接等处理,但那属于渲染层问题,不改变排砖网格本身。
6.2 吊顶跌级与设备点位
吊顶参数化比地面更复杂的地方在于它是三维结构。除了顶面平面网格,还要生成跌级侧面、收口线条和设备检修口开洞。
这里分享一个实用的简化策略:先把吊顶看成“标高面 + 轮廓线”的组合,再通过挤出生成侧面。跌级就是轮廓线向内部缩进并降低标高,从而形成一个台阶。
// src/parametric/CeilingSolver.ts export interface CeilingStep { height: number; // 该级标高(相对楼板底) inset: number; // 向内缩进距离 mm } export interface CeilingRegion { boundary: Array<{ x: number; y: number }>; steps: CeilingStep[]; } export interface CeilingMeshData { topPolygon: Array<{ x: number; y: number; z: number }>; sidePolygons: Array<Array<{ x: number; y: number; z: number }>>; openings: Array<{ x: number; y: number; width: number; height: number }>; } /** * 根据吊顶区域和跌级配置生成网格数据。 * 实现思路:逐级向内偏移轮廓线,并记录每级侧面的顶点。 */ export function generateCeilingMesh(region: CeilingRegion): CeilingMeshData { const topPolygon = region.boundary.map(p => ({ x: p.x, y: p.y, z: 0 })); const sidePolygons: Array<Array<{ x: number; y: number; z: number }>> = []; const openings: Array<{ x: number; y: number; width: number; height: number }> = []; let currentBoundary = region.boundary; let currentZ = 0; for (const step of region.steps) { const nextZ = step.height; const side: Array<{ x: number; y: number; z: number }> = []; // 对每一条边生成侧面 for (let i = 0; i < currentBoundary.length; i++) { const p1 = currentBoundary[i]; const p2 = currentBoundary[(i + 1) % currentBoundary.length]; side.push({ x: p1.x, y: p1.y, z: currentZ }); side.push({ x: p2.x, y: p2.y, z: currentZ }); side.push({ x: p2.x, y: p2.y, z: nextZ }); side.push({ x: p1.x, y: p1.y, z: nextZ }); } sidePolygons.push(side); // 下一级轮廓向内偏移 currentBoundary = offsetPolygon(currentBoundary, -step.inset); currentZ = nextZ; } // 设备洞口定位:在最后一级顶面上按设备族参数开洞 openings.push({ x: 1200, y: 800, width: 600, height: 200, // 示例:中央空调出风口 }); return { topPolygon, sidePolygons, openings }; } function offsetPolygon( boundary: Array<{ x: number; y: number }>, inset: number ): Array<{ x: number; y: number }> { // 实际项目使用 Clipper 库做多边形偏移 // 这里简化示意 const center = boundary.reduce( (acc, p) => ({ x: acc.x + p.x / boundary.length, y: acc.y + p.y / boundary.length }), { x: 0, y: 0 } ); return boundary.map(p => { const dx = p.x - center.x; const dy = p.y - center.y; const len = Math.sqrt(dx * dx + dy * dy); return { x: p.x + (dx / len) * inset, y: p.y + (dy / len) * inset, }; }); }注意,offsetPolygon里的简化实现只适合演示,真实项目的吊顶轮廓往往带弧线、内凹结构,必须使用专门的多边形偏移库,比如 Clipper2 的 WASM 版本。
6.3 墙地顶收口规则
三块模型都生成之后,收口处理是让效果“落地”的关键。常见的收口问题有这么几个:
- 墙砖完成面与地砖完成面之间,是否需要留 2mm 施工缝;
- 吊顶侧板与墙面之间,是否要留伸缩缝;
- 踢脚线的高度应该基于哪个完成面来计算。
我们在规则引擎里为每个收口点定义了一个jointRule。当墙、地、顶三个模块都计算出结果后,程序自动检查收口区域是否满足规则,不满足时给出 warning,而不是直接生成错误模型。
这套机制让设计师在显示器前就能提前发现现场问题,而不至于等施工队进场后才发现。
7. 施工合理性校验与数据输出
参数化模型生成后,不能直接交给施工。我们需要做一道关键的“安检”:校验施工合理性。校验通过后,再输出施工端需要的图纸、清单和加工数据。
7.1 规则校验:硬碰撞 + 软碰撞 + 工艺区间校验
我们把校验分成三个级别:
硬碰撞:两个实体在空间上发生了直接的几何相交,必须报错。例如墙体和吊顶侧板穿透、水管和电管交叉。
软碰撞:空间上不重合,但不满足安装规范。例如空调出风口与灯槽间距不足 300mm,筒灯边缘离墙太近。
工艺区间校验:数值在合理区间之外。例如地砖窄边小于 50mm,墙面完成面厚度导致门套宽度不够。
// src/validate/ScriptValidator.ts export interface ValidationIssue { level: 'error' | 'warning'; code: string; message: string; elementId: string; } export function validateBimModel(model: { walls: unknown[]; floors: unknown[]; ceilings: unknown[]; }): ValidationIssue[] { const issues: ValidationIssue[] = []; // 模拟墙体与吊顶的高度冲突检测 for (const wall of model.walls as any[]) { const wallTop = wall.baseOffset + wall.height; for (const ceiling of model.ceilings as any[]) { const ceilingBottom = ceiling.height; if (wallTop > ceilingBottom) { issues.push({ level: 'error', code: 'WALL_CEILING_OVERLAP', message: `墙体 ${wall.id} 顶面高于吊顶底面 ${ceilingBottom}mm`, elementId: wall.id, }); } } } // 模拟门窗洞口与梁底的净高校验 for (const opening of model.openings as any[]) { if (opening.sillHeight + opening.height > 2400) { issues.push({ level: 'warning', code: 'OPENING_HEADROOM_LOW', message: '洞口顶面过高,可能影响梁底净高,请与结构专业复核', elementId: opening.wallId, }); } } return issues; }实际工程中,我们把校验做成异步任务。设计师点“校验”按钮后,前端把几何数据发给后端,后端跑完整的碰撞检测算法,通过消息队列把结果推回前端。之所以这样做,是因为完整工地的模型可能包含几十万面片,浏览器端跑会白屏。
7.2 施工信息输出:图纸、清单和加工单
参数化模型最大的价值,是它天然可以输出结构化施工信息,而不是一张静态效果图。
以墙体为例,我们在编辑器里点击“生成施工信息”,后端从参数模型中提取以下内容:
- 每一面墙的长度、高度、面积、体积;
- 各构造层的材料类型和用量;
- 门窗洞口的位置、尺寸、过梁信息;
- 与相邻墙体的连接方式(直角、T 型、斜角)。
这些数据可以直接导出为 JSON 或 Excel 格式的施工清单。对于工厂加工场景,我们还可以直接输出 CNC 加工单,比如橱柜台面、岩板背景墙的切割尺寸。
7.3 BIM 模型导出:从编辑器到仿真引擎
关于“bim模型导入到unreal”这类需求,我们的做法是:在数据服务层维护一个统一的模型导出模块,支持导出 FBX、OBJ、GLTF/GLB 等格式。
这里有一个很关键的工程经验:不要直接从三维网格导出 FBX,而是从参数模型重新生成导出网格。
原因在于,编辑器内部的网格经过过多轮局部修改,可能有顶点合并和 UV 错乱问题。直接从参数模型导出,可以保证导出的模型干净、层级清晰、材质可对应。具体流程是:
参数模型 -> 按规则重新生成三角网格 -> 烘焙材质/UV -> 导出 FBX/GLB这样导出的模型导入 Unreal 或 Unity 后,可以保持正确的世界坐标和比例,材质也能一一对应。模型文件下载功能也基于同一套导出模块,只是压缩格式和分辨率不同。
8. 常见问题与排查思路
这套系统落地过程中,我们踩过不少坑。挑几个高频的列在下面,供后来者参考。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 墙体生成后出现破面/闪烁 | 相邻墙段构造层重叠或间隙过大 | 检查墙段端点的法线方向和粗糙度值 | 统一墙段端点的合并容差,对相邻墙段做顶点焊接 |
| 门窗洞口位置偏差超过 5mm | 洞口的start偏移基于墙轴线,但墙线方向判断错误 | 打印墙段 start/end 向量与洞口的坐标系 | 统一洞口偏移量的坐标系定义,并增加自检函数 |
| 地砖排砖结果乱,出现大量窄条 | 起铺点未遵循“入口优先”规则 | 检查startPoint是否来自门洞中心 | 修正排砖的起点选择逻辑,增加窄边自动调整 |
| 吊顶跌级侧面渲染方向反了 | 侧面顶点绕序不符合逆时针规则 | 查看面法线方向是否指向房间外侧 | 调整sidePolygons的点序生成逻辑 |
| 校验结果有时差 | 前端几何数据在发送前未做简化 | 检查发送数据的大小和几何精度 | 使用轻量化几何或后端异步校验 |
| 导出 FBX 后材质丢失 | 材质命名或路径中包含特殊字符 | 检查导出日志 | 在导出前对资源名做安全处理 |
这里特别想强调一个问题:很多“看起来是渲染问题”的现象,根因其实是参数错误。比如墙体破面,多数不是 Three.js 渲染设置问题,而是墙段端点没有精确合并。调试时不要第一反应就去调渲染参数,应该先检查数据层的输出。
9. 工程化建议与后续演进
最后分享几条我们沉淀下来的工程化建议,不一定适合所有团队,但应该能帮你少踩一些共通的坑。
9.1 规则先行,几何紧随
我们最初犯过一个错误:先写了几何生成逻辑,再回头搞参数规则,结果几何代码越写越复杂,每加一种新的墙面造型都要改老代码。
后来重构为“规则先行”后,几何变成规则的执行器。新增墙面造型时,通常只需要增加一条规则,几何层代码基本不需要动。
9.2 参数快照是救命稻草
家装方案改动频繁,一个方案可能反复改十几次。我们在每次保存时都记录参数快照,而不是只保存最终网格。这样无论哪一版出了问题,都可以回到任意参数版本,重新生成几何。
这也让“多方案对比”变成可能:同时生成两套参数模型的轻量化预览,让业主快速对比不同方案对造价和效果的影响。
9.3 性能优化要分级
浏览器端建模性能优化,我们遵循一个原则:交互过程用轻量几何,确认状态用完整几何。
- 拖拽墙体时,只更新墙体的包围盒和几条轮廓线,不做三角网格重算;
- 松开鼠标后,后台调度完整几何计算;
- 完整计算完成后,再替换轻量几何。
这样既保证了操作的流畅性,又不牺牲最终模型的精度。
9.4 全生命周期数据意识
回到“全生命周期 bim 深度信息模型开发”这个热词。我们做编辑器时始终提醒自己:模型里的每一个参数、每一条规则,都是未来运维阶段的资产。
比如墙体里记录了某一层用的是 A 类难燃材料,那么将来房屋改造拆除时,就能自动提示施工人员注意防火隔离要求。如果我们当初只记录“墙体长宽高”,这些信息就永久丢失了。
所以后续演进方向主要有三个:
- 建立材质库与工艺库的联动,把材质防火等级、隔声性能等属性接入规则引擎;
- 在施工清单中加入施工顺序和工期估算,让“参数化设计”进一步走向“参数化施工管理”;
- 把模型导出能力标准化,打通从编辑器到工厂 CNC 加工的自动化链路。
墙地顶参数化施工做好之后,编辑器的价值就不再是“做一套好看的效果图”,而是变成设计师和施工队之间的“翻译器”。设计师改一个吊顶标高,工长手里的图纸、清单和加工数据同步更新。这个数据同步的精度和速度,才是自研家装云编辑器真正的护城河。