平面图内部标注线保真度修复实录:Pascal 编辑器施工尺寸基线与房间侧门宽标注的源码级剖析
【免费下载链接】editorOpen-source 3D architectural editor with a local CLI, MCP tools, and practical workflows for humans and AI agents.项目地址: https://gitcode.com/GitHub_Trending/editor93/editor
导读
本文以仓库根目录下的 design-qa.md 视觉对比文档为主线,完整还原 Pascal 开源 3D 建筑编辑器(editor93/editor)中一次针对平面图内部标注线(internal dimension lines)保真度的质量修复过程:源视觉稿中标注线塌缩到墙体上、文本变成"悬浮数字",以及封闭外围墙大门的房间侧宽度标注缺失。文章将逐层拆解问题的 P0 定位、offsetDistance偏移机制的根本原因、自动基线与渲染器的调用链、房间侧开门链的规划逻辑,以及由 61 个通过的测试构成的回归保障。读完你既能复现这套"以源视觉为基准的 QA 对比"方法论,也能理解施工尺寸标注从规划(plan)到几何(geometry)再到 SVG 渲染的完整数据流。
一、问题背景:平面图标注 QA 的对比目标与判定标准
design-qa.md是一个典型的视觉对比 QA 记录,它定义了本次修复的验收基线:
- 源视觉真值(Source visual truth):两张来自系统剪贴板的参考截图,作为渲染结果的事实基准;
- 实现截图(Implementation screenshot):本地编辑器导出的平面图预览,视口 1280 × 720;
- 期望状态(Intended state):内部标注的基线(baseline)、引出线(witness lines)、刻度线(ticks)和数值(values)清晰脱离墙体渲染,且封闭外围墙(enclosed perimeter walls)上的门在房间内侧获得宽度标注。
这份文档本身记录了 QA 过程中的一个关键阻塞(P0):本地编辑器预览停留在加载指示器上,点击 2D 后 3D 仍处于选中态,导致"同一场景状态的浏览器渲染对比"无法完成。这是仓库 QA 流程的典型形态——几何层可用测试断言,像素层需要人工截图验收,两者缺一不可。
二、问题定位:基线坐标被显式钉在引出点,offsetDistance被覆盖
design-qa.md的"Comparison History"给出了问题的最早形态:
Earlier P0: internal baseline coordinates were explicitly equal to witness coordinates, overriding
offsetDistanceand collapsing lines onto walls.
也就是说,内部标注的基线两端坐标被显式赋值为引出点(witness)坐标,等于把偏移距离写死为 0,最终标注线全部贴死在墙面上,数值字符串读起来像一堆"脱离上下文的悬浮文本"。
2.1 根源:buildDimensionStringGeometry的偏移默认值语义
几何组装的统一入口在 packages/nodes/src/shared/dimension-string.ts:
export function buildDimensionStringGeometry(input: DimensionStringGeometryInput): FloorplanGeometry { return { kind: 'dimension-string', segments: input.segments.map((segment) => ({ ... })), offsetNormal: input.offsetNormal, offsetDistance: input.offsetDistance ?? 0, // 未提供时默认 0 extensionStartGap: input.extensionStartGap, extensionOvershoot: input.extensionOvershoot ?? 0, ... } }这里语义非常关键:dimensionStart/dimensionEnd是可选的——当调用方不显式提供基线坐标时,渲染器会依据offsetNormal × offsetDistance自动推导基线位置;反之,如果调用方把基线坐标显式设为与 witness 相同,那么无论offsetDistance配置成多少,都被"覆盖"成 0。
2.2 修复一:保留省略的自动基线,让渲染器应用配置偏移
design-qa.md记录的修复方向是:
Fix: preserve omitted automatic baselines so the renderer applies the configured offset; add enclosed room-side opening chains for perimeter walls.
对应到代码,墙施工尺寸规划在 packages/nodes/src/wall/construction-dimensions.ts 的buildInteriorWallDimensions中,产出PlannedConstructionDimension时只给 witness 起点/终点与偏移量,不写死dimensionStart/dimensionEnd:
planned.push({ tier: 'interior', start: pointAt(start), end: pointAt(end), offsetNormal: normal, offsetDistance: standard.openingChainOffset, // 默认 0.55 }) planned.push({ tier: 'interior-overall', start: pointAt(spanStart), end: pointAt(spanEnd), offsetNormal: normal, offsetDistance: openingSpans.length > 0 ? standard.wallSpanOffset : standard.openingChainOffset, })随后由renderPlannedConstructionDimensions(同文件 L383-L416)把偏移原样透传给buildDimensionStringGeometry。渲染端 SVG 组件在收到没有显式基线的dimension-string时,会按偏移自动生成基线——这正是回归测试 packages/editor/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx 所断言的:
test('offsets automatic dimension-string lines when no explicit baseline is supplied', () => { const automaticString = { kind: 'dimension-string', segments: [{ start: [0, 0], end: [2, 0], text: '2m' }], offsetNormal: [0, 1], offsetDistance: 0.55, ... } // 渲染后基线 y 坐标应为 0.55 expect(markup).toContain('data-floorplan-dimension-default-y1="0.55"') expect(markup).toContain('data-floorplan-dimension-default-y2="0.55"') })需要说明的是,手动施工尺寸(packages/nodes/src/construction-dimension/floorplan.ts 的buildLinearOrChord)走的是另一条路径:布局函数resolveConstructionDimensionLayout会解析出明确的基线,因此那里offsetDistance: 0是正确语义——"显式基线"与"自动基线"两种模式必须区分对待,这正是本次 BUG 修复的关键认知。
三、修复二:封闭外围墙房间侧开门链(perimeter door room-side widths)
design-qa.md记录的第二项缺口:左侧较大的外围墙门在源状态下没有房间侧宽度标注。修复后:
Generated plans now include room-side opening chains for enclosed perimeter walls in all four orientations, including a left-side door.
3.1 房间侧法向的判定:enclosedRoomSideNormal
在 construction-dimensions.ts 中,enclosedRoomSideNormal负责确定"房间在墙的哪一侧":
function enclosedRoomSideNormal(wall, walls): FloorplanPoint | null { const outward = exteriorNormal(wall) if (!outward) return null ... const { frontClearance, backClearance } = interiorDimensionClearances(wall, walls, tangent) const inward = negate(outward) const inwardClearance = dot(inward, front) >= 0 ? frontClearance : backClearance return inwardClearance === null ? null : inward }核心逻辑:取外墙朝外的法向,翻转为朝内方向,再结合interiorDimensionClearances(沿两侧做射线求交,得到最近的墙/弧线距离,见同文件 L775-L800)判定该方向确实被围合,从而得到合法的"房间侧法向"。之后buildInteriorWallDimensions以normalOverride形式接收该法向,沿房间内侧生成开门宽度链与整墙跨度标注。
3.2 回归覆盖:四个方向的 perimeter door 都要有房间侧宽度
规划层测试 packages/nodes/src/wall/construction-dimensions.test.ts 用上/右/下/左四堵封闭外围墙分别挂载不同宽度的门,逐一断言:
test('dimensions perimeter door widths on the room side in every wall orientation', () => { const top = wall({ id: 'wall_top', end: [6, 0] }) const right = wall({ id: 'wall_right', start: [6, 0], end: [6, -6], frontSide: 'exterior', backSide: 'interior' }) const bottom = wall({ id: 'wall_bottom', start: [6, -6], end: [0, -6], frontSide: 'exterior', backSide: 'interior' }) const left = wall({ id: 'wall_left', start: [0, -6], end: [0, 0], frontSide: 'exterior', backSide: 'interior' }) const cases = [ { wall: top, normal: [0, -1], width: 1.2 }, { wall: right, normal: [-1, 0], width: 1.3 }, { wall: bottom, normal: [0, 1], width: 1.4 }, { wall: left, normal: [1, 0], width: 1.5 }, ] ... for (const { wall: host, normal, width } of cases) { const roomSideDimensions = (plan.get(host.id) ?? []).filter( (entry) => (entry.tier === 'interior' || entry.tier === 'interior-overall') && entry.offsetNormal[0] * normal[0] + entry.offsetNormal[1] * normal[1] > 0.99, // 方向须朝向房间内侧 ) expect(roomSideDimensions.length).toBeGreaterThan(0) expect(dimensionTexts(renderPlannedConstructionDimensions(roomSideDimensions, 'metric'))) .toContain(`${width}m`) } })这个测试同时验证了三点:标注链存在、方向确实朝向房间内侧(点积 > 0.99)、渲染出的文本包含对应门宽。仓库内还有"对边为弧形墙时房间侧门/窗标注不丢失"的变体用例(同测试文件 L694 起),说明弧形边界场景也被覆盖。
四、标注偏移背后的绘制标准配置
上述 0.55 m、1.05 m 等数值并非魔法数字,而是出自统一的绘制标准配置 packages/nodes/src/shared/construction-dimension-standards.ts:
export const DEFAULT_CONSTRUCTION_DIMENSION_STANDARD = { datumPolicy: 'wall-face', // 基准策略:中心线 / 墙面 / 结构面 / 完成面 intersectionReferencePolicy: 'single', terminator: 'architectural-tick', // 建筑刻度线 textPosition: 'above', // 文字在基线上方 imperialPrecision: '1/16', metricNotation: 'meters', openingChainOffset: 0.55, // 开门/窗链距墙面偏移(m)——本次修复的回归值 wallSpanOffset: 1.05, // 整墙跨度标注距墙面偏移(m) firstOpeningWidthOffset: 0.62, firstGeneralTierOffset: 0.55, tierSpacing: 0.62, // 多级标注带间距(m) extensionStartGap: 0.075, // 引出线起点离墙间隙 extensionOvershoot: 0.12, // 引出线超出基线长度 } satisfies ConstructionDimensionDrawingStandard类型定义(同文件 L7-L21)还包含datumPolicy: 'centerline' | 'wall-face' | 'structural-face' | 'finish-face'四种基准策略。datumPolicy会直接传导到墙施工尺寸的基准距离计算(如wallDatumOffset中centerline返回 0、其余策略按getWallThickness(wall) / 2计算,见 packages/nodes/src/construction-dimension/floorplan.ts),理解这一层才能解释"偏移从哪来、被谁消费"。
在层级规划中,buildLevelWallConstructionDimensionPlan(construction-dimensions.ts)会把标注按tier分层:opening-widths → openings → partitions → structure → jogs → overall → structural-overall,内部墙再追加interior/interior-overall。测试 construction-dimensions.test.ts 验证了分区墙 + 门 + 窗的完整链条:开门窗链偏移 0.55、整墙跨度偏移 1.05,且逐段断言"渲染基线相对 witness 的净偏移等于配置值"。
五、回归证据与验收清单
design-qa.md记录的修复后证据与当前仓库测试状态一致:
| 验证项 | 结果 | 依据 |
|---|---|---|
| 尺寸 / 墙体 / 平面图 / 注册表测试 | 61 passed, 0 failed | design-qa.md 记录 |
| Nodes 包构建 | 通过 | design-qa.md 记录 |
| Editor 包类型检查 | 被无关的resolveFloorplanExportViewport缺失导出阻塞(floorplan-export.test.ts引用) | design-qa.md 记录 |
| Biome 检查 | 通过 | design-qa.md 记录 |
| Git diff 空白检查 | 通过 | design-qa.md 记录 |
| 浏览器控制台警告/错误 | 无 | design-qa.md 记录 |
其中"自动基线偏移 0.55 m"由 SVG 渲染器回归测试断言(floorplan-dimension-renderer.test.tsx),"四方向外围墙房间侧门宽"由规划器回归测试断言(construction-dimensions.test.ts)。
文档同时给出了浏览器渲染证据仍被阻塞的验收清单(Implementation Checklist),这是像素级验收的必经步骤:
- 恢复本地编辑器预览;
- 在 2D 中重新打开目标房间;
- 确认每条内部字符串都有可见的平行基线、引出线与刻度线;
- 确认左侧大门显示其房间侧宽度标注。
后续抛光建议(Follow-up Polish)则强调:内部标注线对比度(contrast)的重新评估必须等到修正后的几何在目标场景中可见之后再进行——即几何正确性是像素级评审的前置条件。
六、方法论沉淀:几何可测、像素须验
从design-qa.md这份 QA 记录可以提炼出一套可复用的平面图标注保真度验收方法:
- 双证据体系:几何层用测试断言(偏移值、tier 顺序、方向点积、渲染文本),像素层用截图对比(源视觉真值 vs 实现截图);
- 明确的 P0 分级:浏览器渲染证据缺失即 P0,因为它直接阻塞"屏幕空间线可见性、碰撞与门宽放置"的最终验收;
- 显式/自动基线语义分离:
dimension-string几何中"未提供基线坐标"意味着渲染器按offsetDistance自动推导基线;任何显式钉死基线的行为都会覆盖偏移——这是本次缺陷的根因,也是后续新增标注类型时最容易踩的坑; - 方向敏感的房间侧标注:外围墙的门宽标注必须落在房间内侧,由
enclosedRoomSideNormal+interiorDimensionClearances共同判定,并以点积断言保护。
对希望深入源码的读者,推荐按以下顺序追踪数据流:绘制标准配置(construction-dimension-standards.ts)→ 墙标注规划与渲染(construction-dimensions.ts)→ 几何统一装配(dimension-string.ts)→ SVG 渲染器回归(floorplan-dimension-renderer.test.tsx)。结合本文与 design-qa.md 对照阅读,即可完整复现"发现问题 → 定位根因 → 修复 → 几何回归 → 像素验收"的完整闭环。
【免费下载链接】editorOpen-source 3D architectural editor with a local CLI, MCP tools, and practical workflows for humans and AI agents.项目地址: https://gitcode.com/GitHub_Trending/editor93/editor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考