Skip to main content

geop_ops_assembly/
residuals.rs

1//! What a constraint mate asks of the geometry of two placed parts, as
2//! residuals: a point, a line or a plane of each — where its part puts it
3//! ([`Geometry::placed`]) — and what they must be to each other
4//! ([`Kind`]), each residual a length that vanishes where it holds
5//! ([`residuals`]).
6//!
7//! Everything placed is computed in dual numbers ([`D`]), so that every
8//! residual carries its derivatives (see `crate::mechanism`).
9//!
10//! A mate that holds a point on a curve or a face of the other part
11//! ([`Kind::Path`], [`Kind::Tangent`]) asks where on it: the foot point's
12//! parameters ([`Kind::feet`]) are values of the program's state, solved
13//! with the poses, and its residuals are the follower's point less the foot
14//! point ([`contact_residuals`]).
15
16use geop_core_geometry::{nurb_curve::NurbCurve3D, nurb_surface::NurbSurface3D};
17use geop_core_math::{
18    geop_error::{GeopError, GeopResult, WithContext},
19    primitives::{Motion, Pose},
20    scalars::{Number, Scalar, as_f64},
21    vector::{Vector, Vector3},
22    with_context,
23};
24use geop_ops_model::selection::{Role, Shape};
25use serde::{Deserialize, Serialize};
26
27pub use crate::solving::D;
28
29/// Where a pose puts the points of a part and turns its directions — or,
30/// with none, where they are: of a part that never moves.
31pub(crate) struct Placing(Option<Motion<D>>);
32
33impl Placing {
34    pub(crate) fn new(pose: Option<&Pose<D>>) -> Self {
35        Placing(pose.map(Pose::motion))
36    }
37
38    /// The point `at` of the part, where the part is.
39    pub(crate) fn point(&self, at: &Vector3) -> Vector3<D> {
40        self.moved(&at.map(D::cst))
41    }
42
43    /// The point `at` of the part, carrying derivatives of its own, where
44    /// the part is.
45    pub(crate) fn moved(&self, at: &Vector3<D>) -> Vector3<D> {
46        match &self.0 {
47            Some(m) => m.apply(at),
48            None => *at,
49        }
50    }
51
52    /// The direction `d` of the part, turned as the part is.
53    pub(crate) fn turn(&self, d: &Vector3<D>) -> Vector3<D> {
54        match &self.0 {
55            Some(m) => m.rotate(d),
56            None => *d,
57        }
58    }
59}
60
61/// The rows that keep `along` parallel to the direction `across` is
62/// square to — facing either way: its parts along the two directions of
63/// `across`, each scaled by the part's size `scale`.
64///
65/// Two rows, of rank two wherever they nearly are parallel. A cross
66/// product, three rows, has rank three while they are not quite parallel
67/// and two once they are, and a row whose slope fades as the mate closes
68/// is one the minimizer can neither drop nor resolve (a 6-axis arm
69/// stalled on it).
70pub(crate) fn parallel(across: &[Vector3<D>; 2], along: &Vector3<D>, scale: D) -> [D; 2] {
71    across.map(|x| x.prod_dot(along).mul(scale))
72}
73
74/// A point, a line or a plane of a part, in its own frame — or what a
75/// point is kept on: a curve, a face's surface.
76#[derive(Clone, Debug, PartialEq)]
77pub enum Geometry {
78    Point {
79        at: Vector3,
80    },
81    /// The line through `point` along `direction` (any non-zero length).
82    Line {
83        point: Vector3,
84        direction: Vector3,
85    },
86    /// The plane through `point` normal to `normal` (any non-zero length).
87    Plane {
88        point: Vector3,
89        normal: Vector3,
90    },
91    /// A circle's center and its radius: what is kept that far off a face
92    /// it rolls on.
93    Rim {
94        center: Vector3,
95        radius: Scalar,
96    },
97    /// A curve: an edge's, a sketch's.
98    Curve {
99        curve: NurbCurve3D,
100    },
101    /// A face's surface, its normal out of its solid.
102    Surface {
103        surface: NurbSurface3D,
104    },
105}
106
107impl Geometry {
108    fn kind(&self) -> &'static str {
109        match self {
110            Geometry::Point { .. } => "a point",
111            Geometry::Line { .. } => "a line",
112            Geometry::Plane { .. } => "a plane",
113            Geometry::Rim { .. } => "a circle",
114            Geometry::Curve { .. } => "a curve",
115            Geometry::Surface { .. } => "a face",
116        }
117    }
118
119    /// What `shape` gives used as `role`, if it fits it: a point — the
120    /// center of a circle is one — a line, an axis as a line, a plane.
121    fn as_role(role: Role, shape: &Shape) -> Option<Geometry> {
122        let line = |a: &geop_core_geometry::shape::Axis| Geometry::Line {
123            point: a.point,
124            direction: a.direction,
125        };
126        match role {
127            Role::Point => (shape.point.as_ref())
128                .or(shape.arc.as_ref().map(|a| &a.circle.center))
129                .map(|at| Geometry::Point { at: *at }),
130            Role::Line => shape.line.as_ref().map(line),
131            Role::Round => shape.round.as_ref().map(line),
132            Role::Plane => shape.plane.as_ref().map(|p| Geometry::Plane {
133                point: *p.origin(),
134                normal: *p.w(),
135            }),
136            _ => None,
137        }
138    }
139}
140
141/// What a constraint asks of its two features.
142#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
143#[serde(tag = "type", rename_all = "snake_case")]
144pub enum Kind {
145    /// They touch: two points coincide, a point lies on a line or a plane,
146    /// two lines are one, a line lies in a plane, two planes are one
147    /// (facing either way).
148    Coincident,
149    /// Two lines — axes, as a round edge or face has — are one.
150    Concentric,
151    /// Lines and planes run parallel: two lines, two planes' normals, a
152    /// line along a plane.
153    Parallel,
154    /// Lines and planes stand square: two lines, two planes, a line along a
155    /// plane's normal.
156    Perpendicular,
157    /// They are `value` apart: two points, a point and a line or a plane,
158    /// or two parallel lines or planes, or a line parallel to a plane.
159    Distance {
160        #[serde(with = "as_f64")]
161        value: Scalar,
162    },
163    /// The lines (or planes' normals) meet at `value` degrees.
164    Angle {
165        #[serde(with = "as_f64")]
166        value: Scalar,
167    },
168    /// The second entity keeps touching the first, a face, flat or curved:
169    /// a circle — a roller's rim, a pin's — keeps its radius off the face,
170    /// its center on the face's offset, a point stays on the face. A cam
171    /// and its follower, a pin in a slot of any shape.
172    Tangent,
173    /// The second entity — a point, a circle's center — runs along the
174    /// first, a curve: a path mate, a pin along a slot's center line.
175    Path,
176}
177
178impl Kind {
179    pub fn label(&self) -> &'static str {
180        match self {
181            Kind::Coincident => "Coincident",
182            Kind::Concentric => "Concentric",
183            Kind::Parallel => "Parallel",
184            Kind::Perpendicular => "Perpendicular",
185            Kind::Distance { .. } => "Distance",
186            Kind::Angle { .. } => "Angle",
187            Kind::Tangent => "Tangent",
188            Kind::Path => "Path",
189        }
190    }
191
192    /// The names of the parameters of the foot point on the first entity
193    /// it is kept at, each a value of the state: the curve's `t`, the
194    /// face's `u` and `v`. None for a kind that keeps no point on a curve
195    /// or a face.
196    pub fn feet(&self) -> &'static [&'static str] {
197        match self {
198            Kind::Path => &["t"],
199            Kind::Tangent => &["u", "v"],
200            _ => &[],
201        }
202    }
203
204    /// What an entity of a constraint of this kind can be, in the order its
205    /// geometry is taken (see [`Kind::geometry`]): for one that touches or
206    /// keeps a distance, a point before a line before a plane before a round
207    /// face's axis; for a concentric one, an axis; for one that turns, a
208    /// direction.
209    ///
210    /// A flat face is its plane before it is anything round: the cap of a
211    /// revolved cylinder is a disc, a surface of revolution round the
212    /// cylinder's axis, and a part standing on it stands on the plane, not
213    /// with the axis in it. Only a concentric mate wants the disc's axis.
214    pub fn roles(&self) -> &'static [Role] {
215        match self {
216            Kind::Coincident | Kind::Distance { .. } => {
217                &[Role::Point, Role::Line, Role::Plane, Role::Round]
218            }
219            Kind::Concentric => &[Role::Round, Role::Line],
220            Kind::Parallel | Kind::Perpendicular | Kind::Angle { .. } => {
221                &[Role::Line, Role::Plane, Role::Round]
222            }
223            Kind::Tangent => &[Role::Circle, Role::Point],
224            Kind::Path => &[Role::Curve, Role::Circle, Role::Point],
225        }
226    }
227
228    /// The geometry the `k`th entity of a constraint of this kind
229    /// contributes, `shape` — as the first of its [`Kind::roles`] it fits —
230    /// and `surface` and `curve`, the wall of a face and the run of an edge,
231    /// asked of a tangent's first and of a path's first only: of a tangent
232    /// constraint, the
233    /// face first and then the circle or the point that touches it; of a
234    /// path, the curve and then the point that runs along it.
235    pub fn geometry(
236        &self,
237        k: usize,
238        shape: &Shape,
239        surface: impl FnOnce() -> GeopResult<Option<NurbSurface3D>>,
240        curve: impl FnOnce() -> GeopResult<Option<NurbCurve3D>>,
241    ) -> GeopResult<Option<Geometry>> {
242        Ok(match (self, k) {
243            (Kind::Tangent, 0) => surface()?.map(|surface| Geometry::Surface { surface }),
244            (Kind::Tangent, _) => (shape.arc.as_ref())
245                .map(|arc| Geometry::Rim {
246                    center: arc.circle.center,
247                    radius: arc.circle.radius,
248                })
249                .or_else(|| Geometry::as_role(Role::Point, shape)),
250            (Kind::Path, 0) => curve()?.map(|curve| Geometry::Curve { curve }),
251            (Kind::Path, _) => Geometry::as_role(Role::Point, shape),
252            _ => (self.roles().iter()).find_map(|&role| Geometry::as_role(role, shape)),
253        })
254    }
255
256    /// What the `k`th entity of a constraint of this kind has to be, in
257    /// words, where it is not what [`Kind::roles`] says: of a tangent and of
258    /// a path, each entity something of its own.
259    pub fn needs(&self, k: usize) -> Option<&'static str> {
260        match (self, k) {
261            (Kind::Tangent, 0) => Some("a face"),
262            (Kind::Tangent, _) => Some("a circle, an arc or a point"),
263            (Kind::Path, 0) => Some("an edge or a curve"),
264            (Kind::Path, _) => Some("a point or a circle"),
265            _ => None,
266        }
267    }
268
269    /// Checks that it can hold between `a` and `b` at all: a distance
270    /// between two lines, say, but no angle at a point.
271    pub fn fits(&self, a: &Geometry, b: &Geometry) -> GeopResult<()> {
272        use Geometry::*;
273        let directed = |g: &Geometry| !matches!(g, Point { .. });
274        let flat = |g: &Geometry| matches!(g, Point { .. } | Line { .. } | Plane { .. });
275        let fits = match self {
276            Kind::Tangent => matches!(a, Surface { .. }) && matches!(b, Point { .. } | Rim { .. }),
277            Kind::Path => matches!(a, Curve { .. }) && matches!(b, Point { .. }),
278            _ if !(flat(a) && flat(b)) => false,
279            Kind::Coincident | Kind::Distance { .. } => true,
280            Kind::Concentric => matches!((a, b), (Line { .. }, Line { .. })),
281            Kind::Parallel | Kind::Perpendicular | Kind::Angle { .. } => directed(a) && directed(b),
282        };
283        let (a, b) = (a.kind(), b.kind());
284        match fits {
285            true => Ok(()),
286            false => Err(GeopError::new(format!(
287                "a {} constraint cannot hold between {} and {}",
288                self.label().to_lowercase(),
289                a,
290                b
291            ))),
292        }
293    }
294}
295
296/// A line's direction or a plane's normal where it is, and two directions
297/// square to it and to each other.
298pub struct Direction {
299    along: Vector3<D>,
300    across: [Vector3<D>; 2],
301}
302
303/// A feature where its part puts it.
304pub enum World {
305    Point(Vector3<D>),
306    Line(Vector3<D>, Direction),
307    Plane(Vector3<D>, Direction),
308}
309
310impl World {
311    /// Its direction: a line's, a plane's normal.
312    fn direction(&self) -> Option<&Direction> {
313        match self {
314            World::Point(_) => None,
315            World::Line(_, d) | World::Plane(_, d) => Some(d),
316        }
317    }
318}
319
320impl Geometry {
321    /// Where `pose` puts it — or, with none, where it is: of a part that
322    /// never moves. The directions square to a line or a plane are chosen
323    /// in the part's own frame — a free choice — and turn with it.
324    pub fn placed(&self, pose: Option<&Pose<D>>) -> GeopResult<World> {
325        let placing = Placing::new(pose);
326        // Of the part's own direction, which no state moves: in plain
327        // numbers, the same values dual numbers would carry with no slope.
328        let direction = |d: &Vector3| -> GeopResult<Direction> {
329            let d = d.normalize()?;
330            Ok(Direction {
331                along: placing.turn(&d.map(D::cst)),
332                across: d.across()?.map(|v| placing.turn(&v.map(D::cst))),
333            })
334        };
335        Ok(match self {
336            Geometry::Point { at } => World::Point(placing.point(at)),
337            Geometry::Line {
338                point: p,
339                direction: d,
340            } => World::Line(placing.point(p), direction(d)?),
341            Geometry::Plane {
342                point: p,
343                normal: n,
344            } => World::Plane(placing.point(p), direction(n)?),
345            Geometry::Rim { center, .. } => World::Point(placing.point(center)),
346            Geometry::Curve { .. } | Geometry::Surface { .. } => {
347                return Err(GeopError::new(format!(
348                    "{} is placed by where on it its foot point is",
349                    self.kind()
350                )));
351            }
352        })
353    }
354
355    /// The point of a follower — a point, a circle's center — where `pose`
356    /// puts it.
357    fn follower(&self, pose: Option<&Pose<D>>) -> GeopResult<Vector3<D>> {
358        match self {
359            Geometry::Point { at } | Geometry::Rim { center: at, .. } => {
360                Ok(Placing::new(pose).point(at))
361            }
362            _ => Err(GeopError::new(format!("{} follows nothing", self.kind()))),
363        }
364    }
365
366    /// How far a follower keeps off what it touches: a circle its radius.
367    fn radius(&self) -> Option<Scalar> {
368        match self {
369            Geometry::Rim { radius, .. } => Some(*radius),
370            _ => None,
371        }
372    }
373
374    /// The point of a curve at `feet`, `[t]`, or of a surface at `[u, v]`
375    /// — off it by `offset` along its normal — in the part's own frame,
376    /// with the derivatives the feet carry. A parameter along which the
377    /// curve or surface closes on itself runs on round it, past one end
378    /// from the other. Past an end where it does not, which a solve may try
379    /// on its way, it goes on along its tangents there: the feet's limits
380    /// keep a solution within (see [`Geometry::parameters`]).
381    fn foot(&self, feet: &[D], offset: Option<Scalar>) -> GeopResult<Vector3<D>> {
382        let domains = self.parameters();
383        if domains.len() != feet.len() {
384            return Err(GeopError::new(format!(
385                "{} has no foot point at {} parameters",
386                self.kind(),
387                feet.len()
388            )));
389        }
390        // Each parameter where it is evaluated, and the step from there to
391        // the foot's — of no length unless past an end it does not close
392        // at.
393        let (at, steps): (Vec<Scalar>, Vec<D>) = (feet.iter().zip(&domains))
394            .map(|(x, foot)| {
395                let x = foot.round(*x);
396                let at = x.v.clamped(foot.lo, foot.hi);
397                (at, x.sub(D::cst(at)))
398            })
399            .unzip();
400        match self {
401            Geometry::Curve { curve } => {
402                let (p, dp) = (curve.evaluate(at[0])?, curve.tangent(at[0])?);
403                Ok(first_order(&p, &[(&dp, &steps[0])]))
404            }
405            Geometry::Surface { surface } => {
406                let (du, dv) = (&steps[0], &steps[1]);
407                let order = if offset.is_some() { 2 } else { 1 };
408                let s = surface.partials(at[0], at[1], order)?;
409                let point = first_order(&s[0][0], &[(&s[1][0], du), (&s[0][1], dv)]);
410                let Some(offset) = offset else {
411                    return Ok(point);
412                };
413                // The normal, its derivatives by the second partials: of
414                // the tangents, each to first order.
415                let su = first_order(&s[1][0], &[(&s[2][0], du), (&s[1][1], dv)]);
416                let sv = first_order(&s[0][1], &[(&s[1][1], du), (&s[0][2], dv)]);
417                let normal = su.prod_cross(&sv).normalize()?;
418                Ok(point.add(&normal.prod_scalar(D::cst(offset))))
419            }
420            _ => unreachable!("only a curve or a surface has feet"),
421        }
422    }
423
424    /// The parameters of its foot point: a curve's `t`, a surface's `u`
425    /// and `v` — each its domain, and whether it closes there.
426    pub fn parameters(&self) -> Vec<Foot> {
427        // Closed where its last control points could be its first: of a
428        // circle, or a surface swept round one, they are the same.
429        let ends = |a: &[Vector<4>], b: &[Vector<4>]| {
430            a.len() == b.len() && a.iter().zip(b).all(|(p, q)| p.could_be_equal(q))
431        };
432        let foot = |(lo, hi): (Scalar, Scalar), closes| Foot { lo, hi, closes };
433        match self {
434            Geometry::Curve { curve } => {
435                let points = &curve.control_points;
436                let closes = ends(&points[..1], &points[points.len() - 1..]);
437                vec![foot(curve.domain(), closes)]
438            }
439            Geometry::Surface { surface } => {
440                let (nu, nv) = (surface.num_u, surface.num_v);
441                let at = |i: usize, j: usize| surface.control_points[i * nv + j];
442                let row = |i: usize| (0..nv).map(|j| at(i, j)).collect::<Vec<_>>();
443                let column = |j: usize| (0..nu).map(|i| at(i, j)).collect::<Vec<_>>();
444                vec![
445                    foot(surface.domain_u(), ends(&row(0), &row(nu - 1))),
446                    foot(surface.domain_v(), ends(&column(0), &column(nv - 1))),
447                ]
448            }
449            _ => Vec::new(),
450        }
451    }
452}
453
454/// A parameter of a foot point: its domain, and whether the curve or
455/// surface closes on itself along it — then the parameter has no end, and
456/// a value past one stands for the point as far past the other.
457#[derive(Clone, Copy, Debug, PartialEq)]
458pub struct Foot {
459    pub lo: Scalar,
460    pub hi: Scalar,
461    pub closes: bool,
462}
463
464impl Foot {
465    /// `x`, brought into the domain by whole periods where it closes: which
466    /// of the values that stand for one point is evaluated is a free
467    /// choice.
468    fn round(&self, x: D) -> D {
469        if !self.closes {
470            return x;
471        }
472        let (lo, period) = (self.lo.to_f64(), self.hi.sub(self.lo).to_f64());
473        let turns = ((x.v.to_f64() - lo) / period).floor();
474        match turns == 0.0 {
475            true => x,
476            false => x.sub(D::cst(self.hi.sub(self.lo).mul(Scalar::from_f64(turns)))),
477        }
478    }
479}
480
481/// `value` plus each `(slope, by)` pair's slope times the step `by`, to
482/// first order: a point of a curve or a surface, by its parameters' steps.
483fn first_order(value: &Vector3, steps: &[(&Vector3, &D)]) -> Vector3<D> {
484    Vector3::from_array(std::array::from_fn(|i| {
485        (steps.iter()).fold(D::cst(value[i]), |sum, (slope, by)| {
486            sum.add(by.mul(D::cst(slope[i])))
487        })
488    }))
489}
490
491/// The residuals of a constraint of `kind` that keeps the follower `b` on
492/// `a` — a point on a curve, a circle or a point touching a face — at the
493/// foot point `feet` (see [`Kind::feet`]), where `pa` and `pb` put them:
494/// the follower's point less the foot point, three lengths.
495pub fn contact_residuals(
496    kind: &Kind,
497    a: &Geometry,
498    pa: Option<&Pose<D>>,
499    b: &Geometry,
500    pb: Option<&Pose<D>>,
501    feet: &[D],
502) -> GeopResult<Vec<D>> {
503    let ctx = with_context!("a {} constraint", kind.label().to_lowercase());
504    let foot = a.foot(feet, b.radius()).with_context(ctx)?;
505    let on = Placing::new(pa).moved(&foot);
506    Ok(b.follower(pb)
507        .with_context(ctx)?
508        .sub(&on)
509        .to_array()
510        .to_vec())
511}
512
513/// Where on `a` the foot point of the follower `b` is, where `pa` and `pb`
514/// put them: of a curve its `t`, of a surface its `u` and `v`, nearest the
515/// follower's point. A seed for a solve, which finds the true one: a free
516/// choice.
517pub fn seed_feet(
518    a: &Geometry,
519    pa: Option<&Pose>,
520    b: &Geometry,
521    pb: Option<&Pose>,
522) -> GeopResult<Vec<Scalar>> {
523    let at = match b {
524        Geometry::Point { at } | Geometry::Rim { center: at, .. } => *at,
525        _ => return Err(GeopError::new(format!("{} follows nothing", b.kind()))),
526    };
527    let world = pb.map_or(at, |pose| pose.apply(&at));
528    let local = pa.map_or(world, |pose| pose.inverse().apply(&world));
529    match a {
530        Geometry::Curve { curve } => Ok(vec![curve.closest_point(&local)?.0]),
531        Geometry::Surface { surface } => {
532            let (u, v, _) = surface.closest_point(&local)?;
533            Ok(vec![u, v])
534        }
535        _ => Err(GeopError::new(format!("{} has no foot point", a.kind()))),
536    }
537}
538
539/// The residuals of a constraint of `kind` between `a` and `b`, where they
540/// are, each a length — a direction's scaled by the part's size `scale`.
541pub fn residuals(kind: &Kind, a: &World, b: &World, scale: D) -> GeopResult<Vec<D>> {
542    use World::*;
543    let l = scale;
544    let mut out = Vec::new();
545    // Lines and planes run parallel — facing either way — where the one's
546    // direction has no part across the other's.
547    let parallel = |d: &Direction, e: &Direction, out: &mut Vec<D>| {
548        out.extend(self::parallel(&d.across, &e.along, l))
549    };
550    // How far `p` is off the line through `q` along `d`: as two rows across
551    // it, for the same reason, and as a distance.
552    let off_line = |p: &Vector3<D>, q: &Vector3<D>, d: &Direction, out: &mut Vec<D>| {
553        out.extend(d.across.map(|x| x.prod_dot(&p.sub(q))))
554    };
555    let distance_off_line =
556        |p: &Vector3<D>, q: &Vector3<D>, d: &Direction| p.sub(q).prod_cross(&d.along).norm();
557    let along = |p: &Vector3<D>, q: &Vector3<D>, n: &Direction| n.along.prod_dot(&p.sub(q));
558    let dot = |d: &Direction, e: &Direction| d.along.prod_dot(&e.along).mul(l);
559    let directions = || -> GeopResult<(&Direction, &Direction)> {
560        match (a.direction(), b.direction()) {
561            (Some(d), Some(e)) => Ok((d, e)),
562            _ => Err(GeopError::new(format!(
563                "a {} constraint needs two directions",
564                kind.label().to_lowercase()
565            ))),
566        }
567    };
568    match kind {
569        Kind::Coincident => match (a, b) {
570            (Point(p), Point(q)) => out.extend(p.sub(q).to_array()),
571            (Point(p), Line(q, d)) | (Line(q, d), Point(p)) => off_line(p, q, d, &mut out),
572            (Point(p), Plane(q, n)) | (Plane(q, n), Point(p)) => out.push(along(p, q, n)),
573            (Line(p, d), Line(q, e)) => {
574                parallel(d, e, &mut out);
575                off_line(q, p, d, &mut out);
576            }
577            (Line(p, d), Plane(q, n)) | (Plane(q, n), Line(p, d)) => {
578                out.push(dot(d, n));
579                out.push(along(p, q, n));
580            }
581            (Plane(p, n), Plane(q, m)) => {
582                parallel(n, m, &mut out);
583                out.push(along(q, p, n));
584            }
585        },
586        // Two lines share their axis where they coincide.
587        Kind::Concentric => match (a, b) {
588            (Line(..), Line(..)) => return residuals(&Kind::Coincident, a, b, scale),
589            _ => return Err(GeopError::new("a concentric constraint needs two lines")),
590        },
591        Kind::Parallel | Kind::Perpendicular => {
592            let (d, e) = directions()?;
593            // A line along a plane runs square to its normal, and a line
594            // square to a plane runs along its normal: mixed pairs swap.
595            let mixed = matches!((a, b), (Line(..), Plane(..)) | (Plane(..), Line(..)));
596            match matches!(kind, Kind::Parallel) != mixed {
597                true => parallel(d, e, &mut out),
598                false => out.push(dot(d, e)),
599            }
600        }
601        Kind::Distance { value } => {
602            let value = D::cst(*value);
603            match (a, b) {
604                (Line(p, d), Line(q, e)) => {
605                    parallel(d, e, &mut out);
606                    out.push(distance_off_line(q, p, d).sub(value));
607                }
608                (Line(p, d), Plane(q, n)) | (Plane(q, n), Line(p, d)) => {
609                    out.push(dot(d, n));
610                    out.push(along(p, q, n).abs().sub(value));
611                }
612                (Plane(p, n), Plane(q, m)) => {
613                    parallel(n, m, &mut out);
614                    out.push(along(q, p, n).abs().sub(value));
615                }
616                (Point(p), Point(q)) => out.push(p.sub(q).norm().sub(value)),
617                (Point(p), Line(q, d)) | (Line(q, d), Point(p)) => {
618                    out.push(distance_off_line(p, q, d).sub(value))
619                }
620                (Point(p), Plane(q, n)) | (Plane(q, n), Point(p)) => {
621                    out.push(along(p, q, n).abs().sub(value))
622                }
623            }
624        }
625        // One condition, so one row: `d·e = cos θ`, divided by `sin θ`, its
626        // slope there, so that the row is a length per turn like every
627        // other. The length of a cross product, as a second row, has no
628        // slope at all where the directions are parallel, and the two rows
629        // together say one thing twice. At 0° or 180° the condition is two —
630        // the directions parallel — and `d·e` has no slope there either:
631        // those are held as parallel ones are, and `d·e = cos θ` says which
632        // way, so that 180° turns the part half way round, as 179.9° does,
633        // not back to 0°. That row is flat where it holds: it adds no
634        // condition there, only makes the other way round no solution
635        // (the sum of squares has a maximum, not a minimum, at it).
636        Kind::Angle { value } => {
637            let (d, e) = directions()?;
638            let radians = D::cst(*value).degrees_to_radians()?;
639            let sin = radians.sin();
640            let cos = d.along.prod_dot(&e.along).sub(radians.cos());
641            if sin.could_be_equal(D::ZERO) {
642                parallel(d, e, &mut out);
643                out.push(cos.mul(l));
644            } else {
645                out.push(cos.mul(D::ONE.div(sin)?).mul(l));
646            }
647        }
648        Kind::Tangent | Kind::Path => {
649            return Err(GeopError::new(format!(
650                "a {} constraint holds where its foot point is: see `contact_residuals`",
651                kind.label().to_lowercase()
652            )));
653        }
654    }
655    Ok(out)
656}