Skip to main content

geop_core_sketch/
solve.rs

1//! Solving a sketch: every constraint contributes residuals that are zero
2//! exactly when it holds — a term of the sketch's variables — and
3//! [`geop_core_math::solvers::system`], on the driver every solve of the
4//! kernel shares ([`geop_core_math::solvers::constrained`]), holds them,
5//! pulls a dragged point, and encloses the exact solution.
6//!
7//! **Variables.** Each class of coincident points (see
8//! [`Sketch::point_classes`]) is one `(x, y)` pair — coincidence is not a
9//! residual at all, it removes two degrees of freedom by construction. Each
10//! arc adds its half sweep, each circle its radius.
11//!
12//! **Units.** Every residual is a length: dimensionless ones (angles,
13//! parallelism) are multiplied by the sketch's characteristic size, so no
14//! constraint kind dominates the objective just by its choice of units, and
15//! the convergence test is a single relative length.
16//!
17//! **Honest enclosures.** Everything is computed in the sketch's own scalar
18//! — any [`Scalar`] — each residual as a [`geop_core_math::dual::Dual`] of
19//! it, so the `sin`/`sqrt`/`PI` a geometric formula can't help but need are
20//! enclosed rather than quietly rounded away. A residual that becomes
21//! genuinely undecidable (a division or square root at the edge of its
22//! domain) makes that point infeasible.
23use crate::{
24    geometry::{Arc, line_distance},
25    profile::basis_at,
26    sketch::{Constraint, ConstraintId, CurveId, CurveKind, Enclosure, Point, PointId, Sketch},
27    variables::{PointLayout, Residuals, SketchPoint, Standing, Variables},
28};
29use geop_core_math::solvers::system::{self, Phase};
30use geop_core_math::{
31    geop_error::{GeopError, GeopResult},
32    scalars::{Number, Scalar, as_f64},
33    vector::Vector2,
34};
35
36/// The most variables a single constraint may depend on. The largest
37/// constraint, a point of a spline's offset ([`Constraint::OffsetAt`]),
38/// touches the four control points of two cubics a point depends on and
39/// a circle's radius.
40const MAX_LOCAL_VARS: usize = 17;
41
42/// A residual with its gradient with respect to its constraint's own
43/// variables.
44type Dual = geop_core_math::dual::Dual<MAX_LOCAL_VARS>;
45
46use serde::{Deserialize, Serialize};
47use std::collections::BTreeMap;
48
49/// Maps sketch entities to solver variables: each class of points a pair
50/// (see [`PointLayout`]), then every arc's half sweep and circle's radius.
51struct Layout {
52    points: PointLayout<2>,
53    /// The arc's half sweep or the circle's radius, for every arc and circle.
54    curve_var: BTreeMap<CurveId, usize>,
55}
56
57impl SketchPoint<2> for Point {
58    fn fixed(&self) -> bool {
59        self.fixed
60    }
61
62    fn coordinates(&self) -> [Scalar; 2] {
63        [self.x, self.y]
64    }
65
66    fn set_coordinates(&mut self, [x, y]: [Scalar; 2]) {
67        (self.x, self.y) = (x, y);
68    }
69}
70
71impl Layout {
72    fn new(sketch: &Sketch) -> Self {
73        let mut points = PointLayout::new(&sketch.points, &sketch.point_classes());
74        let curve_var = sketch
75            .curves
76            .iter()
77            .filter(|(_, c)| matches!(c.kind, CurveKind::Arc { .. } | CurveKind::Circle { .. }))
78            .map(|(&id, c)| (id, points.push(!c.fixed)))
79            .collect();
80        Layout { points, curve_var }
81    }
82
83    /// The current variable values, read from the sketch: the points as
84    /// [`PointLayout::read`] reads them, each curve's own variable as
85    /// sharp as they.
86    fn read(&self, sketch: &Sketch) -> GeopResult<Vec<Scalar>> {
87        let mut x = vec![Scalar::ZERO; self.points.len()];
88        self.points.read(&sketch.points, &mut x);
89        for (id, c) in &sketch.curves {
90            match c.kind {
91                CurveKind::Arc { sweep, .. } => {
92                    x[self.curve_var[id]] = sweep.div(Scalar::TWO)?.sharpen()
93                }
94                CurveKind::Circle { radius, .. } => x[self.curve_var[id]] = radius,
95                _ => {}
96            }
97        }
98        Ok(x)
99    }
100
101    /// Write variable values back into the sketch — all but what is fixed,
102    /// which stays as given.
103    fn write(&self, sketch: &mut Sketch, x: &[Scalar]) {
104        self.points.write(&mut sketch.points, x);
105        for (id, c) in sketch.curves.iter_mut().filter(|(_, c)| !c.fixed) {
106            match &mut c.kind {
107                CurveKind::Arc { sweep, .. } => {
108                    *sweep = Scalar::TWO.mul(x[self.curve_var[id]]).sharpen()
109                }
110                CurveKind::Circle { radius, .. } => *radius = x[self.curve_var[id]].abs().sharpen(),
111                _ => {}
112            }
113        }
114    }
115
116    /// The variables a curve depends on.
117    fn curve_vars(&self, sketch: &Sketch, c: CurveId, out: &mut Vec<usize>) {
118        for p in sketch.curves[&c].points() {
119            self.points.point_vars(p, out);
120        }
121        out.extend(self.curve_var.get(&c));
122    }
123}
124
125/// Read-only view of the variables for evaluating one constraint, with that
126/// constraint's own variables `seeded` as [`Scalar`] values.
127struct Geo<'a, T> {
128    sketch: &'a Sketch,
129    layout: &'a Layout,
130    seeded: &'a [(usize, T)],
131}
132
133impl<T: Number> Geo<'_, T> {
134    fn var(&self, i: usize) -> T {
135        self.seeded
136            .iter()
137            .find(|(j, _)| *j == i)
138            .map(|(_, t)| *t)
139            .expect("every variable a constraint reads is seeded")
140    }
141    fn point(&self, p: PointId) -> Vector2<T> {
142        let i = self.layout.points.point_var[&p];
143        Vector2::from_array([self.var(i), self.var(i + 1)])
144    }
145    fn line(&self, c: CurveId) -> (Vector2<T>, Vector2<T>) {
146        match &self.sketch.curves[&c].kind {
147            CurveKind::Line { start, end } => (self.point(*start), self.point(*end)),
148            k => unreachable!("validated to be a line: {k:?}"),
149        }
150    }
151    fn arc(&self, c: CurveId) -> Option<Arc<T>> {
152        match &self.sketch.curves[&c].kind {
153            CurveKind::Arc { start, end, .. } => Some(Arc {
154                s: self.point(*start),
155                e: self.point(*end),
156                half: self.var(self.layout.curve_var[&c]),
157            }),
158            _ => None,
159        }
160    }
161    /// `(center, radius)` of a circle or arc.
162    fn round(&self, c: CurveId) -> GeopResult<(Vector2<T>, T)> {
163        Ok(match &self.sketch.curves[&c].kind {
164            CurveKind::Circle { center, .. } => (
165                self.point(*center),
166                self.var(self.layout.curve_var[&c]).abs(),
167            ),
168            CurveKind::Arc { .. } => {
169                let a = self.arc(c).unwrap();
170                (a.center()?, a.radius()?)
171            }
172            k => unreachable!("validated to be a circle or arc: {k:?}"),
173        })
174    }
175    /// Unit tangent of an open curve at its start (`at_end = false`) or end,
176    /// in the direction of travel.
177    fn end_tangent(&self, c: CurveId, at_end: bool) -> GeopResult<Vector2<T>> {
178        Ok(match &self.sketch.curves[&c].kind {
179            CurveKind::Line { .. } => {
180                let (s, e) = self.line(c);
181                e.sub(&s).normalize()?
182            }
183            CurveKind::Arc { .. } => {
184                let a = self.arc(c).unwrap();
185                if at_end {
186                    a.tangent_end()?
187                } else {
188                    a.tangent_start()?
189                }
190            }
191            CurveKind::Spline { control_points, .. } => {
192                let n = control_points.len();
193                let (p, q) = if at_end {
194                    (control_points[n - 2], control_points[n - 1])
195                } else {
196                    (control_points[0], control_points[1])
197                };
198                self.point(q).sub(&self.point(p)).normalize()?
199            }
200            k => unreachable!("validated to be an open curve: {k:?}"),
201        })
202    }
203}
204
205/// How a [`Constraint::Tangent`] is enforced, decided once per solve from
206/// the operands' connectivity and their starting configuration.
207#[derive(Clone, Copy, Debug)]
208enum TangentMode {
209    /// The curves share an endpoint: their tangents there are parallel.
210    Endpoint { a_end: bool, b_end: bool },
211    /// Line `line` touches circle/arc `round`.
212    LineRound { line: CurveId, round: CurveId },
213    /// Two circles/arcs touch, from inside or outside — whichever is closer
214    /// to holding when the solve starts, so a solve never flips the
215    /// configuration the designer drew.
216    RoundRound { internal: bool },
217}
218
219/// One constraint, ready to evaluate.
220struct Prepared<'a> {
221    id: ConstraintId,
222    constraint: &'a Constraint,
223    vars: Vec<usize>,
224    tangent: Option<TangentMode>,
225    /// For a [`Constraint::OffsetAt`], the splines' points as sums of
226    /// their control points.
227    offset: Option<OffsetBasis>,
228}
229
230/// The points of a [`Constraint::OffsetAt`]: of `a`, with its derivative,
231/// and of `b`, as sums of their control points (see [`spline_basis`]).
232struct OffsetBasis {
233    a: Vec<(PointId, Scalar, Scalar)>,
234    b: Vec<(PointId, Scalar, Scalar)>,
235}
236
237/// The spline `curve`'s point at `t` — and its derivative there, if
238/// `derivative` — as a sum of its control points (see [`basis_at`]).
239fn spline_basis(
240    sketch: &Sketch,
241    curve: CurveId,
242    t: Scalar,
243    derivative: bool,
244) -> GeopResult<Vec<(PointId, Scalar, Scalar)>> {
245    let CurveKind::Spline {
246        control_points,
247        shape,
248    } = &sketch.curve(curve)?.kind
249    else {
250        return Err(GeopError::new(format!("curve {curve} is no spline")));
251    };
252    Ok(
253        basis_at(control_points.len(), shape.as_ref(), t, derivative)?
254            .into_iter()
255            .map(|(j, value, slope)| (control_points[j], value, slope))
256            .collect(),
257    )
258}
259
260/// Whether `a` and `b` are both fixed points: then a coincidence between
261/// them is no merging of variables — neither can move — but a fact about
262/// where they were given, which holds or does not.
263fn both_fixed(sketch: &Sketch, a: PointId, b: PointId) -> bool {
264    sketch.points[&a].fixed && sketch.points[&b].fixed
265}
266
267/// Everything needed to evaluate the objective, fixed for one solve.
268struct Problem<'a> {
269    sketch: &'a Sketch,
270    layout: Layout,
271    constraints: Vec<Prepared<'a>>,
272    /// Characteristic length of the sketch; see the module docs.
273    scale: Scalar,
274}
275
276impl<'a> Problem<'a> {
277    fn new(sketch: &'a Sketch) -> GeopResult<Self> {
278        sketch.validate()?;
279        let layout = Layout::new(sketch);
280        let x0 = layout.read(sketch)?;
281
282        // The diagonal of the points' bounding box, or a circle's diameter
283        // if that is larger — one, where the sketch has no size at all.
284        let span = |k: usize| {
285            let values = || sketch.points.values().map(move |p| p.xy()[k]);
286            let hi = values().reduce(Scalar::max).unwrap_or(Scalar::ZERO);
287            let lo = values().reduce(Scalar::min).unwrap_or(Scalar::ZERO);
288            hi.sub(lo)
289        };
290        let diagonal = span(0).mul(span(0)).add(span(1).mul(span(1))).sqrt()?;
291        let scale = sketch
292            .curves
293            .values()
294            .filter_map(|c| match c.kind {
295                CurveKind::Circle { radius, .. } => Some(Scalar::TWO.mul(radius.abs())),
296                _ => None,
297            })
298            .fold(diagonal, Scalar::max);
299        let scale = if scale.is_finite() && scale.definitely_greater(Scalar::ZERO) {
300            scale
301        } else {
302            Scalar::ONE
303        };
304
305        let mut constraints = Vec::new();
306        for (&id, c) in &sketch.constraints {
307            use Constraint::*;
308            let mut vars = Vec::new();
309            let mut offset_basis = None;
310            let pt = |p: &PointId, vars: &mut Vec<usize>| layout.points.point_vars(*p, vars);
311            match c {
312                // Coincidence of two fixed points is a fact about where
313                // they are, checked as given.
314                Coincident { a, b } if both_fixed(sketch, *a, *b) => {}
315                // Any other coincidence is built into the layout.
316                Coincident { .. } => continue,
317                Fix { point, .. } => pt(point, &mut vars),
318                Distance { a, b, .. } | DistanceX { a, b, .. } | DistanceY { a, b, .. } => {
319                    pt(a, &mut vars);
320                    pt(b, &mut vars);
321                }
322                PointOnCurve { point, curve }
323                | Midpoint { point, curve }
324                | Center { point, curve }
325                | PointLineDistance {
326                    point, line: curve, ..
327                } => {
328                    pt(point, &mut vars);
329                    layout.curve_vars(sketch, *curve, &mut vars);
330                }
331                Symmetric { a, b, line } | Moved { a, b, by: line } => {
332                    pt(a, &mut vars);
333                    pt(b, &mut vars);
334                    layout.curve_vars(sketch, *line, &mut vars);
335                }
336                // Only the two sweeps.
337                EqualSweep { a, b } => vars.extend([layout.curve_var[a], layout.curve_var[b]]),
338                Horizontal { line: curve }
339                | Vertical { line: curve }
340                | Length { curve, .. }
341                | Radius { curve, .. }
342                | Diameter { curve, .. } => layout.curve_vars(sketch, *curve, &mut vars),
343                Parallel { a, b }
344                | Perpendicular { a, b }
345                | Collinear { a, b }
346                | Tangent { a, b }
347                | Equal { a, b }
348                | Concentric { a, b }
349                | Offset { a, b, .. }
350                | Angle { a, b, .. } => {
351                    layout.curve_vars(sketch, *a, &mut vars);
352                    layout.curve_vars(sketch, *b, &mut vars);
353                }
354                // The control points the two points depend on, and the
355                // circle's radius alone.
356                OffsetAt {
357                    a, at, b, on, by, ..
358                } => {
359                    let basis = OffsetBasis {
360                        a: spline_basis(sketch, *a, *at, true)?,
361                        b: spline_basis(sketch, *b, *on, false)?,
362                    };
363                    for (p, ..) in basis.a.iter().chain(&basis.b) {
364                        pt(p, &mut vars);
365                    }
366                    vars.push(layout.curve_var[by]);
367                    offset_basis = Some(basis);
368                }
369            }
370            vars.sort_unstable();
371            vars.dedup();
372            if vars.len() > MAX_LOCAL_VARS {
373                return Err(GeopError::new(format!(
374                    "constraint {c:?} depends on {} variables, more than the supported {MAX_LOCAL_VARS}",
375                    vars.len()
376                )));
377            }
378            let tangent = match c {
379                Tangent { a, b } => Some(Self::tangent_mode(sketch, &layout, &x0, *a, *b)?),
380                _ => None,
381            };
382            constraints.push(Prepared {
383                id,
384                constraint: c,
385                vars,
386                tangent,
387                offset: offset_basis,
388            });
389        }
390
391        Ok(Problem {
392            sketch,
393            layout,
394            constraints,
395            scale,
396        })
397    }
398
399    fn tangent_mode(
400        sketch: &Sketch,
401        layout: &Layout,
402        x: &[Scalar],
403        a: CurveId,
404        b: CurveId,
405    ) -> GeopResult<TangentMode> {
406        if let Some((a_end, b_end)) = sketch.shared_endpoint(a, b)? {
407            return Ok(TangentMode::Endpoint { a_end, b_end });
408        }
409        let is_line = |c: CurveId| matches!(sketch.curves[&c].kind, CurveKind::Line { .. });
410        if is_line(a) {
411            return Ok(TangentMode::LineRound { line: a, round: b });
412        }
413        if is_line(b) {
414            return Ok(TangentMode::LineRound { line: b, round: a });
415        }
416        let seeded: Vec<(usize, Scalar)> = x.iter().copied().enumerate().collect();
417        let geo = Geo {
418            sketch,
419            layout,
420            seeded: &seeded,
421        };
422        let ((ca, ra), (cb, rb)) = (geo.round(a)?, geo.round(b)?);
423        let d = ca.sub(&cb).try_norm()?;
424        let internal = d.sub(ra.sub(rb).abs()).abs();
425        let external = d.sub(ra.add(rb)).abs();
426        Ok(TangentMode::RoundRound {
427            internal: internal.definitely_less(external),
428        })
429    }
430
431    fn residuals(
432        &self,
433        p: &Prepared<'_>,
434        geo: &Geo<'_, Dual>,
435        out: &mut Vec<Dual>,
436    ) -> GeopResult<()> {
437        use Constraint::*;
438        let c = Dual::cst;
439        let scale = c(self.scale);
440        let half = c(Scalar::ONE.div(Scalar::TWO)?);
441        match *p.constraint {
442            Coincident { a, b } => {
443                let (pa, pb) = (&self.sketch.points[&a], &self.sketch.points[&b]);
444                out.extend([c(pb.x.sub(pa.x)), c(pb.y.sub(pa.y))]);
445            }
446            Fix { point, x, y } => {
447                let q = geo.point(point);
448                out.extend([q[0].sub(c(x)), q[1].sub(c(y))]);
449            }
450            Distance { a, b, value } => {
451                out.push(geo.point(b).sub(&geo.point(a)).try_norm()?.sub(c(value)))
452            }
453            DistanceX { a, b, value } => {
454                out.push(geo.point(b)[0].sub(geo.point(a)[0]).sub(c(value)))
455            }
456            DistanceY { a, b, value } => {
457                out.push(geo.point(b)[1].sub(geo.point(a)[1]).sub(c(value)))
458            }
459            PointOnCurve { point, curve } => {
460                let q = geo.point(point);
461                out.push(match &self.sketch.curves[&curve].kind {
462                    CurveKind::Line { .. } => {
463                        let (s, e) = geo.line(curve);
464                        line_distance(s, e, q)?
465                    }
466                    CurveKind::Arc { .. } => geo.arc(curve).unwrap().circle_residual(q)?,
467                    _ => {
468                        let (c, r) = geo.round(curve)?;
469                        q.sub(&c).try_norm()?.sub(r)
470                    }
471                });
472            }
473            Midpoint { point, curve } => {
474                let q = geo.point(point);
475                let m = match geo.arc(curve) {
476                    Some(a) => a.arc_mid()?,
477                    None => {
478                        let (s, e) = geo.line(curve);
479                        s.add(&e).prod_scalar(half)
480                    }
481                };
482                out.extend([q[0].sub(m[0]), q[1].sub(m[1])]);
483            }
484            Center { point, curve } => {
485                let (q, center) = (geo.point(point), geo.round(curve)?.0);
486                out.extend([q[0].sub(center[0]), q[1].sub(center[1])]);
487            }
488            Symmetric { a, b, line } => {
489                let (pa, pb) = (geo.point(a), geo.point(b));
490                let (s, e) = geo.line(line);
491                let mid = pa.add(&pb).prod_scalar(half);
492                out.push(line_distance(s, e, mid)?);
493                out.push(pb.sub(&pa).prod_dot(&e.sub(&s).normalize()?));
494            }
495            Moved { a, b, by } => {
496                // The motion carrying `by`'s start `s` onto its end `e`:
497                // `q ↦ e + R(q - s)`, `R` the turn by its sweep — none
498                // along a line. Smooth through a straight arc, where it is
499                // the shift along the chord.
500                let from = geo.point(a);
501                let image = match geo.arc(by) {
502                    Some(arc) => {
503                        let turn = arc.half.mul(c(Scalar::TWO));
504                        arc.e.add(&from.sub(&arc.s).rotate(turn.cos(), turn.sin()))
505                    }
506                    None => {
507                        let (s, e) = geo.line(by);
508                        e.add(&from.sub(&s))
509                    }
510                };
511                let to = geo.point(b);
512                out.extend([to[0].sub(image[0]), to[1].sub(image[1])]);
513            }
514            EqualSweep { a, b } => {
515                let half = |k: CurveId| geo.var(self.layout.curve_var[&k]);
516                out.push(half(b).sub(half(a)).mul(scale));
517            }
518            OffsetAt { by, across, .. } => {
519                let basis = p.offset.as_ref().expect("prepared with its basis");
520                let sum = |terms: &[(PointId, Scalar, Scalar)], slope: bool| {
521                    terms.iter().fold(
522                        Vector2::from_array([c(Scalar::ZERO); 2]),
523                        |acc, &(q, v, d)| {
524                            acc.add(&geo.point(q).prod_scalar(c(if slope { d } else { v })))
525                        },
526                    )
527                };
528                let tangent = sum(&basis.a, true).normalize()?;
529                let normal = Vector2::from_array([tangent[1].neg(), tangent[0]]);
530                let off = sum(&basis.b, false).sub(&sum(&basis.a, false));
531                if !across {
532                    out.push(off.prod_dot(&tangent));
533                }
534                let radius = geo.var(self.layout.curve_var[&by]).abs();
535                out.push(off.prod_dot(&normal).abs().sub(radius));
536            }
537            Offset { a, b, value } => match self.sketch.curves[&a].kind {
538                CurveKind::Line { .. } => {
539                    // Both ends of `b` the same distance from `a`'s line:
540                    // parallel, and that distance `value`.
541                    let (sa, ea) = geo.line(a);
542                    let (sb, eb) = geo.line(b);
543                    let (ds, de) = (line_distance(sa, ea, sb)?, line_distance(sa, ea, eb)?);
544                    out.push(ds.sub(de));
545                    out.push(ds.abs().sub(c(value)));
546                }
547                _ => {
548                    let ((ca, ra), (cb, rb)) = (geo.round(a)?, geo.round(b)?);
549                    out.extend([ca[0].sub(cb[0]), ca[1].sub(cb[1])]);
550                    out.push(rb.sub(ra).abs().sub(c(value)));
551                }
552            },
553            PointLineDistance { point, line, value } => {
554                let (s, e) = geo.line(line);
555                out.push(line_distance(s, e, geo.point(point))?.abs().sub(c(value)));
556            }
557            Horizontal { line } => {
558                let (s, e) = geo.line(line);
559                out.push(e[1].sub(s[1]));
560            }
561            Vertical { line } => {
562                let (s, e) = geo.line(line);
563                out.push(e[0].sub(s[0]));
564            }
565            Parallel { a, b } | Perpendicular { a, b } | Angle { a, b, .. } => {
566                let (sa, ea) = geo.line(a);
567                let (sb, eb) = geo.line(b);
568                let (ua, ub) = (ea.sub(&sa).normalize()?, eb.sub(&sb).normalize()?);
569                let (cross, dot) = (ua.prod_cross(&ub), ua.prod_dot(&ub));
570                // sin(angle(a, b) - target), in units of length.
571                let r = match *p.constraint {
572                    Parallel { .. } => cross,
573                    Perpendicular { .. } => dot,
574                    Angle { value, .. } => cross.mul(c(value.cos())).sub(dot.mul(c(value.sin()))),
575                    _ => unreachable!(),
576                };
577                out.push(r.mul(scale));
578            }
579            Collinear { a, b } => {
580                let (sa, ea) = geo.line(a);
581                let (sb, eb) = geo.line(b);
582                out.push(line_distance(sa, ea, sb)?);
583                out.push(line_distance(sa, ea, eb)?);
584            }
585            Tangent { a, b } => match p.tangent.unwrap() {
586                TangentMode::Endpoint { a_end, b_end } => {
587                    let (ta, tb) = (geo.end_tangent(a, a_end)?, geo.end_tangent(b, b_end)?);
588                    out.push(ta.prod_cross(&tb).mul(scale));
589                }
590                TangentMode::LineRound { line, round } => {
591                    let (s, e) = geo.line(line);
592                    let (c, r) = geo.round(round)?;
593                    out.push(line_distance(s, e, c)?.abs().sub(r));
594                }
595                TangentMode::RoundRound { internal } => {
596                    let ((ca, ra), (cb, rb)) = (geo.round(a)?, geo.round(b)?);
597                    let d = ca.sub(&cb).try_norm()?;
598                    out.push(if internal {
599                        d.sub(ra.sub(rb).abs())
600                    } else {
601                        d.sub(ra.add(rb))
602                    });
603                }
604            },
605            Equal { a, b } => {
606                let size = |c: CurveId| -> GeopResult<Dual> {
607                    Ok(match &self.sketch.curves[&c].kind {
608                        CurveKind::Line { .. } => {
609                            let (s, e) = geo.line(c);
610                            e.sub(&s).try_norm()?
611                        }
612                        _ => geo.round(c)?.1,
613                    })
614                };
615                out.push(size(a)?.sub(size(b)?));
616            }
617            Concentric { a, b } => {
618                let (ca, cb) = (geo.round(a)?.0, geo.round(b)?.0);
619                out.extend([ca[0].sub(cb[0]), ca[1].sub(cb[1])]);
620            }
621            Length { curve, value } => {
622                let length = match geo.arc(curve) {
623                    Some(a) => a.length()?,
624                    None => {
625                        let (s, e) = geo.line(curve);
626                        e.sub(&s).try_norm()?
627                    }
628                };
629                out.push(length.sub(c(value)));
630            }
631            Radius { curve, value } => out.push(match geo.arc(curve) {
632                // `2 R |sin θ| - L` rather than `L / (2 |sin θ|) - R`: the
633                // same zero set, but finite for a nearly straight arc.
634                Some(a) => c(Scalar::TWO.mul(value))
635                    .mul(a.half.sin().abs())
636                    .sub(a.chord_length()?),
637                None => geo.round(curve)?.1.sub(c(value)),
638            }),
639            Diameter { curve, value } => out.push(match geo.arc(curve) {
640                // As for the radius: `D |sin θ| - L`.
641                Some(a) => c(value).mul(a.half.sin().abs()).sub(a.chord_length()?),
642                None => geo.round(curve)?.1.sub(c(value.div(Scalar::TWO)?)),
643            }),
644        }
645        Ok(())
646    }
647}
648
649impl<'a> Residuals<MAX_LOCAL_VARS> for Problem<'a> {
650    fn offsets(&self) -> Vec<Option<usize>> {
651        self.layout.points.offsets()
652    }
653
654    fn constraints(&self) -> usize {
655        self.constraints.len()
656    }
657
658    fn reads(&self, c: usize) -> &[usize] {
659        &self.constraints[c].vars
660    }
661
662    fn residuals(&self, c: usize, values: &[Dual], out: &mut Vec<Dual>) -> GeopResult<()> {
663        let prepared = &self.constraints[c];
664        let seeded: Vec<(usize, Dual)> = prepared
665            .vars
666            .iter()
667            .copied()
668            .zip(values.iter().copied())
669            .collect();
670        let geo = Geo {
671            sketch: self.sketch,
672            layout: &self.layout,
673            seeded: &seeded,
674        };
675        Problem::residuals(self, prepared, &geo, out)
676    }
677
678    fn name(&self, c: usize) -> String {
679        let c = &self.constraints[c];
680        format!("{} {:?}", c.id, c.constraint)
681    }
682
683    fn scale(&self) -> Scalar {
684        self.scale
685    }
686}
687
688/// The planar sketch's system (see [`Variables`]).
689type Planar<'p, 'a> = Variables<'p, Problem<'a>, MAX_LOCAL_VARS>;
690
691/// The outcome of a solve.
692#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
693pub struct SolveReport {
694    /// Every constraint holds (to
695    /// [`geop_core_math::solvers::system::RELATIVE_TOLERANCE`] of the
696    /// sketch size).
697    pub converged: bool,
698    /// Largest remaining constraint residual, in sketch units: an upper
699    /// bound.
700    #[serde(with = "as_f64")]
701    pub max_residual: Scalar,
702    pub iterations: usize,
703    /// Each minimization of the solve.
704    pub phases: Vec<Phase>,
705    /// Remaining degrees of freedom: variables minus independent
706    /// constraints — which are independent decided over the box an
707    /// enclosure of the solution chose them over (see
708    /// [`geop_core_math::solvers::system::System::freedom`]).
709    pub dof: usize,
710    /// Per point: whether it can still move without violating a constraint.
711    pub free_points: BTreeMap<PointId, bool>,
712    /// Per curve: whether any of its points or its own sweep/radius can still
713    /// change.
714    pub free_curves: BTreeMap<CurveId, bool>,
715    /// The constraints left unsatisfied (conflicting or unreachable), empty
716    /// when `converged`.
717    pub failed_constraints: Vec<ConstraintId>,
718    /// Why no true solution near where the solve ended is proven, if none
719    /// is: the constraints that conflict there, by id, or what is singular
720    /// (see [`Sketch::enclose`], which encloses it).
721    #[serde(default)]
722    pub unproven: Vec<String>,
723}
724
725impl Sketch {
726    /// The sketch's geometry as the kernel builds on it (see [`Enclosure`]):
727    /// an enclosure of the exact solution of its constraints near the
728    /// solved positions, each variable the constraints leave free exactly
729    /// as drawn (see [`System::enclose`]).
730    ///
731    /// A sketch whose constraints are not met is no solution of them, and is
732    /// built exactly as drawn: that is all there is to build. Fails if the
733    /// solution cannot be proven — a sketch meeting its constraints only at
734    /// a singular configuration.
735    pub fn enclose(&self) -> GeopResult<Enclosure> {
736        let ctx = |e: GeopError| e.with_context("enclosing the sketch's solution");
737        let problem = Problem::new(self).map_err(ctx)?;
738        let params = problem.layout.read(self)?;
739        let variables = Planar::new(&problem, params, Vec::new());
740        let start = variables.start();
741        if !variables.system().held(&start)?.converged {
742            return Ok(Enclosure::as_drawn(self));
743        }
744        // What is fixed is as given; the rest as enclosed.
745        let x = variables.enclosed(&start).map_err(ctx)?;
746        let layout = &problem.layout;
747        Ok(Enclosure {
748            points: (layout.points.point_var.keys())
749                .map(|&p| (p, Vector2::from_array(layout.points.point(&x, p))))
750                .collect(),
751            params: self
752                .curves
753                .iter()
754                .filter_map(|(&id, c)| {
755                    let v = x[*layout.curve_var.get(&id)?];
756                    Some(match c.kind {
757                        CurveKind::Arc { .. } => (id, v.mul(Scalar::TWO)),
758                        _ => (id, v.abs()),
759                    })
760                })
761                .collect(),
762        })
763    }
764
765    /// Move every point (and arc sweep, circle radius) so all constraints
766    /// hold, changing the sketch as little as the constraints allow.
767    ///
768    /// The sketch is updated even if the solve does not converge, to the
769    /// closest configuration found — the report says which constraints could
770    /// not be met.
771    pub fn solve(&mut self) -> GeopResult<SolveReport> {
772        self.solve_with_drag(&[])
773    }
774
775    /// Like [`Sketch::solve`], while pulling each `(point, target)` towards
776    /// its target as far as the constraints allow — interactive dragging.
777    pub fn solve_with_drag(&mut self, drags: &[(PointId, Vector2)]) -> GeopResult<SolveReport> {
778        let problem = Problem::new(self)?;
779        let start = problem.layout.read(self)?;
780        let (variables, solved, x) =
781            Planar::dragged(&problem, start, &problem.layout.points.point_var, drags)?;
782        let report = variables.report(&solved.values, solved.phases, solved.enclosure)?;
783        // Where the solve put the points is the sketch's state, and a free
784        // choice: sharp.
785        let x: Vec<Scalar> = x.into_iter().map(Scalar::sharpen).collect();
786        problem.layout.write(self, &x);
787        Ok(report)
788    }
789}
790
791impl Planar<'_, '_> {
792    /// How the sketch stands with the free variables at `blocks`: which
793    /// constraints hold, and what can still move.
794    fn report(
795        &self,
796        blocks: &[Scalar],
797        phases: Vec<Phase>,
798        solved: system::Enclosure<Scalar>,
799    ) -> GeopResult<SolveReport> {
800        let Standing {
801            held,
802            dof,
803            free: free_vars,
804            unproven,
805        } = self.standing(blocks, Some(solved))?;
806        let problem = self.problem;
807        let failed_constraints = (held.failed.iter())
808            .map(|&i| problem.constraints[i].id)
809            .collect::<Vec<_>>();
810        let points = &problem.layout.points;
811        let free_points = (points.point_var.keys())
812            .map(|&p| (p, points.point(&free_vars, p).contains(&true)))
813            .collect();
814        let free_curves = problem
815            .sketch
816            .curves
817            .keys()
818            .map(|&c| {
819                let mut vars = Vec::new();
820                problem.layout.curve_vars(problem.sketch, c, &mut vars);
821                (c, vars.iter().any(|&i| free_vars[i]))
822            })
823            .collect();
824        Ok(SolveReport {
825            converged: failed_constraints.is_empty(),
826            max_residual: held.max_residual,
827            iterations: phases.iter().map(|p| p.iterations).sum(),
828            phases,
829            dof,
830            free_points,
831            free_curves,
832            failed_constraints,
833            unproven,
834        })
835    }
836}
837
838#[cfg(test)]
839mod tests {
840    use std::f64::consts::PI;
841
842    use geop_core_math::{
843        scalars::Scalar,
844        solvers::system::{Equations, Term},
845    };
846
847    use super::*;
848
849    fn n(x: f64) -> Scalar {
850        Scalar::from_f64(x)
851    }
852
853    /// At points drawn exactly, every residual and every slope of it is
854    /// known to rounding: the solver takes a step only where the merit
855    /// definitely drops, and a residual wider than its arithmetic needs
856    /// would hide every step.
857    #[test]
858    fn residuals_are_sharp_where_the_points_are() {
859        let mut s = Sketch::new();
860        let a = s.add_point(n(-1.0), n(0.1));
861        let b = s.add_point(n(1.2), n(-0.1));
862        let c = s.add_point(n(0.1), n(1.5));
863        let base = s.add_line(a, b);
864        let left = s.add_line(c, a);
865        let right = s.add_line(c, b);
866        let m = s.add_point(n(0.0), n(0.0));
867        let axis_top = s.add_point(n(0.0), n(2.0));
868        let axis = s.add_line(m, axis_top);
869        s.constrain(Constraint::Midpoint {
870            point: m,
871            curve: base,
872        });
873        s.constrain(Constraint::Symmetric { a, b, line: axis });
874        s.constrain(Constraint::Equal { a: left, b: right });
875        s.constrain(Constraint::Angle {
876            a: left,
877            b: right,
878            value: n(PI / 3.0),
879        });
880        let problem = Problem::new(&s).unwrap();
881        let params = problem.layout.read(&s).unwrap();
882        let variables = Planar::new(&problem, params, Vec::new());
883        let terms: Vec<Term> = (0..problem.constraints.len())
884            .map(Term::Constraint)
885            .collect();
886        for (k, made) in variables
887            .evaluate(&variables.start(), &terms)
888            .iter()
889            .enumerate()
890        {
891            let made = made.as_ref().unwrap();
892            let (v, row) = (&made.values, &made.jacobian);
893            for x in (v.iter()).chain(row.iter().flat_map(|(_, rows)| rows.iter().flatten())) {
894                let width = x.width().to_f64();
895                assert!(
896                    width <= 1e-12 * (1.0 + x.to_f64().abs()),
897                    "residual {k}: {x:?} in {v:?}, {row:?}"
898                );
899            }
900        }
901    }
902
903    /// A rectangle drawn a little crooked, its constraints saying it twice
904    /// over: every corner square, and each pair of opposite sides parallel
905    /// too. Its first corner held at the origin, its sides `2` and `1`
906    /// long, and its bottom horizontal if `level`. Its sides.
907    fn rectangle_said_twice(s: &mut Sketch, level: bool) -> Vec<CurveId> {
908        let p = [[0.0, 0.0], [2.05, 0.1], [1.9, 1.07], [-0.08, 0.95]]
909            .map(|q| s.add_point(n(q[0]), n(q[1])));
910        let l: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
911        s.constrain(Constraint::Fix {
912            point: p[0],
913            x: n(0.0),
914            y: n(0.0),
915        });
916        for i in 0..4 {
917            s.constrain(Constraint::Perpendicular {
918                a: l[i],
919                b: l[(i + 1) % 4],
920            });
921        }
922        for i in 0..2 {
923            s.constrain(Constraint::Parallel {
924                a: l[i],
925                b: l[i + 2],
926            });
927        }
928        if level {
929            s.constrain(Constraint::Horizontal { line: l[0] });
930        }
931        for (i, value) in [(0, 2.0), (1, 1.0)] {
932            s.constrain(Constraint::Length {
933                curve: l[i],
934                value: n(value),
935            });
936        }
937        l
938    }
939
940    /// The far corner of a rectangle as enclosed: where its second side
941    /// ends.
942    fn far_corner(s: &Sketch, sides: &[CurveId], e: &Enclosure) -> Vector2 {
943        let (_, end) = s.curves[&sides[1]].endpoints().unwrap();
944        e.points[&end]
945    }
946
947    /// A profile between two edges of a part projected into the sketch, both
948    /// on one straight side of it (the microduck's head): its closing line is
949    /// made vertical, which its ends on the two edges already say. The
950    /// edges' ends are given as the projection encloses them, and their
951    /// rounding (`-25.000000000000004` against `-24.999999999999993`) tilts
952    /// each a hair off vertical, so the vertical row is independent by a
953    /// pivot of `1e-15` at sharp ends: taken as independent, nothing could
954    /// be proven. Over the ends as enclosed, that pivot could be zero, and the
955    /// vertical is the redundant constraint it is.
956    #[test]
957    fn a_line_closing_on_two_projected_edges_of_one_side_is_proven() {
958        let given = |x: f64| Scalar::new(x - 1e-13, x + 1e-13);
959        let mut s = Sketch::new();
960        let fixed = |s: &mut Sketch, x: f64, y: f64| s.add_fixed_point(given(x), given(y));
961        let upper = {
962            let a = fixed(&mut s, -25.000000000000004, 14.52958965872012);
963            let b = fixed(&mut s, -24.999999999999993, -11.926666666666662);
964            s.add_line(a, b)
965        };
966        let lower = {
967            let a = fixed(&mut s, -24.999999999999993, -62.64416256853966);
968            let b = fixed(&mut s, -25.000000000000007, -11.926666666666666);
969            s.add_line(a, b)
970        };
971        for c in [upper, lower] {
972            let curve = s.curves.get_mut(&c).unwrap();
973            (curve.fixed, curve.construction) = (true, true);
974        }
975        let p24 = s.add_point(n(-24.999999999999993), n(-19.650020988468217));
976        let p26 = s.add_point(n(-13.56352402378605), n(-23.519644932457258));
977        let p28 = s.add_point(n(-13.56352402378605), n(-35.413048566902255));
978        let p36 = s.add_point(n(-24.999999999999996), n(-39.2826725108913));
979        let l27 = s.add_line(p24, p26);
980        let a29 = s.add_arc(p26, p28, n(-2.489060692764724));
981        let l38 = s.add_line(p28, p36);
982        let l40 = s.add_line(p36, p24);
983        for c in [
984            Constraint::PointOnCurve {
985                point: p24,
986                curve: upper,
987            },
988            Constraint::Tangent { a: l27, b: a29 },
989            Constraint::PointOnCurve {
990                point: p36,
991                curve: lower,
992            },
993            Constraint::Tangent { a: a29, b: l38 },
994            Constraint::Vertical { line: l40 },
995            Constraint::Tangent { a: l38, b: a29 },
996            Constraint::DistanceX {
997                a: p26,
998                b: p28,
999                value: n(0.0),
1000            },
1001        ] {
1002            s.constrain(c);
1003        }
1004        let report = s.solve().unwrap();
1005        assert!(report.converged, "{report:?}");
1006        s.enclose().unwrap();
1007    }
1008
1009    /// Four right angles where three would do, and parallels besides: the
1010    /// redundant constraints hold wherever the others do, and the solution
1011    /// is proven — level, every coordinate determined; turned, the turn
1012    /// left as drawn.
1013    #[test]
1014    fn a_rectangle_said_twice_is_proven() {
1015        let mut s = Sketch::new();
1016        let sides = rectangle_said_twice(&mut s, true);
1017        let report = s.solve().unwrap();
1018        assert!(report.converged && report.dof == 0, "{report:?}");
1019        assert!(report.unproven.is_empty(), "{report:?}");
1020        let e = s.enclose().unwrap();
1021        let corner = far_corner(&s, &sides, &e);
1022        assert!(
1023            corner[0].could_be_equal(n(2.0)) && corner[1].could_be_equal(n(1.0)),
1024            "{corner:?}"
1025        );
1026        assert!(corner[0].width().to_f64() < 1e-12, "{corner:?}");
1027
1028        let mut s = Sketch::new();
1029        let sides = rectangle_said_twice(&mut s, false);
1030        let report = s.solve().unwrap();
1031        assert!(report.converged && report.dof == 1, "{report:?}");
1032        let e = s.enclose().unwrap();
1033        let corner = far_corner(&s, &sides, &e);
1034        let diagonal = corner[0].mul(corner[0]).add(corner[1].mul(corner[1]));
1035        assert!(diagonal.could_be_equal(n(5.0)), "{diagonal:?}");
1036    }
1037
1038    /// Two lengths of one rectangle a hundred-billionth apart: closer than
1039    /// the solver's tolerance, so the solve meets both, but they conflict,
1040    /// and proving the solution says so, naming both.
1041    #[test]
1042    fn conflicting_lengths_are_named() {
1043        let mut s = Sketch::new();
1044        let sides = rectangle_said_twice(&mut s, true);
1045        let top = s.constrain(Constraint::Length {
1046            curve: sides[2],
1047            value: n(2.0 + 1e-11),
1048        });
1049        let report = s.solve().unwrap();
1050        assert!(report.converged, "{report:?}");
1051        // The solve says so too: no true solution is proven.
1052        let [why] = &report.unproven[..] else {
1053            panic!("{report:?}");
1054        };
1055        assert!(why.contains("conflict"), "{why}");
1056        let e = s.enclose().unwrap_err();
1057        let message = e.root_message();
1058        let bottom = s
1059            .constraints
1060            .iter()
1061            .find(|(_, c)| matches!(c, Constraint::Length { curve, .. } if *curve == sides[0]))
1062            .map(|(id, _)| *id)
1063            .unwrap();
1064        assert!(message.contains("conflict"), "{message}");
1065        for id in [top, bottom] {
1066            assert!(message.contains(&format!("{id} ")), "{id}: {message}");
1067        }
1068    }
1069}