欢迎光临
我们一直在努力

CAD算法研究:圆形绘制功能开发深度设计文档

12 种圆形绘制模式的架构设计、几何算法与交互集成

摘要: 本文档深入设计并实现了 CAD 草图编辑器中的圆形绘制功能,覆盖从基础构造到高级相切求解的 12 种绘制模式。架构上采用几何计算层与 UI 交互层完全解耦的分层策略,通过统一的 compute() 入口和 Mode 枚举实现模式映射,使预览与最终绘制共用同一套逻辑。交互层面设计了「拾取-预览-选解-创建」三步模型,并利用 共享 QAction 机制 保证工具条与菜单两处入口的完全一致。针对数值求解的收敛性、一维搜索精度等关键技术挑战,给出了梯度下降、粗扫描 + 逐级精细等解决方案,最终实现零警告编译通过。

目录

  • 需求分析与模式梳理
  • 架构设计:分层解耦与模式映射
  • 12 种圆形绘制模式详解
  • 交互设计:拾取-预览-选解-创建
  • 菜单与工具条集成:共享 Action 机制
  • 技术挑战与解决方案
  • 总结与展望
  • 1. 需求分析与模式梳理

    1.1 原始需求

    根据附图开发圆形绘制功能,要求实现 12 种圆形绘制方式,并将功能入口分别放置在两处:

    • 【草图绘制】菜单下“圆”子项的二级菜单
    • 工具条上【圆】按钮下的二级下拉菜单

    核心约束:两处入口的 菜单结构与功能逻辑必须完全一致。

    1.2 12 种绘制方式识别

    根据附图分析,圆形绘制涵盖从基础点到点构造到高级相切求解共 12 种模式,按几何复杂度可分为三个层次:

    层次模式几何核心
    基础构造 1. 圆心+点 两点距离即半径
    基础构造 2. 两点画圆 两点为直径端点
    基础构造 3. 两点+半径 弦中垂线 + 勾股定理
    基础构造 4. 三点定圆 外接圆(行列式法)
    基础构造 5. 圆心+半径数值 参数输入条
    相切构造 6. 相切两图元+点 梯度下降数值求解
    相切构造 7. 相切一图元+两点 中垂线参数搜索
    相切构造 8. 相切两图元+半径 偏移轨迹求交法
    复合求解 9. 相切三图元 阿波罗尼斯问题
    复合求解 10. 内切圆 三直线角平分线
    复合求解 11. 同心圆 取已有圆心 + 新半径
    复合求解 12. 由弧转圆 提取圆心半径补全

    2. 架构设计:分层解耦与模式映射

    2.1 设计参照

    圆形绘制模块的架构设计 参照已有的 LineModes 模块,采用相同的分层策略:几何计算层与 UI 交互层完全解耦。这样做的好处是:几何算法可被命令行、无界面自测和视图层共用,无需为每个入口重复编写逻辑。

    2.2 模块结构

    #mermaid-svg-iv6BbeXOzRBh5saC{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iv6BbeXOzRBh5saC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iv6BbeXOzRBh5saC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iv6BbeXOzRBh5saC .error-icon{fill:#552222;}#mermaid-svg-iv6BbeXOzRBh5saC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iv6BbeXOzRBh5saC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iv6BbeXOzRBh5saC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iv6BbeXOzRBh5saC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iv6BbeXOzRBh5saC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iv6BbeXOzRBh5saC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iv6BbeXOzRBh5saC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iv6BbeXOzRBh5saC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iv6BbeXOzRBh5saC .marker.cross{stroke:#333333;}#mermaid-svg-iv6BbeXOzRBh5saC svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iv6BbeXOzRBh5saC p{margin:0;}#mermaid-svg-iv6BbeXOzRBh5saC .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-iv6BbeXOzRBh5saC .cluster-label text{fill:#333;}#mermaid-svg-iv6BbeXOzRBh5saC .cluster-label span{color:#333;}#mermaid-svg-iv6BbeXOzRBh5saC .cluster-label span p{background-color:transparent;}#mermaid-svg-iv6BbeXOzRBh5saC .label text,#mermaid-svg-iv6BbeXOzRBh5saC span{fill:#333;color:#333;}#mermaid-svg-iv6BbeXOzRBh5saC .node rect,#mermaid-svg-iv6BbeXOzRBh5saC .node circle,#mermaid-svg-iv6BbeXOzRBh5saC .node ellipse,#mermaid-svg-iv6BbeXOzRBh5saC .node polygon,#mermaid-svg-iv6BbeXOzRBh5saC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iv6BbeXOzRBh5saC .rough-node .label text,#mermaid-svg-iv6BbeXOzRBh5saC .node .label text,#mermaid-svg-iv6BbeXOzRBh5saC .image-shape .label,#mermaid-svg-iv6BbeXOzRBh5saC .icon-shape .label{text-anchor:middle;}#mermaid-svg-iv6BbeXOzRBh5saC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iv6BbeXOzRBh5saC .rough-node .label,#mermaid-svg-iv6BbeXOzRBh5saC .node .label,#mermaid-svg-iv6BbeXOzRBh5saC .image-shape .label,#mermaid-svg-iv6BbeXOzRBh5saC .icon-shape .label{text-align:center;}#mermaid-svg-iv6BbeXOzRBh5saC .node.clickable{cursor:pointer;}#mermaid-svg-iv6BbeXOzRBh5saC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iv6BbeXOzRBh5saC .arrowheadPath{fill:#333333;}#mermaid-svg-iv6BbeXOzRBh5saC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iv6BbeXOzRBh5saC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iv6BbeXOzRBh5saC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iv6BbeXOzRBh5saC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iv6BbeXOzRBh5saC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iv6BbeXOzRBh5saC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iv6BbeXOzRBh5saC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iv6BbeXOzRBh5saC .cluster text{fill:#333;}#mermaid-svg-iv6BbeXOzRBh5saC .cluster span{color:#333;}#mermaid-svg-iv6BbeXOzRBh5saC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iv6BbeXOzRBh5saC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iv6BbeXOzRBh5saC rect.text{fill:none;stroke-width:0;}#mermaid-svg-iv6BbeXOzRBh5saC .icon-shape,#mermaid-svg-iv6BbeXOzRBh5saC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iv6BbeXOzRBh5saC .icon-shape p,#mermaid-svg-iv6BbeXOzRBh5saC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iv6BbeXOzRBh5saC .icon-shape .label rect,#mermaid-svg-iv6BbeXOzRBh5saC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iv6BbeXOzRBh5saC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iv6BbeXOzRBh5saC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iv6BbeXOzRBh5saC :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    数据模型

    几何计算层

    UI 交互层

    setActiveTool()

    activeTool()

    circle::compute()

    geo::*()

    addElement()

    GraphicsView拾取 · 预览 · 选解 · 创建

    MainWindow菜单 · 工具条 · Action 映射

    CircleModesMode 枚举 · compute() 主入口

    GeometrycircleFrom3Points · segCircleIntersectcircleCircleIntersect · distPointSegment

    SketchModelelements · lineParams · activeTool

    图 1:圆形绘制功能模块架构

    2.3 核心接口设计

    CircleModes.h 定义了三个核心抽象:

    Mode 枚举:将 12 种绘制方式映射为强类型枚举,避免字符串散布:

    enum class Mode {
    CenterPoint, // 1. 圆心 + 圆周点
    TwoPoint, // 2. 两点画圆(直径)
    TwoPointRadius, // 3. 两点 + 半径
    ThreePoint, // 4. 三点定圆
    CenterRadius, // 5. 圆心 + 半径数值
    Tan2Ent1Pt, // 6. 相切两图元 + 过一点
    Tan1Ent2Pt, // 7. 相切一图元 + 过两点
    Tan2EntRadius, // 8. 相切两图元 + 半径
    Tan3Ent, // 9. 相切三图元
    Inscribed, // 10. 三直线内切圆
    Concentric, // 11. 同心圆
    ByArc // 12. 由弧转圆
    };

    Result 结构体:统一的计算结果容器,支持多解场景:

    struct Result {
    bool valid = false;
    Vec3 center = Vec3(0, 0, 0);
    double radius = 0;
    QString msg; // 错误信息
    std::vector<Vec3> altCenters; // 备选圆心(多解时)
    };

    compute() 主入口:接收模式、已收集点、参考图元 ID、光标位置、图元列表和参数,返回当前应绘制的圆。这一设计使得 预览和最终绘制共用同一套逻辑——预览时传入光标位置作为“当前点”,最终绘制时传入确认点击的位置。

    ReqCount 需求描述:每种模式需要的参考图元数、点数和是否使用参数输入条的半径值:

    struct ReqCount { int refs; int pts; bool usesParam; };

    设计决策

    选择 compute() 统一入口而非每模式独立调用,是因为视图层需要在不关心具体模式的前提下统一处理预览和完成。模式之间的差异——拾取几个图元、几个点、是否需要半径参数——被 requirements() 和 allowedTypes() 两个查询函数封装,视图层通过查询而非硬编码来驱动交互流程。

    3. 12 种圆形绘制模式详解

    3.1 基础构造(模式 1-5)

    模式 1:圆心 + 圆周点

    最基础的画圆方式。圆心由用户指定,半径为圆心到圆周点的距离。实现直接调用 Vec3::distTo():

    Result centerPoint(const Vec3& c, const Vec3& p) {
    Result r;
    const double rad = c.distTo(p);
    if (rad < 1e-6) { r.msg = "半径为 0"; return r; }
    r.valid = true; r.center = c; r.radius = rad;
    return r;
    }

    模式 2:两点画圆(直径)

    两点作为直径的两个端点,圆心为两点的中点,半径为两点距离的一半:

    r.center = Vec3((a.x + b.x) * 0.5, (a.y + b.y) * 0.5, 0);
    r.radius = d * 0.5;

    模式 3:两点 + 半径

    两点定义弦,半径决定圆弧曲率。几何上,圆心位于弦的 中垂线 上,通过勾股定理计算偏移高度 h = sqrt(R^2 – (L/2)^2)。当 R < L/2 时半径不足,返回错误。此模式有两个候选圆心(弦的两侧),取离光标较近的作为主解,另一个存入 altCenters 供预览提示。

    模式 4:三点定圆

    过任意三点的唯一外接圆。调用 geo::circleFrom3Points(),内部使用 行列式法 求解。当三点共线时行列式为 0,返回错误信息。

    模式 5:圆心 + 半径数值

    圆心由点击指定,半径从参数输入条获取。与模式 1 的区别在于半径来自参数而非第二个点的距离。当参数未设置时使用默认值 10mm 用于预览。

    3.2 相切构造(模式 6-8)

    模式 6:相切两图元 + 过一点

    要求圆过点 P 且同时与两个图元(直线或圆/弧)相切。这是一个 超约束问题,一般情形下无解析解。

    算法:梯度下降法最小化目标函数:

    F(cx, cy) = (d1 – r)^2 + (d2 – r)^2

    其中:
    r = dist(center, P) // 圆过 P
    d1 = dist(center, e1) // 到图元1的距离
    d2 = dist(center, e2) // 到图元2的距离
    目标: d1 = r 且 d2 = r

    梯度通过 数值差分 计算(对 cx、cy 各微扰 eps),学习率 0.5,最大迭代 500 次。收敛判据为目标函数值 < 1e-14 或梯度范数趋零。

    模式 7:相切一图元 + 过两点

    圆过 P1、P2 且与图元 e 相切。利用几何特性:圆心必在 P1P2 的中垂线上,将二维搜索降为一维参数搜索。

    算法:沿中垂线方向扫描参数 t(范围 -1000 到 1000,步长 0.5),对每个候选圆心计算 dist(center, e) – dist(center, P1) 的误差。粗扫描定位最优区间后,逐级精细搜索(步长 0.1 → 0.01 → …),直到误差 < 0.01mm。

    多解处理

    中垂线两侧各有一个解。主解存储在 Result.center,另一解存入 altCenters。最终由 tan1Ent2Pt() 包装函数根据光标位置选择更近的解作为主解。

    模式 8:相切两图元 + 半径

    给定半径,求与两个图元相切的圆。此问题有解析解——使用偏移轨迹求交法。

    算法:对每个图元生成其“偏移轨迹”(即相切圆的圆心轨迹):

    • 直线段 → 两条偏移线(沿法线方向偏移 ±r)
    • 圆 → 两个偏移圆(外切:R+r,内切:|R-r|)

    然后对两个图元的偏移轨迹两两求交,所有交点即为候选圆心。根据图元组合分为四种情形:

    图元组合偏移求交方式最大候选数
    直线 + 直线 偏移线 × 偏移线 → lineLineIntersect 4
    直线 + 圆 偏移线 × 偏移圆 → segCircleIntersect 8
    圆 + 直线 偏移圆 × 偏移线 → segCircleIntersect 8
    圆 + 圆 偏移圆 × 偏移圆 → circleCircleIntersect 8

    最终从所有候选中选取离光标最近的作为主解。

    3.3 复合求解(模式 9-12)

    模式 9:相切三图元(阿波罗尼斯问题)

    经典的 阿波罗尼斯问题:求与三个给定图元同时相切的圆。根据图元类型分两种策略:

    三直线:三条直线围成三角形,内切圆位于角平分线交点。使用梯度下降最小化点到三直线距离差的平方和,初始猜测为三个交点的重心:

    F = (d1-d2)^2 + (d1-d3)^2 + (d2-d3)^2
    → 目标: d1 = d2 = d3 = r

    含圆情况:无法用角平分线法,同样使用梯度下降最小化三图元距离差。初始猜测取三图元中心的平均值,若离光标较近则从光标位置开始搜索。

    模式 10:三直线内切圆

    模式 9 的特例——限定三图元均为直线。直接调用 tan3Ent(),传入 Vec3(0,0,0) 作为光标(此时无需选解,因为三直线内切圆通常只有唯一解)。

    模式 11:同心圆

    取已有圆/弧的圆心,配以新半径。参考图元必须是 Circle 或 Arc 类型,新半径来自参数输入条或光标到圆心的距离。

    模式 12:由弧转圆

    将圆弧图元转换为完整圆:提取圆弧的圆心和半径,生成一个完整的 Circle 图元。这是最简单的模式,仅做数据类型转换。

    4. 交互设计:拾取-预览-选解-创建

    4.1 交互流程总览

    #mermaid-svg-VI2oISfkUzzTzBRW{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VI2oISfkUzzTzBRW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VI2oISfkUzzTzBRW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VI2oISfkUzzTzBRW .error-icon{fill:#552222;}#mermaid-svg-VI2oISfkUzzTzBRW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VI2oISfkUzzTzBRW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VI2oISfkUzzTzBRW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VI2oISfkUzzTzBRW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VI2oISfkUzzTzBRW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VI2oISfkUzzTzBRW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VI2oISfkUzzTzBRW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VI2oISfkUzzTzBRW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VI2oISfkUzzTzBRW .marker.cross{stroke:#333333;}#mermaid-svg-VI2oISfkUzzTzBRW svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VI2oISfkUzzTzBRW p{margin:0;}#mermaid-svg-VI2oISfkUzzTzBRW .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-VI2oISfkUzzTzBRW .cluster-label text{fill:#333;}#mermaid-svg-VI2oISfkUzzTzBRW .cluster-label span{color:#333;}#mermaid-svg-VI2oISfkUzzTzBRW .cluster-label span p{background-color:transparent;}#mermaid-svg-VI2oISfkUzzTzBRW .label text,#mermaid-svg-VI2oISfkUzzTzBRW span{fill:#333;color:#333;}#mermaid-svg-VI2oISfkUzzTzBRW .node rect,#mermaid-svg-VI2oISfkUzzTzBRW .node circle,#mermaid-svg-VI2oISfkUzzTzBRW .node ellipse,#mermaid-svg-VI2oISfkUzzTzBRW .node polygon,#mermaid-svg-VI2oISfkUzzTzBRW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VI2oISfkUzzTzBRW .rough-node .label text,#mermaid-svg-VI2oISfkUzzTzBRW .node .label text,#mermaid-svg-VI2oISfkUzzTzBRW .image-shape .label,#mermaid-svg-VI2oISfkUzzTzBRW .icon-shape .label{text-anchor:middle;}#mermaid-svg-VI2oISfkUzzTzBRW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VI2oISfkUzzTzBRW .rough-node .label,#mermaid-svg-VI2oISfkUzzTzBRW .node .label,#mermaid-svg-VI2oISfkUzzTzBRW .image-shape .label,#mermaid-svg-VI2oISfkUzzTzBRW .icon-shape .label{text-align:center;}#mermaid-svg-VI2oISfkUzzTzBRW .node.clickable{cursor:pointer;}#mermaid-svg-VI2oISfkUzzTzBRW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VI2oISfkUzzTzBRW .arrowheadPath{fill:#333333;}#mermaid-svg-VI2oISfkUzzTzBRW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VI2oISfkUzzTzBRW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VI2oISfkUzzTzBRW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VI2oISfkUzzTzBRW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VI2oISfkUzzTzBRW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VI2oISfkUzzTzBRW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VI2oISfkUzzTzBRW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VI2oISfkUzzTzBRW .cluster text{fill:#333;}#mermaid-svg-VI2oISfkUzzTzBRW .cluster span{color:#333;}#mermaid-svg-VI2oISfkUzzTzBRW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VI2oISfkUzzTzBRW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VI2oISfkUzzTzBRW rect.text{fill:none;stroke-width:0;}#mermaid-svg-VI2oISfkUzzTzBRW .icon-shape,#mermaid-svg-VI2oISfkUzzTzBRW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VI2oISfkUzzTzBRW .icon-shape p,#mermaid-svg-VI2oISfkUzzTzBRW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VI2oISfkUzzTzBRW .icon-shape .label rect,#mermaid-svg-VI2oISfkUzzTzBRW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VI2oISfkUzzTzBRW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VI2oISfkUzzTzBRW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VI2oISfkUzzTzBRW :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    refs > 0

    refs = 0

    实时预览(鼠标移动时)

    updatePreview(cur)

    circle::compute(pendingPts, pickedIds, cur)

    showPreview(circle, altCenters)

    用户选择圆模式e.g. circle_t2e1p

    查询 ReqCount

    拾取参考图元pickRefElement()

    refs 足够?

    需自动完成?ByArc/Inscribed

    compute() → finalizeCircleEx()

    拾取几何点snapWorld()

    pts 足够?

    单解自动完成?2P/2PR/CR

    compute() → finalizeCircleEx()

    点击选解光标位置确定最近解

    compute() → finalizeCircleEx()

    图 2:圆形绘制交互流程

    4.2 三步交互模型

    所有圆形子模式共用统一的 handleCircleSubTool() 处理函数,交互分为三个阶段:

    Step 1:收集参考图元。当模式需要参考图元时(如相切模式),每次点击调用 pickRefElement(),限定可拾取的图元类型(由 allowedTypes() 查询)。拾取后立即设置 drawing_ = true 并刷新预览。ByArc 和 Inscribed 模式拾取完参考图元即自动完成,无需点输入。

    Step 2:收集几何点。通过 snapWorld() 将屏幕坐标转换为捕捉后的世界坐标点。单解模式(TwoPoint、TwoPointRadius、CenterRadius)在收集够点时自动完成,不需要额外选解点击。

    Step 3:多解工具的选解点击。对于有多个候选解的模式(如三点圆的预览、相切求解),最后一步由用户点击位置确定选择哪个解——compute() 接收当前光标位置,内部通过 pickNearest() 选取最近的候选。

    4.3 实时预览

    鼠标移动时,updatePreview() 被触发,调用 circle::compute() 并传入当前光标位置。计算结果若有效,则以实线绘制预览圆,同时将 altCenters 中的备选解以 虚线圆 提示。这让用户在确认前就能看到所有可能的解。

    4.4 捕捉配置

    每种圆模式切换时自动配置合适的捕捉开关:

    模式组端点圆心切点交点
    圆心+点 / 圆心+半径 / 同心圆
    三点圆 / 两点画圆 / 两点+半径 / 相切+两点
    相切两图元+点 / 相切+半径 / 相切三图元 / 内切圆 / 由弧转圆

    5. 菜单与工具条集成:共享 Action 机制

    5.1 一致性设计核心

    两处入口一致性的核心实现是共享同一组 QAction 对象。工具条创建的所有圆模式 Action 都被注册到 toolActions_ 映射表中,菜单构建时直接从中取用,不创建新的 Action。

    #mermaid-svg-2CqdNzKZWFY7E67C{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2CqdNzKZWFY7E67C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2CqdNzKZWFY7E67C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2CqdNzKZWFY7E67C .error-icon{fill:#552222;}#mermaid-svg-2CqdNzKZWFY7E67C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2CqdNzKZWFY7E67C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2CqdNzKZWFY7E67C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2CqdNzKZWFY7E67C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2CqdNzKZWFY7E67C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2CqdNzKZWFY7E67C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2CqdNzKZWFY7E67C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2CqdNzKZWFY7E67C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2CqdNzKZWFY7E67C .marker.cross{stroke:#333333;}#mermaid-svg-2CqdNzKZWFY7E67C svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2CqdNzKZWFY7E67C p{margin:0;}#mermaid-svg-2CqdNzKZWFY7E67C .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-2CqdNzKZWFY7E67C .cluster-label text{fill:#333;}#mermaid-svg-2CqdNzKZWFY7E67C .cluster-label span{color:#333;}#mermaid-svg-2CqdNzKZWFY7E67C .cluster-label span p{background-color:transparent;}#mermaid-svg-2CqdNzKZWFY7E67C .label text,#mermaid-svg-2CqdNzKZWFY7E67C span{fill:#333;color:#333;}#mermaid-svg-2CqdNzKZWFY7E67C .node rect,#mermaid-svg-2CqdNzKZWFY7E67C .node circle,#mermaid-svg-2CqdNzKZWFY7E67C .node ellipse,#mermaid-svg-2CqdNzKZWFY7E67C .node polygon,#mermaid-svg-2CqdNzKZWFY7E67C .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2CqdNzKZWFY7E67C .rough-node .label text,#mermaid-svg-2CqdNzKZWFY7E67C .node .label text,#mermaid-svg-2CqdNzKZWFY7E67C .image-shape .label,#mermaid-svg-2CqdNzKZWFY7E67C .icon-shape .label{text-anchor:middle;}#mermaid-svg-2CqdNzKZWFY7E67C .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2CqdNzKZWFY7E67C .rough-node .label,#mermaid-svg-2CqdNzKZWFY7E67C .node .label,#mermaid-svg-2CqdNzKZWFY7E67C .image-shape .label,#mermaid-svg-2CqdNzKZWFY7E67C .icon-shape .label{text-align:center;}#mermaid-svg-2CqdNzKZWFY7E67C .node.clickable{cursor:pointer;}#mermaid-svg-2CqdNzKZWFY7E67C .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2CqdNzKZWFY7E67C .arrowheadPath{fill:#333333;}#mermaid-svg-2CqdNzKZWFY7E67C .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2CqdNzKZWFY7E67C .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2CqdNzKZWFY7E67C .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2CqdNzKZWFY7E67C .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2CqdNzKZWFY7E67C .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2CqdNzKZWFY7E67C .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2CqdNzKZWFY7E67C .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2CqdNzKZWFY7E67C .cluster text{fill:#333;}#mermaid-svg-2CqdNzKZWFY7E67C .cluster span{color:#333;}#mermaid-svg-2CqdNzKZWFY7E67C div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2CqdNzKZWFY7E67C .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2CqdNzKZWFY7E67C rect.text{fill:none;stroke-width:0;}#mermaid-svg-2CqdNzKZWFY7E67C .icon-shape,#mermaid-svg-2CqdNzKZWFY7E67C .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2CqdNzKZWFY7E67C .icon-shape p,#mermaid-svg-2CqdNzKZWFY7E67C .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2CqdNzKZWFY7E67C .icon-shape .label rect,#mermaid-svg-2CqdNzKZWFY7E67C .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2CqdNzKZWFY7E67C .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2CqdNzKZWFY7E67C .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2CqdNzKZWFY7E67C :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    草图绘制菜单

    共享 QAction 池

    工具条圆按钮

    addAction(a)

    addAction(a)

    triggered →

    circle_btnQToolButton

    circleMenuQMenu(下拉)

    circle / circle_2p / …12个 QAction

    圆形绘制方式QMenu(二级)

    model_->setActiveTool(id)

    图 3:共享 Action 机制——工具条与菜单指向同一组 QAction

    5.2 工具条集成

    工具条上的圆按钮是一个 QToolButton,设置为 MenuButtonPopup 模式——点击按钮主体激活默认模式(圆心 + 点),点击箭头展开下拉菜单。每个模式通过 addCircleMode lambda 创建 QAction:

    auto addCircleMode = [&](const QString& zh, const QString& en, const QString& tip) {
    QAction* a = new QAction(zh, this);
    a->setCheckable(true);
    a->setActionGroup(group); // 互斥选择
    connect(a, &QAction::triggered, this, [this, en] {
    model_->setActiveTool(en);
    });
    circleMenu->addAction(a);
    addAction(a); // 注册到主窗口(支持快捷键)
    toolActions_.insert(en, a); // 注册到共享池
    };

    5.3 菜单集成

    草图绘制菜单中,圆形子菜单通过遍历 toolActions_ 直接引用已有的 QAction:

    QMenu* circleModesMenu = m->addMenu(icons::make("circle"), "圆形绘制方式");
    const QStringList circleModeIds = {
    "circle", "circle_2p", "circle_2pr", "circle3p", "circle_cr",
    "circle_t2e1p", "circle_t1e2p", "circle_t2er", "circle_t3e",
    "circle_inscribed", "circle_concentric", "circle_byarc"
    };
    for (const QString& id : circleModeIds) {
    if (QAction* a = toolActions_.value(id))
    circleModesMenu->addAction(a); // 共享同一 QAction
    }

    5.4 一致性保证

    共享 Action 带来的自动同步

    • 选中状态同步: 从工具条切换到“三点圆”,菜单中的“三点圆”项立即显示 checked 状态(同一 QAction 对象)
    • 快捷键统一: 默认模式“圆心+点”绑定快捷键 C,两处入口均可触发
    • 信号唯一: 每个 QAction 只有一个 triggered 信号连接,无论从哪里点击,执行路径完全相同
    • 零同步代码: 不需要额外编写菜单-工具条状态同步逻辑,Qt 的 QAction 机制天然支持

    6. 技术挑战与解决方案

    6.1 变量名冲突

    问题: tan3Ent() 函数中,局部变量 const double r(半径)与 Result r(返回值)在同一作用域内同名,导致编译错误 request for member 'msg' in 'r', which is of non-class type 'const double'。

    解决: 将 Result r 重命名为 Result res,保留 r 作为半径变量名(与数学惯例一致)。同样的冲突在函数内两个分支中各出现一次,统一修复。

    6.2 数值求解的收敛性

    问题: 模式 6(相切两图元+点)使用梯度下降,当初始猜测远离解时可能不收敛或陷入局部最小值。

    解决: 采用两个策略提升鲁棒性:

    • 初始猜测取光标位置 cur——用户移动鼠标时预览实时跟随,实际上在引导收敛方向
    • 收敛容差设为 0.01mm(CAD 精度要求),避免过度迭代
    • 最大迭代 500 次,超过即返回失败信息

    6.3 一维搜索的精度

    问题: 模式 7(相切一图元 + 两点)沿中垂线搜索,单步扫描精度与范围矛盾——范围大则步长大漏解,步长小则计算量大。

    解决: 粗扫描 + 逐级精细 两阶段策略:

    // 粗扫描:t ∈ [-1000, 1000],步长 0.5
    for (double t = 1000; t <= 1000; t += 0.5) { ... }

    // 精细搜索:在最优解附近逐步缩小步长
    for (double dt = 0.1; dt > 1e-6; dt *= 0.1) {
    for (double t = bestT 5*dt; t <= bestT + 5*dt; t += dt) { ... }
    }

    精度提升三个数量级,最终误差 < 1e-6,满足 CAD 精度要求。

    6.4 编译警告清理

    问题: 初始实现存在 7 个编译器警告(未使用变量、未使用参数、未使用函数)。

    解决: 逐一清理,确保 零警告编译:

    • 移除未使用的 r0 变量(梯度计算中的中间值)
    • 未使用参数 cur 改为 /*cur*/ 显式标注
    • 移除未调用的 ptSegDist()、bisectorDirs()、signedDistEnt() 三个内部工具函数
    • 移除未使用的 addCandidates lambda

    7. 总结与展望

    7.1 实现成果

    • 12 种圆形绘制模式全部实现,覆盖从基础构造到阿波罗尼斯问题
    • 工具条下拉菜单与草图绘制二级菜单完全一致,共享 QAction 机制
    • 零警告编译通过(SketchCAD + SketchSelfTest 双目标)
    • 几何算法层与 UI 层完全解耦,支持命令行调用和无界面自测

    7.2 文件清单

    文件角色状态
    CircleModes.h 接口定义:Mode 枚举、Result 结构、12 个构造函数声明 新建
    CircleModes.cpp 几何算法实现:12 种模式 + 数值求解器 新建
    GraphicsView.h/.cpp 交互层:拾取、预览、选解、创建 修改
    MainWindow.cpp 菜单与工具条集成 修改
    CMakeLists.txt 构建系统:加入 CircleModes.cpp 到所有目标 修改

    7.3 可改进方向

    • 解析解: 模式 6/7/9 目前使用数值法,可针对特定图元组合推导解析公式提升精度和速度
    • 多解交互: 当前通过光标位置选解,可增加 Tab 键循环切换备选解
    • 性能优化: 模式 7 的一维扫描可用二分法替代线性扫描
    赞(0)
    未经允许不得转载:171主机测评 » CAD算法研究:圆形绘制功能开发深度设计文档
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址