Rectangular and circular sketch patterns stay linked to their source: editing the pattern's count, spacing or the source geometry updates the copies, and the copies keep their point and radius relations. Patterns are saved under format 20; older designs keep their independent copies.
2402 lines
103 KiB
Rust
2402 lines
103 KiB
Rust
//! The constraint solver.
|
||
//!
|
||
//! Unknowns are the coordinates of every non-fixed point and the radius of every circle. Each
|
||
//! constraint contributes residuals (zero when satisfied, in millimetres so that all rows have a
|
||
//! comparable scale). The system is split into connected components and each is solved with a
|
||
//! damped Gauss–Newton (Levenberg–Marquardt) iteration on a dense Jacobian built from local
|
||
//! central differences. Small steps from the current positions are preferred, so
|
||
//! under-constrained sketches move as little as possible. Afterwards the rank of the Jacobian
|
||
//! gives the remaining degrees of freedom and which points are fully determined.
|
||
//!
|
||
//! Unknowns the constraints pin down one after another from the constants (a point at the
|
||
//! crossing of two loci on known points, see [`pinned`]) are eliminated exactly, without
|
||
//! the dense matrices: values the constructive seed placed stay constants for LM, and the rank
|
||
//! analysis runs only on what remains.
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use solidcraft_geom::{Seg2, Vec2};
|
||
|
||
use crate::decompose::Budget;
|
||
use crate::linalg::{Mat, determined_vars, solve as lin_solve};
|
||
use crate::model::{ConstraintKind, CurveKind, Sketch};
|
||
use crate::union_find::{find, union};
|
||
|
||
#[path = "redundancy.rs"]
|
||
mod redundancy;
|
||
pub use redundancy::{CONSISTENT, constraint_residual, determined_by};
|
||
|
||
const MAX_ITERS: usize = 500;
|
||
/// Variables in one connected component. Each component is solved as a dense system: an n by n
|
||
/// matrix for the Levenberg-Marquardt step, rows by n for the Jacobian. 4,000 variables is a
|
||
/// 128 MB matrix; past that the request grows to gigabytes and an allocation failure aborts the
|
||
/// process, which no panic guard can intercept. A component this size is reported as failed and
|
||
/// its geometry left alone.
|
||
const MAX_COMPONENT_VARS: usize = 4_000;
|
||
/// Converged when every residual is below this (mm).
|
||
const TOL: f64 = 1e-9;
|
||
/// A solution is accepted when every residual is below this (mm).
|
||
const ACCEPT: f64 = 1e-6;
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum SolveStatus {
|
||
/// All constraints satisfied.
|
||
Solved,
|
||
/// The constraints conflict (or the solver could not satisfy them); geometry was left as it
|
||
/// was before solving.
|
||
Failed,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct SolveReport {
|
||
pub status: SolveStatus,
|
||
/// Remaining degrees of freedom (0 = fully constrained).
|
||
pub dof: usize,
|
||
pub iterations: usize,
|
||
pub max_residual: f64,
|
||
/// Constraints still violated after a failed solve, and the ones in `folded`.
|
||
pub failing: Vec<String>,
|
||
/// Constraints whose orientation (a triangle's turn, an offset's side) the solve for edited
|
||
/// driving values flipped relative to the stored solved state, where continuation could
|
||
/// not keep it. The solve keeps that best result (an evaluation shows it with a warning);
|
||
/// a command that changes values refuses it.
|
||
#[serde(default)]
|
||
pub folded: Vec<String>,
|
||
/// Components this solve stepped from the stored solved state to the edited values.
|
||
#[serde(default)]
|
||
pub continuations: usize,
|
||
/// Per point: fully determined by constraints (fixed points count as determined).
|
||
pub point_determined: Vec<bool>,
|
||
/// Per curve: all of its defining values determined.
|
||
pub curve_determined: Vec<bool>,
|
||
/// Constraints that remove no degree of freedom: everything they ask for is already implied
|
||
/// by the constraints before them (a second "vertical" on a side two rectangles share).
|
||
/// They hold, so they are not a conflict; they are shown as redundant. Only from a full
|
||
/// analysis, and only for components of at most a few hundred variables.
|
||
#[serde(default)]
|
||
pub redundant: Vec<String>,
|
||
}
|
||
|
||
impl SolveReport {
|
||
pub fn ok(&self) -> bool {
|
||
self.status == SolveStatus::Solved
|
||
}
|
||
pub fn fully_constrained(&self) -> bool {
|
||
self.dof == 0
|
||
}
|
||
/// Solved, and no orientation of the stored state flipped: what a value edit accepts.
|
||
pub fn keeps_orientation(&self) -> bool {
|
||
self.ok() && self.folded.is_empty()
|
||
}
|
||
/// Why a solve failed or turned over: the constraints whose orientation the edit flips,
|
||
/// or else `conflict` of the constraints still violated (listed, comma-separated).
|
||
pub fn failure(&self, conflict: impl FnOnce(&str) -> String) -> String {
|
||
if self.folded.is_empty() {
|
||
conflict(&self.failing.join(", "))
|
||
} else {
|
||
format!("the new values would flip the sketch over ({} would change orientation)", self.folded.join(", "))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Variable layout: point → (x var, y var), circle → radius var. The point coordinates come
|
||
/// first, in pairs (variables `0..points`), then the radii.
|
||
struct Layout {
|
||
pvar: Vec<Option<usize>>,
|
||
rvar: Vec<Option<usize>>,
|
||
n: usize,
|
||
/// The point coordinates' variables: `0..points`.
|
||
points: usize,
|
||
}
|
||
|
||
impl Layout {
|
||
/// The variable is a point coordinate.
|
||
fn is_point(&self, v: usize) -> bool {
|
||
v < self.points
|
||
}
|
||
/// The first variable of the unknown `v` belongs to: its point's x, or the radius itself.
|
||
fn unit(&self, v: usize) -> usize {
|
||
if self.is_point(v) { v & !1 } else { v }
|
||
}
|
||
/// The variables of the unknown starting at `first`.
|
||
fn width(&self, first: usize) -> usize {
|
||
if self.is_point(first) { 2 } else { 1 }
|
||
}
|
||
}
|
||
|
||
/// Geometry held in place for one solve: point indices and curves whose radius is held.
|
||
#[derive(Clone, Debug, Default, PartialEq)]
|
||
pub struct Hold {
|
||
pub points: Vec<usize>,
|
||
pub radii: Vec<usize>,
|
||
}
|
||
|
||
/// Per point: a constant for this solve (locked — fixed, fixed by a constraint, a point of a
|
||
/// fixed curve, linked — or held).
|
||
pub(crate) fn held_points(sk: &Sketch, hold: &Hold) -> Vec<bool> {
|
||
let mut fixed = sk.locked_points();
|
||
for p in &hold.points {
|
||
if let Some(f) = fixed.get_mut(*p) {
|
||
*f = true;
|
||
}
|
||
}
|
||
fixed
|
||
}
|
||
|
||
fn layout(sk: &Sketch, hold: &Hold) -> Layout {
|
||
let mut n = 0;
|
||
let pvar = held_points(sk, hold)
|
||
.iter()
|
||
.map(|f| {
|
||
if *f {
|
||
None
|
||
} else {
|
||
n += 2;
|
||
Some(n - 2)
|
||
}
|
||
})
|
||
.collect();
|
||
let points = n;
|
||
let rvar = sk
|
||
.curves
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, c)| match c.kind {
|
||
CurveKind::Circle { .. } | CurveKind::Ellipse { .. } if c.link.is_none() && !c.fixed && !hold.radii.contains(&i) => {
|
||
n += 1;
|
||
Some(n - 1)
|
||
}
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
Layout { pvar, rvar, n, points }
|
||
}
|
||
|
||
/// Read-only view of the sketch with variable values substituted.
|
||
struct State<'a> {
|
||
sk: &'a Sketch,
|
||
lay: &'a Layout,
|
||
x: &'a [f64],
|
||
}
|
||
|
||
impl State<'_> {
|
||
fn p(&self, i: usize) -> Vec2 {
|
||
match self.lay.pvar.get(i).copied().flatten() {
|
||
Some(v) => Vec2::new(self.x.get(v).copied().unwrap_or(0.0), self.x.get(v + 1).copied().unwrap_or(0.0)),
|
||
None => self.sk.points.get(i).map(|p| p.pos).unwrap_or_default(),
|
||
}
|
||
}
|
||
fn line(&self, l: usize) -> (Vec2, Vec2) {
|
||
match self.sk.curves.get(l).map(|c| &c.kind) {
|
||
Some(CurveKind::Line { a, b }) => (self.p(*a), self.p(*b)),
|
||
_ => (Vec2::ZERO, Vec2::X),
|
||
}
|
||
}
|
||
/// Centre and radius of a circle or arc.
|
||
fn round(&self, c: usize) -> (Vec2, f64) {
|
||
match self.sk.curves.get(c).map(|c| &c.kind) {
|
||
Some(CurveKind::Circle { c: ci, r }) => {
|
||
let r = match self.lay.rvar.get(c).copied().flatten() {
|
||
Some(v) => self.x.get(v).copied().unwrap_or(*r),
|
||
None => *r,
|
||
};
|
||
(self.p(*ci), r)
|
||
}
|
||
Some(CurveKind::Arc { c: ci, a, .. }) => {
|
||
let cc = self.p(*ci);
|
||
(cc, cc.dist(self.p(*a)))
|
||
}
|
||
_ => (Vec2::ZERO, 0.0),
|
||
}
|
||
}
|
||
fn is_line(&self, c: usize) -> bool {
|
||
matches!(self.sk.curves.get(c).map(|c| &c.kind), Some(CurveKind::Line { .. }))
|
||
}
|
||
fn is_freeform(&self, c: usize) -> bool {
|
||
self.sk.curves.get(c).is_some_and(|c| c.kind.is_freeform())
|
||
}
|
||
/// Polyline of a free-form curve with trial values.
|
||
fn poly(&self, c: usize) -> Vec<Vec2> {
|
||
match self.sk.curves.get(c).map(|c| &c.kind) {
|
||
Some(CurveKind::Ellipse { c: ci, m, r }) => {
|
||
let r = match self.lay.rvar.get(c).copied().flatten() {
|
||
Some(v) => self.x.get(v).copied().unwrap_or(*r),
|
||
None => *r,
|
||
};
|
||
crate::curves::ellipse_polyline(self.p(*ci), self.p(*m), r)
|
||
}
|
||
Some(CurveKind::Spline { pts, control, degree, periodic }) => {
|
||
let p = self.spline_points(pts, *periodic);
|
||
crate::curves::spline_polyline_mode(&p, *control, *degree, *periodic)
|
||
}
|
||
Some(CurveKind::Conic { a, b, apex, rho }) => crate::curves::conic_polyline(self.p(*a), self.p(*apex), self.p(*b), *rho),
|
||
_ => Vec::new(),
|
||
}
|
||
}
|
||
/// Point-on-curve constraints use the exact shape. A polyline residual would move an
|
||
/// exact crossing onto its chord, changing geometry merely by splitting it.
|
||
fn spline_points(&self, pts: &[usize], periodic: bool) -> Vec<Vec2> {
|
||
let mut points: Vec<Vec2> = pts.iter().map(|i| self.p(*i)).collect();
|
||
if periodic
|
||
&& let Some(first) = points.first().copied()
|
||
&& let Some(last) = points.last_mut()
|
||
{
|
||
*last = first;
|
||
}
|
||
points
|
||
}
|
||
fn freeform_dist(&self, c: usize, q: Vec2) -> f64 {
|
||
let segs: Vec<Seg2> = match self.sk.curves.get(c).map(|c| &c.kind) {
|
||
Some(CurveKind::Ellipse { c: ci, m, .. }) => {
|
||
let center = self.p(*ci);
|
||
let u = self.p(*m) - center;
|
||
let a = u.len();
|
||
let r = self.radius_of(c);
|
||
if a <= 1e-12 || r <= 1e-12 {
|
||
return q.dist(center);
|
||
}
|
||
let delta = q - center;
|
||
let x = delta.dot(u) / (a * a);
|
||
let y = delta.cross(u) / (a * r);
|
||
return (x.hypot(y) - 1.0) * a.min(r);
|
||
}
|
||
Some(CurveKind::Spline { pts, control, degree, periodic }) => {
|
||
let p = self.spline_points(pts, *periodic);
|
||
if p.len() < 2 {
|
||
return q.dist(p.first().copied().unwrap_or_default());
|
||
}
|
||
if *control {
|
||
let k = (*degree as usize).min(solidcraft_geom::MAX_BEZIER_DEGREE).min(p.len().saturating_sub(1)).max(1);
|
||
crate::curves::bspline_beziers(&p, k).iter().filter_map(|span| solidcraft_geom::bezier(span)).collect()
|
||
} else {
|
||
crate::curves::fit_beziers_mode(&p, *periodic)
|
||
.unwrap_or_default()
|
||
.into_iter()
|
||
.map(|[p0, p1, p2, p3]| Seg2::Cubic { p0, p1, p2, p3 })
|
||
.collect()
|
||
}
|
||
}
|
||
Some(CurveKind::Conic { a, b, apex, rho }) => {
|
||
vec![Seg2::Conic { a: self.p(*a), b: self.p(*b), apex: self.p(*apex), w: rho / (1.0 - rho).max(1e-9) }]
|
||
}
|
||
_ => Vec::new(),
|
||
};
|
||
segs.iter()
|
||
.map(|seg| {
|
||
let t = seg.closest_param(q);
|
||
let delta = q - seg.point_at(t);
|
||
let d = delta.len();
|
||
(d, if seg.derivative(t).cross(delta) >= 0.0 { d } else { -d })
|
||
})
|
||
.min_by(|a, b| a.0.total_cmp(&b.0))
|
||
.map(|(_, signed)| signed)
|
||
.unwrap_or_else(|| crate::curves::polyline_signed_dist(&self.poly(c), q))
|
||
}
|
||
/// Radius of a circle or arc; an ellipse's minor radius.
|
||
fn radius_of(&self, c: usize) -> f64 {
|
||
match self.sk.curves.get(c).map(|x| &x.kind) {
|
||
Some(CurveKind::Ellipse { r, .. }) => match self.lay.rvar.get(c).copied().flatten() {
|
||
Some(v) => self.x.get(v).copied().unwrap_or(*r),
|
||
None => *r,
|
||
},
|
||
_ => self.round(c).1,
|
||
}
|
||
}
|
||
fn ends(&self, c: usize) -> Option<(usize, usize)> {
|
||
self.sk.curves.get(c)?.kind.ends()
|
||
}
|
||
/// Length along an arc (counter-clockwise from its start).
|
||
fn arc_length(&self, c: usize) -> f64 {
|
||
match self.sk.curves.get(c).map(|c| &c.kind) {
|
||
Some(CurveKind::Arc { c: ci, a, b }) => {
|
||
let (cc, pa, pb) = (self.p(*ci), self.p(*a), self.p(*b));
|
||
let mut sw = (pb - cc).angle() - (pa - cc).angle();
|
||
while sw <= 1e-12 {
|
||
sw += std::f64::consts::TAU;
|
||
}
|
||
cc.dist(pa) * sw
|
||
}
|
||
_ => 0.0,
|
||
}
|
||
}
|
||
fn arc_sweep(&self, c: usize) -> f64 {
|
||
let Some(curve) = self.sk.curves.get(c) else { return 0.0 };
|
||
let CurveKind::Arc { c: cp, a, b } = curve.kind else { return 0.0 };
|
||
let sweep = ((self.p(b) - self.p(cp)).angle() - (self.p(a) - self.p(cp)).angle()).rem_euclid(std::f64::consts::TAU);
|
||
if curve.reversed { -sweep } else { sweep }
|
||
}
|
||
fn slot_length(&self, a: usize, b: usize) -> f64 {
|
||
let (ca, ra) = self.round(a);
|
||
let (cb, rb) = self.round(b);
|
||
ca.dist(cb) + ra + rb
|
||
}
|
||
/// Curvature vector of a curve at its point `p` (towards the centre of curvature).
|
||
fn curvature_at(&self, c: usize, p: usize) -> Vec2 {
|
||
let Some(kind) = self.sk.curves.get(c).map(|c| &c.kind) else { return Vec2::ZERO };
|
||
match kind {
|
||
CurveKind::Line { .. } | CurveKind::Ellipse { .. } => Vec2::ZERO,
|
||
CurveKind::Circle { .. } | CurveKind::Arc { .. } => {
|
||
let (cc, r) = self.round(c);
|
||
(cc - self.p(p)) / (r * r).max(1e-24)
|
||
}
|
||
CurveKind::Spline { pts, control, degree, periodic } => {
|
||
let q = self.spline_points(pts, *periodic);
|
||
let at_end = pts.last() == Some(&p) && pts.first() != Some(&p);
|
||
crate::curves::end_curvature(&|t| crate::curves::spline_point_mode(&q, *control, *degree, *periodic, t), at_end)
|
||
}
|
||
CurveKind::Conic { a, b, apex, rho } => {
|
||
let (pa, pb, px) = (self.p(*a), self.p(*b), self.p(*apex));
|
||
crate::curves::end_curvature(&|t| crate::curves::conic_point(pa, px, pb, *rho, t), p == *b)
|
||
}
|
||
}
|
||
}
|
||
/// Unit tangent of any curve at its point `p` (an end point for open free-form curves).
|
||
fn tangent_at(&self, c: usize, p: usize) -> Option<Vec2> {
|
||
match self.sk.curves.get(c).map(|c| &c.kind)? {
|
||
CurveKind::Line { a, b } => (self.p(*b) - self.p(*a)).normalized(),
|
||
CurveKind::Circle { .. } | CurveKind::Arc { .. } => (self.p(p) - self.round(c).0).perp().normalized(),
|
||
CurveKind::Spline { pts, control, periodic, .. } => {
|
||
let q = self.spline_points(pts, *periodic);
|
||
crate::curves::spline_end_tangent_mode(&q, *control, *periodic, pts.last() == Some(&p) && pts.first() != Some(&p))
|
||
}
|
||
CurveKind::Conic { a, b, apex, .. } => {
|
||
if p == *a {
|
||
(self.p(*apex) - self.p(*a)).normalized()
|
||
} else if p == *b {
|
||
(self.p(*b) - self.p(*apex)).normalized()
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
CurveKind::Ellipse { .. } => {
|
||
let poly = self.poly(c);
|
||
let q = self.p(p);
|
||
let k = (0..poly.len().saturating_sub(1)).min_by(|i, j| poly[*i].dist(q).total_cmp(&poly[*j].dist(q)))?;
|
||
(*poly.get(k + 1)? - *poly.get(k)?).normalized()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn wrap_angle(a: f64) -> f64 {
|
||
let t = std::f64::consts::TAU;
|
||
let mut a = a % t;
|
||
if a > std::f64::consts::PI {
|
||
a -= t;
|
||
} else if a <= -std::f64::consts::PI {
|
||
a += t;
|
||
}
|
||
a
|
||
}
|
||
|
||
/// Distance from `p` to the infinite line through `a`, `b` (signed by side).
|
||
fn line_dist(p: Vec2, a: Vec2, b: Vec2) -> f64 {
|
||
let d = b - a;
|
||
let l = d.len().max(1e-12);
|
||
d.cross(p - a) / l
|
||
}
|
||
|
||
/// Residual rows of one constraint.
|
||
fn residuals(s: &State, k: &ConstraintKind, out: &mut Vec<f64>) {
|
||
use ConstraintKind::*;
|
||
match *k {
|
||
PatternPoint { p, q, center, angle, offset } => {
|
||
let c = center.map(|c| s.p(c)).unwrap_or(Vec2::ZERO);
|
||
let d = s.p(p) - c;
|
||
let (sin, cos) = angle.sin_cos();
|
||
let delta = s.p(q) - c - Vec2::new(d.x * cos - d.y * sin, d.x * sin + d.y * cos) - offset;
|
||
out.extend([delta.x, delta.y]);
|
||
}
|
||
PatternRadius { a, b } => out.push(s.radius_of(a) - s.radius_of(b)),
|
||
// Offset distances drive derived geometry in refresh_offsets, never source DOFs.
|
||
OffsetDistance { .. } => {}
|
||
Coincident { p, q } => {
|
||
let d = s.p(p) - s.p(q);
|
||
out.extend([d.x, d.y]);
|
||
}
|
||
PointOnCurve { p, c } => {
|
||
let pp = s.p(p);
|
||
if s.is_freeform(c) {
|
||
out.push(s.freeform_dist(c, pp));
|
||
} else if s.is_line(c) {
|
||
let (a, b) = s.line(c);
|
||
out.push(line_dist(pp, a, b));
|
||
} else {
|
||
let (cc, r) = s.round(c);
|
||
out.push(pp.dist(cc) - r);
|
||
}
|
||
}
|
||
Horizontal { l } => {
|
||
let (a, b) = s.line(l);
|
||
out.push(b.y - a.y);
|
||
}
|
||
Vertical { l } => {
|
||
let (a, b) = s.line(l);
|
||
out.push(b.x - a.x);
|
||
}
|
||
HorizontalPoints { p, q } => out.push(s.p(q).y - s.p(p).y),
|
||
VerticalPoints { p, q } => out.push(s.p(q).x - s.p(p).x),
|
||
Parallel { a, b } | Perpendicular { a, b } => {
|
||
let (a0, a1) = s.line(a);
|
||
let (b0, b1) = s.line(b);
|
||
let (da, db) = (a1 - a0, b1 - b0);
|
||
// The sine (parallel) or cosine (perpendicular) of the angle between the lines:
|
||
// independent of their lengths, so the solver can't satisfy it by shrinking a line
|
||
// to nothing (scaling by the summed lengths did, collapsing dragged rectangles).
|
||
let scale = (da.len() * db.len()).max(1e-12);
|
||
let v = if matches!(k, Parallel { .. }) { da.cross(db) } else { da.dot(db) };
|
||
out.push(v / scale);
|
||
}
|
||
Collinear { a, b } => {
|
||
let (a0, a1) = s.line(a);
|
||
let (b0, b1) = s.line(b);
|
||
out.push(line_dist(b0, a0, a1));
|
||
out.push(line_dist(b1, a0, a1));
|
||
}
|
||
Tangent { a, b } => {
|
||
let (la, lb) = (s.is_line(a), s.is_line(b));
|
||
if s.is_freeform(a) || s.is_freeform(b) {
|
||
// Without a shared end point: the curves touch (closest approach zero).
|
||
let (f, o) = if s.is_freeform(a) { (a, b) } else { (b, a) };
|
||
let poly = s.poly(f);
|
||
// Signed gap at the closest approach (smooth through zero, unlike its size).
|
||
let closest = |v: &mut dyn Iterator<Item = f64>| v.fold(f64::INFINITY, |m: f64, x| if x.abs() < m.abs() { x } else { m });
|
||
let d = if s.is_line(o) {
|
||
let (p0, p1) = s.line(o);
|
||
closest(&mut poly.iter().map(|q| line_dist(*q, p0, p1)))
|
||
} else if s.is_freeform(o) {
|
||
let other = s.poly(o);
|
||
closest(&mut poly.iter().map(|q| crate::curves::polyline_signed_dist(&other, *q)))
|
||
} else {
|
||
let (cc, r) = s.round(o);
|
||
closest(&mut poly.iter().map(|q| q.dist(cc) - r))
|
||
};
|
||
out.push(if d.is_finite() { d } else { 0.0 });
|
||
} else if la || lb {
|
||
let (l, c) = if la { (a, b) } else { (b, a) };
|
||
let (p0, p1) = s.line(l);
|
||
let (cc, r) = s.round(c);
|
||
out.push(line_dist(cc, p0, p1).abs() - r);
|
||
} else {
|
||
let (c1, r1) = s.round(a);
|
||
let (c2, r2) = s.round(b);
|
||
let d = c1.dist(c2);
|
||
let ext = d - (r1 + r2);
|
||
let int = d - (r1 - r2).abs();
|
||
out.push(if ext.abs() <= int.abs() { ext } else { int });
|
||
}
|
||
}
|
||
Equal { a, b } => {
|
||
if s.is_line(a) {
|
||
let (a0, a1) = s.line(a);
|
||
let (b0, b1) = s.line(b);
|
||
out.push(a0.dist(a1) - b0.dist(b1));
|
||
} else {
|
||
out.push(s.round(a).1 - s.round(b).1);
|
||
}
|
||
}
|
||
Concentric { a, b } => {
|
||
let d = s.round(a).0 - s.round(b).0;
|
||
out.extend([d.x, d.y]);
|
||
}
|
||
Midpoint { p, l } => {
|
||
let (a, b) = s.line(l);
|
||
let d = s.p(p) - (a + b) * 0.5;
|
||
out.extend([d.x, d.y]);
|
||
}
|
||
Symmetric { p, q, l } => {
|
||
let (a, b) = s.line(l);
|
||
let (pp, qq) = (s.p(p), s.p(q));
|
||
let dir = b - a;
|
||
let len = dir.len().max(1e-12);
|
||
out.push(line_dist((pp + qq) * 0.5, a, b));
|
||
out.push((qq - pp).dot(dir) / len);
|
||
}
|
||
Fix { .. } => {}
|
||
Distance { p, q, value } => out.push(s.p(p).dist(s.p(q)) - value),
|
||
DistanceX { p, q, value } => out.push((s.p(q).x - s.p(p).x).abs() - value),
|
||
DistanceY { p, q, value } => out.push((s.p(q).y - s.p(p).y).abs() - value),
|
||
PointLineDistance { p, l, value } => {
|
||
let (a, b) = s.line(l);
|
||
out.push(line_dist(s.p(p), a, b).abs() - value);
|
||
}
|
||
Length { l, value } => {
|
||
let (a, b) = s.line(l);
|
||
out.push(a.dist(b) - value);
|
||
}
|
||
LineAngle { l, value } => {
|
||
let (a, b) = s.line(l);
|
||
// A dimension with no typed length must not satisfy its angle by collapsing
|
||
// the line. Scale with the original geometry, independently of the unknowns.
|
||
let scale = match s.sk.curves.get(l).map(|c| &c.kind) {
|
||
Some(CurveKind::Line { a, b }) => s.sk.point(*a).zip(s.sk.point(*b)).map(|(a, b)| a.dist(b)).unwrap_or(1.0),
|
||
_ => 1.0,
|
||
};
|
||
out.push(wrap_angle((b - a).angle() - value) * scale.max(1.0));
|
||
}
|
||
ArcSweep { c, value } => out.push((s.arc_sweep(c) - value) * s.sk.radius(c).unwrap_or(1.0).max(1.0)),
|
||
SlotOverallLength { a, b, value } => out.push(s.slot_length(a, b) - value),
|
||
Radius { c, value } => out.push(s.radius_of(c) - value),
|
||
Diameter { c, value } => out.push(2.0 * s.round(c).1 - value),
|
||
Angle { a, b, value, flip } => {
|
||
let (a0, a1) = s.line(a);
|
||
let (b0, b1) = s.line(b);
|
||
let (da, db) = (a1 - a0, b1 - b0);
|
||
let ang = da.cross(db).atan2(da.dot(db));
|
||
let scale = 0.5 * (da.len() + db.len());
|
||
let target = if flip { value + std::f64::consts::PI } else { value };
|
||
out.push(wrap_angle(ang - target) * scale.max(1e-6));
|
||
}
|
||
Smooth { a, b } => {
|
||
let shared = match (s.ends(a), s.ends(b)) {
|
||
(Some((a0, a1)), Some((b0, b1))) => [a0, a1].into_iter().find(|x| *x == b0 || *x == b1),
|
||
_ => None,
|
||
};
|
||
let Some(p) = shared else {
|
||
out.extend([0.0, 0.0, 0.0]);
|
||
return;
|
||
};
|
||
match (s.tangent_at(a, p), s.tangent_at(b, p)) {
|
||
(Some(u), Some(v)) => out.push(u.cross(v) * 10.0),
|
||
_ => out.push(0.0),
|
||
}
|
||
let k = s.curvature_at(a, p) - s.curvature_at(b, p);
|
||
out.extend([k.x * 100.0, k.y * 100.0]);
|
||
}
|
||
ArcLength { c, value } => out.push(s.arc_length(c) - value),
|
||
LinearDiameter { p, l, value } => {
|
||
let (a, b) = s.line(l);
|
||
out.push(2.0 * line_dist(s.p(p), a, b).abs() - value);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Measured value of a dimension (for driven dimensions).
|
||
fn measure(s: &State, k: &ConstraintKind) -> Option<f64> {
|
||
use ConstraintKind::*;
|
||
Some(match *k {
|
||
Distance { p, q, .. } => s.p(p).dist(s.p(q)),
|
||
DistanceX { p, q, .. } => (s.p(q).x - s.p(p).x).abs(),
|
||
DistanceY { p, q, .. } => (s.p(q).y - s.p(p).y).abs(),
|
||
PointLineDistance { p, l, .. } => {
|
||
let (a, b) = s.line(l);
|
||
line_dist(s.p(p), a, b).abs()
|
||
}
|
||
Length { l, .. } => {
|
||
let (a, b) = s.line(l);
|
||
a.dist(b)
|
||
}
|
||
LineAngle { l, value } => {
|
||
let (a, b) = s.line(l);
|
||
value + wrap_angle((b - a).angle() - value)
|
||
}
|
||
ArcSweep { c, .. } => s.arc_sweep(c),
|
||
SlotOverallLength { a, b, .. } => s.slot_length(a, b),
|
||
Radius { c, .. } => s.radius_of(c),
|
||
Diameter { c, .. } => 2.0 * s.round(c).1,
|
||
Angle { a, b, flip, .. } => {
|
||
let (a0, a1) = s.line(a);
|
||
let (b0, b1) = s.line(b);
|
||
let (da, db) = (a1 - a0, b1 - b0);
|
||
let ang = da.cross(db).atan2(da.dot(db)) - if flip { std::f64::consts::PI } else { 0.0 };
|
||
ang.rem_euclid(std::f64::consts::TAU)
|
||
}
|
||
ArcLength { c, .. } => s.arc_length(c),
|
||
LinearDiameter { p, l, .. } => {
|
||
let (a, b) = s.line(l);
|
||
2.0 * line_dist(s.p(p), a, b).abs()
|
||
}
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
/// What a dimension measures on the sketch as it stands (mm, radians for angles); `None` for a
|
||
/// constraint that is not a dimension.
|
||
pub fn measure_dimension(sk: &Sketch, k: &ConstraintKind) -> Option<f64> {
|
||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||
measure(&State { sk, lay: &lay, x: &[] }, k).filter(|v| v.is_finite())
|
||
}
|
||
|
||
/// One residual block: implicit arc radius equality, a user constraint, or a tangency at a known
|
||
/// touch point (better conditioned than the distance form when an end point is on the curve).
|
||
enum Block<'a> {
|
||
Closure { a: usize, b: usize },
|
||
Arc { c: usize, a: usize, b: usize },
|
||
User(usize, &'a ConstraintKind),
|
||
TangentAt(usize, &'a ConstraintKind, usize),
|
||
}
|
||
|
||
fn copy_block<'a>(b: &Block<'a>) -> Block<'a> {
|
||
match b {
|
||
Block::Closure { a, b } => Block::Closure { a: *a, b: *b },
|
||
Block::Arc { c, a, b } => Block::Arc { c: *c, a: *a, b: *b },
|
||
Block::User(i, k) => Block::User(*i, k),
|
||
Block::TangentAt(i, k, p) => Block::TangentAt(*i, k, *p),
|
||
}
|
||
}
|
||
|
||
fn block_residuals(s: &State, b: &Block, out: &mut Vec<f64>) {
|
||
match b {
|
||
Block::Closure { a, b } => {
|
||
let d = s.p(*a) - s.p(*b);
|
||
out.extend([d.x, d.y]);
|
||
}
|
||
Block::Arc { c, a, b } => {
|
||
let cc = s.p(*c);
|
||
out.push(cc.dist(s.p(*b)) - cc.dist(s.p(*a)));
|
||
}
|
||
Block::User(_, k) => residuals(s, k, out),
|
||
Block::TangentAt(_, k, p) => {
|
||
let ConstraintKind::Tangent { a, b } = **k else { return };
|
||
let pt = s.p(*p);
|
||
if s.is_freeform(a) || s.is_freeform(b) {
|
||
// Directions at the shared end parallel.
|
||
match (s.tangent_at(a, *p), s.tangent_at(b, *p)) {
|
||
(Some(u), Some(v)) => out.push(u.cross(v) * 10.0),
|
||
_ => out.push(0.0),
|
||
}
|
||
} else if s.is_line(a) || s.is_line(b) {
|
||
let (l, c) = if s.is_line(a) { (a, b) } else { (b, a) };
|
||
let (p0, p1) = s.line(l);
|
||
let (cc, _) = s.round(c);
|
||
let d = p1 - p0;
|
||
// The radius to the touch point is perpendicular to the line.
|
||
out.push((pt - cc).dot(d) / d.len().max(1e-12));
|
||
} else {
|
||
let (c1, _) = s.round(a);
|
||
let (c2, _) = s.round(b);
|
||
let (u, v) = (pt - c1, pt - c2);
|
||
// Both centres on one line through the touch point.
|
||
out.push(u.cross(v) / (u.len() + v.len()).max(1e-12));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Every (point, curve) pair of a point-on-curve constraint, sorted (for [`touch_point`]).
|
||
pub(crate) fn points_on_curves(sk: &Sketch) -> Vec<(usize, usize)> {
|
||
let mut on: Vec<(usize, usize)> = sk
|
||
.constraints
|
||
.iter()
|
||
.filter_map(|c| match c.kind {
|
||
ConstraintKind::PointOnCurve { p, c } => Some((p, c)),
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
on.sort_unstable();
|
||
on
|
||
}
|
||
|
||
/// A point where a tangency must happen: an end point shared by the two curves, or a line end
|
||
/// point constrained onto the round curve (`on_curve` from [`points_on_curves`]).
|
||
pub(crate) fn touch_point(sk: &Sketch, k: &ConstraintKind, on_curve: &[(usize, usize)]) -> Option<usize> {
|
||
let ConstraintKind::Tangent { a, b } = *k else { return None };
|
||
let ends = |c: usize| -> Vec<usize> {
|
||
match sk.curves.get(c).map(|c| &c.kind) {
|
||
Some(k) => k.ends().map(|(a, b)| vec![a, b]).unwrap_or_default(),
|
||
None => Vec::new(),
|
||
}
|
||
};
|
||
let (ea, eb) = (ends(a), ends(b));
|
||
if let Some(p) = ea.iter().find(|p| eb.contains(p)) {
|
||
return Some(*p);
|
||
}
|
||
let on = |p: usize, c: usize| on_curve.binary_search(&(p, c)).is_ok();
|
||
ea.iter().find(|p| on(**p, b)).or_else(|| eb.iter().find(|p| on(**p, a))).copied()
|
||
}
|
||
|
||
/// Variables a block depends on, and whether it reads a point that is a constant of the solve.
|
||
fn block_vars(sk: &Sketch, lay: &Layout, b: &Block) -> (Vec<usize>, bool) {
|
||
let mut v = Vec::new();
|
||
let mut constant = false;
|
||
// Adds a point's variables; true when it is a constant.
|
||
let pt = |v: &mut Vec<usize>, p: usize| match lay.pvar.get(p).copied().flatten() {
|
||
Some(x) => {
|
||
v.extend([x, x + 1]);
|
||
false
|
||
}
|
||
None => true,
|
||
};
|
||
for r in block_refs(sk, b) {
|
||
let (c, fits): (usize, fn(&CurveKind) -> bool) = match r {
|
||
Ref::Point(p) => {
|
||
constant |= pt(&mut v, p);
|
||
continue;
|
||
}
|
||
Ref::Curve(c) => (c, |_| true),
|
||
Ref::Line(c) => (c, |k| matches!(k, CurveKind::Line { .. })),
|
||
Ref::Round(c) => (c, |k| matches!(k, CurveKind::Circle { .. } | CurveKind::Arc { .. })),
|
||
};
|
||
// A curve of another kind reads as a constant (see `State::line`, `State::round`).
|
||
let Some(k) = sk.curves.get(c).map(|c| &c.kind).filter(|k| fits(k)) else {
|
||
constant = true;
|
||
continue;
|
||
};
|
||
for q in k.point_ids() {
|
||
constant |= pt(&mut v, q);
|
||
}
|
||
v.extend(lay.rvar.get(c).copied().flatten());
|
||
}
|
||
v.sort_unstable();
|
||
v.dedup();
|
||
(v, constant)
|
||
}
|
||
|
||
/// What a block reads: a point, or a curve (its points and its radius), of any kind or read
|
||
/// as a line or as a circle or arc (the residual reads another kind as a constant).
|
||
#[derive(Clone, Copy)]
|
||
enum Ref {
|
||
Point(usize),
|
||
Curve(usize),
|
||
Line(usize),
|
||
Round(usize),
|
||
}
|
||
|
||
/// What a block reads.
|
||
fn block_refs(sk: &Sketch, b: &Block) -> Vec<Ref> {
|
||
use ConstraintKind::*;
|
||
use Ref::{Curve as C, Line as L, Point as P, Round as R};
|
||
let is = |c: usize, f: fn(&CurveKind) -> bool| sk.curves.get(c).is_some_and(|x| f(&x.kind));
|
||
let line = |c: usize| is(c, |k| matches!(k, CurveKind::Line { .. }));
|
||
let freeform = |c: usize| is(c, CurveKind::is_freeform);
|
||
// A tangency reads a free-form curve as such, else a line and a round, or two rounds.
|
||
let tangent = |a: usize, b: usize| {
|
||
if freeform(a) || freeform(b) {
|
||
[C(a), C(b)]
|
||
} else if line(a) {
|
||
[L(a), R(b)]
|
||
} else if line(b) {
|
||
[R(a), L(b)]
|
||
} else {
|
||
[R(a), R(b)]
|
||
}
|
||
};
|
||
match b {
|
||
Block::Closure { a, b } => vec![P(*a), P(*b)],
|
||
Block::Arc { c, a, b } => vec![P(*c), P(*a), P(*b)],
|
||
Block::User(_, k) => match **k {
|
||
PatternPoint { p, q, center, .. } => vec![P(p), P(q)].into_iter().chain(center.map(P)).collect(),
|
||
PatternRadius { a, b } => vec![C(a), C(b)],
|
||
Coincident { p, q }
|
||
| HorizontalPoints { p, q }
|
||
| VerticalPoints { p, q }
|
||
| Distance { p, q, .. }
|
||
| DistanceX { p, q, .. }
|
||
| DistanceY { p, q, .. } => vec![P(p), P(q)],
|
||
PointOnCurve { p, c } => vec![P(p), C(c)],
|
||
Midpoint { p, l } | PointLineDistance { p, l, .. } | LinearDiameter { p, l, .. } => vec![P(p), L(l)],
|
||
Horizontal { l } | Vertical { l } | Length { l, .. } | LineAngle { l, .. } => vec![L(l)],
|
||
// Another kind reads a constant value, whatever its points: as good as any.
|
||
ArcSweep { c, .. } | Radius { c, .. } | Diameter { c, .. } | ArcLength { c, .. } => vec![C(c)],
|
||
Parallel { a, b } | Perpendicular { a, b } | Collinear { a, b } | Angle { a, b, .. } => vec![L(a), L(b)],
|
||
Equal { a, b } if line(a) => vec![L(a), L(b)],
|
||
Equal { a, b } | Concentric { a, b } | SlotOverallLength { a, b, .. } => vec![R(a), R(b)],
|
||
Tangent { a, b } => tangent(a, b).to_vec(),
|
||
Smooth { a, b } => vec![C(a), C(b)],
|
||
Symmetric { p, q, l } => vec![P(p), P(q), L(l)],
|
||
Fix { .. } | OffsetDistance { .. } => vec![],
|
||
},
|
||
Block::TangentAt(_, k, p) => match **k {
|
||
ConstraintKind::Tangent { a, b } if freeform(a) || freeform(b) => vec![P(*p), C(a), C(b)],
|
||
ConstraintKind::Tangent { a, b } => [P(*p)].into_iter().chain(tangent(a, b)).collect(),
|
||
_ => vec![P(*p)],
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Residual vector and Jacobian (rows × all n vars) for the given blocks.
|
||
fn system(sk: &Sketch, lay: &Layout, blocks: &[(Block, Vec<usize>)], x: &mut [f64], with_j: bool) -> (Vec<f64>, Option<Mat>, Vec<usize>) {
|
||
let mut r = Vec::new();
|
||
let mut owner = Vec::new();
|
||
for (bi, (b, _)) in blocks.iter().enumerate() {
|
||
let before = r.len();
|
||
block_residuals(&State { sk, lay, x }, b, &mut r);
|
||
owner.extend(std::iter::repeat_n(bi, r.len() - before));
|
||
}
|
||
if !with_j {
|
||
return (r, None, owner);
|
||
}
|
||
let mut j = Mat::zeros(r.len(), lay.n);
|
||
let mut row = 0;
|
||
let mut plus = Vec::new();
|
||
let mut minus = Vec::new();
|
||
for (b, vars) in blocks {
|
||
plus.clear();
|
||
block_residuals(&State { sk, lay, x }, b, &mut plus);
|
||
let rows = plus.len();
|
||
for &v in vars {
|
||
let Some(x0) = x.get(v).copied() else { continue };
|
||
let h = 1e-7 * x0.abs().max(1.0);
|
||
if let Some(xv) = x.get_mut(v) {
|
||
*xv = x0 + h;
|
||
}
|
||
plus.clear();
|
||
block_residuals(&State { sk, lay, x }, b, &mut plus);
|
||
if let Some(xv) = x.get_mut(v) {
|
||
*xv = x0 - h;
|
||
}
|
||
minus.clear();
|
||
block_residuals(&State { sk, lay, x }, b, &mut minus);
|
||
if let Some(xv) = x.get_mut(v) {
|
||
*xv = x0;
|
||
}
|
||
for k in 0..rows {
|
||
let d = (plus.get(k).copied().unwrap_or(0.0) - minus.get(k).copied().unwrap_or(0.0)) / (2.0 * h);
|
||
j.set(row + k, v, d);
|
||
}
|
||
}
|
||
row += rows;
|
||
}
|
||
(r, Some(j), owner)
|
||
}
|
||
|
||
/// Residuals and the Jacobian restricted to `vars` (columns in that order; `col` maps a global
|
||
/// variable to its column, usize::MAX elsewhere). Blocks only touch their component's variables,
|
||
/// so a component's Jacobian is small even in a huge sketch.
|
||
fn local_system(
|
||
sk: &Sketch,
|
||
lay: &Layout,
|
||
blocks: &[(Block, Vec<usize>)],
|
||
x: &mut [f64],
|
||
col: &[usize],
|
||
ncols: usize,
|
||
) -> (Vec<f64>, Mat, Vec<usize>) {
|
||
let (r, _, owner) = system(sk, lay, blocks, x, false);
|
||
let mut j = Mat::zeros(r.len(), ncols);
|
||
let mut row = 0;
|
||
let mut plus = Vec::new();
|
||
let mut minus = Vec::new();
|
||
for (b, vars) in blocks {
|
||
plus.clear();
|
||
block_residuals(&State { sk, lay, x }, b, &mut plus);
|
||
let rows = plus.len();
|
||
for &v in vars {
|
||
let Some(c) = col.get(v).copied().filter(|c| *c < ncols) else { continue };
|
||
let Some(x0) = x.get(v).copied() else { continue };
|
||
let h = 1e-7 * x0.abs().max(1.0);
|
||
if let Some(xv) = x.get_mut(v) {
|
||
*xv = x0 + h;
|
||
}
|
||
plus.clear();
|
||
block_residuals(&State { sk, lay, x }, b, &mut plus);
|
||
if let Some(xv) = x.get_mut(v) {
|
||
*xv = x0 - h;
|
||
}
|
||
minus.clear();
|
||
block_residuals(&State { sk, lay, x }, b, &mut minus);
|
||
if let Some(xv) = x.get_mut(v) {
|
||
*xv = x0;
|
||
}
|
||
for k in 0..rows {
|
||
let d = (plus.get(k).copied().unwrap_or(0.0) - minus.get(k).copied().unwrap_or(0.0)) / (2.0 * h);
|
||
j.set(row + k, c, d);
|
||
}
|
||
}
|
||
row += rows;
|
||
}
|
||
(r, j, owner)
|
||
}
|
||
|
||
fn max_abs(v: &[f64]) -> f64 {
|
||
v.iter().fold(0.0_f64, |m, x| if x.is_finite() { m.max(x.abs()) } else { f64::INFINITY })
|
||
}
|
||
fn norm2(v: &[f64]) -> f64 {
|
||
v.iter().map(|x| x * x).sum()
|
||
}
|
||
|
||
/// Levenberg–Marquardt on one component (variables `vars`, blocks `blocks`). Returns the
|
||
/// iterations and the largest residual left.
|
||
fn lm(sk: &Sketch, lay: &Layout, blocks: &[(Block, Vec<usize>)], vars: &[usize], x: &mut Vec<f64>, col: &[usize]) -> (usize, f64) {
|
||
lm_capped(sk, lay, blocks, vars, x, col, MAX_ITERS)
|
||
}
|
||
|
||
/// [`lm`] with at most `max_iters` iterations.
|
||
fn lm_capped(
|
||
sk: &Sketch,
|
||
lay: &Layout,
|
||
blocks: &[(Block, Vec<usize>)],
|
||
vars: &[usize],
|
||
x: &mut Vec<f64>,
|
||
col: &[usize],
|
||
max_iters: usize,
|
||
) -> (usize, f64) {
|
||
let n = vars.len();
|
||
#[cfg(test)]
|
||
lm_vars::add(n);
|
||
let mut lambda = 1e-3;
|
||
let (mut r, _, _) = system(sk, lay, blocks, x, false);
|
||
let mut f = norm2(&r);
|
||
for it in 0..max_iters {
|
||
if max_abs(&r) < TOL {
|
||
return (it, max_abs(&r));
|
||
}
|
||
let (_, j, _) = local_system(sk, lay, blocks, x, col, n);
|
||
// Normal equations of this component.
|
||
let mut a = Mat::zeros(n, n);
|
||
let mut g = vec![0.0; n];
|
||
for row in 0..j.rows {
|
||
let ri = r.get(row).copied().unwrap_or(0.0);
|
||
let nz: Vec<(usize, f64)> = (0..n).map(|li| (li, j.get(row, li))).filter(|(_, d)| *d != 0.0).collect();
|
||
for &(li, di) in &nz {
|
||
if let Some(gi) = g.get_mut(li) {
|
||
*gi += di * ri;
|
||
}
|
||
for &(lk, dk) in &nz {
|
||
a.add(li, lk, di * dk);
|
||
}
|
||
}
|
||
}
|
||
let mut improved = false;
|
||
// Damping floor: variables the residuals barely see must not take huge steps.
|
||
let floor = (0..n).map(|i| a.get(i, i)).fold(0.0_f64, f64::max) * 1e-4;
|
||
for _ in 0..30 {
|
||
let mut m = a.clone();
|
||
for i in 0..n {
|
||
let d = a.get(i, i);
|
||
m.add(i, i, lambda * (d.max(floor) + 1e-9) + 1e-12);
|
||
}
|
||
let rhs: Vec<f64> = g.iter().map(|v| -v).collect();
|
||
let Some(step) = lin_solve(m, rhs) else {
|
||
lambda *= 10.0;
|
||
continue;
|
||
};
|
||
let mut trial = x.clone();
|
||
for (li, &v) in vars.iter().enumerate() {
|
||
if let (Some(t), Some(s)) = (trial.get_mut(v), step.get(li)) {
|
||
*t += s;
|
||
}
|
||
}
|
||
let (rt, _, _) = system(sk, lay, blocks, &mut trial, false);
|
||
let ft = norm2(&rt);
|
||
if ft.is_finite() && ft < f {
|
||
*x = trial;
|
||
r = rt;
|
||
f = ft;
|
||
lambda = (lambda / 3.0).max(1e-12);
|
||
improved = true;
|
||
break;
|
||
}
|
||
lambda *= 4.0;
|
||
if lambda > 1e12 {
|
||
break;
|
||
}
|
||
}
|
||
if !improved {
|
||
return (it, max_abs(&r));
|
||
}
|
||
}
|
||
(max_iters, max_abs(&r))
|
||
}
|
||
|
||
/// The variables LM was given and its runs on this thread, for tests that count what the
|
||
/// construction leaves to it.
|
||
#[cfg(test)]
|
||
pub(crate) mod lm_vars {
|
||
use std::cell::Cell;
|
||
|
||
thread_local! {
|
||
static VARS: Cell<usize> = const { Cell::new(0) };
|
||
static RUNS: Cell<usize> = const { Cell::new(0) };
|
||
}
|
||
|
||
pub(super) fn add(n: usize) {
|
||
VARS.with(|c| c.set(c.get() + n));
|
||
RUNS.with(|c| c.set(c.get() + 1));
|
||
}
|
||
|
||
/// The variables since the last call.
|
||
pub(crate) fn take() -> usize {
|
||
VARS.with(|c| c.replace(0))
|
||
}
|
||
|
||
/// The LM runs since the last call.
|
||
pub(crate) fn runs() -> usize {
|
||
RUNS.with(|c| c.replace(0))
|
||
}
|
||
}
|
||
|
||
/// The pinned analysis is exact (see [`pinned`]); in this crate's tests every analysis is
|
||
/// checked against the dense analysis of the whole component wherever that one is reliable:
|
||
/// its Jacobian finite, and its answer the same from a thousandth to a thousand times its
|
||
/// tolerance. (Where it is not, the Jacobian is nearly singular and the two analyses may
|
||
/// decide its rank differently: the dense one on the whole matrix, the pinned one on the
|
||
/// remainder after eliminating what the constraints pin down, and which pivots fall under the
|
||
/// tolerance depends on that order.)
|
||
#[cfg(test)]
|
||
fn assert_dense_agrees(j: &Mat, rank: usize, determined: &[bool]) {
|
||
let at = |tol: f64| if j.rows > 0 { determined_vars(j, tol) } else { (vec![false; j.cols], 0) };
|
||
let dense = at(1e-7);
|
||
if j.data.iter().any(|v| !v.is_finite()) || at(1e-10) != dense || at(1e-4) != dense {
|
||
return;
|
||
}
|
||
assert_eq!((rank, determined), (dense.1, dense.0.as_slice()), "the pinned rank analysis disagrees with the dense one");
|
||
}
|
||
|
||
/// Least squared singular value the rows pinning an unknown must have on its own columns
|
||
/// (a singular value of 1e-4, a thousand times the rank analysis' pivot tolerance)…
|
||
const PIN_RANK: f64 = 1e-8;
|
||
/// …and the least ratio of its squared singular values (a condition number of at most 1e4), so
|
||
/// that the 2 × 2 test is decided well clear of rounding.
|
||
const PIN_CONDITION: f64 = 1e-8;
|
||
|
||
/// What [`pinned`] works for.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
enum Pin {
|
||
/// The solve: the candidates are where the seed constructed them, and every row that pins
|
||
/// one must hold there (below `TOL`), as LM will not move it.
|
||
Verified,
|
||
/// The rank analysis of a component some block ties to a constant.
|
||
Anchored,
|
||
/// The rank analysis of a component no block ties to a constant: the point whose first
|
||
/// variable this is counts as a constant (it takes out the translation, see `pinned`).
|
||
Floating(usize),
|
||
}
|
||
|
||
impl Pin {
|
||
/// The first variable of the point taken as a constant.
|
||
fn anchor(self) -> Option<usize> {
|
||
match self {
|
||
Pin::Floating(a) => Some(a),
|
||
Pin::Verified | Pin::Anchored => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Map `keep` to columns `0..` and the other variables of `all` to none.
|
||
fn columns(col: &mut [usize], keep: &[usize], all: &[usize]) {
|
||
for v in all {
|
||
if let Some(c) = col.get_mut(*v) {
|
||
*c = usize::MAX;
|
||
}
|
||
}
|
||
for (k, v) in keep.iter().enumerate() {
|
||
if let Some(c) = col.get_mut(*v) {
|
||
*c = k;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The variables of `vars` neither pinned nor of the point at `anchor` (sorted, as `vars`).
|
||
fn unpinned(vars: &[usize], pin: &[bool], anchor: Option<usize>) -> Vec<usize> {
|
||
let anchored = |v: usize| anchor.is_some_and(|a| v == a || v == a + 1);
|
||
vars.iter().zip(pin).filter(|(v, p)| !**p && !anchored(**v)).map(|(v, _)| *v).collect()
|
||
}
|
||
|
||
/// The blocks that read one of `vars` (sorted).
|
||
fn blocks_reading<'a>(blocks: &[(Block<'a>, Vec<usize>)], vars: &[usize]) -> Vec<(Block<'a>, Vec<usize>)> {
|
||
blocks.iter().filter(|(_, bv)| bv.iter().any(|v| vars.binary_search(v).is_ok())).map(|(b, v)| (copy_block(b), v.clone())).collect()
|
||
}
|
||
|
||
/// Which variables of one component (`vars`, sorted; `blocks` its blocks) the constraints pin
|
||
/// down one unknown after another: an unknown is pinned when the rows of the blocks that read
|
||
/// nothing else but constants and unknowns pinned before it have full column rank on its own
|
||
/// columns (well conditioned: `PIN_RANK`, `PIN_CONDITION`). Only `candidate` unknowns are
|
||
/// pinned; for [`Pin::Verified`] also only where those rows hold.
|
||
///
|
||
/// Why this is exact. Order the pinned unknowns as they were pinned, P, and call the rest R.
|
||
/// Each pinned unknown has its rows S_u, which read nothing after it in P and nothing in R, so
|
||
/// the rows S = ∪ S_u of the Jacobian restricted to the columns of P form a block lower
|
||
/// triangular matrix whose diagonal blocks have full column rank, and they are zero on R. For
|
||
/// any vector (p, r) in the null space of J, S gives p = 0, so the null space is
|
||
/// {(0, r) : J_R r = 0}, where J_R is J's columns of R (every row, those of S included, are zero
|
||
/// there). Hence:
|
||
/// - rank J = |P| + rank J_R: the analysis eliminates P by construction and runs the dense rank
|
||
/// analysis on J_R only;
|
||
/// - every pinned variable is determined (zero in every null vector), and a variable of R is
|
||
/// determined in J exactly when it is in J_R.
|
||
///
|
||
/// For the solve, the same structure means the pinned values are the only ones near them that
|
||
/// satisfy S, given the constants: freezing them at verified values and solving R with LM finds
|
||
/// a solution of the whole system whenever one exists with P where the seed put it. (When LM
|
||
/// cannot solve R that way, the component is solved whole, as without pinning.)
|
||
///
|
||
/// A component no block ties to a constant ([`Pin::Floating`]) is invariant under
|
||
/// translation: every residual depends only on differences of its points (and radii, which a
|
||
/// translation leaves alone), so the translations t_x, t_y are null vectors of J. Removing one
|
||
/// point a's two columns then keeps the rank, as any null vector of J is a null vector of J
|
||
/// without a (all of whose entries on a are zero) plus a translation: rank J = rank J_{-a}, and
|
||
/// J_{-a} is analysed as above with a as a constant. The translations move every point, so no
|
||
/// point coordinate of such a component is determined; a radius is determined when it is in
|
||
/// J_{-a} (the translations leave it alone).
|
||
fn pinned(
|
||
sk: &Sketch,
|
||
lay: &Layout,
|
||
blocks: &[(Block, Vec<usize>)],
|
||
vars: &[usize],
|
||
x: &mut [f64],
|
||
candidate: &dyn Fn(usize) -> bool,
|
||
pin: Pin,
|
||
) -> Vec<bool> {
|
||
let nv = vars.len();
|
||
let local = |v: usize| vars.binary_search(&v).ok();
|
||
// Per local unknown (by its first variable's index): pinned, or the anchor.
|
||
let mut done = vec![false; nv];
|
||
if let Some(a) = pin.anchor().and_then(local).and_then(|a| done.get_mut(a)) {
|
||
*a = true;
|
||
}
|
||
let block_units: Vec<Vec<usize>> = blocks
|
||
.iter()
|
||
.map(|(_, bv)| {
|
||
let mut u: Vec<usize> = bv.iter().filter_map(|v| local(lay.unit(*v))).collect();
|
||
u.dedup();
|
||
u
|
||
})
|
||
.collect();
|
||
let mut of_unit: Vec<Vec<usize>> = vec![Vec::new(); nv];
|
||
for (bi, us) in block_units.iter().enumerate() {
|
||
for u in us {
|
||
if let Some(list) = of_unit.get_mut(*u) {
|
||
list.push(bi);
|
||
}
|
||
}
|
||
}
|
||
let open = |done: &[bool], u: &usize| !done.get(*u).copied().unwrap_or(true);
|
||
// Per block: its unknowns not pinned yet. A block is ready for its last one.
|
||
let mut pending: Vec<usize> = block_units.iter().map(|us| us.iter().filter(|u| open(&done, u)).count()).collect();
|
||
let mut ready: Vec<usize> = (0..blocks.len()).filter(|bi| pending.get(*bi) == Some(&1)).rev().collect();
|
||
let mut gram = vec![[0.0_f64; 3]; nv];
|
||
let mut bad = vec![false; nv];
|
||
let mut queue = Vec::new();
|
||
let mut out = vec![false; nv];
|
||
loop {
|
||
while let Some(bi) = ready.pop() {
|
||
let (Some(us), Some((block, _))) = (block_units.get(bi), blocks.get(bi)) else { continue };
|
||
let Some(&u) = us.iter().find(|u| open(&done, u)) else { continue };
|
||
let Some(&first) = vars.get(u) else { continue };
|
||
if bad.get(u).copied().unwrap_or(true) || !candidate(first) {
|
||
continue;
|
||
}
|
||
let Some(g) = gram.get_mut(u) else { continue };
|
||
let residual = accumulate(sk, lay, block, &[first, first + 1][..lay.width(first)], x, g);
|
||
if pin == Pin::Verified
|
||
&& !(residual < TOL)
|
||
&& let Some(b) = bad.get_mut(u)
|
||
{
|
||
*b = true;
|
||
continue;
|
||
}
|
||
queue.push(u);
|
||
}
|
||
let Some(u) = queue.pop() else { break };
|
||
let (Some(&first), Some(&g)) = (vars.get(u), gram.get(u)) else { continue };
|
||
let w = lay.width(first);
|
||
if !open(&done, &u) || bad.get(u).copied().unwrap_or(true) || !full_rank(g, w) {
|
||
continue;
|
||
}
|
||
for k in u..u + w {
|
||
if let (Some(d), Some(o)) = (done.get_mut(k), out.get_mut(k)) {
|
||
(*d, *o) = (true, true);
|
||
}
|
||
}
|
||
for &bi in of_unit.get(u).into_iter().flatten() {
|
||
if let Some(p) = pending.get_mut(bi) {
|
||
*p = p.saturating_sub(1);
|
||
if *p == 1 {
|
||
ready.push(bi);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Add the rows of block `b`, restricted to the columns of `unit` (one or two variables), to
|
||
/// `gram` (their AᵀA: [a00, a01, a11]), with the same differences as [`local_system`]; returns
|
||
/// the block's largest residual.
|
||
fn accumulate(sk: &Sketch, lay: &Layout, b: &Block, unit: &[usize], x: &mut [f64], gram: &mut [f64; 3]) -> f64 {
|
||
let mut r = Vec::new();
|
||
block_residuals(&State { sk, lay, x }, b, &mut r);
|
||
let mut d = [Vec::new(), Vec::new()];
|
||
let (mut plus, mut minus) = (Vec::new(), Vec::new());
|
||
for (&v, dk) in unit.iter().zip(&mut d) {
|
||
let Some(x0) = x.get(v).copied() else { continue };
|
||
let h = 1e-7 * x0.abs().max(1.0);
|
||
for (at, out) in [(x0 + h, &mut plus), (x0 - h, &mut minus)] {
|
||
if let Some(xv) = x.get_mut(v) {
|
||
*xv = at;
|
||
}
|
||
out.clear();
|
||
block_residuals(&State { sk, lay, x }, b, out);
|
||
}
|
||
if let Some(xv) = x.get_mut(v) {
|
||
*xv = x0;
|
||
}
|
||
*dk = plus.iter().zip(&minus).map(|(p, m)| (p - m) / (2.0 * h)).collect();
|
||
}
|
||
let [d0, d1] = &d;
|
||
for k in 0..r.len() {
|
||
let (a, b) = (d0.get(k).copied().unwrap_or(0.0), d1.get(k).copied().unwrap_or(0.0));
|
||
gram[0] += a * a;
|
||
gram[1] += a * b;
|
||
gram[2] += b * b;
|
||
}
|
||
max_abs(&r)
|
||
}
|
||
|
||
/// The rows behind `gram` (see [`accumulate`]) have full, well-conditioned column rank on a
|
||
/// unit of `width` variables.
|
||
fn full_rank(gram: [f64; 3], width: usize) -> bool {
|
||
let [a, b, c] = gram;
|
||
let (min, max) = if width == 1 {
|
||
(a, a)
|
||
} else {
|
||
let max = 0.5 * (a + c + ((a - c).powi(2) + 4.0 * b * b).sqrt());
|
||
(if max > 0.0 { (a * c - b * b) / max } else { 0.0 }, max)
|
||
};
|
||
min.is_finite() && max.is_finite() && min >= PIN_RANK && min >= PIN_CONDITION * max
|
||
}
|
||
|
||
/// Solve the sketch in place. On failure the geometry is restored and the report lists the
|
||
/// violated constraints.
|
||
pub fn solve(sk: &mut Sketch) -> SolveReport {
|
||
solve_with(sk, &Hold::default(), None, Analysis::Full, None)
|
||
}
|
||
|
||
/// Solve with some geometry held as if it were fixed (for this solve only): applying a
|
||
/// constraint between two entities holds the first so only the second moves. Everything else
|
||
/// moves as little as it can. On failure the geometry is restored.
|
||
pub fn solve_holding(sk: &mut Sketch, hold: &Hold) -> SolveReport {
|
||
solve_with(sk, hold, None, Analysis::Full, None)
|
||
}
|
||
|
||
/// What a solve works out besides placing the geometry.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Analysis {
|
||
/// Degrees of freedom and which geometry the constraints determine: a rank analysis of the
|
||
/// Jacobian, the costly part of solving an already solved sketch.
|
||
Full,
|
||
/// Only whether it solved: the report's `dof` is 0 and its determined flags are empty. For
|
||
/// solves whose report is only checked with [`SolveReport::ok`] (the frames of a drag).
|
||
StatusOnly,
|
||
}
|
||
|
||
/// [`solve`] with some geometry held as if it were fixed (for this solve only): applying a
|
||
/// constraint between two entities holds the first so only the second moves; a drag holds what
|
||
/// it drags. Everything else moves as little as it can. On failure the geometry is restored.
|
||
///
|
||
/// `reference`, after an interactive move: the points' positions from before it. Points the
|
||
/// constraints pin down keep the orientation they had there rather than the one the move left
|
||
/// them in. Points appended after the reference was taken use their current positions; a
|
||
/// reference longer than the point list is ignored.
|
||
///
|
||
/// `budget`: the work the seed's cluster decomposition may do, shared with other solves (the
|
||
/// steps of a live drag frame, [`Budget::frame`]); [`Budget::full`] for this solve alone when
|
||
/// `None`.
|
||
pub fn solve_with(sk: &mut Sketch, hold: &Hold, reference: Option<&[Vec2]>, analysis: Analysis, budget: Option<&mut Budget>) -> SolveReport {
|
||
let mut own = Budget::full();
|
||
solve_from(sk, hold, reference, analysis, None, budget.unwrap_or(&mut own))
|
||
}
|
||
|
||
/// Driving values by constraint id (see [`Sketch::driving_values`]).
|
||
pub type DrivingValues = std::collections::BTreeMap<String, f64>;
|
||
|
||
/// [`solve`] after driving values changed: the sketch's geometry is its stored solved state,
|
||
/// solved for `old` (by constraint id; constraints added since have none). A component the
|
||
/// construction leaves to the iteration (a triconnected one) can fail to converge, or fold a
|
||
/// constrained triangle over, when solved for the new values in one jump; it is then solved
|
||
/// again by stepping its changed values from old to new (see `continue_values`). Where no
|
||
/// value changed a solve behaves as [`solve`].
|
||
pub fn solve_after_value_edit(sk: &mut Sketch, old: &DrivingValues) -> SolveReport {
|
||
solve_after_edit(sk, old, None)
|
||
}
|
||
|
||
/// [`solve_after_value_edit`] of a sketch whose linked geometry may have moved as well: once
|
||
/// its links are refreshed ([`Sketch::update_link`]), the linked points are where their sources
|
||
/// are now and everything else is still the stored solved state. `stored` has every point's
|
||
/// position in that state ([`Sketch::positions_in`]; `None` when no link moved), and the sides
|
||
/// and turns the solve keeps are read there, as [`solve_with`] reads them from the positions
|
||
/// before an interactive move. Read where the links are now, a point dimensioned from a
|
||
/// projected line is on the far side of it as soon as the source moves the line past the point.
|
||
pub fn solve_after_edit(sk: &mut Sketch, old: &DrivingValues, stored: Option<&[Vec2]>) -> SolveReport {
|
||
let old_values: Vec<Option<f64>> = sk.constraints.iter().map(|c| old.get(&c.id).copied()).collect();
|
||
solve_from(sk, &Hold::default(), stored, Analysis::Full, Some(&old_values), &mut Budget::full())
|
||
}
|
||
|
||
impl Sketch {
|
||
/// Each dimension's driving value by constraint id, as [`solve_after_value_edit`] takes
|
||
/// them.
|
||
pub fn driving_values(&self) -> DrivingValues {
|
||
self.constraints.iter().filter_map(|c| Some((c.id.clone(), c.kind.value()?))).collect()
|
||
}
|
||
}
|
||
|
||
/// The variables' values where the sketch is now.
|
||
fn initial_x(sk: &Sketch, lay: &Layout) -> Vec<f64> {
|
||
let mut x = vec![0.0; lay.n];
|
||
for (i, p) in sk.points.iter().enumerate() {
|
||
if let Some(v) = lay.pvar.get(i).copied().flatten() {
|
||
set_point(&mut x, v, p.pos);
|
||
}
|
||
}
|
||
for (i, c) in sk.curves.iter().enumerate() {
|
||
if let (CurveKind::Circle { r, .. } | CurveKind::Ellipse { r, .. }, Some(v)) = (&c.kind, lay.rvar.get(i).copied().flatten())
|
||
&& let Some(s) = x.get_mut(v)
|
||
{
|
||
*s = *r;
|
||
}
|
||
}
|
||
x
|
||
}
|
||
|
||
/// The solve's blocks in order (the curves' own relations, then the driving constraints in
|
||
/// constraint order) with the variables each reads, and per block whether it reads a constant
|
||
/// point.
|
||
fn build_blocks<'a>(sk: &'a Sketch, lay: &Layout, on_curve: &[(usize, usize)]) -> (Vec<(Block<'a>, Vec<usize>)>, Vec<bool>) {
|
||
let mut raw = Vec::new();
|
||
for c in &sk.curves {
|
||
if let CurveKind::Spline { pts, periodic: true, .. } = &c.kind
|
||
&& let (Some(a), Some(b)) = (pts.first(), pts.last())
|
||
&& a != b
|
||
{
|
||
raw.push(Block::Closure { a: *a, b: *b });
|
||
}
|
||
if let CurveKind::Arc { c, a, b } = c.kind {
|
||
raw.push(Block::Arc { c, a, b });
|
||
}
|
||
}
|
||
for (i, c) in sk.constraints.iter().enumerate() {
|
||
if c.driven || matches!(c.kind, ConstraintKind::OffsetDistance { .. }) {
|
||
continue;
|
||
}
|
||
raw.push(match touch_point(sk, &c.kind, on_curve) {
|
||
Some(p) => Block::TangentAt(i, &c.kind, p),
|
||
None => Block::User(i, &c.kind),
|
||
});
|
||
}
|
||
let mut blocks: Vec<(Block, Vec<usize>)> = Vec::with_capacity(raw.len());
|
||
let mut reads_constant = Vec::with_capacity(raw.len());
|
||
for bl in raw {
|
||
let (vars, constant) = block_vars(sk, lay, &bl);
|
||
blocks.push((bl, vars));
|
||
reads_constant.push(constant);
|
||
}
|
||
|
||
(blocks, reads_constant)
|
||
}
|
||
|
||
#[cfg(not(test))]
|
||
fn solve_from(
|
||
sk: &mut Sketch,
|
||
hold: &Hold,
|
||
reference: Option<&[Vec2]>,
|
||
analysis: Analysis,
|
||
old_values: Option<&[Option<f64>]>,
|
||
budget: &mut Budget,
|
||
) -> SolveReport {
|
||
solve_inner(sk, hold, reference, analysis, old_values, budget)
|
||
}
|
||
|
||
/// In this crate's tests every solve is repeated without the constructive seed: the seed only
|
||
/// chooses where the iteration starts, so status, degrees of freedom and determined flags must
|
||
/// not depend on it, nor the (empty) failing list of a solved sketch. The one difference
|
||
/// allowed is the seed's purpose: the iteration from the plain start fails where the seeded
|
||
/// one solves. Which constraints a failed solve leaves violated depends on where the iteration
|
||
/// stopped; the seed may only narrow it.
|
||
#[cfg(test)]
|
||
fn solve_from(
|
||
sk: &mut Sketch,
|
||
hold: &Hold,
|
||
reference: Option<&[Vec2]>,
|
||
analysis: Analysis,
|
||
old_values: Option<&[Option<f64>]>,
|
||
budget: &mut Budget,
|
||
) -> SolveReport {
|
||
use crate::construct::switch;
|
||
if !(switch::seed_on() && switch::differential_on()) {
|
||
return solve_inner(sk, hold, reference, analysis, old_values, budget);
|
||
}
|
||
let mut plain = sk.clone();
|
||
let without = switch::without_seed(|| solve_inner(&mut plain, hold, reference, analysis, old_values, &mut Budget::full()));
|
||
let with = solve_inner(sk, hold, reference, analysis, old_values, budget);
|
||
assert_same_outcome(&with, &without);
|
||
with
|
||
}
|
||
|
||
/// The outcome the constructive seed must not change (see `solve_from`).
|
||
#[cfg(test)]
|
||
fn assert_same_outcome(with: &SolveReport, without: &SolveReport) {
|
||
if with.ok() && !without.ok() {
|
||
return;
|
||
}
|
||
assert_eq!(
|
||
(with.status, with.dof, &with.point_determined, &with.curve_determined),
|
||
(without.status, without.dof, &without.point_determined, &without.curve_determined),
|
||
"the constructive seed changed the solve's outcome"
|
||
);
|
||
// Turned-over orientations depend on where the iteration went; only conflicts count.
|
||
let conflicts = |r: &SolveReport| r.failing.iter().filter(|c| !r.folded.contains(c)).cloned().collect::<Vec<_>>();
|
||
if with.ok() {
|
||
assert_eq!(conflicts(with), conflicts(without));
|
||
} else {
|
||
assert!(conflicts(with).iter().all(|c| without.failing.contains(c)), "seeded {with:?}, unseeded {without:?}");
|
||
}
|
||
}
|
||
|
||
/// Write a point's position into its two variables starting at `v`.
|
||
/// An arc's ends follow its centre: when the seed moved the centre and did not construct an end,
|
||
/// that end starts carried by the centre's displacement (the arc translates, keeping its radius).
|
||
/// Left behind, the ends of an arc whose centre is put on the origin are turned by LM instead of
|
||
/// following it (reference test 20-slot-constrained).
|
||
fn carry_with_seeded(x: &mut [f64], x0: &[f64], constructed: &[bool], blocks: &[(Block, Vec<usize>)], lay: &Layout) {
|
||
let var = |p: usize| lay.pvar.get(p).copied().flatten();
|
||
let built = |v: usize| constructed.get(v).copied().unwrap_or(false);
|
||
let mut carried: Vec<(usize, Vec2)> = Vec::new();
|
||
for (block, _) in blocks {
|
||
let Block::Arc { c, a, b } = *block else { continue };
|
||
let Some(vc) = var(c).filter(|v| built(*v)) else { continue };
|
||
let (Some(now), Some(was)) = (x.get(vc..vc + 2), x0.get(vc..vc + 2)) else { continue };
|
||
let d = Vec2::new(now[0] - was[0], now[1] - was[1]);
|
||
if d.x == 0.0 && d.y == 0.0 {
|
||
continue;
|
||
}
|
||
carried.extend([a, b].into_iter().filter_map(var).filter(|v| !built(*v)).map(|v| (v, d)));
|
||
}
|
||
for (v, d) in carried {
|
||
if let Some([px, py]) = x.get_mut(v..v + 2) {
|
||
*px += d.x;
|
||
*py += d.y;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn set_point(x: &mut [f64], v: usize, at: Vec2) {
|
||
if let Some([sx, sy]) = x.get_mut(v..v.saturating_add(2)) {
|
||
*sx = at.x;
|
||
*sy = at.y;
|
||
}
|
||
}
|
||
|
||
/// Exactly opposite directions sit on the wrapped angle's derivative discontinuity: nudge one
|
||
/// free endpoint of each such line into a continuous branch before Newton iterations.
|
||
fn nudge_line_angles(sk: &Sketch, lay: &Layout, x: &mut [f64]) {
|
||
for constraint in &sk.constraints {
|
||
if constraint.driven {
|
||
continue;
|
||
}
|
||
let ConstraintKind::LineAngle { l, value } = constraint.kind else { continue };
|
||
let Some(CurveKind::Line { a, b }) = sk.curves.get(l).map(|c| &c.kind) else { continue };
|
||
let state = State { sk, lay, x };
|
||
let (pa, pb) = (state.p(*a), state.p(*b));
|
||
let delta = wrap_angle((pb - pa).angle() - value);
|
||
if delta.abs() < std::f64::consts::PI - 1e-5 || !delta.is_finite() {
|
||
continue;
|
||
}
|
||
let d = Vec2::from_angle((pb - pa).angle() - delta.signum() * 0.01) * pa.dist(pb);
|
||
let (point, at) = if lay.pvar.get(*b).copied().flatten().is_some() { (*b, pa + d) } else { (*a, pb - d) };
|
||
if let Some(index) = lay.pvar.get(point).copied().flatten() {
|
||
set_point(x, index, at);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Continuation steps an edit in sixty-fourths of it: the smallest sub-step is one of them.
|
||
const SUBSTEPS: usize = 64;
|
||
/// LM iterations one continuation may spend over all its sub-steps (taken and halved).
|
||
const CONTINUATION_ITERS: usize = 4 * MAX_ITERS;
|
||
/// Triangles of distance dimensions whose turn one component's fold check reads.
|
||
const MAX_TRIPLES: usize = 1024;
|
||
/// Neighbour visits while finding those triangles.
|
||
const MAX_TRIPLE_WORK: usize = 100_000;
|
||
|
||
/// An orientation the constraints leave open, which an edit of their values must keep: the
|
||
/// mirror image is "the other solution" (Owen 1991 §5: whether three dimensioned points occur
|
||
/// clockwise or counter-clockwise; an unsigned offset's side).
|
||
#[derive(Clone, Copy, Debug)]
|
||
enum Orient {
|
||
/// The turn of three points pairwise joined by distances (coincident corners merged).
|
||
Turn([usize; 3]),
|
||
/// Which side of `p` the point `q` is along x (`DistanceX` of the given size).
|
||
SideX(usize, usize, f64),
|
||
/// The same along y (`DistanceY`).
|
||
SideY(usize, usize, f64),
|
||
/// Which side of line `l` the point `p` is on (`PointLineDistance`, `LinearDiameter`).
|
||
OfLine(usize, usize, f64),
|
||
}
|
||
|
||
/// An orientation's signed offset in a state (a corner's distance from the opposite side, an
|
||
/// offset along its axis or from its line), the size it is measured against and how far the
|
||
/// coordinates are from the origin.
|
||
fn offset(s: &State, o: Orient) -> Option<(f64, f64, f64)> {
|
||
let coords = |ps: &[Vec2]| ps.iter().fold(0.0_f64, |m, p| m.max(crate::loci::mag(*p)));
|
||
Some(match o {
|
||
Orient::Turn([a, b, c]) => {
|
||
let (pa, pb, pc) = (s.p(a), s.p(b), s.p(c));
|
||
let size = pa.dist(pb).max(pa.dist(pc)).max(pb.dist(pc));
|
||
((pb - pa).normalized()?.cross(pc - pa), size, coords(&[pa, pb, pc]))
|
||
}
|
||
Orient::SideX(p, q, size) => {
|
||
let (pp, pq) = (s.p(p), s.p(q));
|
||
(pq.x - pp.x, size, coords(&[pp, pq]))
|
||
}
|
||
Orient::SideY(p, q, size) => {
|
||
let (pp, pq) = (s.p(p), s.p(q));
|
||
(pq.y - pp.y, size, coords(&[pp, pq]))
|
||
}
|
||
Orient::OfLine(p, l, size) => {
|
||
let (a, b) = s.line(l);
|
||
let pp = s.p(p);
|
||
((b - a).normalized()?.cross(pp - a), size, coords(&[pp, a, b]))
|
||
}
|
||
})
|
||
}
|
||
|
||
/// The sign of an orientation in a state; `None` when too close to zero to tell. It is the
|
||
/// constructive seed's side test (`loci::side`, the rounding of the coordinates and a tiny
|
||
/// share of the size), so the seed and the fold check agree on which way a triangle turns.
|
||
/// Belongs in `loci` beside `side` (moved there once the lines-and-rounds work is merged).
|
||
fn orientation(s: &State, o: Orient) -> Option<f64> {
|
||
offset(s, o).and_then(|(delta, size, coords)| crate::loci::side(delta, size, coords))
|
||
}
|
||
|
||
/// A reference orientation must be clear: an offset of at least this share of its size (for a
|
||
/// triangle, a height of 1/1000 of its longest side, about 0.06°). Nearer degenerate, solver
|
||
/// noise decides the sign, and no edit is refused for turning it over.
|
||
const ORIENTED: f64 = 1e-3;
|
||
|
||
/// The orientation of the stored state that an edit must keep: its sign where it is clear
|
||
/// (see [`ORIENTED`]), else `None`.
|
||
fn reference_orientation(s: &State, o: Orient) -> Option<f64> {
|
||
offset(s, o).filter(|(delta, size, _)| delta.abs() > ORIENTED * size).and_then(|(delta, size, coords)| crate::loci::side(delta, size, coords))
|
||
}
|
||
|
||
/// A driving value `t` of the way from `old` to `new`: angles of a line or between two lines
|
||
/// turn the short way round.
|
||
fn value_at(kind: &ConstraintKind, old: f64, new: f64, t: f64) -> f64 {
|
||
if t >= 1.0 {
|
||
return new;
|
||
}
|
||
match kind {
|
||
ConstraintKind::LineAngle { .. } | ConstraintKind::Angle { .. } => old + wrap_angle(new - old) * t,
|
||
_ => old + (new - old) * t,
|
||
}
|
||
}
|
||
|
||
/// Whether a value can be stepped from `old` to `new`: not a signed arc sweep that changes
|
||
/// sign (the arc's end points swap; there is no path of arcs between the two).
|
||
fn steppable(kind: &ConstraintKind, old: f64, new: f64) -> bool {
|
||
!(matches!(kind, ConstraintKind::ArcSweep { .. }) && old * new <= 0.0)
|
||
}
|
||
|
||
/// One component's driving values changed by an edit, and the orientations of the stored
|
||
/// state.
|
||
struct ValueEdit {
|
||
/// Block index, constraint index, the constraint (with its new value) and its old value.
|
||
changed: Vec<(usize, usize, ConstraintKind, f64)>,
|
||
/// Orientations with the constraints that set them and their sign in the stored state.
|
||
orients: Vec<(Orient, Vec<usize>, Option<f64>)>,
|
||
}
|
||
|
||
impl ValueEdit {
|
||
/// The values of `sub` changed against `old` (per constraint), `before` being the stored
|
||
/// state; `None` when nothing changed.
|
||
fn of(before: &State, sub: &[(Block, Vec<usize>)], old: &[Option<f64>]) -> Option<Self> {
|
||
let changed: Vec<_> = sub
|
||
.iter()
|
||
.enumerate()
|
||
.filter_map(|(k, (b, _))| {
|
||
let Block::User(ci, kind) = b else { return None };
|
||
let (o, n) = (old.get(*ci).copied().flatten()?, kind.value()?);
|
||
(o.is_finite() && n.is_finite() && o != n).then(|| (k, *ci, (*kind).clone(), o))
|
||
})
|
||
.collect();
|
||
if changed.is_empty() {
|
||
return None;
|
||
}
|
||
let orients = orients(before, sub).into_iter().map(|(o, cs)| (o, cs, reference_orientation(before, o))).collect();
|
||
Some(ValueEdit { changed, orients })
|
||
}
|
||
|
||
/// The stored state is solved for the old values: there is a solved state to continue
|
||
/// from and orientations to keep (a sketch stored unsolved has neither).
|
||
fn starts_solved(&self, before: &State, sub: &[(Block, Vec<usize>)]) -> bool {
|
||
let olds = self.kinds_at(0);
|
||
let mut blocks: Vec<(Block, Vec<usize>)> = sub.iter().map(|(b, v)| (copy_block(b), v.clone())).collect();
|
||
self.point_at(&mut blocks, &olds);
|
||
max_abs(&system(before.sk, before.lay, &blocks, &mut before.x.to_vec(), false).0) < ACCEPT
|
||
}
|
||
|
||
/// The values can be stepped (see [`steppable`]).
|
||
fn continuable(&self) -> bool {
|
||
self.changed.iter().all(|(_, _, k, o)| k.value().is_some_and(|n| steppable(k, *o, n)))
|
||
}
|
||
|
||
/// The changed constraints `j` sixty-fourths of the way from their old values to the new.
|
||
fn kinds_at(&self, j: usize) -> Vec<ConstraintKind> {
|
||
let t = j as f64 / SUBSTEPS as f64;
|
||
self.changed
|
||
.iter()
|
||
.map(|(_, _, kind, o)| {
|
||
let mut k = kind.clone();
|
||
if let Some(n) = kind.value() {
|
||
k.set_value(value_at(kind, *o, n, t));
|
||
}
|
||
k
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Point the changed constraints' blocks at `kinds` (as [`ValueEdit::kinds_at`] gives them).
|
||
fn point_at<'a>(&self, blocks: &mut [(Block<'a>, Vec<usize>)], kinds: &'a [ConstraintKind]) {
|
||
for ((k, ci, ..), kind) in self.changed.iter().zip(kinds) {
|
||
if let Some(slot) = blocks.get_mut(*k) {
|
||
slot.0 = Block::User(*ci, kind);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The constraints setting an orientation of the stored state that `now` turns the other way.
|
||
fn flipped<'a>(&'a self, now: &'a State) -> impl Iterator<Item = &'a usize> + 'a {
|
||
self.orients.iter().filter(|(o, _, before)| before.zip(orientation(now, *o)).is_some_and(|(b, n)| b != n)).flat_map(|(_, cs, _)| cs)
|
||
}
|
||
|
||
/// Solved, keeping every orientation of the stored state.
|
||
fn holds(&self, residual: f64, now: &State) -> bool {
|
||
residual < ACCEPT && self.flipped(now).next().is_none()
|
||
}
|
||
}
|
||
|
||
/// The orientations the driving constraints of `sub` leave open: triangles of distances and
|
||
/// line lengths (coincident points count as one, the corners of a triangle of lines), and the
|
||
/// sides of unsigned offsets. Each with the constraints that set it; capped.
|
||
fn orients(s: &State, sub: &[(Block, Vec<usize>)]) -> Vec<(Orient, Vec<usize>)> {
|
||
use std::collections::{BTreeMap, BTreeSet};
|
||
let free = |p: usize| s.lay.pvar.get(p).copied().flatten().is_some();
|
||
let line = |l: usize| match s.sk.curves.get(l).map(|c| &c.kind) {
|
||
Some(CurveKind::Line { a, b }) => Some((*a, *b)),
|
||
_ => None,
|
||
};
|
||
let mut out = Vec::new();
|
||
let mut joins = Vec::new();
|
||
let mut edges = Vec::new();
|
||
for (b, _) in sub {
|
||
let Block::User(ci, kind) = b else { continue };
|
||
match **kind {
|
||
ConstraintKind::Coincident { p, q } => joins.push((p, q)),
|
||
ConstraintKind::Distance { p, q, .. } => edges.push((p, q, *ci)),
|
||
ConstraintKind::Length { l, .. } => edges.extend(line(l).map(|(p, q)| (p, q, *ci))),
|
||
ConstraintKind::DistanceX { p, q, value } if free(p) || free(q) => out.push((Orient::SideX(p, q, value), vec![*ci])),
|
||
ConstraintKind::DistanceY { p, q, value } if free(p) || free(q) => out.push((Orient::SideY(p, q, value), vec![*ci])),
|
||
ConstraintKind::PointLineDistance { p, l, value } => out.push((Orient::OfLine(p, l, value), vec![*ci])),
|
||
ConstraintKind::LinearDiameter { p, l, value } => out.push((Orient::OfLine(p, l, value / 2.0), vec![*ci])),
|
||
_ => {}
|
||
}
|
||
}
|
||
// Union-find over the points these constraints name (not the whole sketch).
|
||
let mut local: BTreeMap<usize, usize> = BTreeMap::new();
|
||
for (p, q) in joins.iter().copied().chain(edges.iter().map(|&(p, q, _)| (p, q))) {
|
||
for i in [p, q] {
|
||
let n = local.len();
|
||
local.entry(i).or_insert(n);
|
||
}
|
||
}
|
||
let mut parent: Vec<usize> = (0..local.len()).collect();
|
||
let at = |i: usize| local.get(&i).copied().unwrap_or(0);
|
||
for &(p, q) in &joins {
|
||
union(&mut parent, at(p), at(q));
|
||
}
|
||
// Each group stands for the first of its points met; it is free when any of them is.
|
||
let mut rep: BTreeMap<usize, usize> = BTreeMap::new();
|
||
let mut free_groups: BTreeSet<usize> = BTreeSet::new();
|
||
let mut adj: BTreeMap<usize, BTreeMap<usize, usize>> = BTreeMap::new();
|
||
for &(p, q, ci) in &edges {
|
||
let [a, b] = [p, q].map(|i| {
|
||
let r = *rep.entry(find(&mut parent, at(i))).or_insert(i);
|
||
if free(i) {
|
||
free_groups.insert(r);
|
||
}
|
||
r
|
||
});
|
||
if a != b {
|
||
adj.entry(a).or_default().entry(b).or_insert(ci);
|
||
adj.entry(b).or_default().entry(a).or_insert(ci);
|
||
}
|
||
}
|
||
let mut work = 0usize;
|
||
let mut triangles = 0usize;
|
||
for (&a, na) in &adj {
|
||
for (&b, &ab) in na.range(a + 1..) {
|
||
let Some(nb) = adj.get(&b) else { continue };
|
||
for (&c, &ac) in na.range(b + 1..) {
|
||
work += 1;
|
||
if work > MAX_TRIPLE_WORK || triangles >= MAX_TRIPLES {
|
||
return out;
|
||
}
|
||
if let Some(&bc) = nb.get(&c)
|
||
&& [a, b, c].iter().any(|p| free_groups.contains(p))
|
||
{
|
||
triangles += 1;
|
||
out.push((Orient::Turn([a, b, c]), vec![ab, bc, ac]));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Dimension continuation: solve the component `sub` from `x` (its previous solved state) for
|
||
/// its changed values stepped from old to new, each sub-step from the previous solution. A
|
||
/// sub-step that fails or flips an orientation of the previous state is halved; the step
|
||
/// doubles again after one that holds. Gives up below one [`SUBSTEPS`]th of the edit or after
|
||
/// [`CONTINUATION_ITERS`] iterations. Returns the iterations and whether the new values were
|
||
/// reached; `x` holds the last sub-step's solution either way.
|
||
fn continue_values(
|
||
sk: &Sketch,
|
||
lay: &Layout,
|
||
sub: &[(Block, Vec<usize>)],
|
||
vars: &[usize],
|
||
col: &[usize],
|
||
x: &mut [f64],
|
||
edit: &ValueEdit,
|
||
) -> (usize, bool) {
|
||
// Every sub-step lands on a sixty-fourth: the changed constraints at each, built once.
|
||
let arena: Vec<Vec<ConstraintKind>> = (0..=SUBSTEPS).map(|j| edit.kinds_at(j)).collect();
|
||
let mut blocks: Vec<(Block, Vec<usize>)> = sub.iter().map(|(b, v)| (copy_block(b), v.clone())).collect();
|
||
let (mut t, mut h, mut spent) = (0, SUBSTEPS / 2, 0);
|
||
let mut trial = x.to_vec();
|
||
while t < SUBSTEPS && spent < CONTINUATION_ITERS {
|
||
let next = (t + h).min(SUBSTEPS);
|
||
if let Some(kinds) = arena.get(next) {
|
||
edit.point_at(&mut blocks, kinds);
|
||
}
|
||
trial.copy_from_slice(x);
|
||
let (its, residual) = lm_capped(sk, lay, &blocks, vars, &mut trial, col, MAX_ITERS.min(CONTINUATION_ITERS - spent));
|
||
spent += its.max(1);
|
||
if edit.holds(residual, &State { sk, lay, x: &trial }) {
|
||
x.copy_from_slice(&trial);
|
||
t = next;
|
||
h = (2 * h).min(SUBSTEPS);
|
||
} else if h > 1 {
|
||
h /= 2;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
(spent, t >= SUBSTEPS)
|
||
}
|
||
|
||
fn solve_inner(
|
||
sk: &mut Sketch,
|
||
hold: &Hold,
|
||
reference: Option<&[Vec2]>,
|
||
analysis: Analysis,
|
||
old_values: Option<&[Option<f64>]>,
|
||
budget: &mut Budget,
|
||
) -> SolveReport {
|
||
sk.prune_patterns();
|
||
sk.refresh_pattern_shapes();
|
||
let mut orientation_before = Vec::new();
|
||
for constraint in &sk.constraints {
|
||
if !constraint.driven
|
||
&& let ConstraintKind::ArcSweep { c, value } = constraint.kind
|
||
&& value.is_finite()
|
||
&& let Some(curve) = sk.curves.get_mut(c)
|
||
&& let CurveKind::Arc { a, b, .. } = &mut curve.kind
|
||
&& curve.reversed != (value < 0.0)
|
||
{
|
||
orientation_before.push((c, *a, *b, curve.reversed));
|
||
std::mem::swap(a, b);
|
||
curve.reversed = value < 0.0;
|
||
}
|
||
}
|
||
let lay = layout(sk, hold);
|
||
let mut x = initial_x(sk, &lay);
|
||
let x0 = x.clone();
|
||
let on_curve = points_on_curves(sk);
|
||
// Start from the constructed positions of the points the constraints pin down (see
|
||
// `construct`); LM still verifies every constraint from there. A touching placement does
|
||
// not pin a point down well and is left where it is (`Placement::anchors`). `unseeded`
|
||
// keeps the start the solve would have without the seed, for the variables the seed moved:
|
||
// a component the seeded start cannot solve is solved again from there, and only if the
|
||
// seed moved its start.
|
||
let mut unseeded = None;
|
||
// Per variable: the seed constructed it (a candidate to stay where it was put, see `pinned`).
|
||
let mut constructed = vec![false; lay.n];
|
||
if crate::construct::switch::seed_on() {
|
||
let given: Vec<bool> = lay.pvar.iter().map(Option::is_none).collect();
|
||
// A drag step solves against its reference and moves only held points. Any other solve
|
||
// applies edits: also the one after linked geometry moved, whose reference is the
|
||
// stored state.
|
||
let edits = reference.is_none() || old_values.is_some();
|
||
let seeded = crate::construct::seed(sk, &given, reference, edits, &on_curve, budget);
|
||
for (i, (at, how)) in seeded.pos.iter().zip(&seeded.placed_by).enumerate() {
|
||
if let (Some(at), Some(how), Some(v)) = (at, how, lay.pvar.get(i).copied().flatten())
|
||
&& how.anchors()
|
||
{
|
||
set_point(&mut x, v, *at);
|
||
// A floated cluster's pose is a guess at freedom the constraints leave: LM's
|
||
// start, never pinned.
|
||
if how.builds()
|
||
&& let Some(c) = constructed.get_mut(v..v + 2)
|
||
{
|
||
c.fill(true);
|
||
}
|
||
}
|
||
}
|
||
for (r, v) in seeded.radius.iter().zip(&lay.rvar) {
|
||
if let (Some(r), Some(v)) = (r, v)
|
||
&& let Some(slot) = x.get_mut(*v)
|
||
{
|
||
*slot = *r;
|
||
if let Some(c) = constructed.get_mut(*v) {
|
||
*c = true;
|
||
}
|
||
}
|
||
}
|
||
if x != x0 {
|
||
let mut plain = x0.clone();
|
||
nudge_line_angles(sk, &lay, &mut plain);
|
||
unseeded = Some(plain);
|
||
}
|
||
}
|
||
nudge_line_angles(sk, &lay, &mut x);
|
||
|
||
// The blocks, and per block whether it reads a constant point.
|
||
let (blocks, reads_constant) = build_blocks(sk, &lay, &on_curve);
|
||
|
||
carry_with_seeded(&mut x, &x0, &constructed, &blocks, &lay);
|
||
// The plain start, for the variables whose start the seed (or carrying along it) moved.
|
||
let unseeded = unseeded.map(|plain: Vec<f64>| plain.into_iter().zip(&x).map(|(p, s)| (p != *s).then_some(p)).collect::<Vec<_>>());
|
||
|
||
// Connected components over variables.
|
||
let mut parent: Vec<usize> = (0..lay.n).collect();
|
||
for (_, vars) in &blocks {
|
||
if let Some(&first) = vars.first() {
|
||
for &v in vars.iter().skip(1) {
|
||
union(&mut parent, first, v);
|
||
}
|
||
}
|
||
}
|
||
let mut comps: Vec<(usize, Vec<usize>, Vec<usize>)> = Vec::new(); // (root, vars, block idx)
|
||
let mut comp_of: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
|
||
for v in 0..lay.n {
|
||
let r = find(&mut parent, v);
|
||
match comp_of.get(&r) {
|
||
Some(&k) => {
|
||
if let Some(c) = comps.get_mut(k) {
|
||
c.1.push(v);
|
||
}
|
||
}
|
||
None => {
|
||
comp_of.insert(r, comps.len());
|
||
comps.push((r, vec![v], Vec::new()));
|
||
}
|
||
}
|
||
}
|
||
let mut constant_blocks = Vec::new();
|
||
for (bi, (_, vars)) in blocks.iter().enumerate() {
|
||
match vars.first() {
|
||
Some(&v) => {
|
||
let r = find(&mut parent, v);
|
||
if let Some(c) = comp_of.get(&r).and_then(|k| comps.get_mut(*k)) {
|
||
c.2.push(bi);
|
||
}
|
||
}
|
||
None => constant_blocks.push(bi),
|
||
}
|
||
}
|
||
// Column of each variable in the system being solved or analysed (usize::MAX outside it).
|
||
let mut col = vec![usize::MAX; lay.n];
|
||
|
||
let mut iterations = 0;
|
||
let mut continuations = 0;
|
||
let mut folded: Vec<String> = Vec::new();
|
||
let all_blocks = blocks;
|
||
let oversized = |vars: &[usize]| vars.len() > MAX_COMPONENT_VARS;
|
||
let blocks_of = |bis: &mut dyn Iterator<Item = usize>| -> Vec<(Block, Vec<usize>)> {
|
||
bis.filter_map(|bi| all_blocks.get(bi).map(|(b, v)| (copy_block(b), v.clone()))).collect()
|
||
};
|
||
for (_, vars, bis) in &comps {
|
||
if bis.is_empty() || oversized(vars) {
|
||
continue;
|
||
}
|
||
let sub = blocks_of(&mut bis.iter().copied());
|
||
let start: Vec<f64> = vars.iter().filter_map(|v| x.get(*v).copied()).collect();
|
||
let mut residual = max_abs(&system(sk, &lay, &sub, &mut x, false).0);
|
||
// Already solved (by the seed, or before): nothing to iterate.
|
||
if !(residual < TOL) {
|
||
columns(&mut col, vars, vars);
|
||
// What the seed constructed and the constraints pin down stays where the seed put it
|
||
// (see `pinned`); LM solves the rest with those values as constants.
|
||
let (mut pinned_failed, mut solved) = (false, false);
|
||
if vars.iter().any(|v| constructed.get(*v).copied().unwrap_or(false)) {
|
||
let candidate = |v: usize| constructed.get(v).copied().unwrap_or(false);
|
||
let pin = pinned(sk, &lay, &sub, vars, &mut x, &candidate, Pin::Verified);
|
||
if pin.contains(&true) {
|
||
let rest = unpinned(vars, &pin, None);
|
||
let rest_blocks = blocks_reading(&sub, &rest);
|
||
columns(&mut col, &rest, vars);
|
||
let (its, r) = if rest.is_empty() { (0, 0.0) } else { lm(sk, &lay, &rest_blocks, &rest, &mut x, &col) };
|
||
iterations += its;
|
||
residual = r;
|
||
solved = residual < ACCEPT;
|
||
if !solved {
|
||
// The rest cannot be solved with the pinned values: the whole component is.
|
||
for (&v, s0) in vars.iter().zip(&start) {
|
||
if let Some(s) = x.get_mut(v) {
|
||
*s = *s0;
|
||
}
|
||
}
|
||
columns(&mut col, vars, vars);
|
||
pinned_failed = true;
|
||
}
|
||
}
|
||
}
|
||
// A seeded start can leave LM stuck where the plain start solves (a partly
|
||
// constructed component): the seeded solution is kept whenever it solves, otherwise a
|
||
// component the seed touched is solved again from where it would have started without
|
||
// the seed. After a failed pinned solve, the whole component goes straight to that
|
||
// start (the seeded one is where the pinned solve failed), or to its own start when
|
||
// the seed did not move it.
|
||
let reseeded = unseeded.as_ref().filter(|plain| vars.iter().any(|v| plain.get(*v).is_some_and(Option::is_some)));
|
||
if !solved && !(pinned_failed && reseeded.is_some()) {
|
||
let (its, r) = lm(sk, &lay, &sub, vars, &mut x, &col);
|
||
iterations += its;
|
||
residual = r;
|
||
}
|
||
if !(residual < ACCEPT)
|
||
&& let Some(plain) = reseeded
|
||
{
|
||
for (&v, s0) in vars.iter().zip(&start) {
|
||
if let Some(s) = x.get_mut(v) {
|
||
*s = plain.get(v).copied().flatten().unwrap_or(*s0);
|
||
}
|
||
}
|
||
let (its, r) = lm(sk, &lay, &sub, vars, &mut x, &col);
|
||
iterations += its;
|
||
residual = r;
|
||
}
|
||
}
|
||
// Continuation (below) solves the whole component: the pinned values belong to the new
|
||
// values, not to the ones it steps through.
|
||
columns(&mut col, vars, vars);
|
||
// After an edit of driving values from a solved state, a component the one-jump solve
|
||
// left unsolved, or turned over (an orientation of that state flipped), is solved by
|
||
// continuation from there. Its result replaces the one-jump result only when it solves
|
||
// and keeps every orientation; a solved result that still flips one is refused.
|
||
if let Some(old) = old_values
|
||
&& let Some(edit) = ValueEdit::of(&State { sk, lay: &lay, x: &x0 }, &sub, old)
|
||
&& !edit.holds(residual, &State { sk, lay: &lay, x: &x })
|
||
&& edit.starts_solved(&State { sk, lay: &lay, x: &x0 }, &sub)
|
||
{
|
||
if edit.continuable() {
|
||
continuations += 1;
|
||
// This component's variables restart from the previous state; the components
|
||
// solved before it keep their solutions (LM here touches only `vars`).
|
||
let mut from = x.clone();
|
||
for &v in vars {
|
||
if let (Some(s), Some(s0)) = (from.get_mut(v), x0.get(v)) {
|
||
*s = *s0;
|
||
}
|
||
}
|
||
let (its, mut done) = continue_values(sk, &lay, &sub, vars, &col, &mut from, &edit);
|
||
iterations += its;
|
||
if !done {
|
||
// The last sub-step's solution, solved for the new values in one jump.
|
||
let (its, r) = lm(sk, &lay, &sub, vars, &mut from, &col);
|
||
iterations += its;
|
||
done = edit.holds(r, &State { sk, lay: &lay, x: &from });
|
||
}
|
||
if done {
|
||
x = from;
|
||
residual = 0.0;
|
||
}
|
||
}
|
||
if residual < ACCEPT {
|
||
let now = State { sk, lay: &lay, x: &x };
|
||
for ci in edit.flipped(&now) {
|
||
if let Some(c) = sk.constraints.get(*ci)
|
||
&& !folded.contains(&c.id)
|
||
{
|
||
folded.push(c.id.clone());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Final residuals and analysis, component by component (the Jacobian is block diagonal:
|
||
// its rank is the sum of the components' ranks).
|
||
let mut max_residual = 0.0_f64;
|
||
let mut failing: Vec<String> = Vec::new();
|
||
let mut det = vec![false; lay.n];
|
||
let mut rank = 0;
|
||
let mut redundant: Vec<String> = Vec::new();
|
||
// A component too large to solve is a failure, not an allocation.
|
||
if comps.iter().any(|(_, vars, bis)| !bis.is_empty() && oversized(vars)) {
|
||
max_residual = f64::MAX;
|
||
}
|
||
let mut check = |r: &[f64], owner: &[usize], blocks: &[(Block, Vec<usize>)], failing: &mut Vec<String>| {
|
||
max_residual = max_residual.max(max_abs(r));
|
||
for (row, v) in r.iter().enumerate() {
|
||
if !(v.abs() < ACCEPT)
|
||
&& let Some((Block::User(ci, _) | Block::TangentAt(ci, _, _), _)) = owner.get(row).and_then(|bi| blocks.get(*bi))
|
||
&& let Some(c) = sk.constraints.get(*ci)
|
||
&& !failing.contains(&c.id)
|
||
{
|
||
failing.push(c.id.clone());
|
||
}
|
||
}
|
||
};
|
||
for (_, vars, bis) in &comps {
|
||
if oversized(vars) {
|
||
continue;
|
||
}
|
||
let sub = blocks_of(&mut bis.iter().copied());
|
||
if sub.is_empty() {
|
||
continue;
|
||
}
|
||
let (r, _, owner) = system(sk, &lay, &sub, &mut x, false);
|
||
check(&r, &owner, &sub, &mut failing);
|
||
if analysis == Analysis::StatusOnly {
|
||
continue;
|
||
}
|
||
// The rank analysis runs on what `pinned` leaves: see there for why that is exact.
|
||
let floating = bis.iter().all(|bi| !reads_constant.get(*bi).copied().unwrap_or(true));
|
||
let pin = match vars.iter().copied().find(|v| lay.is_point(*v)) {
|
||
Some(anchor) if floating => Pin::Floating(anchor),
|
||
_ => Pin::Anchored,
|
||
};
|
||
let pin_flags = pinned(sk, &lay, &sub, vars, &mut x, &|_| true, pin);
|
||
let rest = unpinned(vars, &pin_flags, pin.anchor());
|
||
columns(&mut col, &rest, vars);
|
||
let (_, j, _) = local_system(sk, &lay, &blocks_reading(&sub, &rest), &mut x, &col, rest.len());
|
||
let (rest_det, rest_rank) = if j.rows > 0 { determined_vars(&j, 1e-7) } else { (vec![false; rest.len()], 0) };
|
||
let comp_rank = pin_flags.iter().filter(|p| **p).count() + rest_rank;
|
||
rank += comp_rank;
|
||
// More rows than rank: some constraint may be implied by the others.
|
||
if r.len() > comp_rank && vars.len() <= redundancy::MAX_REDUNDANCY_VARS {
|
||
let (dependent, _) = redundancy::dependent_blocks(sk, &lay, &sub, vars, &mut x, None);
|
||
for ((b, _), d) in sub.iter().zip(dependent) {
|
||
if d && let Some(c) = redundancy::block_constraint(b).and_then(|ci| sk.constraints.get(ci))
|
||
&& !redundant.contains(&c.id)
|
||
{
|
||
redundant.push(c.id.clone());
|
||
}
|
||
}
|
||
}
|
||
for (&v, pinned) in vars.iter().zip(&pin_flags) {
|
||
let flag = match rest.binary_search(&v) {
|
||
Ok(k) => rest_det.get(k).copied().unwrap_or(false),
|
||
Err(_) => *pinned,
|
||
};
|
||
// A floating component's translation moves every point.
|
||
let moves = matches!(pin, Pin::Floating(_)) && lay.is_point(v);
|
||
if let Some(slot) = det.get_mut(v) {
|
||
*slot = flag && !moves;
|
||
}
|
||
}
|
||
#[cfg(test)]
|
||
{
|
||
columns(&mut col, vars, vars);
|
||
let (_, j, _) = local_system(sk, &lay, &sub, &mut x, &col, vars.len());
|
||
let reduced: Vec<bool> = vars.iter().map(|v| det.get(*v).copied().unwrap_or(false)).collect();
|
||
assert_dense_agrees(&j, comp_rank, &reduced);
|
||
}
|
||
}
|
||
let consts = blocks_of(&mut constant_blocks.iter().copied());
|
||
if !consts.is_empty() {
|
||
let (r, _, owner) = system(sk, &lay, &consts, &mut x, false);
|
||
check(&r, &owner, &consts, &mut failing);
|
||
}
|
||
// Weld the whole connected closure graph, preserving every immutable node.
|
||
// Validate the actual prospective geometry before accepting a numerical solve.
|
||
let has_periodic = sk.curves.iter().any(|c| matches!(c.kind, CurveKind::Spline { periodic: true, .. }));
|
||
let periodic_positions = if has_periodic {
|
||
let state = State { sk, lay: &lay, x: &x };
|
||
let mut positions = (0..sk.points.len()).map(|i| state.p(i)).collect::<Vec<_>>();
|
||
let mut parent = (0..positions.len()).collect::<Vec<_>>();
|
||
for curve in &sk.curves {
|
||
if let CurveKind::Spline { pts, periodic: true, .. } = &curve.kind
|
||
&& let (Some(a), Some(b)) = (pts.first(), pts.last())
|
||
&& *a < parent.len()
|
||
&& *b < parent.len()
|
||
{
|
||
union(&mut parent, *b, *a);
|
||
}
|
||
}
|
||
let mut representatives = std::collections::BTreeMap::new();
|
||
let mut valid = true;
|
||
for i in 0..positions.len() {
|
||
let root = find(&mut parent, i);
|
||
if let Some(previous) = representatives.get(&root).copied() {
|
||
let fixed = |p| lay.pvar.get(p).is_some_and(Option::is_none);
|
||
if fixed(i) && fixed(previous) && positions[i] != positions[previous] {
|
||
valid = false;
|
||
}
|
||
if fixed(i) && !fixed(previous) {
|
||
representatives.insert(root, i);
|
||
}
|
||
} else {
|
||
representatives.insert(root, i);
|
||
}
|
||
}
|
||
for (i, position) in positions.iter_mut().enumerate() {
|
||
if let Some(index) = representatives.get(&find(&mut parent, i)) {
|
||
*position = state.p(*index);
|
||
}
|
||
}
|
||
for curve in &sk.curves {
|
||
if let CurveKind::Spline { pts, control, degree, periodic: true } = &curve.kind {
|
||
let points = pts.iter().map(|i| positions.get(*i).copied()).collect::<Option<Vec<_>>>();
|
||
if *control || *degree != 3 || points.as_ref().is_none_or(|p| crate::periodic::validate_periodic_points(p).is_err()) {
|
||
valid = false;
|
||
}
|
||
}
|
||
}
|
||
if !valid {
|
||
max_residual = max_residual.max(1.0);
|
||
None
|
||
} else {
|
||
Some(positions)
|
||
}
|
||
} else {
|
||
None
|
||
};
|
||
let status = if max_residual < ACCEPT { SolveStatus::Solved } else { SolveStatus::Failed };
|
||
for id in &folded {
|
||
if !failing.contains(id) {
|
||
failing.push(id.clone());
|
||
}
|
||
}
|
||
let use_x = if status == SolveStatus::Solved { &x } else { &x0 };
|
||
// Write back.
|
||
for (i, p) in sk.points.iter_mut().enumerate() {
|
||
if let Some(v) = lay.pvar.get(i).copied().flatten() {
|
||
p.pos = Vec2::new(use_x.get(v).copied().unwrap_or(p.pos.x), use_x.get(v + 1).copied().unwrap_or(p.pos.y));
|
||
}
|
||
}
|
||
for (i, c) in sk.curves.iter_mut().enumerate() {
|
||
if let (CurveKind::Circle { r, .. } | CurveKind::Ellipse { r, .. }, Some(v)) = (&mut c.kind, lay.rvar.get(i).copied().flatten()) {
|
||
*r = use_x.get(v).copied().unwrap_or(*r).abs().max(1e-9);
|
||
}
|
||
}
|
||
if status == SolveStatus::Solved
|
||
&& let Some(positions) = periodic_positions
|
||
{
|
||
for (point, position) in sk.points.iter_mut().zip(positions) {
|
||
point.pos = position;
|
||
}
|
||
}
|
||
if status == SolveStatus::Failed {
|
||
for (ci, a_before, b_before, reversed) in orientation_before.into_iter().rev() {
|
||
if let Some(curve) = sk.curves.get_mut(ci)
|
||
&& let CurveKind::Arc { a, b, .. } = &mut curve.kind
|
||
{
|
||
*a = a_before;
|
||
*b = b_before;
|
||
curve.reversed = reversed;
|
||
}
|
||
}
|
||
}
|
||
let vdet = |v: Option<usize>| v.is_none_or(|v| det.get(v).copied().unwrap_or(false));
|
||
let pdet = |p: usize| match lay.pvar.get(p).copied().flatten() {
|
||
Some(v) => vdet(Some(v)) && vdet(Some(v + 1)),
|
||
None => true,
|
||
};
|
||
// Driven dimensions measure the solved geometry.
|
||
let fixed_lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||
let measured: Vec<Option<f64>> =
|
||
sk.constraints.iter().map(|c| if c.driven { measure(&State { sk, lay: &fixed_lay, x: &[] }, &c.kind) } else { None }).collect();
|
||
for (c, m) in sk.constraints.iter_mut().zip(measured) {
|
||
if let Some(v) = m
|
||
&& v.is_finite()
|
||
{
|
||
c.kind.set_value(v);
|
||
}
|
||
}
|
||
let (dof, point_determined, curve_determined) = match analysis {
|
||
Analysis::Full => (
|
||
lay.n.saturating_sub(rank),
|
||
(0..sk.points.len()).map(pdet).collect(),
|
||
sk.curves.iter().enumerate().map(|(i, c)| c.kind.point_ids().into_iter().all(pdet) && vdet(lay.rvar.get(i).copied().flatten())).collect(),
|
||
),
|
||
Analysis::StatusOnly => (0, Vec::new(), Vec::new()),
|
||
};
|
||
SolveReport {
|
||
status,
|
||
dof,
|
||
iterations,
|
||
max_residual: if max_residual.is_finite() { max_residual } else { f64::MAX },
|
||
failing,
|
||
folded,
|
||
continuations,
|
||
point_determined,
|
||
curve_determined,
|
||
redundant,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod continuation_unit_tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn angles_step_the_short_way_round() {
|
||
let line = ConstraintKind::LineAngle { l: 0, value: 0.0 };
|
||
let (old, new) = (179f64.to_radians(), (-179f64).to_radians());
|
||
let half = value_at(&line, old, new, 0.5);
|
||
assert!((half - 180f64.to_radians()).abs() < 1e-12, "{}", half.to_degrees());
|
||
let first = value_at(&line, old, new, 1.0 / SUBSTEPS as f64);
|
||
assert!((first - old).abs() < 2f64.to_radians() / 32.0, "a sixty-fourth of 2°, not of 358°");
|
||
assert_eq!(value_at(&line, old, new, 1.0), new);
|
||
let distance = ConstraintKind::Distance { p: 0, q: 1, value: 0.0 };
|
||
assert_eq!(value_at(&distance, 10.0, 20.0, 0.5), 15.0);
|
||
}
|
||
|
||
#[test]
|
||
fn a_sweep_changing_sign_is_not_stepped() {
|
||
let sweep = ConstraintKind::ArcSweep { c: 0, value: 0.0 };
|
||
assert!(!steppable(&sweep, 1.0, -1.0));
|
||
assert!(steppable(&sweep, 1.0, 2.0));
|
||
assert!(steppable(&ConstraintKind::LineAngle { l: 0, value: 0.0 }, 3.0, -3.0));
|
||
}
|
||
|
||
#[test]
|
||
fn only_clear_orientations_are_kept() {
|
||
// A triangle flat to solver noise has a side by the seed's test but none to keep.
|
||
let mut sk = Sketch::new();
|
||
let a = sk.add_point(Vec2::new(0.0, 0.0), None).unwrap();
|
||
let b = sk.add_point(Vec2::new(40.0, 0.0), None).unwrap();
|
||
let c = sk.add_point(Vec2::new(20.0, 1e-6), None).unwrap();
|
||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||
let turn = Orient::Turn([a, b, c]);
|
||
let state =
|
||
|sk: &Sketch| (orientation(&State { sk, lay: &lay, x: &[] }, turn), reference_orientation(&State { sk, lay: &lay, x: &[] }, turn));
|
||
assert_eq!(state(&sk), (Some(1.0), None));
|
||
sk.points[c].pos = Vec2::new(20.0, 0.1);
|
||
assert_eq!(state(&sk), (Some(1.0), Some(1.0)));
|
||
// A side offset well below its dimension's size is no reference either.
|
||
let side = Orient::SideX(a, c, 20.0);
|
||
sk.points[c].pos = Vec2::new(1e-6, 5.0);
|
||
assert_eq!(reference_orientation(&State { sk: &sk, lay: &lay, x: &[] }, side), None);
|
||
}
|
||
|
||
#[test]
|
||
fn offsets_have_a_side() {
|
||
let mut sk = Sketch::new();
|
||
let p = sk.add_point(Vec2::new(0.0, 0.0), None).unwrap();
|
||
let q = sk.add_point(Vec2::new(10.0, 5.0), None).unwrap();
|
||
let r = sk.add_point(Vec2::new(0.0, 10.0), None).unwrap();
|
||
let l = sk.add_line(Vec2::new(-5.0, -5.0), Vec2::new(5.0, -5.0), None, None, None).unwrap();
|
||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||
let sign = |sk: &Sketch, o: Orient| orientation(&State { sk, lay: &lay, x: &[] }, o);
|
||
let checks = [Orient::SideX(p, q, 10.0), Orient::SideY(p, q, 5.0), Orient::OfLine(q, l, 10.0), Orient::Turn([p, q, r])];
|
||
let before: Vec<_> = checks.iter().map(|o| sign(&sk, *o)).collect();
|
||
assert!(before.iter().all(Option::is_some), "{before:?}");
|
||
// q mirrored through p and below the line: every side and the turn flip.
|
||
sk.points[q].pos = Vec2::new(-10.0, -15.0);
|
||
let after: Vec<_> = checks.iter().map(|o| sign(&sk, *o)).collect();
|
||
for (b, a) in before.iter().zip(&after) {
|
||
assert_eq!(b.zip(*a).map(|(b, a)| b == a), Some(false), "{before:?} {after:?}");
|
||
}
|
||
// On the line: no side to tell.
|
||
sk.points[q].pos = Vec2::new(0.0, -5.0);
|
||
assert_eq!(sign(&sk, Orient::OfLine(q, l, 10.0)), None);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod typed_dimension_tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn failed_signed_sweep_restores_arc_orientation_and_coordinates() {
|
||
let mut sk = Sketch::new();
|
||
let c = sk.add_arc(Vec2::ZERO, Vec2::new(10.0, 0.0), Vec2::new(0.0, 10.0), [Some(0), None, None], None).unwrap();
|
||
let id = sk.add_constraint(ConstraintKind::ArcSweep { c, value: std::f64::consts::FRAC_PI_2 }, None).unwrap();
|
||
sk.curves[c].fixed = true;
|
||
assert!(solve(&mut sk).ok());
|
||
let before = sk.curves.clone();
|
||
let points = sk.points.clone();
|
||
sk.constraints.iter_mut().find(|constraint| constraint.id == id).unwrap().kind.set_value(-std::f64::consts::FRAC_PI_2);
|
||
assert!(!solve(&mut sk).ok());
|
||
assert_eq!(sk.curves, before);
|
||
assert_eq!(sk.points, points);
|
||
}
|
||
|
||
/// A constraint read on a curve of the wrong kind (a midpoint of an arc, pushed past
|
||
/// validation) reads constants for it: its component is tied to them, not floating, and the
|
||
/// pinned analysis agrees with the dense one (checked in every test solve).
|
||
#[test]
|
||
fn a_curve_read_as_another_kind_ties_its_component() {
|
||
let mut sk = Sketch::new();
|
||
let arc = sk.add_arc(Vec2::new(5.0, 5.0), Vec2::new(15.0, 5.0), Vec2::new(5.0, 15.0), [None, None, None], None).unwrap();
|
||
let p = sk.add_point(Vec2::new(3.0, 1.0), None).unwrap();
|
||
sk.constraints.push(crate::model::Constraint {
|
||
id: "m".into(),
|
||
kind: ConstraintKind::Midpoint { p, l: arc },
|
||
param: None,
|
||
driven: false,
|
||
text: None,
|
||
});
|
||
let rep = solve(&mut sk);
|
||
assert!(rep.ok(), "{rep:?}");
|
||
assert!(rep.point_determined[p], "the midpoint of the constant line: {rep:?}");
|
||
assert_eq!(rep.dof, 5, "the arc's centre and ends, less its equal radii: {rep:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_status_only_solve_places_the_geometry_as_a_full_one() {
|
||
let mut sk = Sketch::new();
|
||
let l = sk.add_line(Vec2::ZERO, Vec2::new(10.0, 3.0), Some(0), None, None).unwrap();
|
||
sk.add_constraint(ConstraintKind::LineAngle { l, value: 0.5 }, None).unwrap();
|
||
let mut quick = sk.clone();
|
||
let full = solve(&mut sk);
|
||
let status = solve_with(&mut quick, &Hold::default(), None, Analysis::StatusOnly, None);
|
||
assert!(full.ok() && status.ok());
|
||
assert_eq!(quick, sk);
|
||
assert_eq!(full.dof, 1, "the end slides along the line");
|
||
assert_eq!((status.dof, status.point_determined.len(), status.curve_determined.len()), (0, 0, 0));
|
||
}
|
||
|
||
#[test]
|
||
fn signed_dimensions_validate_kinds_values_and_survive_curve_removal() {
|
||
let mut sk = Sketch::new();
|
||
let l = sk.add_line(Vec2::ZERO, Vec2::new(10.0, 10.0), Some(0), None, None).unwrap();
|
||
assert!(sk.add_constraint(ConstraintKind::ArcSweep { c: l, value: 1.0 }, None).is_err());
|
||
assert!(sk.add_constraint(ConstraintKind::LineAngle { l, value: f64::NAN }, None).is_err());
|
||
sk.add_constraint(ConstraintKind::LineAngle { l, value: -std::f64::consts::FRAC_PI_4 }, None).unwrap();
|
||
assert!(solve(&mut sk).ok());
|
||
let CurveKind::Line { a, b } = sk.curves[l].kind else { panic!("line") };
|
||
let direction = sk.points[b].pos - sk.points[a].pos;
|
||
assert!((direction.angle() + std::f64::consts::FRAC_PI_4).abs() < 1e-7);
|
||
sk.remove_curves(&[l]);
|
||
assert!(sk.constraints.is_empty());
|
||
}
|
||
|
||
/// One connected component is solved as a dense system, so its variable count sets the
|
||
/// allocation: 10,000 chained points are 20,000 variables and a 3.2 GB matrix. The solve
|
||
/// has to report failure instead of asking for the memory, because an allocation failure
|
||
/// aborts the process where a failed solve does not.
|
||
#[test]
|
||
fn components_too_large_to_solve_fail_instead_of_allocating() {
|
||
let mut sk = Sketch::new();
|
||
let mut previous = 0;
|
||
for i in 1..10_000 {
|
||
let p = sk.add_point(Vec2::new(0.001 * i as f64, 0.0), None).unwrap();
|
||
sk.add_constraint(ConstraintKind::Coincident { p: previous, q: p }, None).unwrap();
|
||
previous = p;
|
||
}
|
||
let before = sk.points.clone();
|
||
let t = std::time::Instant::now();
|
||
let report = solve(&mut sk);
|
||
assert_eq!(report.status, SolveStatus::Failed);
|
||
// Geometry is left as it was.
|
||
assert_eq!(sk.points, before);
|
||
assert!(t.elapsed() < std::time::Duration::from_secs(10), "took {:?}", t.elapsed());
|
||
}
|
||
|
||
/// A sketch whose components each stay under the cap still solves.
|
||
#[test]
|
||
fn many_small_components_still_solve() {
|
||
let mut sk = Sketch::new();
|
||
for i in 0..200 {
|
||
let a = sk.add_point(Vec2::new(i as f64, 0.0), None).unwrap();
|
||
let b = sk.add_point(Vec2::new(i as f64, 5.0), None).unwrap();
|
||
let l = sk.add_line_pts(a, b, None).unwrap();
|
||
sk.add_constraint(ConstraintKind::Vertical { l }, None).unwrap();
|
||
}
|
||
assert!(solve(&mut sk).ok());
|
||
}
|
||
}
|