文档
草图解算器
由 Levenberg–Marquardt 求解的受约束 2D 草图,具有自由度、自由点和命名冲突。
从英文自动翻译。 查看英文原文
求解器做什么
草图解算器将一组点、线、圆和弧以及几何和尺寸约束转换为具体坐标。它住在 packages/core/src/cad/sketch.ts (522 行)并导出自 @cadcamfun/core 作为 solveSketch, sketchProfiles 和 polygonArea。参数化历史首先将其表达式解析为数字,然后调用求解器;解决的型材进给挤压、旋转和型腔。
| 输入 | 输出 |
|---|---|
| 实体:点、线、圆、弧 | 具有已解坐标和半径的相同实体 |
| 约束:24 种,可选命名 | 状态、自由度、自由点、冗余方程、冲突 |
| options: maxIterations (200), tolerance (1e-9) | 使用的迭代次数和剩余的最大残差 |
数据模型
点是未知数。每条直线、圆和弧都通过 id 引用点,因此共享端点的两条线共享该点并一起移动。圆又增加了一个未知数,即它的半径。圆弧是圆上的一个圆心和两个点,从 start 至 end;它的半径是|开始-中心|并且求解器添加自己的方程保持 end 在同一个圆上。
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 |
coincident | a, b | 两点是一 | 2 |
horizontal / vertical | 线 | 一条线是水平的或垂直的 | 1 |
horizontalPoints / verticalPoints | a, b | 两点共享 y / x | 1 |
distance | a、b、值 | 点对点距离 | 1 |
distanceX / distanceY | a、b、值 | 沿一个轴的有符号偏移 | 1 |
length | 线、值 | 线长 | 1 |
parallel / perpendicular | a, b | 两条线之间 | 1 |
equalLength | a, b | 两行 | 1 |
equalRadius | a, b | 两个圆或圆弧 | 1 |
radius / diameter | 圆,值 | 圆或弧的大小 | 1 |
angle | a、b、值 | 从a线到b线,逆时针,弧度 | 1 |
pointOnLine / pointOnCircle | 点、线|圆圈 | 发生率 | 1 |
midpoint | 点、线 | 点在一条线的中间 | 2 |
symmetric | a、b、线 | 关于一条线的镜像对 | 2 |
tangentLineCircle | 线、圆 | 线接触圆 | 1 |
tangentCircles | a、b、内部? | 外部 r1 + r2 或内部 |r1 − r2| | 1 |
concentric | a, b | 中心重合 | 2 |
求解:莱文伯格-马夸特
求解器使用 Levenberg-Marquardt 和中心差雅可比行列式最小化残差平方和。每一步都通过 Cholesky 分解来求解阻尼法方程;接受降低残差的步骤,阻尼下降,拒绝不降低残差的步骤,阻尼增加。由于每一步都是减少残差的最小更改,因此拖动点会像在绘图板上一样拖动草图的其余部分。
- 建立每个约束的残差函数,并记录哪个约束拥有每个方程。
- Start with λ = 1e-3. Solve (JᵀJ + λ·max(1, diag)) Δx = −Jᵀr; if Cholesky fails, multiply λ by 10 and retry.
- Accept the step if the residual norm drops (λ = max(λ/5, 1e-12)); otherwise λ ×= 10.
- 当残差低于时停止
tolerance(1e-9) 或之后maxIterations(200). - 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).
| 持续的 | 价值 | 在哪里 |
|---|---|---|
| 最大迭代次数默认值 | 200 | SolveOptions |
| 容差默认值 | 1e-9 | SolveOptions |
| 初始阻尼 λ | 1e-3 | solveSketch |
| 阻尼地板 | 1e-12 | solveSketch |
| 如果残差 ≤ 则求解 | 1e-6 | 地位 |
| 等级公差 | 1e-7 | nullSpace |
| 自由未知阈值 | 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 而不是被丢弃。
上面的槽解决为 over-constrained 具有自由度 0 和 1 冗余方程: top 一旦 c1–a1 和 c2–b1 为垂直且 c1、c2 共享 y,则隐含水平。解决仍然成功;状态仅标记重复项。
故障排除
| 症状 | 原因 | 使固定 |
|---|---|---|
SketchError: Duplicate entity id | 两个实体共享一个 id | id 在点、线、圆和弧上是唯一的 |
SketchError: Unknown unknown | 约束命名了不存在的点或圆 | 检查约束中的 id |
conflicting 有一个很长的冲突列表 | 一个错误的维度会使多个方程偏离零 | 修复第一个条目;其余的通常是抵押品 |
failed | 一切的开始都远非解决方案 | 更接近预期的形状;提高最大迭代次数 |
个人资料缺失于 loops | 链条不会闭合:它处于 open | 使端点共享 id,或添加重合约束 |
| 圆的点太少 | 对于小半径来说公差太粗 | 将较小的公差传递给 sketchProfiles |