Skip to main content

geop_core_sketch/
offset.rs

1//! Offsetting chains of lines, arcs and splines — or circles — by a
2//! distance, the offset tied to them by constraints so it follows them.
3//!
4//! **One dimension.** The offset of the chain's first curve is an
5//! [`Constraint::Offset`] — the distance, a dimension to give. Every other
6//! offset curve is only [`Constraint::Parallel`] to its line or
7//! [`Constraint::Concentric`] with its arc, and the distance carries over
8//! from one to the next at every corner:
9//!
10//! - where the chain runs on smoothly — [`Constraint::Tangent`] there, by
11//!   constraint — the offsets meet tangentially too;
12//! - where it turns away from the offset side, a gap opens: closed by an
13//!   arc round the corner, tangent to both offsets ([`Corners::Round`]), or
14//!   by extending both offsets until they meet ([`Corners::Extend`]);
15//! - where it turns towards the offset side, the offsets cross: both are
16//!   cut back to where they meet;
17//! - where it runs on straight — the corner's offsets end closer than the
18//!   solver tells points apart, as the halves of a mirrored outline meet
19//!   on the mirror line — they meet square to the corner.
20//!
21//! An arc round a corner has the corner as its center, so its radius is the
22//! distance of both offsets it touches. Where offsets meet, a construction
23//! circle about the corner, touching both, says the same. An open chain's
24//! offset ends square to the chain's ends.
25//!
26//! **Splines.** The offset of a spline is no spline. It is fitted: a cubic
27//! spline through points of the true offset, with as many control points
28//! as it takes to come within [`FIT_TOLERANCE`] of the sketch's size of it
29//! everywhere, proven ([`NurbCurve2D::offset_miss`]). Each of those points
30//! is held to the spline, at its parameter, the distance away along its
31//! normal ([`Constraint::OffsetAt`]), the distance a construction circle's
32//! radius about a corner of the chain — the dimension, if the spline comes
33//! first. Where the fit ends at a corner it ends on the true offset, and
34//! the curve the chain runs on with meets it there; where it ends where
35//! offsets cross, its end is held only across the spline, and slides along
36//! the offset to where the other one is.
37//!
38//! **Crossings.** Where a piece is too short for the corners at its ends,
39//! or offsets run into each other — offsetting into a notch narrower than
40//! twice the distance — the offset drawn round the chain crosses itself.
41//! It is cut where it does, and what lies nearer the chain than the
42//! distance dropped: what is left runs on from part to part at the
43//! crossings, each part still tied to its source. A part cut off at both
44//! ends from the corners that carry the distance holds it by a circle
45//! about its source's corner touching it, as large as one about the first
46//! part's: one dimension still says how far all of it is.
47//!
48//! **Several chains at once.** An outline and its holes, offset together,
49//! each go into the same area: of two closed chains one inside the other,
50//! one inward and the other outward. Each is run the way that has that
51//! area on its left, so that where their offsets cross they are cut and
52//! run on from one into the other, as a chain's own do; a part of another
53//! chain holds the distance as a part cut off does. A circle among them
54//! keeps its offset whole, held by a construction circle on it, level with
55//! its center, as large as the distance.
56//!
57//! Refused: an arc offset past its center, a spline offset past its
58//! center of curvature, a circle's offset that another crosses, and an
59//! offset that leaves nothing.
60
61use std::rc::Rc;
62
63use geop_core_geometry::nurb_curve::NurbCurve2D;
64use geop_core_math::solvers::system::RELATIVE_TOLERANCE;
65use geop_core_math::{
66    geop_error::{GeopError, GeopResult},
67    scalars::Scalar,
68    vector::Vector2,
69};
70use serde::{Deserialize, Serialize};
71
72use crate::{
73    plain::{
74        self, P2, Plain, add, angle_between, bounds, cross, distance, dot, perp, scale,
75        segments_cross, sub, unit, wrap, xy,
76    },
77    profile::{Chain, ProfileEdge, basis_at, spline_nurbs},
78    sketch::{Constraint, ConstraintId, CurveId, CurveKind, PointId, Sketch},
79};
80
81/// How near a spline's fitted offset comes to the true one, at least: this
82/// part of the sketch's size. How hard the fit tries; the fit says how near
83/// it came.
84const FIT_TOLERANCE: f64 = 1e-4;
85
86/// The most control points a spline's fitted offset is given.
87const MOST_FIT_POINTS: usize = 256;
88
89/// How finely a spline, or its offset, is sampled to find where it crosses
90/// something, and how near a point it comes: each crossing and nearest
91/// point is refined on the spline itself after.
92const SAMPLES: usize = 256;
93
94/// How an offset closes the gap where its chain turns away from it.
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub enum Corners {
98    /// An arc round the corner.
99    #[default]
100    Round,
101    /// Both offsets extended until they meet.
102    Extend,
103}
104
105/// An offset made: the curves of the offset, and the dimension of its
106/// distance.
107#[derive(Clone, Debug, PartialEq)]
108pub struct Offsetted {
109    pub curves: Vec<CurveId>,
110    pub distance: ConstraintId,
111}
112
113/// What a part of an offset drawn round a chain is of: the offset of the
114/// chain's `i`-th piece, or the arc round the corner after it.
115#[derive(Clone, Copy, Debug)]
116enum Of {
117    Piece(usize),
118    Round(usize),
119}
120
121/// A part of an offset drawn round a chain, kept where it was cut at its
122/// crossings: of the `elem`-th thing drawn, running from its start
123/// (`from_start`) or a crossing to its end (`to_end`) or a crossing.
124#[derive(Clone, Debug)]
125struct Sub {
126    elem: usize,
127    shape: Shape,
128    from_start: bool,
129    to_end: bool,
130}
131
132/// A spline of the chain moved `distance` to the chain's left — not at
133/// all, for the spline itself — in plain numbers: where along it is a
134/// fraction `w` of the spline's domain, run the way the chain runs it.
135#[derive(Debug)]
136struct SplineOffset {
137    spline: NurbCurve2D,
138    reversed: bool,
139    distance: f64,
140    /// [`SAMPLES`] points along it, evenly in `w`: where to start looking.
141    samples: Vec<P2>,
142}
143
144impl SplineOffset {
145    fn new(spline: NurbCurve2D, reversed: bool, distance: f64) -> GeopResult<Self> {
146        let mut offset = SplineOffset {
147            spline,
148            reversed,
149            distance,
150            samples: Vec::new(),
151        };
152        offset.samples = (0..=SAMPLES)
153            .map(|k| offset.at(k as f64 / SAMPLES as f64))
154            .collect::<GeopResult<_>>()?;
155        Ok(offset)
156    }
157
158    /// The spline's own parameter at `w`.
159    fn t(&self, w: f64) -> f64 {
160        if self.reversed { 1.0 - w } else { w }
161    }
162
163    /// The spline's unit tangent at `w`, the way the chain runs.
164    fn tangent(&self, w: f64) -> GeopResult<P2> {
165        let d = plain::of(&self.spline.tangent(Scalar::from_f64(self.t(w)))?);
166        let d = if self.reversed { scale(d, -1.0) } else { d };
167        unit(d).ok_or_else(|| GeopError::new("a spline with no tangent"))
168    }
169
170    fn at(&self, w: f64) -> GeopResult<P2> {
171        let p = plain::of(&self.spline.evaluate(Scalar::from_f64(self.t(w)))?);
172        Ok(add(p, scale(perp(self.tangent(w)?), self.distance)))
173    }
174
175    /// Where between `lo` and `hi` it comes nearest `q`: the nearest of the
176    /// samples, then Newton on the curve itself.
177    fn nearest(&self, q: P2, lo: f64, hi: f64) -> f64 {
178        let n = SAMPLES as f64;
179        let mut best = (f64::INFINITY, lo);
180        for k in 0..SAMPLES {
181            let (a, b) = (self.samples[k], self.samples[k + 1]);
182            let ab = sub(b, a);
183            let f = (dot(sub(q, a), ab) / dot(ab, ab).max(f64::MIN_POSITIVE)).clamp(0.0, 1.0);
184            let w = ((k as f64 + f) / n).clamp(lo, hi);
185            let d = distance(q, add(a, scale(ab, f)));
186            if d < best.0 {
187                best = (d, w);
188            }
189        }
190        let mut w = best.1;
191        let h = 1e-7;
192        for _ in 0..16 {
193            let (Ok(p), Ok(a), Ok(b)) = (self.at(w), self.at(w - h), self.at(w + h)) else {
194                break;
195            };
196            let v = scale(sub(b, a), 0.5 / h);
197            let step = dot(sub(q, p), v) / dot(v, v).max(f64::MIN_POSITIVE);
198            let next = (w + step).clamp(lo, hi);
199            if next == w {
200                break;
201            }
202            w = next;
203        }
204        w
205    }
206}
207
208/// A piece of the chain or of its offset, in plain numbers: a line, circle
209/// or arc, or the part of a spline or its offset from `from` to `to`, each
210/// a fraction `w` of the whole (see [`SplineOffset`]).
211#[derive(Clone, Debug)]
212enum Shape {
213    Plain(Plain),
214    Spline {
215        of: Rc<SplineOffset>,
216        from: f64,
217        to: f64,
218    },
219}
220
221impl Shape {
222    fn w(from: f64, to: f64, u: f64) -> f64 {
223        from + u * (to - from)
224    }
225
226    /// The point `u` along it, from 0 at its start to 1 at its end.
227    fn at(&self, u: f64) -> P2 {
228        match self {
229            Shape::Plain(p) => p.at(u),
230            Shape::Spline { of, from, to } => of
231                .at(Self::w(*from, *to, u))
232                .unwrap_or_else(|_| of.samples[0]),
233        }
234    }
235
236    fn tangent(&self, u: f64) -> P2 {
237        match self {
238            Shape::Plain(p) => p.tangent(u),
239            Shape::Spline { of, from, to } => {
240                let t = of.tangent(Self::w(*from, *to, u)).unwrap_or([1.0, 0.0]);
241                if to < from { scale(t, -1.0) } else { t }
242            }
243        }
244    }
245
246    /// Where along it `q`, a point of it, is (see [`Plain::param`]).
247    fn param(&self, q: P2) -> f64 {
248        match self {
249            Shape::Plain(p) => p.param(q),
250            Shape::Spline { of, from, to } => (of.nearest(q, 0.0, 1.0) - from) / (to - from),
251        }
252    }
253
254    /// Where along it — in `[0, 1]` — it comes nearest `q`.
255    fn nearest(&self, q: P2) -> f64 {
256        match self {
257            Shape::Plain(p) => p.nearest(q),
258            Shape::Spline { of, from, to } => {
259                (of.nearest(q, from.min(*to), from.max(*to)) - from) / (to - from)
260            }
261        }
262    }
263
264    fn part(&self, u0: f64, u1: f64) -> Shape {
265        match self {
266            Shape::Plain(p) => Shape::Plain(p.part(u0, u1)),
267            Shape::Spline { of, from, to } => Shape::Spline {
268                of: of.clone(),
269                from: Self::w(*from, *to, u0),
270                to: Self::w(*from, *to, u1),
271            },
272        }
273    }
274
275    /// The whole line or circle it lies on, or the whole spline.
276    fn full(&self) -> Shape {
277        match self {
278            Shape::Plain(p) => Shape::Plain(p.full()),
279            Shape::Spline { of, .. } => Shape::Spline {
280                of: of.clone(),
281                from: 0.0,
282                to: 1.0,
283            },
284        }
285    }
286
287    /// Points along it, for finding where it crosses a spline: an
288    /// infinite line as a segment far longer than anything near it.
289    fn polyline(&self) -> Vec<P2> {
290        match self {
291            Shape::Plain(Plain::Line {
292                a,
293                b,
294                infinite: true,
295            }) => {
296                let far = scale(sub(*b, *a), 1e6);
297                vec![sub(*a, far), add(*b, far)]
298            }
299            Shape::Plain(p) => p.polyline(0.0, 1.0),
300            Shape::Spline { of, from, to } => {
301                let (lo, hi) = (from.min(*to), from.max(*to));
302                let n = SAMPLES as f64;
303                let mut ws = vec![lo];
304                ws.extend(
305                    ((lo * n).floor() as usize + 1..=(hi * n).ceil() as usize)
306                        .map(|k| k as f64 / n)
307                        .filter(|&w| w > lo && w < hi),
308                );
309                ws.push(hi);
310                ws.into_iter()
311                    .map(|w| of.at(w).unwrap_or(of.samples[0]))
312                    .collect()
313            }
314        }
315    }
316
317    /// Where it and `other` cross, kept where both run: on a spline,
318    /// found on the samples and refined on the curves.
319    fn crossings(&self, other: &Shape) -> Vec<P2> {
320        let (Shape::Plain(a), Shape::Plain(b)) = (self, other) else {
321            let (p, q) = (self.polyline(), other.polyline());
322            let mut found = Vec::new();
323            for s in p.windows(2) {
324                for t in q.windows(2) {
325                    if let Some(x) = segments_cross(s[0], s[1], t[0], t[1]) {
326                        found.push(refined(self, other, x));
327                    }
328                }
329            }
330            return found;
331        };
332        a.crossings(b, false)
333    }
334}
335
336/// Where `a` and `b` cross near `x`: Newton on the two curves, each
337/// stepped along its tangent.
338fn refined(a: &Shape, b: &Shape, x: P2) -> P2 {
339    let (mut u, mut v) = (a.param(x), b.param(x));
340    let h = 1e-7;
341    for _ in 0..16 {
342        let (p, q) = (a.at(u), b.at(v));
343        let da = scale(sub(a.at(u + h), a.at(u - h)), 0.5 / h);
344        let db = scale(sub(b.at(v + h), b.at(v - h)), 0.5 / h);
345        // p + da du = q + db dv.
346        let r = sub(q, p);
347        let det = cross(da, db);
348        if det == 0.0 {
349            break;
350        }
351        let (du, dv) = (cross(r, db) / det, cross(r, da) / det);
352        if du == 0.0 && dv == 0.0 {
353            break;
354        }
355        (u, v) = (u + du, v + dv);
356    }
357    a.at(u)
358}
359
360/// A piece of a chain, along the chain's direction, in plain numbers,
361/// moved `d` to its left.
362fn oriented(sketch: &Sketch, curve: CurveId, reversed: bool, d: f64) -> GeopResult<Shape> {
363    let plain = match &sketch.curve(curve)?.kind {
364        CurveKind::Line { start, end } => {
365            let (a, b) = (xy(sketch, *start), xy(sketch, *end));
366            let (a, b) = if reversed { (b, a) } else { (a, b) };
367            Plain::Line {
368                a,
369                b,
370                infinite: false,
371            }
372        }
373        CurveKind::Arc { start, end, sweep } => {
374            let (s, e, sweep) = (xy(sketch, *start), xy(sketch, *end), sweep.to_f64());
375            let arc = if reversed {
376                Plain::arc(e, s, -sweep)
377            } else {
378                Plain::arc(s, e, sweep)
379            };
380            arc.ok_or_else(|| {
381                GeopError::new(format!("arc {curve} is too straight to have a center"))
382            })?
383        }
384        CurveKind::Circle { center, radius } => Plain::Round {
385            center: xy(sketch, *center),
386            radius: radius.to_f64(),
387            span: None,
388        },
389        CurveKind::Spline {
390            control_points,
391            shape,
392        } => {
393            if shape.as_ref().is_some_and(|s| s.degree > 3) {
394                return Err(GeopError::new(format!(
395                    "spline {curve} is of degree {}: only splines of degree three or less can be offset",
396                    shape.as_ref().map_or(0, |s| s.degree)
397                )));
398            }
399            let points: Vec<Vector2> = control_points
400                .iter()
401                .map(|p| sketch.points[p].xy())
402                .collect();
403            let spline = spline_nurbs(&points, shape.as_ref())?;
404            let of = SplineOffset::new(spline, reversed, d)?;
405            // Past a center of curvature the offset turns back on itself.
406            for k in 0..SAMPLES {
407                let w = (k as f64 + 0.5) / SAMPLES as f64;
408                let along = sub(of.samples[k + 1], of.samples[k]);
409                if dot(along, of.tangent(w)?) <= 0.0 {
410                    return Err(GeopError::new(format!(
411                        "spline {curve} cannot be offset by {d}: it bends tighter than that, and its offset would turn back on itself"
412                    )));
413                }
414            }
415            return Ok(Shape::Spline {
416                of: Rc::new(of),
417                from: 0.0,
418                to: 1.0,
419            });
420        }
421        CurveKind::Text { .. } => {
422            return Err(GeopError::new(format!(
423                "text {curve} cannot be offset: its outlines are the font's, set again where it is placed"
424            )));
425        }
426    };
427    if d == 0.0 {
428        return Ok(Shape::Plain(plain));
429    }
430    offset_plain(plain, d).map(Shape::Plain).ok_or_else(|| {
431        GeopError::new(format!(
432            "{curve} cannot be offset by {d} towards its center: it is smaller than that"
433        ))
434    })
435}
436
437/// `piece`'s offset by `d` to its left: none for an arc or circle offset
438/// past its center.
439fn offset_plain(piece: Plain, d: f64) -> Option<Plain> {
440    Some(match piece {
441        Plain::Line { a, b, infinite } => {
442            let n = scale(perp(unit(sub(b, a))?), d);
443            Plain::Line {
444                a: add(a, n),
445                b: add(b, n),
446                infinite,
447            }
448        }
449        Plain::Round {
450            center,
451            radius,
452            span,
453        } => {
454            // To the left is towards the center going counter-clockwise.
455            let turn = span.map_or(1.0, |(_, sweep)| sweep.signum());
456            let radius = radius - turn * d;
457            if radius <= 0.0 {
458                return None;
459            }
460            Plain::Round {
461                center,
462                radius,
463                span,
464            }
465        }
466    })
467}
468
469/// Where along `plain` — as [`Plain::param`], but going on past its ends
470/// rather than round — the point `q` of its full line or circle is,
471/// reckoned from its start for `from_start`, else from its end.
472fn unwrapped(plain: &Plain, q: P2, from_start: bool) -> f64 {
473    match *plain {
474        Plain::Line { .. } => plain.param(q),
475        Plain::Round {
476            center,
477            span: Some((start, sweep)),
478            ..
479        } => {
480            let r = sub(q, center);
481            let at = r[1].atan2(r[0]);
482            let base = if from_start { start } else { start + sweep };
483            let off = wrap(at - base) * sweep.signum() / sweep.abs();
484            if from_start { off } else { 1.0 + off }
485        }
486        Plain::Round { span: None, .. } => 0.0,
487    }
488}
489
490/// A spline fitted to an offset: its control points, and where it passes
491/// through the offset — its own parameter there, and the offset spline's.
492type Fit = (Vec<P2>, Vec<(f64, f64)>);
493
494/// A cubic spline fitted to the offset by `d` to its left of `spline`
495/// from `t0` to `t1` of its domain: its control points, and the
496/// parameters at which it passes through the offset, each with the
497/// spline's parameter there — as many as it takes to come within
498/// `tolerance` of the offset everywhere (see the module docs).
499fn fitted(
500    spline: &NurbCurve2D,
501    d: f64,
502    (t0, t1): (f64, f64),
503    tolerance: f64,
504    first: usize,
505) -> GeopResult<Fit> {
506    let mut m = first.max(4);
507    loop {
508        let knots = cubic_knots(m)?;
509        let breaks: Vec<f64> = knots.iter().map(|k| k.to_f64()).collect();
510        let spline_breaks: Vec<f64> = spline.breakpoints().iter().map(|k| k.to_f64()).collect();
511        // Where it passes through the offset: its ends, and the Greville
512        // points between, moved off any knot of either, where a point of a
513        // spline depends on more control points than in a span.
514        let mut at: Vec<(f64, f64)> = Vec::with_capacity(m);
515        for k in 0..m {
516            let mut s = (breaks[k + 1] + breaks[k + 2] + breaks[k + 3]) / 3.0;
517            if k > 0 && k + 1 < m {
518                let gap = 1.0 / (m - 3) as f64 / 8.0;
519                let on_knot = |s: f64| {
520                    let t = t0 + s * (t1 - t0);
521                    breaks.iter().any(|&b| (b - s).abs() < gap)
522                        || spline_breaks
523                            .iter()
524                            .any(|&b| (b - t).abs() < gap * (t1 - t0).abs())
525                };
526                for nudge in [0.0, 1.0, -1.0, 2.0, -2.0] {
527                    if !on_knot(s + nudge * gap) {
528                        s += nudge * gap;
529                        break;
530                    }
531                }
532            }
533            at.push((s, t0 + s * (t1 - t0)));
534        }
535        let target = |t: f64| -> GeopResult<P2> {
536            let t = Scalar::from_f64(t);
537            let p = plain::of(&spline.evaluate(t)?);
538            let n = perp(
539                unit(plain::of(&spline.tangent(t)?))
540                    .ok_or_else(|| GeopError::new("a spline with no tangent"))?,
541            );
542            Ok(add(p, scale(n, d)))
543        };
544        // The collocation system: row k the basis at `at[k].0`.
545        let mut rows: Vec<Vec<f64>> = Vec::with_capacity(m);
546        for &(s, _) in &at {
547            let mut row = vec![0.0; m];
548            for (j, value, _) in basis_at(m, None, Scalar::from_f64(s), false)? {
549                row[j] = value.to_f64();
550            }
551            rows.push(row);
552        }
553        let targets: Vec<P2> = at
554            .iter()
555            .map(|&(_, t)| target(t))
556            .collect::<GeopResult<_>>()?;
557        let control = solve_dense(rows, targets)?;
558        let fit = spline_nurbs(
559            &control
560                .iter()
561                .map(|p| Vector2::from_array(p.map(Scalar::from_f64)))
562                .collect::<Vec<_>>(),
563            None,
564        )?;
565        let miss = spline.offset_miss(
566            Scalar::from_f64(d),
567            (Scalar::from_f64(t0), Scalar::from_f64(t1)),
568            &fit,
569            Scalar::from_f64(tolerance),
570        )?;
571        if !miss.definitely_greater(Scalar::from_f64(tolerance)) {
572            return Ok((control, at));
573        }
574        if 2 * m > MOST_FIT_POINTS {
575            return Err(GeopError::new(format!(
576                "a spline's offset by {d} is not fitted within {tolerance} by {m} control points: it is {miss:?} off"
577            )));
578        }
579        m *= 2;
580    }
581}
582
583/// The clamped uniform cubic knots of `m` control points, as a drawn
584/// spline has them.
585fn cubic_knots(m: usize) -> GeopResult<Vec<Scalar>> {
586    let spans = m - 3;
587    let mut knots = vec![Scalar::ZERO; 4];
588    for i in 1..spans {
589        knots.push(Scalar::from_ratio(i as i64, spans as i64)?);
590    }
591    knots.extend([Scalar::ONE; 4]);
592    Ok(knots)
593}
594
595/// `x` with `rows · x = targets`, by elimination with partial pivoting.
596fn solve_dense(mut rows: Vec<Vec<f64>>, mut targets: Vec<P2>) -> GeopResult<Vec<P2>> {
597    let m = rows.len();
598    for c in 0..m {
599        let pivot = (c..m)
600            .max_by(|&a, &b| rows[a][c].abs().total_cmp(&rows[b][c].abs()))
601            .expect("a row");
602        if rows[pivot][c] == 0.0 {
603            return Err(GeopError::new("the fit's collocation system is singular"));
604        }
605        rows.swap(c, pivot);
606        targets.swap(c, pivot);
607        for r in 0..m {
608            if r != c && rows[r][c] != 0.0 {
609                let f = rows[r][c] / rows[c][c];
610                let pivot_row = rows[c].clone();
611                for (x, p) in rows[r][c..].iter_mut().zip(&pivot_row[c..]) {
612                    *x -= f * p;
613                }
614                targets[r] = sub(targets[r], scale(targets[c], f));
615            }
616        }
617    }
618    Ok((0..m)
619        .map(|c| scale(targets[c], 1.0 / rows[c][c]))
620        .collect())
621}
622
623/// Per chain of `chains`, the distance its offset goes to its own left:
624/// the first's `d`, and each other's into the same area as the first's —
625/// of two closed chains one inside the other (an outline and a hole) one
626/// inward and the other outward, as they run the same way round or not;
627/// of two side by side both inward or both outward. An open chain goes to
628/// its own left as the first does.
629fn sides(sketch: &Sketch, chains: &[Chain], d: f64) -> GeopResult<Vec<f64>> {
630    // Each closed chain as a polygon of its pieces as drawn.
631    let mut polygons: Vec<Option<Vec<P2>>> = Vec::new();
632    for chain in chains {
633        if !chain.closed {
634            polygons.push(None);
635            continue;
636        }
637        let mut points = Vec::new();
638        for e in &chain.edges {
639            let piece = oriented(sketch, e.curve, e.reversed, 0.0)?;
640            for k in 0..32 {
641                points.push(piece.at(k as f64 / 32.0));
642            }
643        }
644        polygons.push(Some(points));
645    }
646    let area = |q: &[P2]| {
647        (0..q.len())
648            .map(|k| cross(q[k], q[(k + 1) % q.len()]))
649            .sum::<f64>()
650    };
651    let inside = |p: P2, q: &[P2]| {
652        let mut odd = false;
653        for k in 0..q.len() {
654            let (a, b) = (q[k], q[(k + 1) % q.len()]);
655            if (a[1] > p[1]) != (b[1] > p[1])
656                && p[0] < a[0] + (p[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0])
657            {
658                odd = !odd;
659            }
660        }
661        odd
662    };
663    // How many of the other closed chains a closed one lies inside.
664    let depth = |c: usize| -> usize {
665        let Some(q) = &polygons[c] else {
666            return 0;
667        };
668        (0..chains.len())
669            .filter(|&o| o != c)
670            .filter(|&o| polygons[o].as_ref().is_some_and(|r| inside(q[0], r)))
671            .count()
672    };
673    let (Some(first), base) = (&polygons[0], depth(0)) else {
674        return Ok(vec![d; chains.len()]);
675    };
676    let turn = area(first).signum();
677    Ok((0..chains.len())
678        .map(|c| match &polygons[c] {
679            Some(q) if c > 0 => {
680                let same = area(q).signum() == turn;
681                let nested = (depth(c) + base) % 2 == 1;
682                if same != nested { d } else { -d }
683            }
684            _ => d,
685        })
686        .collect())
687}
688
689impl Sketch {
690    /// How far `p` is from the chain `chain`: positive to its left.
691    pub fn chain_side(&self, chain: &Chain, p: P2) -> GeopResult<f64> {
692        let mut best: Option<(f64, f64)> = None;
693        for edge in &chain.edges {
694            let piece = oriented(self, edge.curve, edge.reversed, 0.0)?;
695            let t = piece.nearest(p);
696            let q = piece.at(t);
697            let d = distance(p, q);
698            if best.is_none_or(|(b, _)| d < b) {
699                let side = cross(piece.tangent(t), sub(p, q)).signum();
700                best = Some((d, side * d));
701            }
702        }
703        Ok(best.map_or(0.0, |(_, signed)| signed))
704    }
705
706    /// Offsets the chains `curves` (see [`Sketch::chains`]) by `distance`
707    /// — the first to its left, to its right if negative, and every other
708    /// one into the same area: away from the first where it lies inside or
709    /// round it, as a hole's or an outline's — its corners as `corners`
710    /// says (see the module docs). Returns the curves made, and the
711    /// dimension of the distance.
712    pub fn offset(
713        &mut self,
714        curves: &[CurveId],
715        distance: Scalar,
716        corners: Corners,
717    ) -> GeopResult<Offsetted> {
718        let chains = self.chains(curves)?;
719        let d = distance.to_f64();
720        if d == 0.0 || !d.is_finite() {
721            return Err(GeopError::new(format!("cannot offset by {d}")));
722        }
723        let value = distance.abs();
724
725        // A circle alone: one about the same center.
726        if let [chain] = &chains[..]
727            && let [edge] = &chain.edges[..]
728            && let CurveKind::Circle { center, radius } = self.curves[&edge.curve].kind
729        {
730            let c = edge.curve;
731            let r = radius.to_f64() - d;
732            if r <= 0.0 {
733                return Err(GeopError::new(format!(
734                    "{c} cannot be offset by {d} towards its center: it is smaller than that"
735                )));
736            }
737            let circle = self.add_circle(center, Scalar::from_f64(r));
738            let distance = self.constrain(Constraint::Offset {
739                a: c,
740                b: circle,
741                value,
742            });
743            return Ok(Offsetted {
744                curves: vec![circle],
745                distance,
746            });
747        }
748        // Each chain's distance to its own left: into the area the first
749        // one's offset goes into.
750        let towards = sides(self, &chains, d)?;
751        // Each run the way that has that area on the same side as the
752        // first's: then the offsets, cut where they cross, run on from one
753        // chain's into another's.
754        let chains: Vec<Chain> = (chains.into_iter().zip(&towards))
755            .map(|(chain, &side)| {
756                if side.signum() == d.signum() {
757                    return chain;
758                }
759                Chain {
760                    edges: (chain.edges.into_iter().rev())
761                        .map(|e| ProfileEdge {
762                            reversed: !e.reversed,
763                            ..e
764                        })
765                        .collect(),
766                    closed: chain.closed,
767                }
768            })
769            .collect();
770        let towards = vec![d; chains.len()];
771        // Circles apart: whole, about their centers.
772        let (rings, chains): (Vec<_>, Vec<_>) = chains.into_iter().zip(towards).partition(
773            |(chain, _)| {
774                matches!(&chain.edges[..], [edge] if matches!(self.curves[&edge.curve].kind, CurveKind::Circle { .. }))
775            },
776        );
777        let mut pieces_of: Vec<(CurveId, bool)> = Vec::new();
778        let mut along: Vec<f64> = Vec::new();
779        let mut chain_of: Vec<(usize, usize, bool)> = Vec::new();
780        for (chain, d) in &chains {
781            let start = pieces_of.len();
782            for e in &chain.edges {
783                pieces_of.push((e.curve, e.reversed));
784                along.push(*d);
785                chain_of.push((start, chain.edges.len(), chain.closed));
786            }
787        }
788        let n = pieces_of.len();
789        // The piece after `i` in its chain, and before.
790        let next_piece = |i: usize| -> Option<usize> {
791            let (start, len, closed) = chain_of[i];
792            if i + 1 < start + len {
793                Some(i + 1)
794            } else {
795                closed.then_some(start)
796            }
797        };
798        let previous_piece = |i: usize| -> Option<usize> {
799            let (start, len, closed) = chain_of[i];
800            if i > start {
801                Some(i - 1)
802            } else {
803                closed.then_some(start + len - 1)
804            }
805        };
806        // The finest the solver tells points apart, in this sketch: one
807        // part in `RELATIVE_TOLERANCE` of its size.
808        let size = {
809            let size = bounds(self.points.keys().map(|&p| xy(self, p)))
810                .map_or(0.0, |(lo, hi)| (hi[0] - lo[0]).hypot(hi[1] - lo[1]));
811            // As the solver: one, where the sketch has no size at all.
812            if size > 0.0 { size } else { 1.0 }
813        };
814        let resolution = size / RELATIVE_TOLERANCE as f64;
815        let pieces: Vec<Shape> = pieces_of
816            .iter()
817            .map(|&(c, r)| oriented(self, c, r, 0.0))
818            .collect::<GeopResult<_>>()?;
819        let offsets: Vec<Shape> = (pieces_of.iter().zip(&along))
820            .map(|(&(c, r), &d)| oriented(self, c, r, d))
821            .collect::<GeopResult<_>>()?;
822        // The circles and their offsets.
823        let mut circles: Vec<(CurveId, Shape, Shape)> = Vec::new();
824        for (chain, d) in &rings {
825            // A circle runs counter-clockwise; run the other way, its left
826            // is outward.
827            let c = chain.edges[0].curve;
828            let d = if chain.edges[0].reversed { -*d } else { *d };
829            circles.push((
830                c,
831                oriented(self, c, false, 0.0)?,
832                oriented(self, c, false, d)?,
833            ));
834        }
835        let spline = |i: usize| matches!(pieces[i], Shape::Spline { .. });
836        // Each joint: where the offsets of the pieces before and after it
837        // end and start, and what joins them.
838        enum Joint {
839            /// They meet at one point, held there as `by` says.
840            Shared { at: P2, by: Held },
841            /// An arc round the corner, turning by `sweep`.
842            Round { from: P2, to: P2, sweep: f64 },
843        }
844        /// What keeps offsets meeting at one point at one distance.
845        #[derive(PartialEq)]
846        enum Held {
847            /// They run on smoothly: tangent there too.
848            Tangent,
849            /// A construction circle about the corner touching both.
850            Circle,
851            /// They run on straight: the point square to the corner, as an
852            /// open end is.
853            Square,
854        }
855        let mut joint_of: Vec<Option<Joint>> = Vec::new();
856        for i in 0..n {
857            let Some(j) = next_piece(i) else {
858                joint_of.push(None);
859                continue;
860            };
861            let d = along[i];
862            let (ci, cj) = (pieces_of[i].0, pieces_of[j].0);
863            let p = pieces[i].at(1.0);
864            let (ti, tj) = (pieces[i].tangent(1.0), pieces[j].tangent(0.0));
865            let normal = perp(ti);
866            let smooth = self.tangent(ci, cj);
867            if smooth && dot(ti, tj) < 0.0 {
868                return Err(GeopError::new(format!(
869                    "{ci} and {cj} turn back on each other where they meet: their offsets would cross"
870                )));
871            }
872            let turn = cross(ti, tj);
873            // How far apart the offsets end at the corner: no further than
874            // the solver tells points apart, and there is no corner to
875            // speak of — two lines of a mirrored outline meeting on the
876            // mirror line, say.
877            let (from, to) = (add(p, scale(normal, d)), add(p, scale(perp(tj), d)));
878            let straight = plain::distance(from, to) <= resolution && dot(ti, tj) > 0.0;
879            let lines = matches!(
880                (&offsets[i], &offsets[j]),
881                (
882                    Shape::Plain(Plain::Line { .. }),
883                    Shape::Plain(Plain::Line { .. })
884                )
885            );
886            joint_of.push(Some(if smooth {
887                Joint::Shared {
888                    at: from,
889                    by: Held::Tangent,
890                }
891            } else if straight {
892                Joint::Shared {
893                    at: from,
894                    by: Held::Square,
895                }
896            } else if turn * d < 0.0 && corners == Corners::Round {
897                Joint::Round {
898                    from,
899                    to,
900                    sweep: angle_between(ti, tj),
901                }
902            } else if lines {
903                // Where the offset lines, extended, meet: along the
904                // corner's bisector, well defined however small the turn.
905                if dot(ti, tj) <= -1.0 {
906                    return Err(GeopError::new(format!(
907                        "{ci} and {cj} fold back on each other where they meet: their offsets would cross"
908                    )));
909                }
910                Joint::Shared {
911                    at: add(p, scale(add(normal, perp(tj)), d / (1.0 + dot(ti, tj)))),
912                    by: Held::Circle,
913                }
914            } else {
915                // Where the offsets, extended, meet — the crossing nearest
916                // the corner's own offset.
917                let near = scale(add(from, to), 0.5);
918                let crossings = offsets[i].full().crossings(&offsets[j].full());
919                let Some(at) = crossings
920                    .into_iter()
921                    .min_by(|a, b| plain::distance(*a, near).total_cmp(&plain::distance(*b, near)))
922                else {
923                    return Err(GeopError::new(format!(
924                        "the offsets of {ci} and {cj} by {d} do not meet: round their corner instead"
925                    )));
926                };
927                Joint::Shared {
928                    at,
929                    by: Held::Circle,
930                }
931            }));
932        }
933        // Whether the offsets meet where they cross, cut back or carried on
934        // to there, at the joint `k`: not at the offset of a corner.
935        let crossed = |k: usize| {
936            matches!(
937                joint_of[k],
938                Some(Joint::Shared {
939                    by: Held::Circle,
940                    ..
941                })
942            )
943        };
944        let start_crossed = |i: usize| previous_piece(i).is_some_and(crossed);
945        let end_crossed = crossed;
946
947        // Where each offset piece starts and ends.
948        let start_of = |i: usize| -> P2 {
949            match previous_piece(i).and_then(|k| joint_of[k].as_ref()) {
950                None => offsets[i].at(0.0),
951                Some(Joint::Shared { at, .. }) => *at,
952                Some(Joint::Round { to, .. }) => *to,
953            }
954        };
955        let end_of = |i: usize| -> P2 {
956            match &joint_of[i] {
957                None => offsets[i].at(1.0),
958                Some(Joint::Shared { at, .. }) => *at,
959                Some(Joint::Round { from, .. }) => *from,
960            }
961        };
962        // The offset drawn round the chain, before it is trimmed: each
963        // piece's offset between where it starts and ends — the wrong way
964        // round where the piece is too short for its corners — and the arcs
965        // round the corners between them, in order.
966        let mut path: Vec<(Shape, Of)> = Vec::new();
967        // The element after each along its chain's offset.
968        let mut path_next: Vec<Option<usize>> = Vec::new();
969        let mut chain_start = 0;
970        for (i, offset) in offsets.iter().enumerate() {
971            if chain_of[i].0 == i {
972                chain_start = path.len();
973            }
974            let (s, e) = (start_of(i), end_of(i));
975            let piece = match offset {
976                Shape::Plain(Plain::Line { .. }) => {
977                    (plain::distance(s, e) > resolution).then_some(Shape::Plain(Plain::Line {
978                        a: s,
979                        b: e,
980                        infinite: false,
981                    }))
982                }
983                Shape::Plain(
984                    plain @ Plain::Round {
985                        span: Some((_, sweep)),
986                        ..
987                    },
988                ) => {
989                    let (u, v) = (unwrapped(plain, s, true), unwrapped(plain, e, false));
990                    Plain::arc(s, e, sweep * (v - u)).map(Shape::Plain)
991                }
992                Shape::Spline { of, .. } => {
993                    // At the offset of the spline's own end, exactly that.
994                    let from = if start_crossed(i) {
995                        of.nearest(s, 0.0, 1.0)
996                    } else {
997                        0.0
998                    };
999                    let to = if end_crossed(i) {
1000                        of.nearest(e, 0.0, 1.0)
1001                    } else {
1002                        1.0
1003                    };
1004                    (plain::distance(s, e) > resolution).then(|| Shape::Spline {
1005                        of: of.clone(),
1006                        from,
1007                        to,
1008                    })
1009                }
1010                _ => None,
1011            };
1012            // A piece of no length between its corners leaves its
1013            // neighbours meeting there.
1014            if let Some(piece) = piece {
1015                path.push((piece, Of::Piece(i)));
1016            }
1017            if let Some(Joint::Round { from, to, sweep }) = &joint_of[i]
1018                && let Some(arc) = Plain::arc(*from, *to, *sweep)
1019            {
1020                path.push((Shape::Plain(arc), Of::Round(i)));
1021            }
1022            let (start, len, closed) = chain_of[i];
1023            if i + 1 == start + len {
1024                for k in chain_start..path.len() {
1025                    path_next.push(if k + 1 < path.len() {
1026                        Some(k + 1)
1027                    } else {
1028                        (closed && path.len() > chain_start).then_some(chain_start)
1029                    });
1030                }
1031            }
1032        }
1033        let mut path_previous: Vec<Option<usize>> = vec![None; path.len()];
1034        for (k, next) in path_next.iter().enumerate() {
1035            if let Some(next) = *next {
1036                path_previous[next] = Some(k);
1037            }
1038        }
1039        // A circle's offset is kept whole: one that another offset crosses
1040        // would be cut, which is not supported.
1041        for (k, (c, _, ring)) in circles.iter().enumerate() {
1042            let crossed = path
1043                .iter()
1044                .any(|(shape, _)| !shape.crossings(ring).is_empty())
1045                || (circles.iter().enumerate())
1046                    .any(|(j, (_, _, other))| j != k && !ring.crossings(other).is_empty());
1047            if crossed {
1048                return Err(GeopError::new(format!(
1049                    "the offset of circle {c} by {d} would cross another offset: cutting a circle's offset is not supported"
1050                )));
1051            }
1052        }
1053
1054        // Cut where it crosses itself, and keep what is no nearer the chain
1055        // than the distance: what a piece too short for its corners, or
1056        // offsets running into each other, leave of the offset.
1057        let near_end = |shape: &Shape, q: P2| {
1058            plain::distance(shape.at(0.0), q) <= resolution
1059                || plain::distance(shape.at(1.0), q) <= resolution
1060        };
1061        let to_chain = |q: P2| {
1062            (pieces.iter().chain(circles.iter().map(|(_, c, _)| c)))
1063                .map(|p| plain::distance(q, p.at(p.nearest(q))))
1064                .fold(f64::INFINITY, f64::min)
1065        };
1066        let mut subs: Vec<Sub> = Vec::new();
1067        for (k, (shape, _)) in path.iter().enumerate() {
1068            let mut cuts: Vec<f64> = Vec::new();
1069            for (j, (other, _)) in path.iter().enumerate() {
1070                if j != k {
1071                    cuts.extend(
1072                        shape
1073                            .crossings(other)
1074                            .into_iter()
1075                            .filter(|&q| !near_end(shape, q))
1076                            .map(|q| shape.param(q))
1077                            .filter(|u| (0.0..=1.0).contains(u)),
1078                    );
1079                }
1080            }
1081            cuts.sort_by(f64::total_cmp);
1082            cuts.dedup_by(|a, b| plain::distance(shape.at(*a), shape.at(*b)) <= resolution);
1083            let ts: Vec<f64> = std::iter::once(0.0)
1084                .chain(cuts)
1085                .chain(std::iter::once(1.0))
1086                .collect();
1087            for w in ts.windows(2) {
1088                let part = shape.part(w[0], w[1]);
1089                if to_chain(part.at(0.5)) > d.abs() - resolution {
1090                    subs.push(Sub {
1091                        elem: k,
1092                        shape: part,
1093                        from_start: w[0] == 0.0,
1094                        to_end: w[1] == 1.0,
1095                    });
1096                }
1097            }
1098        }
1099        if subs.is_empty() && circles.is_empty() {
1100            return Err(GeopError::new(format!(
1101                "the offset by {d} leaves nothing: the chain is too short or narrow for it"
1102            )));
1103        }
1104
1105        // Each part on to the one starting where it ends: the next of its
1106        // piece's own along the path where it reaches its end, else the one
1107        // starting at the crossing it was cut at.
1108        let mut next: Vec<Option<usize>> = vec![None; subs.len()];
1109        for a in 0..subs.len() {
1110            let end = subs[a].shape.at(1.0);
1111            let open_end = subs[a].to_end && path_next[subs[a].elem].is_none();
1112            let found: Vec<usize> = (0..subs.len())
1113                .filter(|&b| {
1114                    let joined = if subs[a].to_end {
1115                        subs[b].from_start && Some(subs[b].elem) == path_next[subs[a].elem]
1116                    } else {
1117                        !subs[b].from_start
1118                    };
1119                    b != a && joined && plain::distance(subs[b].shape.at(0.0), end) <= resolution
1120                })
1121                .collect();
1122            match (found.as_slice(), open_end) {
1123                (&[b], false) => next[a] = Some(b),
1124                ([], true) => {}
1125                _ => {
1126                    return Err(GeopError::new(format!(
1127                        "the offset by {d}, trimmed where it crosses itself, does not run on from {end:?} as one curve"
1128                    )));
1129                }
1130            }
1131        }
1132        let mut previous: Vec<Option<usize>> = vec![None; subs.len()];
1133        for (a, b) in next.iter().enumerate() {
1134            if let Some(b) = *b {
1135                if previous[b].is_some() {
1136                    return Err(GeopError::new(format!(
1137                        "the offset by {d}, trimmed where it crosses itself, runs into itself at {:?}",
1138                        subs[b].shape.at(0.0)
1139                    )));
1140                }
1141                previous[b] = Some(a);
1142            }
1143        }
1144        // What joins a part to the next across a corner of the chain, not
1145        // a crossing: the joint after the piece or arc it is of.
1146        let joint_after = |a: usize| -> Option<usize> {
1147            if !subs[a].to_end {
1148                return None;
1149            }
1150            match path[subs[a].elem].1 {
1151                Of::Piece(i) | Of::Round(i) => Some(i),
1152            }
1153        };
1154        // Whether the part `a` is of a spline's offset.
1155        let of_spline = |a: usize| matches!(path[subs[a].elem].1, Of::Piece(i) if spline(i));
1156
1157        // Built: a point where each part ends, shared with the next; then
1158        // the parts, each tied to its source; then what joins them.
1159        let mut start_point: Vec<Option<PointId>> = vec![None; subs.len()];
1160        let mut end_point: Vec<Option<PointId>> = vec![None; subs.len()];
1161        for a in 0..subs.len() {
1162            let p = self.new_point_at(subs[a].shape.at(1.0));
1163            end_point[a] = Some(p);
1164            if let Some(b) = next[a] {
1165                start_point[b] = Some(p);
1166            }
1167        }
1168        for a in 0..subs.len() {
1169            if start_point[a].is_none() {
1170                start_point[a] = Some(self.new_point_at(subs[a].shape.at(0.0)));
1171            }
1172        }
1173        let corner_of = |sketch: &Sketch, i: usize| -> PointId {
1174            let (source, reversed) = pieces_of[i];
1175            let (s, e) = sketch.curves[&source].endpoints().expect("an open curve");
1176            if reversed { s } else { e }
1177        };
1178        let first = (0..subs.len()).find(|&a| matches!(path[subs[a].elem].1, Of::Piece(_)));
1179        if first.is_none() && !subs.is_empty() {
1180            return Err(GeopError::new(format!(
1181                "the offset by {d} leaves only arcs round corners: the chain is too short for it"
1182            )));
1183        }
1184        let first_piece = first.map(|first| match path[subs[first].elem].1 {
1185            Of::Piece(i) | Of::Round(i) => i,
1186        });
1187        // A construction circle about the first part's corner, as large as
1188        // the distance: what a spline's offset, and a part cut off from the
1189        // corners, is held at.
1190        let mut reach: Option<CurveId> = None;
1191        if let Some(first_piece) = first_piece
1192            && (0..subs.len()).any(of_spline)
1193        {
1194            let circle = self.add_circle(corner_of(self, first_piece), value);
1195            self.set_construction(circle, true);
1196            reach = Some(circle);
1197        }
1198        let mut curve_of: Vec<CurveId> = Vec::with_capacity(subs.len());
1199        let mut made = Vec::new();
1200        let mut distance = None;
1201        for a in 0..subs.len() {
1202            let (s, e) = (start_point[a].unwrap(), end_point[a].unwrap());
1203            let curve = match path[subs[a].elem].1 {
1204                Of::Piece(i) => {
1205                    let (source, reversed) = pieces_of[i];
1206                    // Each offset runs the way its source does.
1207                    let (s, e) = if reversed { (e, s) } else { (s, e) };
1208                    let curve = match (&self.curves[&source].kind, &subs[a].shape) {
1209                        (CurveKind::Line { .. }, _) => self.add_line(s, e),
1210                        (
1211                            CurveKind::Arc { .. },
1212                            Shape::Plain(Plain::Round {
1213                                span: Some((_, sweep)),
1214                                ..
1215                            }),
1216                        ) => self.add_arc(
1217                            s,
1218                            e,
1219                            Scalar::from_f64(if reversed { -sweep } else { *sweep }),
1220                        ),
1221                        (CurveKind::Spline { .. }, Shape::Spline { of, from, to }) => {
1222                            // Run along the spline, from `ta` to `tb` of it,
1223                            // its ends held across it only where they are
1224                            // at crossings.
1225                            let (ta, tb) = if reversed {
1226                                (of.t(*to), of.t(*from))
1227                            } else {
1228                                (of.t(*from), of.t(*to))
1229                            };
1230                            let ends_pinned = [
1231                                subs[a].from_start && !start_crossed(i),
1232                                subs[a].to_end && !end_crossed(i),
1233                            ];
1234                            let ends_pinned = if reversed {
1235                                [ends_pinned[1], ends_pinned[0]]
1236                            } else {
1237                                ends_pinned
1238                            };
1239                            let dist = if reversed { -along[i] } else { along[i] };
1240                            let tolerance = FIT_TOLERANCE * size;
1241                            let (control, at) = fitted(
1242                                &of.spline,
1243                                dist,
1244                                (ta, tb),
1245                                tolerance,
1246                                4 * of.spline.control_points.len(),
1247                            )?;
1248                            let last = control.len() - 1;
1249                            let mut points = vec![s];
1250                            for p in &control[1..last] {
1251                                points.push(self.new_point_at(*p));
1252                            }
1253                            points.push(e);
1254                            let fit = self.add_spline(points);
1255                            let by = reach.expect("made for a spline's offset");
1256                            for (k, &(on, at)) in at.iter().enumerate() {
1257                                let across =
1258                                    (k == 0 && !ends_pinned[0]) || (k == last && !ends_pinned[1]);
1259                                self.constrain(Constraint::OffsetAt {
1260                                    a: source,
1261                                    at: Scalar::from_f64(at),
1262                                    b: fit,
1263                                    on: Scalar::from_f64(on),
1264                                    by,
1265                                    across,
1266                                });
1267                            }
1268                            fit
1269                        }
1270                        (kind, _) => unreachable!("chained and trimmed as it is: {kind:?}"),
1271                    };
1272                    if distance.is_none() {
1273                        distance = Some(match self.curves[&source].kind {
1274                            CurveKind::Spline { .. } => self.constrain(Constraint::Radius {
1275                                curve: reach.expect("made for a spline's offset"),
1276                                value,
1277                            }),
1278                            _ => self.constrain(Constraint::Offset {
1279                                a: source,
1280                                b: curve,
1281                                value,
1282                            }),
1283                        });
1284                    } else {
1285                        match self.curves[&source].kind {
1286                            CurveKind::Line { .. } => {
1287                                self.constrain(Constraint::Parallel {
1288                                    a: source,
1289                                    b: curve,
1290                                });
1291                            }
1292                            CurveKind::Spline { .. } => {}
1293                            _ => {
1294                                self.constrain(Constraint::Concentric {
1295                                    a: source,
1296                                    b: curve,
1297                                });
1298                            }
1299                        }
1300                    }
1301                    curve
1302                }
1303                Of::Round(i) => {
1304                    let Shape::Plain(Plain::Round {
1305                        span: Some((_, sweep)),
1306                        ..
1307                    }) = subs[a].shape
1308                    else {
1309                        unreachable!("an arc round a corner");
1310                    };
1311                    let arc = self.add_arc(s, e, Scalar::from_f64(sweep));
1312                    let corner = corner_of(self, i);
1313                    self.constrain(Constraint::Center {
1314                        point: corner,
1315                        curve: arc,
1316                    });
1317                    arc
1318                }
1319            };
1320            curve_of.push(curve);
1321            made.push(curve);
1322        }
1323        // The circle the splines' offsets are held at, as large as the
1324        // first part is from its source.
1325        if let (Some(circle), Some(first)) = (reach, first)
1326            && !of_spline(first)
1327        {
1328            self.constrain(Constraint::Tangent {
1329                a: curve_of[first],
1330                b: circle,
1331            });
1332        }
1333        // Across the chain's corners, as the offsets meet there; a part
1334        // joined so to one whose distance is held holds it too. A spline's
1335        // offset ends where its own points hold it: at the offset of the
1336        // corner, or across it where offsets cross, and the other part's
1337        // own distance places it there.
1338        let mut held: Vec<bool> = (0..subs.len()).map(of_spline).collect();
1339        if let Some(first) = first {
1340            held[first] = true;
1341        }
1342        let mut joined: Vec<(usize, usize)> = Vec::new();
1343        for a in 0..subs.len() {
1344            let (Some(b), Some(i)) = (next[a], joint_after(a)) else {
1345                continue;
1346            };
1347            if !subs[b].from_start {
1348                continue;
1349            }
1350            joined.push((a, b));
1351            let (oa, ob) = (curve_of[a], curve_of[b]);
1352            let splines = [of_spline(a), of_spline(b)];
1353            let joint = joint_of[i]
1354                .as_ref()
1355                .expect("a joint after a piece with one next");
1356            match (path[subs[a].elem].1, path[subs[b].elem].1, joint) {
1357                (Of::Piece(_), Of::Piece(_), Joint::Shared { by, .. }) => match by {
1358                    _ if splines == [true, true] => {}
1359                    Held::Circle if splines[0] || splines[1] => {
1360                        let other = if splines[0] { ob } else { oa };
1361                        let corner = corner_of(self, i);
1362                        let circle = self.add_circle(corner, value);
1363                        self.set_construction(circle, true);
1364                        self.constrain(Constraint::Tangent {
1365                            a: other,
1366                            b: circle,
1367                        });
1368                        self.constrain(Constraint::Equal {
1369                            a: circle,
1370                            b: reach.expect("made for a spline's offset"),
1371                        });
1372                    }
1373                    _ if splines[0] || splines[1] => {}
1374                    Held::Tangent => {
1375                        self.constrain(Constraint::Tangent { a: oa, b: ob });
1376                    }
1377                    Held::Square => {
1378                        let corner = corner_of(self, i);
1379                        self.square_end(pieces_of[i].0, corner, end_point[a].unwrap());
1380                    }
1381                    Held::Circle => {
1382                        let corner = corner_of(self, i);
1383                        let circle = self.add_circle(corner, value);
1384                        self.set_construction(circle, true);
1385                        self.constrain(Constraint::Tangent { a: oa, b: circle });
1386                        self.constrain(Constraint::Tangent { a: ob, b: circle });
1387                    }
1388                },
1389                _ if splines[0] || splines[1] => {}
1390                _ => {
1391                    self.constrain(Constraint::Tangent { a: oa, b: ob });
1392                }
1393            }
1394        }
1395        loop {
1396            let mut grew = false;
1397            for &(a, b) in &joined {
1398                if held[a] != held[b] {
1399                    held[a] = true;
1400                    held[b] = true;
1401                    grew = true;
1402                }
1403            }
1404            if !grew {
1405                break;
1406            }
1407        }
1408        // A part cut off from them at crossings holds the distance by a
1409        // circle about its source's corner, touching it, as large as one
1410        // about the first's: one dimension still says how far all are.
1411        for a in 0..subs.len() {
1412            if held[a] {
1413                continue;
1414            }
1415            let (Some(first), Some(first_piece)) = (first, first_piece) else {
1416                unreachable!("a part of a piece is kept");
1417            };
1418            let reference = *reach.get_or_insert_with(|| {
1419                let circle = self.add_circle(corner_of(self, first_piece), value);
1420                self.set_construction(circle, true);
1421                self.constrain(Constraint::Tangent {
1422                    a: curve_of[first],
1423                    b: circle,
1424                });
1425                circle
1426            });
1427            match path[subs[a].elem].1 {
1428                Of::Piece(i) => {
1429                    let circle = self.add_circle(corner_of(self, i), value);
1430                    self.set_construction(circle, true);
1431                    self.constrain(Constraint::Tangent {
1432                        a: curve_of[a],
1433                        b: circle,
1434                    });
1435                    self.constrain(Constraint::Equal {
1436                        a: circle,
1437                        b: reference,
1438                    });
1439                }
1440                Of::Round(_) => {
1441                    self.constrain(Constraint::Equal {
1442                        a: curve_of[a],
1443                        b: reference,
1444                    });
1445                }
1446            }
1447        }
1448        // An open chain's offset ends square to the chain's ends; a
1449        // spline's, held at its end's offset, does already.
1450        {
1451            for a in 0..subs.len() {
1452                let k = subs[a].elem;
1453                let at_start =
1454                    if subs[a].from_start && path_previous[k].is_none() && previous[a].is_none() {
1455                        true
1456                    } else if subs[a].to_end && path_next[k].is_none() && next[a].is_none() {
1457                        false
1458                    } else {
1459                        continue;
1460                    };
1461                let Of::Piece(i) = path[k].1 else {
1462                    continue;
1463                };
1464                if spline(i) {
1465                    continue;
1466                }
1467                let (source, reversed) = pieces_of[i];
1468                let (s, e) = self.curves[&source].endpoints().expect("a line or arc");
1469                let end = if at_start != reversed { s } else { e };
1470                let image = if at_start {
1471                    start_point[a].unwrap()
1472                } else {
1473                    end_point[a].unwrap()
1474                };
1475                self.square_end(source, end, image);
1476            }
1477        }
1478        // Each circle's offset about its center, as far from it as the
1479        // distance: the first dimension, or a construction circle on it,
1480        // level with its center, as large as the one the rest is held at,
1481        // touching the offset.
1482        for (c, _, ring) in &circles {
1483            let Shape::Plain(Plain::Round { center, radius, .. }) = ring else {
1484                unreachable!("a circle's offset is a circle");
1485            };
1486            let CurveKind::Circle {
1487                center: middle,
1488                radius: source,
1489            } = self.curves[c].kind
1490            else {
1491                unreachable!("a circle");
1492            };
1493            let circle = self.add_circle(middle, Scalar::from_f64(*radius));
1494            made.push(circle);
1495            if distance.is_none() {
1496                distance = Some(self.constrain(Constraint::Offset {
1497                    a: *c,
1498                    b: circle,
1499                    value,
1500                }));
1501                continue;
1502            }
1503            let on = self.new_point_at(add(*center, [source.to_f64(), 0.0]));
1504            self.constrain(Constraint::PointOnCurve {
1505                point: on,
1506                curve: *c,
1507            });
1508            let level = self.add_line(middle, on);
1509            self.set_construction(level, true);
1510            self.constrain(Constraint::Horizontal { line: level });
1511            let touching = self.add_circle(on, value);
1512            self.set_construction(touching, true);
1513            self.constrain(Constraint::Tangent {
1514                a: circle,
1515                b: touching,
1516            });
1517            let reference = match reach {
1518                Some(reach) => reach,
1519                None => {
1520                    let first = first.expect("a part kept, its distance given");
1521                    let first_piece = first_piece.expect("a part kept");
1522                    let circle = self.add_circle(corner_of(self, first_piece), value);
1523                    self.set_construction(circle, true);
1524                    self.constrain(Constraint::Tangent {
1525                        a: curve_of[first],
1526                        b: circle,
1527                    });
1528                    reach = Some(circle);
1529                    circle
1530                }
1531            };
1532            self.constrain(Constraint::Equal {
1533                a: touching,
1534                b: reference,
1535            });
1536        }
1537        Ok(Offsetted {
1538            curves: made,
1539            distance: distance.expect("a part of a piece is kept"),
1540        })
1541    }
1542
1543    /// The curves `curves` as chains (see [`Sketch::chain`]): each made of
1544    /// those joined to each other end to end.
1545    pub fn chains(&self, curves: &[CurveId]) -> GeopResult<Vec<Chain>> {
1546        let class = self.point_classes();
1547        let ends = |c: CurveId| -> GeopResult<Vec<PointId>> {
1548            Ok(match self.curve(c)?.endpoints() {
1549                Some((s, e)) => vec![class[&s], class[&e]],
1550                None => Vec::new(),
1551            })
1552        };
1553        let mut left: Vec<CurveId> = Vec::new();
1554        for &c in curves {
1555            if !left.contains(&c) {
1556                left.push(c);
1557            }
1558        }
1559        let mut chains = Vec::new();
1560        while let Some(first) = left.first().copied() {
1561            let mut group = vec![first];
1562            let mut points = ends(first)?;
1563            left.remove(0);
1564            loop {
1565                let mut joined = None;
1566                for (k, &c) in left.iter().enumerate() {
1567                    if ends(c)?.iter().any(|p| points.contains(p)) {
1568                        joined = Some(k);
1569                        break;
1570                    }
1571                }
1572                let Some(k) = joined else {
1573                    break;
1574                };
1575                let c = left.remove(k);
1576                points.extend(ends(c)?);
1577                group.push(c);
1578            }
1579            chains.push(self.chain(&group)?);
1580        }
1581        if chains.is_empty() {
1582            return Err(GeopError::new("no curves to make a chain of"));
1583        }
1584        Ok(chains)
1585    }
1586
1587    /// Keeps `image`, an end of the offset of `source`, square to `source`
1588    /// at its end `end`: on the line through `end` across a line, or
1589    /// through an arc's center.
1590    fn square_end(&mut self, source: CurveId, end: PointId, image: PointId) {
1591        match self.curves[&source].kind {
1592            CurveKind::Line { .. } => {
1593                let across = self.add_line(end, image);
1594                self.set_construction(across, true);
1595                self.constrain(Constraint::Perpendicular {
1596                    a: source,
1597                    b: across,
1598                });
1599            }
1600            _ => {
1601                let Some(Plain::Round { center, .. }) = Plain::of(self, source, false) else {
1602                    return;
1603                };
1604                let m = self.new_point_at(center);
1605                self.constrain(Constraint::Center {
1606                    point: m,
1607                    curve: source,
1608                });
1609                let radial = self.add_line(m, end);
1610                self.set_construction(radial, true);
1611                self.constrain(Constraint::PointOnCurve {
1612                    point: image,
1613                    curve: radial,
1614                });
1615            }
1616        }
1617    }
1618}
1619
1620#[cfg(test)]
1621mod tests {
1622    use super::*;
1623    use std::f64::consts::PI;
1624
1625    fn n(x: f64) -> Scalar {
1626        Scalar::from_f64(x)
1627    }
1628
1629    /// A `w` x `h` rectangle at the origin, counter-clockwise, fully
1630    /// constrained; its lines, and the dimension of its width.
1631    fn rectangle(s: &mut Sketch, w: f64, h: f64) -> (Vec<CurveId>, ConstraintId) {
1632        let p = [[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]].map(|q| s.add_point(n(q[0]), n(q[1])));
1633        let l: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
1634        s.constrain(Constraint::Fix {
1635            point: p[0],
1636            x: n(0.0),
1637            y: n(0.0),
1638        });
1639        s.constrain(Constraint::Horizontal { line: l[0] });
1640        s.constrain(Constraint::Vertical { line: l[1] });
1641        s.constrain(Constraint::Horizontal { line: l[2] });
1642        s.constrain(Constraint::Vertical { line: l[3] });
1643        let width = s.constrain(Constraint::Length {
1644            curve: l[0],
1645            value: n(w),
1646        });
1647        s.constrain(Constraint::Length {
1648            curve: l[1],
1649            value: n(h),
1650        });
1651        (l, width)
1652    }
1653
1654    /// The bounding box of the points of `curves`, as solved.
1655    fn bounds(s: &Sketch, curves: &[CurveId]) -> [f64; 4] {
1656        let points = curves.iter().flat_map(|c| s.curves[c].points());
1657        let (lo, hi) = plain::bounds(points.map(|p| xy(s, p))).unwrap();
1658        [lo[0], lo[1], hi[0], hi[1]]
1659    }
1660
1661    fn near(a: [f64; 4], b: [f64; 4]) -> bool {
1662        a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-7)
1663    }
1664
1665    fn set(s: &mut Sketch, k: ConstraintId, v: f64) {
1666        match s.constraints.get_mut(&k) {
1667            Some(Constraint::Offset { value, .. } | Constraint::Length { value, .. }) => {
1668                *value = n(v)
1669            }
1670            other => panic!("{other:?}"),
1671        }
1672    }
1673
1674    fn lines(s: &Sketch, curves: &[CurveId]) -> Vec<CurveId> {
1675        curves
1676            .iter()
1677            .copied()
1678            .filter(|c| matches!(s.curves[c].kind, CurveKind::Line { .. }))
1679            .collect()
1680    }
1681
1682    /// Outwards, a rectangle's offset rounds its corners: four lines and
1683    /// four arcs about the corners, one distance for all of them, which
1684    /// the rectangle and the distance both drive.
1685    #[test]
1686    fn a_rectangle_offset_outwards_rounds_its_corners() {
1687        let mut s = Sketch::new();
1688        let (rect, width) = rectangle(&mut s, 2.0, 1.0);
1689        // The rectangle runs counter-clockwise: outwards is to its right.
1690        let made = s.offset(&rect, n(-0.25), Corners::Round).unwrap();
1691        assert_eq!(made.curves.len(), 8);
1692        let report = s.solve().unwrap();
1693        assert!(report.converged && report.dof == 0, "{report:?}");
1694        // The rectangle and its offset nest: one ring between them.
1695        assert_eq!(s.regions().unwrap().len(), 1);
1696        let sides = lines(&s, &made.curves);
1697        assert!(
1698            near(bounds(&s, &sides), [-0.25, -0.25, 2.25, 1.25]),
1699            "{:?}",
1700            bounds(&s, &sides)
1701        );
1702        set(&mut s, made.distance, 0.5);
1703        set(&mut s, width, 3.0);
1704        let report = s.solve().unwrap();
1705        assert!(report.converged, "{report:?}");
1706        assert!(
1707            near(bounds(&s, &sides), [-0.5, -0.5, 3.5, 1.5]),
1708            "{:?}",
1709            bounds(&s, &sides)
1710        );
1711        s.enclose().unwrap();
1712    }
1713
1714    /// Inwards, the offsets meet at the corners, held at one distance by
1715    /// construction circles about them; extended outwards, the same.
1716    #[test]
1717    fn offsets_meet_inwards_and_extended() {
1718        for (d, corners, inner, then) in [
1719            (
1720                0.25,
1721                Corners::Round,
1722                [0.25, 0.25, 1.75, 0.75],
1723                [0.125, 0.125, 1.875, 0.875],
1724            ),
1725            (
1726                -0.25,
1727                Corners::Extend,
1728                [-0.25, -0.25, 2.25, 1.25],
1729                [-0.125, -0.125, 2.125, 1.125],
1730            ),
1731        ] {
1732            let mut s = Sketch::new();
1733            let (rect, _) = rectangle(&mut s, 2.0, 1.0);
1734            let made = s.offset(&rect, n(d), corners).unwrap();
1735            assert_eq!(made.curves.len(), 4);
1736            let report = s.solve().unwrap();
1737            assert!(report.converged && report.dof == 0, "{d}: {report:?}");
1738            assert!(
1739                near(bounds(&s, &made.curves), inner),
1740                "{:?}",
1741                bounds(&s, &made.curves)
1742            );
1743            set(&mut s, made.distance, 0.125);
1744            let report = s.solve().unwrap();
1745            assert!(report.converged, "{report:?}");
1746            assert!(
1747                near(bounds(&s, &made.curves), then),
1748                "{:?}",
1749                bounds(&s, &made.curves)
1750            );
1751            // The rectangle and its offset nest: one ring between them.
1752            assert_eq!(s.regions().unwrap().len(), 1);
1753            s.enclose().unwrap();
1754        }
1755    }
1756
1757    /// A slot runs on smoothly all round: its offset is a slot about it.
1758    #[test]
1759    fn a_slot_offsets_into_a_slot() {
1760        let mut s = Sketch::new();
1761        let p =
1762            [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]].map(|q| s.add_point(n(q[0]), n(q[1])));
1763        let bottom = s.add_line(p[0], p[1]);
1764        let right = s.add_arc(p[1], p[2], n(PI));
1765        let top = s.add_line(p[2], p[3]);
1766        let left = s.add_arc(p[3], p[0], n(PI));
1767        for (a, b) in [(bottom, right), (right, top), (top, left), (left, bottom)] {
1768            s.constrain(Constraint::Tangent { a, b });
1769        }
1770        s.constrain(Constraint::Fix {
1771            point: p[0],
1772            x: n(0.0),
1773            y: n(0.0),
1774        });
1775        s.constrain(Constraint::Horizontal { line: bottom });
1776        s.constrain(Constraint::Length {
1777            curve: bottom,
1778            value: n(2.0),
1779        });
1780        s.constrain(Constraint::Radius {
1781            curve: right,
1782            value: n(0.5),
1783        });
1784        s.constrain(Constraint::Equal { a: left, b: right });
1785        assert!(s.solve().unwrap().converged);
1786        let made = s
1787            .offset(&[top, left, bottom, right], n(-0.25), Corners::Round)
1788            .unwrap();
1789        assert_eq!(made.curves.len(), 4);
1790        let report = s.solve().unwrap();
1791        assert!(report.converged && report.dof == 0, "{report:?}");
1792        assert!(
1793            near(bounds(&s, &made.curves), [0.0, -0.25, 2.0, 1.25]),
1794            "{:?}",
1795            bounds(&s, &made.curves)
1796        );
1797        s.enclose().unwrap();
1798    }
1799
1800    /// An open chain's offset ends square to it.
1801    #[test]
1802    fn open_chains_end_square() {
1803        let mut s = Sketch::new();
1804        let p = [[0.0, 0.0], [2.0, 0.0], [3.0, 1.0]].map(|q| s.add_point(n(q[0]), n(q[1])));
1805        let a = s.add_line(p[0], p[1]);
1806        let b = s.add_arc(p[1], p[2], n(PI / 2.0));
1807        s.constrain(Constraint::Tangent { a, b });
1808        for (q, at) in [(p[0], [0.0, 0.0]), (p[1], [2.0, 0.0]), (p[2], [3.0, 1.0])] {
1809            s.constrain(Constraint::Fix {
1810                point: q,
1811                x: n(at[0]),
1812                y: n(at[1]),
1813            });
1814        }
1815        // Inside the bend: to the left.
1816        let made = s.offset(&[b, a], n(0.5), Corners::Round).unwrap();
1817        let report = s.solve().unwrap();
1818        assert!(report.converged && report.dof == 0, "{report:?}");
1819        assert!(
1820            near(bounds(&s, &made.curves), [0.0, 0.5, 2.5, 1.0]),
1821            "{:?}",
1822            bounds(&s, &made.curves)
1823        );
1824        s.enclose().unwrap();
1825    }
1826
1827    /// A circle offsets into one about the same center.
1828    #[test]
1829    fn circles_offset_about_their_center() {
1830        let mut s = Sketch::new();
1831        let c = s.add_point(n(1.0), n(1.0));
1832        let circle = s.add_circle(c, n(1.0));
1833        let made = s.offset(&[circle], n(-0.5), Corners::Round).unwrap();
1834        assert!(matches!(
1835            s.curves[&made.curves[0]].kind,
1836            CurveKind::Circle { center, radius } if center == c && radius.to_f64() == 1.5
1837        ));
1838        assert!(s.solve().unwrap().converged);
1839    }
1840
1841    /// How far the points of the spline `fit` are from the spline
1842    /// `source`, at most and at least: each sampled point's distance to
1843    /// the nearest of a fine sampling of the source, refined.
1844    fn spread(s: &Sketch, source: CurveId, fit: CurveId) -> (f64, f64) {
1845        let shape = |c: CurveId| oriented(s, c, false, 0.0).unwrap();
1846        let (source, fit) = (shape(source), shape(fit));
1847        let mut range = (f64::INFINITY, 0.0f64);
1848        for k in 0..=200 {
1849            let p = fit.at(k as f64 / 200.0);
1850            let q = source.at(source.nearest(p));
1851            let d = plain::distance(p, q);
1852            range = (range.0.min(d), range.1.max(d));
1853        }
1854        range
1855    }
1856
1857    /// An open spline offset: a fitted spline held to it at the distance
1858    /// of a construction circle's radius, the dimension, which it follows,
1859    /// as it follows the spline; it adds no freedom.
1860    #[test]
1861    fn a_spline_offsets_into_a_fitted_spline() {
1862        let mut s = Sketch::new();
1863        let q = [[0.0, 0.0], [1.0, 0.6], [2.0, -0.3], [3.0, 0.4], [4.0, 0.0]]
1864            .map(|q| s.add_point(n(q[0]), n(q[1])));
1865        let spline = s.add_spline(q.to_vec());
1866        let free = s.solve().unwrap().dof;
1867        let made = s.offset(&[spline], n(0.2), Corners::Round).unwrap();
1868        let [fit] = made.curves[..] else {
1869            panic!("{made:?}");
1870        };
1871        assert!(matches!(s.curves[&fit].kind, CurveKind::Spline { .. }));
1872        let report = s.solve().unwrap();
1873        assert!(report.converged && report.dof == free, "{report:?}");
1874        let (lo, hi) = spread(&s, spline, fit);
1875        assert!(lo > 0.2 - 1e-3 && hi < 0.2 + 1e-3, "{lo} {hi}");
1876        // The distance changed, and the spline's end dragged.
1877        match s.constraints.get_mut(&made.distance) {
1878            Some(Constraint::Radius { value, .. }) => *value = n(0.3),
1879            other => panic!("{other:?}"),
1880        }
1881        s.constrain(Constraint::Fix {
1882            point: q[4],
1883            x: n(4.0),
1884            y: n(0.4),
1885        });
1886        let report = s.solve().unwrap();
1887        assert!(report.converged, "{report:?}");
1888        let (lo, hi) = spread(&s, spline, fit);
1889        assert!(lo > 0.3 - 1e-3 && hi < 0.3 + 1e-3, "{lo} {hi}");
1890        s.enclose().unwrap();
1891    }
1892
1893    /// A closed outline of a line and a spline arching over it, offset in
1894    /// and out: in, the offsets cross at the corners and are cut back to
1895    /// where they do, whichever way corners are asked to close; out,
1896    /// rounded. Each time one ring. Out, extended, the line's offset does
1897    /// not reach the spline's, which ends at the spline's end: refused.
1898    #[test]
1899    fn a_line_and_a_spline_offset_in_and_out() {
1900        for (d, corners) in [
1901            (0.1, Corners::Round),
1902            (0.1, Corners::Extend),
1903            (-0.1, Corners::Round),
1904        ] {
1905            let mut s = Sketch::new();
1906            let p = [[0.0, 0.0], [2.0, 0.0], [2.5, 1.0], [1.0, 2.0], [-0.5, 1.0]]
1907                .map(|q| s.add_point(n(q[0]), n(q[1])));
1908            let base = s.add_line(p[0], p[1]);
1909            let arch = s.add_spline(vec![p[1], p[2], p[3], p[4], p[0]]);
1910            let free = s.solve().unwrap().dof;
1911            let made = s
1912                .offset(&[base, arch], n(d), corners)
1913                .unwrap_or_else(|e| panic!("{d} {corners:?}: {e:?}"));
1914            let report = s.solve().unwrap();
1915            assert!(
1916                report.converged && report.dof == free,
1917                "{d} {corners:?}: {report:?}"
1918            );
1919            assert_eq!(s.regions().unwrap().len(), 1, "{d} {corners:?}");
1920            let fit = *made
1921                .curves
1922                .iter()
1923                .find(|c| matches!(s.curves[c].kind, CurveKind::Spline { .. }))
1924                .unwrap();
1925            let (lo, hi) = spread(&s, arch, fit);
1926            assert!(
1927                lo > 0.1 - 1e-4 && hi < 0.1 + 1e-4,
1928                "{d} {corners:?}: {lo} {hi}"
1929            );
1930        }
1931        let mut s = Sketch::new();
1932        let p = [[0.0, 0.0], [2.0, 0.0], [2.5, 1.0], [1.0, 2.0], [-0.5, 1.0]]
1933            .map(|q| s.add_point(n(q[0]), n(q[1])));
1934        let base = s.add_line(p[0], p[1]);
1935        let arch = s.add_spline(vec![p[1], p[2], p[3], p[4], p[0]]);
1936        let e = s
1937            .offset(&[base, arch], n(-0.1), Corners::Extend)
1938            .unwrap_err();
1939        assert!(e.root_message().contains("do not meet"), "{e}");
1940    }
1941
1942    /// What would cross itself is refused, naming what does.
1943    #[test]
1944    fn offsets_that_would_cross_themselves_are_refused() {
1945        let mut s = Sketch::new();
1946        let (rect, _) = rectangle(&mut s, 2.0, 1.0);
1947        let e = s.offset(&rect, n(0.6), Corners::Round).unwrap_err();
1948        assert!(e.root_message().contains("too short"), "{e}");
1949        let c = s.add_point(n(5.0), n(0.0));
1950        let circle = s.add_circle(c, n(0.5));
1951        let e = s.offset(&[circle], n(0.5), Corners::Round).unwrap_err();
1952        assert!(e.root_message().contains("smaller"), "{e}");
1953        let q = [[0.0, 3.0], [1.0, 4.0], [2.0, 3.0]].map(|q| s.add_point(n(q[0]), n(q[1])));
1954        // A parabola bending right by a radius of 1 at its top, offset 1.5
1955        // into the bend.
1956        let spline = s.add_spline(q.to_vec());
1957        let e = s.offset(&[spline], n(-1.5), Corners::Round).unwrap_err();
1958        assert!(e.root_message().contains("bends tighter"), "{e}");
1959    }
1960
1961    /// A closed square of side `side` at `corner`, counter-clockwise, its
1962    /// corners fixed: its sides.
1963    fn fixed_square(s: &mut Sketch, corner: [f64; 2], side: f64) -> Vec<CurveId> {
1964        let q = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
1965            .map(|q| [corner[0] + side * q[0], corner[1] + side * q[1]]);
1966        let p = q.map(|q| s.add_point(n(q[0]), n(q[1])));
1967        for (&point, q) in p.iter().zip(q) {
1968            s.constrain(Constraint::Fix {
1969                point,
1970                x: n(q[0]),
1971                y: n(q[1]),
1972            });
1973        }
1974        (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect()
1975    }
1976
1977    /// An outline and a hole in it offset at once, both into the material
1978    /// between them though both run counter-clockwise: the outline in, the
1979    /// hole out — a square and a circle hole too. One dimension for all.
1980    #[test]
1981    fn an_outline_and_its_holes_offset_into_the_material() {
1982        let mut s = Sketch::new();
1983        let outline = fixed_square(&mut s, [0.0, 0.0], 4.0);
1984        let hole = fixed_square(&mut s, [1.0, 1.0], 1.0);
1985        let middle = s.add_point(n(3.0), n(3.0));
1986        s.constrain(Constraint::Fix {
1987            point: middle,
1988            x: n(3.0),
1989            y: n(3.0),
1990        });
1991        let round = s.add_circle(middle, n(0.4));
1992        s.constrain(Constraint::Radius {
1993            curve: round,
1994            value: n(0.4),
1995        });
1996        let all: Vec<CurveId> = outline
1997            .iter()
1998            .chain(&hole)
1999            .copied()
2000            .chain([round])
2001            .collect();
2002        let made = s.offset(&all, n(0.2), Corners::Round).unwrap();
2003        let report = s.solve().unwrap();
2004        assert!(report.converged && report.dof == 0, "{report:?}");
2005        let sides: Vec<CurveId> = lines(&s, &made.curves);
2006        assert_eq!(sides.len(), 8, "{made:?}");
2007        let (inner, outer): (Vec<CurveId>, Vec<CurveId>) = sides
2008            .iter()
2009            .partition(|&&c| bounds(&s, &[c])[0] > 0.5 && bounds(&s, &[c])[2] < 3.5);
2010        assert!(
2011            near(bounds(&s, &outer), [0.2, 0.2, 3.8, 3.8]),
2012            "{:?}",
2013            bounds(&s, &outer)
2014        );
2015        assert!(
2016            near(bounds(&s, &inner), [0.8, 0.8, 2.2, 2.2]),
2017            "{:?}",
2018            bounds(&s, &inner)
2019        );
2020        let circle = made
2021            .curves
2022            .iter()
2023            .find(|c| {
2024                matches!(s.curves[c].kind, CurveKind::Circle { .. }) && !s.curves[c].construction
2025            })
2026            .unwrap();
2027        let CurveKind::Circle { radius, .. } = s.curves[circle].kind else {
2028            unreachable!()
2029        };
2030        assert!((radius.to_f64() - 0.6).abs() < 1e-7, "{radius:?}");
2031        set(&mut s, made.distance, 0.1);
2032        assert!(s.solve().unwrap().converged);
2033        assert!(
2034            near(bounds(&s, &outer), [0.1, 0.1, 3.9, 3.9]),
2035            "{:?}",
2036            bounds(&s, &outer)
2037        );
2038        s.enclose().unwrap();
2039    }
2040
2041    /// A hole closer to the outline than twice the distance: the two
2042    /// offsets run into each other and are cut where they cross, the
2043    /// outline's offset and the hole's joined into one curve round both.
2044    #[test]
2045    fn a_hole_near_the_outline_offsets_into_one_curve() {
2046        let mut s = Sketch::new();
2047        let outline = fixed_square(&mut s, [0.0, 0.0], 4.0);
2048        let hole = fixed_square(&mut s, [2.8, 1.5], 1.0);
2049        let all: Vec<CurveId> = outline.iter().chain(&hole).copied().collect();
2050        let made = s.offset(&all, n(0.3), Corners::Round).unwrap();
2051        let report = s.solve().unwrap();
2052        assert!(report.converged && report.dof == 0, "{report:?}");
2053        // What is left between the offsets: one area, round the hole and
2054        // the offsets' meeting.
2055        let loops = s.chain(
2056            &made
2057                .curves
2058                .iter()
2059                .copied()
2060                .filter(|c| !s.curves[c].construction)
2061                .collect::<Vec<_>>(),
2062        );
2063        assert!(loops.is_ok_and(|chain| chain.closed), "{made:?}");
2064        s.enclose().unwrap();
2065    }
2066
2067    /// The chain through a curve runs on as long as it does not branch.
2068    #[test]
2069    fn chains_through_a_curve() {
2070        let mut s = Sketch::new();
2071        let (rect, _) = rectangle(&mut s, 2.0, 1.0);
2072        let mut chain = s.chain_through(rect[2]).unwrap();
2073        chain.sort();
2074        assert_eq!(chain, rect);
2075        let chain = s.chain(&rect).unwrap();
2076        assert!(chain.closed);
2077        // Inside a counter-clockwise loop is to its left.
2078        assert!((s.chain_side(&chain, [1.0, 0.25]).unwrap() - 0.25).abs() < 1e-12);
2079        assert!((s.chain_side(&chain, [1.0, -0.5]).unwrap() + 0.5).abs() < 1e-12);
2080    }
2081
2082    /// Two lines running on in one line — the halves of a mirrored outline
2083    /// meeting on the mirror line — have no corner to round: their offsets
2084    /// meet where they part.
2085    #[test]
2086    fn lines_in_line_offset_straight_on() {
2087        let mut s = Sketch::new();
2088        let p = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]].map(|q| s.add_point(n(q[0]), n(q[1])));
2089        let a = s.add_line(p[0], p[1]);
2090        let b = s.add_line(p[1], p[2]);
2091        for (q, at) in [(p[0], 0.0), (p[1], 1.0), (p[2], 2.0)] {
2092            s.constrain(Constraint::Fix {
2093                point: q,
2094                x: n(at),
2095                y: n(0.0),
2096            });
2097        }
2098        for corners in [Corners::Round, Corners::Extend] {
2099            for d in [0.25, -0.25] {
2100                let mut s = s.clone();
2101                let made = s.offset(&[a, b], n(d), corners).unwrap();
2102                assert_eq!(made.curves.len(), 2, "{corners:?} {d}");
2103                let report = s.solve().unwrap();
2104                assert!(report.converged && report.dof == 0, "{report:?}");
2105                assert!(
2106                    near(bounds(&s, &made.curves), [0.0, d, 2.0, d]),
2107                    "{:?}",
2108                    bounds(&s, &made.curves)
2109                );
2110                s.enclose().unwrap();
2111            }
2112        }
2113    }
2114
2115    /// A rounded rectangle — a `w` x `h` one, its corners filleted by `r` —
2116    /// fully constrained, counter-clockwise: its curves.
2117    fn rounded(s: &mut Sketch, w: f64, h: f64, r: f64) -> Vec<CurveId> {
2118        let q = |x: f64, y: f64| [x, y];
2119        let corners = [
2120            (q(r, 0.0), q(w - r, 0.0)),
2121            (q(w, r), q(w, h - r)),
2122            (q(w - r, h), q(r, h)),
2123            (q(0.0, h - r), q(0.0, r)),
2124        ];
2125        let mut points = Vec::new();
2126        for (a, b) in corners {
2127            points.push(s.add_point(n(a[0]), n(a[1])));
2128            points.push(s.add_point(n(b[0]), n(b[1])));
2129        }
2130        let mut curves = Vec::new();
2131        for k in 0..4 {
2132            let line = s.add_line(points[2 * k], points[2 * k + 1]);
2133            let arc = s.add_arc(points[2 * k + 1], points[(2 * k + 2) % 8], n(PI / 2.0));
2134            curves.extend([line, arc]);
2135        }
2136        for k in 0..8 {
2137            s.constrain(Constraint::Tangent {
2138                a: curves[k],
2139                b: curves[(k + 1) % 8],
2140            });
2141        }
2142        for (k, at) in points.iter().zip([
2143            q(r, 0.0),
2144            q(w - r, 0.0),
2145            q(w, r),
2146            q(w, h - r),
2147            q(w - r, h),
2148            q(r, h),
2149            q(0.0, h - r),
2150            q(0.0, r),
2151        ]) {
2152            s.constrain(Constraint::Fix {
2153                point: *k,
2154                x: n(at[0]),
2155                y: n(at[1]),
2156            });
2157        }
2158        curves
2159    }
2160
2161    /// Offsets of closed chains, in and out, rounded and extended, of
2162    /// several sizes: each solved, fully constrained, and its solution
2163    /// proven.
2164    #[test]
2165    fn closed_offsets_are_proven() {
2166        let mut failures = Vec::new();
2167        for (w, h) in [(2.0, 1.0), (1.0, 1.0), (3.0, 0.5), (1.3, 0.7)] {
2168            for d in [0.1, 0.2, -0.1, -0.3] {
2169                for corners in [Corners::Round, Corners::Extend] {
2170                    for shape in ["rectangle", "rounded"] {
2171                        let mut s = Sketch::new();
2172                        let curves = match shape {
2173                            "rectangle" => rectangle(&mut s, w, h).0,
2174                            _ => rounded(&mut s, w, h, 0.2),
2175                        };
2176                        assert!(s.solve().unwrap().converged);
2177                        let case = format!("{shape} {w}x{h} by {d}, {corners:?}");
2178                        let Ok(_) = s.offset(&curves, n(d), corners) else {
2179                            continue;
2180                        };
2181                        let report = s.solve().unwrap();
2182                        if !report.converged || report.dof != 0 {
2183                            failures.push(format!("{case}: {report:?}"));
2184                        } else if let Err(e) = s.enclose() {
2185                            failures.push(format!("{case}: {}", e.root_message()));
2186                        }
2187                    }
2188                }
2189            }
2190        }
2191        assert!(failures.is_empty(), "{}", failures.join("\n"));
2192    }
2193
2194    /// A closed offset says one thing twice: going round the loop, every
2195    /// offset is at the distance from its source, and each joint holds it
2196    /// there again, so the last joint's tie says what the others already
2197    /// do. Round a crooked pentagon — no side level, no corner square, so
2198    /// no rounding is exactly zero — in and out, rounded and extended, the
2199    /// redundancy holds where the rest does and the solution is proven.
2200    #[test]
2201    fn closed_offsets_of_a_crooked_outline_are_proven() {
2202        let corners = [[0.0, 0.0], [2.1, 0.3], [2.6, 1.7], [1.2, 2.4], [-0.3, 1.3]];
2203        let mut failures = Vec::new();
2204        for d in [0.15, -0.15, 0.4] {
2205            for corners_as in [Corners::Round, Corners::Extend] {
2206                let mut s = Sketch::new();
2207                let p = corners.map(|q| s.add_point(n(q[0]), n(q[1])));
2208                let sides: Vec<CurveId> =
2209                    (0..5).map(|i| s.add_line(p[i], p[(i + 1) % 5])).collect();
2210                for (&point, q) in p.iter().zip(corners) {
2211                    s.constrain(Constraint::Fix {
2212                        point,
2213                        x: n(q[0]),
2214                        y: n(q[1]),
2215                    });
2216                }
2217                let case = format!("by {d}, {corners_as:?}");
2218                if let Err(e) = s.offset(&sides, n(d), corners_as) {
2219                    failures.push(format!("{case}: {}", e.root_message()));
2220                    continue;
2221                }
2222                let report = s.solve().unwrap();
2223                if !report.converged || report.dof != 0 {
2224                    failures.push(format!("{case}: {report:?}"));
2225                } else if let Err(e) = s.enclose() {
2226                    failures.push(format!("{case}: {}", e.root_message()));
2227                }
2228            }
2229        }
2230        assert!(failures.is_empty(), "{}", failures.join("\n"));
2231    }
2232
2233    /// A 4 x 4 square with a notch 1 wide and 2 deep cut down from the
2234    /// middle of its top, offset outward by 0.6: into the notch, its walls'
2235    /// offsets cross and its floor's turns back. What is left is trimmed
2236    /// where the arcs round the notch's top corners cross, over the notch's
2237    /// middle; the rest stays tied to the outline.
2238    #[test]
2239    fn an_offset_over_a_notch_narrower_than_twice_it_is_trimmed() {
2240        let corners = [
2241            [0.0, 0.0],
2242            [4.0, 0.0],
2243            [4.0, 4.0],
2244            [2.5, 4.0],
2245            [2.5, 2.0],
2246            [1.5, 2.0],
2247            [1.5, 4.0],
2248            [0.0, 4.0],
2249        ];
2250        let mut s = Sketch::new();
2251        let p = corners.map(|q| s.add_point(n(q[0]), n(q[1])));
2252        let sides: Vec<CurveId> = (0..8).map(|i| s.add_line(p[i], p[(i + 1) % 8])).collect();
2253        for (&point, q) in p.iter().zip(corners) {
2254            s.constrain(Constraint::Fix {
2255                point,
2256                x: n(q[0]),
2257                y: n(q[1]),
2258            });
2259        }
2260        let made = s.offset(&sides, n(-0.6), Corners::Round).unwrap();
2261        let report = s.solve().unwrap();
2262        assert!(report.converged && report.dof == 0, "{report:?}");
2263        assert!(
2264            near(bounds(&s, &made.curves), [-0.6, -0.6, 4.6, 4.6]),
2265            "{:?}",
2266            bounds(&s, &made.curves)
2267        );
2268        // The notch's walls and floor leave nothing; the arcs round its top
2269        // corners meet over its middle.
2270        let dip = |s: &Sketch, r: f64| {
2271            let q = [2.0, 4.0 + (r * r - 0.25f64).sqrt()];
2272            made.curves
2273                .iter()
2274                .flat_map(|c| s.curves[c].points())
2275                .any(|p| plain::distance(xy(s, p), q) < 1e-7)
2276        };
2277        assert!(dip(&s, 0.6));
2278        assert_eq!(lines(&s, &made.curves).len(), 5);
2279        assert_eq!(made.curves.len(), 5 + 4 + 2);
2280        s.enclose().unwrap();
2281    }
2282
2283    /// The notched square's trimmed offset following its dimension, from
2284    /// 0.6 to 0.7: the arcs round the notch's corners still cross over its
2285    /// middle, higher up. The solve once stopped flat at its first step:
2286    /// its step was solved through normal equations, which squared how
2287    /// ill-conditioned the two arcs held to one crossing are, and met none
2288    /// of the rows it was solved for.
2289    #[test]
2290    fn a_trimmed_offset_follows_its_dimension() {
2291        let mut s = Sketch::new();
2292        let corners = [
2293            [0.0, 0.0],
2294            [4.0, 0.0],
2295            [4.0, 4.0],
2296            [2.5, 4.0],
2297            [2.5, 2.0],
2298            [1.5, 2.0],
2299            [1.5, 4.0],
2300            [0.0, 4.0],
2301        ];
2302        let p = corners.map(|q| s.add_point(n(q[0]), n(q[1])));
2303        let sides: Vec<CurveId> = (0..8).map(|i| s.add_line(p[i], p[(i + 1) % 8])).collect();
2304        for (&point, q) in p.iter().zip(corners) {
2305            s.constrain(Constraint::Fix {
2306                point,
2307                x: n(q[0]),
2308                y: n(q[1]),
2309            });
2310        }
2311        let made = s.offset(&sides, n(-0.6), Corners::Round).unwrap();
2312        s.solve().unwrap();
2313        set(&mut s, made.distance, 0.7);
2314        let report = s.solve().unwrap();
2315        assert!(report.converged, "{report:?}");
2316        assert!(
2317            near(bounds(&s, &made.curves), [-0.7, -0.7, 4.7, 4.7]),
2318            "{:?}",
2319            bounds(&s, &made.curves)
2320        );
2321    }
2322}