跳到内容
CADCAMFUN
文档

文档

草图解算器

由 Levenberg–Marquardt 求解的受约束 2D 草图,具有自由度、自由点和命名冲突。

从英文自动翻译。 查看英文原文

求解器做什么

草图解算器将一组点、线、圆和弧以及几何和尺寸约束转换为具体坐标。它住在 packages/core/src/cad/sketch.ts (522 行)并导出自 @cadcamfun/core 作为 solveSketch, sketchProfiles 和 polygonArea。参数化历史首先将其表达式解析为数字,然后调用求解器;解决的型材进给挤压、旋转和型腔。

输入输出
实体:点、线、圆、弧具有已解坐标和半径的相同实体
约束:24 种,可选命名状态、自由度、自由点、冗余方程、冲突
options: maxIterations (200), tolerance (1e-9)使用的迭代次数和剩余的最大残差
解决了草图拉伸问题 拖动进行轨道旋转·滚动进行缩放·右键拖动进行平移
The fully constrained 40 × 25 rectangle with a Ø10 hole at (20, 12.5), exactly as sketchProfiles returned it (the circle is a 50-point loop at the default 0.01 mm tolerance), extruded 6 mm.

数据模型

点是未知数。每条直线、圆和弧都通过 id 引用点,因此共享端点的两条线共享该点并一起移动。圆又增加了一个未知数,即它的半径。圆弧是圆上的一个圆心和两个点,从 start 至 end;它的半径是|开始-中心|并且求解器添加自己的方程保持 end 在同一个圆上。

实体和草图类型 (sketch.ts)
export interface SketchPoint  { id: string; kind: "point";  x: number; y: number }
export interface SketchLine   { id: string; kind: "line";   p1: PointId; p2: PointId }
export interface SketchCircle { id: string; kind: "circle"; center: PointId; radius: number }
/** Counter-clockwise from `start` to `end` about `center`. */
export interface SketchArc    { id: string; kind: "arc";    center: PointId; start: PointId; end: PointId }

export interface Sketch {
  readonly entities: ReadonlyArray<SketchEntity>
  readonly constraints: ReadonlyArray<Constraint>
}

未知向量被布置为每个点的 x, y 按实体顺序,那么每个圆的 r。由四个点组成的矩形有 8 个未知数;添加一个有自己圆心的圆,它有 11。

约束目录

每个约束都会产生残差方程,当它成立时,残差方程为零。约束可以携带 id;没有它的名字 type#index (例如 vertical#2)在冲突报告中。

类型领域意义方程
fixed点,x?,y?固定一个点(坐标可选)1–2
coincidenta, b两点是一2
horizontal / vertical线一条线是水平的或垂直的1
horizontalPoints / verticalPointsa, b两点共享 y / x1
distancea、b、值点对点距离1
distanceX / distanceYa、b、值沿一个轴的有符号偏移1
length线、值线长1
parallel / perpendiculara, b两条线之间1
equalLengtha, b两行1
equalRadiusa, b两个圆或圆弧1
radius / diameter圆,值圆或弧的大小1
anglea、b、值从a线到b线,逆时针,弧度1
pointOnLine / pointOnCircle点、线|圆圈发生率1
midpoint点、线点在一条线的中间2
symmetrica、b、线关于一条线的镜像对2
tangentLineCircle线、圆线接触圆1
tangentCirclesa、b、内部?外部 r1 + r2 或内部 |r1 − r2|1
concentrica, b中心重合2

求解:莱文伯格-马夸特

求解器使用 Levenberg-Marquardt 和中心差雅可比行列式最小化残差平方和。每一步都通过 Cholesky 分解来求解阻尼法方程;接受降低残差的步骤,阻尼下降,拒绝不降低残差的步骤,阻尼增加。由于每一步都是减少残差的最小更改,因此拖动点会像在绘图板上一样拖动草图的其余部分。

  1. 建立每个约束的残差函数,并记录哪个约束拥有每个方程。
  2. Start with λ = 1e-3. Solve (JᵀJ + λ·max(1, diag)) Δx = −Jᵀr; if Cholesky fails, multiply λ by 10 and retry.
  3. Accept the step if the residual norm drops (λ = max(λ/5, 1e-12)); otherwise λ ×= 10.
  4. 当残差低于时停止 tolerance (1e-9) 或之后 maxIterations (200).
  5. Analyse the Jacobian at the solution: rank by Gaussian elimination with full pivoting (tolerance 1e-7), null-space basis, free unknowns (component above 1e-6).
持续的价值在哪里
最大迭代次数默认值200SolveOptions
容差默认值1e-9SolveOptions
初始阻尼 λ1e-3solveSketch
阻尼地板1e-12solveSketch
如果残差 ≤ 则求解1e-6地位
等级公差1e-7nullSpace
自由未知阈值1e-6免费积分
冲突阈值(每个方程)1e-6冲突

现状和诊断

求解后,自由度 = 未知数 - 秩,冗余方程 = 方程 - 秩。状态根据这两个数字以及残差是否达到 1e-6 得出。

地位健康)状况该怎么办
fully-constrained已解决,自由度 0,无冗余没有什么
under-constrained已解决,自由度 > 0为 id 添加维度 free
over-constrained已解决,多余 > 0删除重复的约束(无害但有噪音)
conflicting未解决,多余 > 0将约束修正为 conflicts[0]
failed没有解决,没有冗余从接近预期形状开始或提高 maxIterations

These results come from running the solver on a rectangle drawn slightly askew: a(0,0) b(38,2) c(41,27) d(−1,24), with a fixed at the origin and the four lines horizontal/vertical.

实际结果,每个添加的约束一个
// + length(l1) = 40
{"status":"under-constrained","dof":1,"free":["c","d"],"iterations":3,"redundant":0}
// + length(l2) = 25
{"status":"fully-constrained","dof":0,"iterations":3,"residual":7.95e-12,
 "points":[["a",0,0],["b",40,0],["c",40,25],["d",0,25]]}
// + length(l3) = 40   (implied by the others)
{"status":"over-constrained","dof":0,"redundant":1}
// + length(l3) = 30   named "bad"
{"status":"conflicting","redundant":1,
 "conflicts":["bad","vertical#2","length#5","vertical#4"],
 "message":"The constraints cannot all hold; the worst miss is bad"}

最少的变化,尤其是惊喜

With height still free, moving c to (55, 31) and solving gave c = (40, 27.5) and d = (0, 27.5): the free height settled at the mean of the dragged c.y (31) and the old d.y (24), because Levenberg–Marquardt makes the smallest change that satisfies the constraints.

API

签名
export const solveSketch: (sketch: Sketch, options?: SolveOptions) => SolveResult
export const sketchProfiles: (sketch: Sketch, tolerance?: number /* 0.01 */) =>
  { readonly loops: Vec2[][]; readonly open: Vec2[][] }
export const polygonArea: (ring: ReadonlyArray<Vec2>) => number   // positive counter-clockwise
export class SketchError extends Error {}

export interface SolveResult {
  sketch: Sketch; status: SolveStatus; dof: number; iterations: number
  residual: number          // largest absolute residual left
  free: string[]            // points and circles that can still move
  conflicts: string[]       // worst first, only when not solved
  redundant: number
  message?: string
}
带孔板,端对端
import { solveSketch, sketchProfiles, polygonArea } from "@cadcamfun/core"

const P = (id: string, x: number, y: number) => ({ id, kind: "point" as const, x, y })
const L = (id: string, p1: string, p2: string) => ({ id, kind: "line" as const, p1, p2 })

const result = solveSketch({
  entities: [P("a", 0, 0), P("b", 38, 2), P("c", 41, 27), P("d", -1, 24),
    L("l1", "a", "b"), L("l2", "b", "c"), L("l3", "c", "d"), L("l4", "d", "a"),
    P("o", 20, 12), { id: "h", kind: "circle", center: "o", radius: 4 }],
  constraints: [
    { type: "fixed", point: "a", x: 0, y: 0 },
    { type: "horizontal", line: "l1" }, { type: "vertical", line: "l2" },
    { type: "horizontal", line: "l3" }, { type: "vertical", line: "l4" },
    { type: "length", line: "l1", value: 40 }, { type: "length", line: "l2", value: 25 },
    { type: "distanceX", a: "a", b: "o", value: 20 }, { type: "distanceY", a: "a", b: "o", value: 12.5 },
    { type: "diameter", circle: "h", value: 10 },
  ],
})
result.status                       // "fully-constrained", dof 0
const { loops } = sketchProfiles(result.sketch)
loops.map((l) => [l.length, polygonArea(l)])
// [[50, 78.333], [4, 1000]]   circle: 50 points; rectangle: 4 points, area 1000

从草图到轮廓

sketchProfiles 将直线和圆弧通过它们的共享点 ID 首尾相连地链接到闭环中,并将每个圆作为其自己的循环返回。弧变成间隔的点,因此弦保持在范围内 tolerance of the true arc (n = ⌈sweep / (2·acos(1 − tol/r))⌉, at least 2); a full circle gets at least 8 points. A chain that does not close is returned in open 而不是被丢弃。
由两条圆弧和两条直线构成的槽 拖动进行轨道旋转·滚动进行缩放·右键拖动进行平移
半径为 30 的 8 毫米槽:共享端点的两条圆弧和两条直线,通过 sketchProfiles 求解并链接到一个 66 点循环(面积 680.739 mm² 相对于精确的 681.062 mm²;多边形是内切的,因此它总是稍小)。

上面的槽解决为 over-constrained 具有自由度 0 和 1 冗余方程: top 一旦 c1–a1 和 c2–b1 为垂直且 c1、c2 共享 y,则隐含水平。解决仍然成功;状态仅标记重复项。

故障排除

症状原因使固定
SketchError: Duplicate entity id两个实体共享一个 idid 在点、线、圆和弧上是唯一的
SketchError: Unknown unknown约束命名了不存在的点或圆检查约束中的 id
conflicting 有一个很长的冲突列表一个错误的维度会使多个方程偏离零修复第一个条目;其余的通常是抵押品
failed一切的开始都远非解决方案更接近预期的形状;提高最大迭代次数
个人资料缺失于 loops链条不会闭合:它处于 open使端点共享 id,或添加重合约束
圆的点太少对于小半径来说公差太粗将较小的公差传递给 sketchProfiles