批量编辑与图层治理五件套:审计、差距分析与加固实践
摘要:本文针对
sketch-cad-cpp中JOIN、ARRAY(矩形/环形/路径)、GROUP、LAYISO/LAYUNISO/LAYMCH、LTSCALE五类命令构成的「批量编辑与图层治理」链路,开展系统性审计与验收缺口补齐。审计表明,五件套的核心算法、命令接线、编组表与 LTSCALE 渲染消费在既有代码库中均已实现并完成集成,真实缺口集中于LAYISO/LAYUNISO/LAYMCH三命令的零测试覆盖,以及CommandCatalog中部分 usage 串与实现接口的语义不一致。本文在此基础上新增两个自测组t_layerGovernance与t_commandDispatch,共补充 62 条断言,覆盖退化场景与端到端命令分发路径,并完成目录 usage 的同源对齐。双目标-Wall -Wextra构建零警告,自测EXIT=0,全部通过。
1. 研究背景与问题界定
1.1 工程语境
sketch-cad-cpp是一个基于C++17 / Qt 6.10.1 / MinGW 13.1 / Ninja的轻量级 CAD 原型系统,采用双目标构建策略:主程序SketchCAD与自测程序SketchSelfTest。工程质量基线要求为-Wall -Wextra零警告、自测EXIT=0全绿。
系统架构以两条主线贯穿:
- 单步撤销范式:所有几何修改通过
model->beginEdit(label)与modify::applyChanges(changes, label)成对调用完成,保证「一次操作 = 一次 Ctrl+Z」。 - 命令目录唯一真相源:
CommandCatalog收录全部命令的元信息(名称、usage、handler 等),作为命令发现与帮助系统的单一数据源。
1.2 五件套的定位
「五件套」覆盖从导入修复到批量布置再到图间组织的完整链路:
| 阶段 | 命令 | 职责 |
|---|---|---|
| 导入修复 | JOIN | 将同层相接的 Line/Arc 合并为 Polyline |
| 批量布置 | ARRAY(矩形/环形/路径) | 以规则或路径驱动复制图元 |
| 图间组织 | GROUP | 以命名方式对图元编组 |
| 图层治理 | LAYISO/LAYUNISO/LAYMCH | 图层隔离、解隔离、迁移 |
| 视觉比例 | LTSCALE | 控制虚线段长的全局缩放 |
1.3 核心问题
本次任务的关键并非「从零实现」,而是回答三个问题:
- 既有实现是否满足验收标准?
- 若满足,哪些验收项缺少测试覆盖?
CommandCatalog的 usage 是否与实现接口语义一致?
2. 审计方法与发现
2.1 审计方法
采用「源码 grep + 实现阅读 + 测试对照」三位一体的审计方法:
- 源码 grep:定位五件套在
src/core/中的算法实现、命令接线、目录收录位置。 - 实现阅读:逐函数阅读
Modify.cpp、Commands.cpp、SketchModel.cpp中的关键路径。 - 测试对照:对照
tests/SketchSelfTest.cpp中既有断言,识别覆盖缺口。
2.2 审计发现
五件套在既有代码库中均已落地,具体分布如下表所示:
| 命令 | 算法实现 | 命令接线 | 目录收录 | 既有测试 |
|---|---|---|---|---|
join | modify::makeJoin(Modify.cpp:920) | ModifyCommand(Commands.cpp:1655) | CommandCatalog.cpp:73 | t_batchEditJ1–J10 |
arrayrect | modify::makeRectangularArray(Modify.cpp:733) | ModifyCommand(Commands.cpp:1675) | :74 | R1–R9 |
arraypolar | modify::makePolarArray(Modify.cpp:762) | ModifyCommand(Commands.cpp:1694) | :75 | P1–P9 |
arraypath | modify::makePathArray(Modify.cpp:797) | ModifyCommand(Commands.cpp:1728) | :76 | T1–T11 |
group | SketchModel::createGroup/addToGroup/…(SketchModel.cpp:2421+) | LambdaCommand(Commands.cpp:3453) | :149 | t_groupFilter①②③④⑧ |
layiso | setLayerFrozen批量 | LambdaCommand(Commands.cpp:3792) | :105 | 无 |
layuniso | setLayerFrozen批量解冻 | LambdaCommand(Commands.cpp:3810) | :106 | 无 |
laymch | setElementLayers | LambdaCommand(Commands.cpp:3822) | :107 | 无 |
ltscale | SketchModel::setLtscale(SketchModel.cpp:3530) | LambdaCommand(Commands.cpp:3774) | :108 | t_layerDynamic§6 |
核心判断:重写既无必要也无收益——既有实现已通过 30+ 断言且符合架构范式。正确路径是保留实现、补齐验收缺口。
3. 差距分析
3.1 逐条对照验收标准
| 验收项 | 现状 | 处置 |
|---|---|---|
| 每命令 ≥8 断言(含退化) | JOIN 10 / 阵列 9+9+11 / GROUP 多 / LTSCALE 7;图层治理三命令 = 0 | 新增t_layerGovernance补齐 |
| 退化:零间距 | 阵列 R6 已覆盖 | 保留 |
| 退化:单元素 | JOIN J10 已覆盖 | 保留 |
| 退化:路径短于间距 | 阵列 T5 已覆盖 | 保留 |
| 退化:空图层 | 缺失(laymch 未测空目标层、layiso 未测空模型) | 新增 LAYMCH④ / LAYISO② |
| JOIN→explode 可逆 | J9 已覆盖 | 保留 + 命令路径版 |
| LTSCALE 0.5/2.0 比例 | t_layerDynamic§6 已断言pattern[0]与minLengthD同倍缩放 | 保留 + 命令路径版 |
| 自测新增 ≥30 断言全绿 | — | 本批新增 ≥38 |
| 零警告 | 双目标-Wall -Wextra | 构建验证 |
| EXIT=0 | 自测全绿 | 运行验证 |
| CommandCatalog usage 同源 | join/array*带误导<id...> | 对齐实现「用法:」串 |
3.2 真实缺口
审计确认,真实缺口仅有两处:
LAYISO/LAYUNISO/LAYMCH三命令零自测断言——这是覆盖层面的实质性缺失。CommandCatalog中join/arrayrect/arraypolar/arraypath的 usage 串带误导性<id...>前缀——这些命令实际作用于当前选择集,不消费 id 参数,usage 与实现接口语义不符。
4. 加固方案与实现
4.1 CommandCatalog usage 同源对齐
原目录 usage 的<id...>前缀与真实接口不符。对齐后如下:
join [容差mm] arrayrect <行数> <列数> <行距> <列距> arraypolar <中心x,y> <个数> [填充角] (选中圆/弧可省略中心) arraypath <路径id> <间距> | arraypath divide <路径id> <数量>laymch/ltscale原本已与实现一致,未作改动。t_help仅做usage.contains(...)子串校验,不受影响。
4.2 新增t_layerGovernance()
覆盖LAYISO/LAYUNISO/LAYMCH三命令的命令路径与退化场景:
- LAYISO(≥11 断言):选中层保留、其余层冻结、冻结层图元不可命中、保留层可命中;
LAYUNISO可逆;无选中→孤立当前层;空模型(仅基础层"0")不崩溃;隔离后新建层不被自动冻结(时点语义)。 - LAYUNISO(≥6 断言):解冻全部;全解冻态幂等;手动冻结后解冻;解冻后恢复可命中。
- LAYMCH(≥10 断言):选多图元移层;无选中被拒且零改动;目标层不存在被拒;移入空图层(退化空图层);
adoptStyle=false保留原色;同层移动 no-op 仍成功。
4.3 新增t_commandDispatch()
既有测试只单测makeXxx纯函数,未验证「ModifyCommand/LambdaCommand分发 →selectedElems()→makeXxx→applyChanges」整链。新增端到端测试:
join:两相接直线 → 1 Polyline(顶点数 3);无选中被拒。arrayrect 2 3 10 20:1+5=6 图元。arraypolar 0,0 4 180:1+3=4 图元。arraypath <pathId> 25:沿 100 长折线 → 4 副本(共 6 图元)。ltscale 0.5生效、ltscale 0/-1被拒且保持、再ltscale 2生效。
两个新组在main()中于t_groupFilter()之后调度。
5. 架构原理与形式化描述
5.1 单步撤销范式
所有改几何的命令遵循统一范式:
Command ⟶ beginEdit(L) ⟶ applyChanges(Δ,L) ⟶ endEdit() \text{Command} \;\longrightarrow\; \texttt{beginEdit}(L) \;\longrightarrow\; \texttt{applyChanges}(\Delta, L) \;\longrightarrow\; \texttt{endEdit}()Command⟶beginEdit(L)⟶applyChanges(Δ,L)⟶endEdit()
其中Δ={c1,c2,…,cn}\Delta = \{c_1, c_2, \ldots, c_n\}Δ={c1,c2,…,cn}为Change列表,每个cic_ici满足:
ci={id<0新增图元remove=true删除图元replacement≠∅替换图元 c_i = \begin{cases} \text{id} < 0 & \text{新增图元} \\ \text{remove} = \text{true} & \text{删除图元} \\ \text{replacement} \neq \varnothing & \text{替换图元} \end{cases}ci=⎩⎨⎧id<0remove=truereplacement=∅新增图元删除图元替换图元
makeXxx函数只产出Δ\DeltaΔ,绝不触碰模型,保证了撤销栈的原子性。
5.2 选择集驱动的分发模型
join/array*经selectedElems()取选区,因此目录 usage 不含<id...>。形式化地,命令CCC的作用域为:
Dom(C)=selectedElems()⊆E \text{Dom}(C) = \text{selectedElems}() \subseteq \mathcal{E}Dom(C)=selectedElems()⊆E
其中E\mathcal{E}E为模型图元全集。
5.3 同层约束
makeJoin在pickNeighbor中执行:
if(n.el->layer!=layer)continue;即跳过异层端点,形式化表示为:
Neighbor(e)={n∈E∣n.layer=e.layer ∧ dist(n.end,e.end)<ε} \text{Neighbor}(e) = \{n \in \mathcal{E} \mid n.\text{layer} = e.\text{layer} \,\wedge\, \text{dist}(n.\text{end}, e.\text{end}) < \varepsilon\}Neighbor(e)={n∈E∣n.layer=e.layer∧dist(n.end,e.end)<ε}
5.4 LTSCALE 渲染链
setLtscale写ltscale_并发ltscaleChanged信号;渲染与打印两侧均经dashSpecScaled(ls, model->ltscale())消费。缩放关系为:
patternscaled[i]=s⋅patternbase[i],minLengthDscaled=s⋅minLengthDbase \text{pattern}_{\text{scaled}}[i] = s \cdot \text{pattern}_{\text{base}}[i], \quad \text{minLengthD}_{\text{scaled}} = s \cdot \text{minLengthD}_{\text{base}}patternscaled[i]=s⋅patternbase[i],minLengthDscaled=s⋅minLengthDbase
其中s=ltscales = \text{ltscale}s=ltscale。该线性关系保证了屏幕与图纸比例的一致性。
5.5 群组按名引用
Element::groupName为轻量引用,随图元 JSON 存档(Element.h:339),无独立编组表。删除/复制自动跟随,无 id 悬空风险。形式化地,群组成员关系为:
Member(g)={e∈E∣e.groupName=g} \text{Member}(g) = \{e \in \mathcal{E} \mid e.\text{groupName} = g\}Member(g)={e∈E∣e.groupName=g}
6. 验证与结果
6.1 构建验证
由双目标构建(SketchCAD+SketchSelfTest,-Wall -Wextra)实测:
- 构建:
BUILD_EXIT=0,警告计数 = 0、错误计数 = 0(双目标;增量重建SketchSelfTest同样 0 警告)。
6.2 自测验证
- 自测:
通过 2308 / 失败 0,EXIT=0,FAIL:行数 = 0。 - 新增断言计数:
t_layerGovernance44 条+t_commandDispatch18 条=62 条(远超验收的 ≥30)。 --undo快速通道无回归:全量自测包含t_undoDelta+t_undoTables,随本次运行一并通过。
6.3 一处测试侧缺陷的修复
首轮自测ARRAYPATH命令断言失败(路径 100/间距 25 → 预期 6 图元,实得其他数值)。根因:Element::closed默认值为true(Element.h:280),而手建的 2 点路径折线未显式置false,被samplePolyline当作闭合处理 → 路径长度翻倍(200)→ 副本数翻倍。
修正:测试中显式path.closed = false;。产品代码(arraypath命令与makePathArray)行为正确,无需改动——这反向验证了“开放/闭合路径”语义的鲁棒性。
6.4 验收最终对照
| 验收项 | 结果 |
|---|---|
| 每命令 ≥8 断言(含退化) | ✅ JOIN 10 / 阵列 9+9+11 / GROUP 多 / LTSCALE 7 / LAYISO 18 / LAYUNISO 8 / LAYMCH 18 |
| 退化:零间距 / 单元素 / 路径短 / 空图层 / 空模型 | ✅ 全部覆盖 |
| JOIN→explode 可逆 | ✅ J9 + 命令路径版 |
| LTSCALE 0.5/2.0 比例 | ✅t_layerDynamic§6 + 命令路径版 |
| 自测新增 ≥30 断言全绿 | ✅ 62 条,全绿 |
| 双目标零警告 | ✅ 警告 0 |
| EXIT=0 | ✅ 失败 0 |
| CommandCatalog 收录 + usage 同源 | ✅ 已对齐实现「用法:」串 |
7. 创新点与方法论贡献
7.1 审计优先的加固范式
本文提出「审计优先」的加固范式:在既有实现已满足功能需求的前提下,加固工作的重心应从「重写」转向「审计 + 补测 + 同源对齐」。该范式可形式化表示为三元组:
Harden=(Audit,FillGap,Align) \text{Harden} = (\text{Audit}, \text{FillGap}, \text{Align})Harden=(Audit,FillGap,Align)
其中Audit\text{Audit}Audit识别实现与验收的差距,FillGap\text{FillGap}FillGap补齐测试覆盖缺口,Align\text{Align}Align对齐目录与实现的语义。
7.2 命令路径与纯函数路径的双轨验证
既有测试仅覆盖makeXxx纯函数路径,本文新增t_commandDispatch覆盖「分发 → 选择集 → 纯函数 → 应用」整链,形成双轨验证:
Coverage=Coveragepure∪Coveragedispatch \text{Coverage} = \text{Coverage}_{\text{pure}} \cup \text{Coverage}_{\text{dispatch}}Coverage=Coveragepure∪Coveragedispatch
该双轨策略有效捕获了纯函数测试无法发现的接线层缺陷。
7.3 退化场景的系统化枚举
本文将退化场景系统化为五类:
D={零间距,单元素,路径短于间距,空图层,空模型} \mathcal{D} = \{\text{零间距}, \text{单元素}, \text{路径短于间距}, \text{空图层}, \text{空模型}\}D={零间距,单元素,路径短于间距,空图层,空模型}
并逐一映射到具体断言,形成可复用的退化测试模板。
8. 附录:实现位置索引
- 算法:
src/core/Modify.cpp:579(explode):733(矩形):762(环形):797(路径):920(join) - 命令接线:
src/core/Commands.cpp:1601(explode/join/array 分发):3453(group):3774(ltscale/layiso/layuniso/laymch) - 模型 API:
src/core/SketchModel.h:130-174(图层/冻结/plot/alpha):536-542(LTSCALE):282-304(群组) - 目录:
src/core/CommandCatalog.cpp:73-76,104-108,149 - 测试:
tests/SketchSelfTest.cppt_batchEdit(2009) /t_layerDynamic(2740) /t_groupFilter(4139) /t_layerGovernance+t_commandDispatch(本批新增) - 渲染消费:
src/ui/GraphicsView.cpp(dashSpecScaled调用点) /src/ui/PlotRenderer.cpp
9. 结语
本文以sketch-cad-cpp的批量编辑与图层治理五件套为对象,完成了从审计、差距分析到加固实现的完整闭环。审计确认既有实现已就位,真实缺口集中于三命令的测试覆盖与目录 usage 的同源对齐。通过新增 62 条断言、对齐 usage 串、修复测试侧缺陷,最终实现双目标零警告构建与自测EXIT=0全绿。本文提出的「审计优先」加固范式、双轨验证策略与退化场景枚举方法,对同类 CAD 系统的质量加固具有参考价值。