2026/10/22

辊刀V2.0:减材 UI 的架构与工程沉淀

知识库

辊刀V2.0:减材 UI 的架构与工程沉淀

本文从辊刀 CAM 软件 V2 减材模式界面模块的开发实践整理,聚焦可迁移的架构模式与工程方法;领域术语沿用公开口径(参见 [[辊刀]])。

一、问题背景:CAM 软件的界面层为什么要单独设计

辊刀制造属于模切(Die Cutting)行业,工艺上分增材(激光熔覆)与减材(CNC 铣削)两大类。CAM 软件的共性矛盾是:业务算力与界面表达的生命周期完全不同——刀路算法、图纸修复、路径规划属于深水区,改动谨慎而缓慢;界面则是用户每天摸几百次的部分,迭代频繁、形态多变。

如果让界面直接抱着业务对象写,两种节奏会互相拖累:改一个拾取交互要读懂刀路规划的上下文,改一个参数面板要担心算法侧的编译依赖。V2 的答案是把减材界面做成独立模块,以清晰的边界把两种节奏隔开:

  • 编译产物是一个动态库,由宿主在减材模式下按需加载;
  • 对外只暴露一个纯虚接口 + 一对创建/销毁符号,其余全部内部实现;
  • 界面模块不含业务算力——刀路、图纸优化、轮廓识别都在算法侧;它只做两件事:把业务模型翻译成屏幕元素,把用户操作翻译成领域命令。

这条边界从第一天就是架构红线,后续所有设计都从它推导。

二、总体架构

2.1 四方协作的拓扑

        ┌─────────────┐   事件(pub/sub)   ┌─────────────┐
        │   业务层     │◄──────────────────►│  界面模块    │
        │ (算力侧)   │   命令事件下发      │ (减材 UI)  │
        └─────────────┘                    └──────┬──────┘
                                                  │ 单一持有
        ┌─────────────┐   反馈门面调用            ▼
        │   宿主框架   │◄────────────────── ┌─────────────┐
        │ (公共 UI)  │   面板/工具栏注入   │  渲染视口    │
        └─────────────┘                    └─────────────┘

四个角色各管一事:

角色职责边界纪律
界面模块减材业务中介者:业务模型 ↔ 屏幕元素的双向翻译不实现业务算力;不直调业务层类方法
业务层刀路/图纸/轮廓等算力,纯事件驱动严禁直调 UI 接口,反馈一律走事件广播
宿主框架公共 UI 基建(主题引擎、Toast、Modal、进度遮罩)界面模块不重复造通用控件
渲染视口3D/2D 可视化内核(独立 DLL)全模块仅一个封装类持有渲染指针,其余经代理访问

2.2 事件契约:跨模块边界的通信

界面与业务不共享头文件,全部交流走事件总线。这里有一条值得单独说的工程经验:

跨动态库边界的类型镜像,没有编译期保护。 编译期的 topic 绑定只在业务侧有效;界面侧是独立编译单元,持有一份手写的事件结构镜像,靠「主题字符串 + 类型」在运行期命中。一旦两侧类型名不同,事件静默丢失;同名但成员不同(ODR 违例),事件照常投递,接收方按自己的布局解读对方内存——读出脏值。

对策不是更聪明的框架,而是更笨的纪律:

  • 镜像与契约逐字节一致(类型名 + 字段顺序 + 字段类型),契约文档不单独维护版本号,以镜像基准为准;
  • 事件载荷只允许值类型 / 智能指针,严禁非拥有裸指针跨事件边界广播;
  • 总线回调异常捕获转日志,爆炸隔离——一个订阅者炸了不影响发布者和其他订阅者;
  • 共享工程数据的写权限用显式标志控制(同一份内存、分时独占写),而不是靠约定。

2.3 模块内部:中介者 + MVVM + 控制器拆分

内部结构是三层分工:

  • 中介者(薄 Facade):对外接口实现,只做装配、分发、刷新、事件转发;
  • MVVM:列表/参数/图层/预览等多个 ViewModel,UI 是被动数据持有者;
  • 步骤拆分控制器:视口显示、NC 导出、图纸导入、工序 CRUD、拾取协调、工艺树……各管一个子域。

两条依赖纪律值得记录:

  1. 依赖方向单向:控制器不得反向持有 Facade 或宿主对象——需要工程数据时经 Provider 在调用时刻实时读取,不缓存成员快照。这一条直接消灭了宿主异步切工程时的一类悬垂崩溃。
  2. 渲染指针单一持有:全模块只有一个封装类接触渲染接口,其余一律走代理。想在别处直连渲染内核?先过这一关。

三、一次关键重构:God Object 拆分

界面模块的核心中介者曾膨胀到 6154 行 / 98 个方法 / 80 个 include——全工程最大的文件。症状是典型的 God Object:改导入要读工序代码,改拾取要带 NC 上下文,单测无法隔离。

拆分的目标不是「变好看」,而是行为零变化:

  • 每步只搬代码,不改语义、不改文案、不改事件顺序;
  • 每步完成的验收固定为:编译通过 → diff 确认只动预期文件 → self-review → 独立提交;
  • 新控制器必须可单独阅读、可单独单测。

拆完 Facade 回落到薄转发层(约四分之一体量),每个子域有了自己的家。几条实施纪律对任何同类重构都适用:

  • 接线模式先统一再动手:哪些依赖构造时注入、哪些调用时实时读、哪些生命周期挂钩置空(防悬垂)——先定模式,再逐模块套用;
  • 搬移保留修饰符:[[nodiscard]]、static 等声明/定义一一对应,搬丢了编译器不总是提醒你;
  • 警惕构建系统的小动作:改了关键头文件后不全量重编,陈旧目标文件会导致运行期堆损坏——这种崩溃的现场离原因隔着十万八千里;
  • 行尾统一:混合换行符的仓库里,编辑后 diff 会冒纯噪声,验收看删除列。

四、向导与校验的归属权

新建项目向导是个「外壳 + 页面注入」的结构:向导壳负责标题栏、步骤条、翻页;各步骤页以校验器注册的方式挂进来,翻页校验全权归属界面模块——返回 false 即拦截翻页并标记出错字段,宿主不参与。

这个「校验归属权」的划分看似小,实际避免了一类常见的扯皮:校验逻辑散在宿主和页面两边时,拦截时机、错误标记、重试路径三个问题会互相踢皮球。一个用例的完整闭环归一个模块,哪怕外壳是别人的。

五、UI 设计

5.1 气质:工业极简

界面走工业极简路线:结构线、留白、克制的强调色;深浅双主题由主题引擎统一供给,业务模块只声明用色语义,不硬编码色值。无边框主窗体 + 自绘基础控件(参数输入、分组面板)保证了视觉一致性,代价是要自己维护一套控件——这笔账划算的前提是控件语义与业务强相关(如参数组、图层卡),纯通用控件仍应来自公共层。

5.2 布局:三栏 + 视口

  • 顶部工具栏:命令分发到各控制器;
  • 左侧栏三标签:工序列表 / 工艺列表 / 图层列表,每个都是「列表 + 详情/预览」拆分;
  • 状态栏:文件名、坐标、开关、进度;
  • 视口区:图纸上屏、刀路显隐、轮廓面选中与高亮,经渲染代理统一调度。

5.3 交互家族:拾取

减材加工的大量操作本质是拾取——选图元、选轮廓面、选加工区域。设计上把拾取做成一个家族:四个专用拾取对话框(外形/间隙/型腔/清根)共享「拾取会话状态机」,含图层条、底部动作条、预览回显的统一骨架,差异收敛在各自的拾取逻辑里。

两条 UX 原则:

  • 拾取产物的口径单点化——「这次拾取选中了什么」只有一个权威描述处,预览、参数、回显都读它,杜绝三处各说各话;
  • 参数记忆——同类工序的参数按槽位记忆上一次取值,是高频重复录入场景里性价比最高的一个功能。

5.4 原型先行

每个重要界面(向导步骤、侧栏状态、工序面板、拾取交互)都先出高保真 HTML 原型再写 Qt 代码,极端细节(空状态、错误态、悬停)在原型里先定下来。Qt 自绘的迭代成本远高于 HTML,把试错留在便宜的一侧。

六、工程规范的底座

模块站在一套全系通用的工程规范上,几条对多模块 C++ 工程有普适价值:

  1. 目录契约:include/(对外接口,按模块名隔离)/ inc_inner/(内部头)/ src/ / test/ 四层分明,跨界 include 一眼可查;
  2. 头文件顺序即正确性问题:某些第三方宏(如 Qt 的 slots 空宏)会污染公共标识符,导致几十行外的语法雪崩——公共头必须前置,这属于「不知道就会 debug 一整天」的知识,要写进规范而不是留在脑子里;
  3. 测试与构建同构:GTest + CTest 随模块走,-DBUILD_TEST=ON 一个开关;
  4. 文档即契约:需求文档、详细设计、事件契约、拆分记录各司其职;设计文档由代码事实校准(「已校正为当前实现」),不做与代码漂移的空中楼阁。

七、结语

回头看,这个界面模块沉淀下来的不是某个功能的做法,而是几条可迁移的判断:

  • 边界先行:算力与表达分居两侧,界面模块不含业务算力——红线画得早,后面全是红利;
  • 事件契约靠纪律:跨模块类型镜像没有编译器保护,逐字节一致 + 载荷约束 + 爆炸隔离,一个都不能少;
  • 重构要「零行为变化」的仪式感:只搬代码、逐步提交、逐步验收,是拆 God Object 唯一不翻车的姿势;
  • 闭环归属单一:校验、拾取口径、参数记忆,一个用例一个权威处;
  • 试错放在便宜的一侧:原型先行,代码跟进。

本文聚焦架构模式与工程实践的通用表达。