Skip to main content

geop_core_sketch/
copies.rs

1//! Copies of sketch geometry that stay copies: mirrored across a line, or
2//! repeated along a line or round a center. Every copied point is tied to
3//! the point it copies by a constraint — [`Constraint::Symmetric`] for a
4//! mirror, [`Constraint::Moved`] for a pattern — and every copied arc's
5//! sweep to the original's ([`Constraint::EqualSweep`]), every circle's
6//! radius ([`Constraint::Equal`]). So the copies follow whatever is done to
7//! the original: dragged, dimensioned, solved.
8//!
9//! **Patterns are structure, not records.** A pattern is its construction
10//! curve `by` — a line for a linear pattern, an arc for a circular one —
11//! and the [`Constraint::Moved`] constraints that name it: each copy is the
12//! previous one moved by `by`. That is all there is to know about it: how
13//! many copies it has ([`Sketch::pattern_count`]) is read off the chain of
14//! constraints, and changing that ([`Sketch::set_pattern_count`]) adds or
15//! removes copies at its end. Its spacing is a dimension of `by` — a
16//! [`Constraint::Length`] of the line, an [`Constraint::Angle`] between
17//! the radii to the arc's ends — edited like any other.
18//!
19//! Positions here are where the copies are placed before the sketch is
20//! solved: free choices, computed in plain numbers (see [`crate::plain`]).
21
22use std::collections::{BTreeMap, BTreeSet, btree_map::Entry};
23
24use geop_core_math::{
25    geop_error::{GeopError, GeopResult},
26    scalars::Scalar,
27    solvers::repeat,
28};
29
30use crate::{
31    plain::{P2, add, distance, reflect, rotate, scale, sub, unit, xy},
32    sketch::{Constraint, ConstraintId, Curve, CurveId, CurveKind, PointId, Sketch},
33};
34
35/// How a pattern moves each copy on from the one before.
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub enum Step {
38    /// Along the line `along` — towards its end, or its start if
39    /// `backwards` — by `spacing`.
40    Along {
41        along: CurveId,
42        backwards: bool,
43        spacing: Scalar,
44    },
45    /// Round the point `center`, counter-clockwise by `angle` radians
46    /// (clockwise if negative).
47    Round { center: PointId, angle: Scalar },
48}
49
50/// A pattern made: its construction curve, and the dimension of its
51/// spacing.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct Made {
54    pub by: CurveId,
55    pub spacing: ConstraintId,
56}
57
58/// The motion of a pattern's step, in plain numbers: `q ↦ e + R(q - s)`.
59#[derive(Clone, Copy, Debug)]
60struct Motion {
61    s: P2,
62    e: P2,
63    turn: f64,
64}
65
66impl Motion {
67    fn apply(&self, q: P2) -> P2 {
68        add(self.e, rotate(sub(q, self.s), self.turn))
69    }
70
71    /// The motion `by` stands for, as drawn: see [`Constraint::Moved`].
72    fn of(sketch: &Sketch, by: CurveId) -> GeopResult<Motion> {
73        let (start, end, turn) = match sketch.curve(by)?.kind {
74            CurveKind::Line { start, end } => (start, end, 0.0),
75            CurveKind::Arc { start, end, sweep } => (start, end, sweep.to_f64()),
76            _ => {
77                return Err(GeopError::new(format!(
78                    "curve {by} is no line or arc: it moves nothing"
79                )));
80            }
81        };
82        Ok(Motion {
83            s: xy(sketch, start),
84            e: xy(sketch, end),
85            turn,
86        })
87    }
88}
89
90impl Sketch {
91    /// A copy of the curve `source`, its points mapped by `point`, tied to
92    /// it where its points cannot say it all: an arc's sweep, a circle's
93    /// radius. As construction geometry if the source is; never fixed.
94    /// `reverse`: a mirror image, which runs the other way round.
95    fn copy_curve(
96        &mut self,
97        source: CurveId,
98        point: &dyn Fn(PointId) -> PointId,
99        reverse: bool,
100    ) -> CurveId {
101        let Curve {
102            kind, construction, ..
103        } = self.curves[&source].clone();
104        let kind = match kind {
105            CurveKind::Line { start, end } => CurveKind::Line {
106                start: point(start),
107                end: point(end),
108            },
109            CurveKind::Arc { start, end, sweep } => {
110                // Mirrored, an arc from `s` to `e` counter-clockwise is one
111                // from `e'` to `s'`, counter-clockwise by as much.
112                let (start, end) = if reverse { (end, start) } else { (start, end) };
113                CurveKind::Arc {
114                    start: point(start),
115                    end: point(end),
116                    sweep,
117                }
118            }
119            CurveKind::Circle { center, radius } => CurveKind::Circle {
120                center: point(center),
121                radius,
122            },
123            CurveKind::Spline {
124                control_points,
125                shape,
126            } => CurveKind::Spline {
127                control_points: control_points.iter().map(|&p| point(p)).collect(),
128                shape,
129            },
130            // A text is read, not mirrored or turned: its copy is the same
131            // text, placed where its anchor's copy is.
132            CurveKind::Text {
133                anchor,
134                text,
135                height,
136                angle,
137            } => CurveKind::Text {
138                anchor: point(anchor),
139                text,
140                height,
141                angle,
142            },
143        };
144        let copy = self.add_curve(kind);
145        self.set_construction(copy, construction);
146        match self.curves[&copy].kind {
147            CurveKind::Arc { .. } => {
148                self.constrain(Constraint::EqualSweep { a: source, b: copy });
149            }
150            CurveKind::Circle { .. } => {
151                self.constrain(Constraint::Equal { a: source, b: copy });
152            }
153            _ => {}
154        }
155        copy
156    }
157
158    /// The curves `curves` checked to be ones a copy can be made of, without
159    /// `except` — what the copy is made by — and without repeats.
160    fn copyable(&self, curves: &[CurveId], except: Option<CurveId>) -> GeopResult<Vec<CurveId>> {
161        let mut out = Vec::new();
162        for &c in curves {
163            self.curve(c)?;
164            if Some(c) != except && !out.contains(&c) {
165                out.push(c);
166            }
167        }
168        if out.is_empty() {
169            return Err(GeopError::new(
170                "nothing to copy: pick curves besides the one copied by",
171            ));
172        }
173        Ok(out)
174    }
175
176    /// Mirrors `curves` across the line `line`, each mirrored point
177    /// [`Constraint::Symmetric`] to its original. A point on the line —
178    /// by constraint, see [`Sketch::on_line`] — is its own mirror image,
179    /// and a point mirrored before across the same line has the image it
180    /// got then: so a half profile ending on the line closes into a whole
181    /// one, and mirroring one curve after another joins the copies up. A
182    /// curve that lies on the line is left out. Returns the new curves.
183    pub fn mirror(&mut self, curves: &[CurveId], line: CurveId) -> GeopResult<Vec<CurveId>> {
184        let CurveKind::Line { start, end } = self.curve(line)?.kind else {
185            return Err(GeopError::new(format!(
186                "curve {line} is no line: only a line can be mirrored across"
187            )));
188        };
189        let curves = self.copyable(curves, Some(line))?;
190        let (a, b) = (xy(self, start), xy(self, end));
191        let class = self.point_classes();
192        let on = self.on_line(line)?;
193        // Images known already: the points' own, and those from mirroring
194        // before.
195        let mut image: BTreeMap<PointId, PointId> = BTreeMap::new();
196        for &p in &on {
197            image.insert(class[&p], p);
198        }
199        for c in self.constraints.values() {
200            if let Constraint::Symmetric { a, b, line: l } = *c
201                && l == line
202            {
203                image.entry(class[&a]).or_insert(b);
204                image.entry(class[&b]).or_insert(a);
205            }
206        }
207        let mut made = Vec::new();
208        for c in curves {
209            let points = self.curves[&c].points();
210            if points.iter().all(|p| on.contains(p)) {
211                continue;
212            }
213            for p in points {
214                if let Entry::Vacant(slot) = image.entry(class[&p]) {
215                    let q = self.new_point_at(reflect(xy(self, p), a, b));
216                    self.constrain(Constraint::Symmetric { a: p, b: q, line });
217                    slot.insert(q);
218                }
219            }
220            let mapped = |p: PointId| image[&class[&p]];
221            made.push(self.copy_curve(c, &mapped, true));
222        }
223        Ok(made)
224    }
225
226    /// Repeats `curves` `count` times in all, the original among them, each
227    /// copy `step` on from the one before (see the module docs). Returns the
228    /// pattern's construction curve and the dimension of its spacing.
229    ///
230    /// The construction curve runs from a point of the original — for a
231    /// circular pattern the one furthest from the center — to that point's
232    /// first copy. A point at a circular pattern's center is its own copy.
233    pub fn pattern(&mut self, curves: &[CurveId], step: &Step, count: usize) -> GeopResult<Made> {
234        if count < 2 {
235            return Err(GeopError::new(format!(
236                "a pattern of {count} is no pattern: it takes 2 or more"
237            )));
238        }
239        match *step {
240            Step::Along { spacing, .. } if !spacing.definitely_greater(Scalar::ZERO) => {
241                return Err(GeopError::new(format!(
242                    "a linear pattern's spacing must be more than 0, not {spacing:?}"
243                )));
244            }
245            Step::Round { angle, .. } => once_round(angle, count)?,
246            _ => {}
247        }
248        let except = match *step {
249            Step::Along { along, .. } => Some(along),
250            Step::Round { .. } => None,
251        };
252        let curves = self.copyable(curves, except)?;
253        let class = self.point_classes();
254        let mut sources: Vec<PointId> = Vec::new();
255        for c in &curves {
256            for p in self.curves[c].points() {
257                if !sources.iter().any(|&q| class[&q] == class[&p]) {
258                    sources.push(p);
259                }
260            }
261        }
262        let (by, spacing, center) = match *step {
263            Step::Along {
264                along,
265                backwards,
266                spacing,
267            } => {
268                let CurveKind::Line { start, end } = self.curve(along)?.kind else {
269                    return Err(GeopError::new(format!(
270                        "curve {along} is no line: a linear pattern runs along a line"
271                    )));
272                };
273                let direction = unit(sub(xy(self, end), xy(self, start))).ok_or_else(|| {
274                    GeopError::new(format!("line {along} has no length: it gives no direction"))
275                })?;
276                let direction = if backwards {
277                    scale(direction, -1.0)
278                } else {
279                    direction
280                };
281                let anchor = sources[0];
282                let first =
283                    self.new_point_at(add(xy(self, anchor), scale(direction, spacing.to_f64())));
284                let by = self.add_line(anchor, first);
285                self.set_construction(by, true);
286                self.constrain(Constraint::Parallel { a: along, b: by });
287                let spacing = self.constrain(Constraint::Length {
288                    curve: by,
289                    value: spacing,
290                });
291                (by, spacing, None)
292            }
293            Step::Round { center, angle } => {
294                self.point(center)?;
295                let c = xy(self, center);
296                let anchor = sources
297                    .iter()
298                    .copied()
299                    .filter(|&p| class[&p] != class[&center])
300                    .max_by(|&p, &q| distance(xy(self, p), c).total_cmp(&distance(xy(self, q), c)))
301                    .filter(|&p| distance(xy(self, p), c) > 0.0)
302                    .ok_or_else(|| {
303                        GeopError::new(format!(
304                            "every point copied is at the center {center}: turning moves nothing"
305                        ))
306                    })?;
307                let turn = angle.to_f64();
308                let at = add(c, rotate(sub(xy(self, anchor), c), turn));
309                let first = self.new_point_at(at);
310                let by = self.add_arc(anchor, first, angle);
311                self.set_construction(by, true);
312                self.constrain(Constraint::Center {
313                    point: center,
314                    curve: by,
315                });
316                let radii = [anchor, first].map(|p| {
317                    let r = self.add_line(center, p);
318                    self.set_construction(r, true);
319                    r
320                });
321                let spacing = self.constrain(Constraint::Angle {
322                    a: radii[0],
323                    b: radii[1],
324                    value: angle,
325                });
326                (by, spacing, Some(center))
327            }
328        };
329        // The first copy of the anchor is the end of `by`: moved by it, as
330        // all copies are — trivially so, which is what marks every point
331        // of the original as copied, a lone one too.
332        let (anchor, first) = self.curves[&by].endpoints().expect("a line or arc");
333        self.constrain(Constraint::Moved {
334            a: anchor,
335            b: first,
336            by,
337        });
338        let mut previous: BTreeMap<PointId, PointId> =
339            sources.iter().map(|&p| (class[&p], p)).collect();
340        if let Some(center) = center {
341            previous.insert(class[&center], center);
342        }
343        let generation = Generation {
344            points: previous,
345            curves,
346        };
347        self.extend_pattern(by, generation, count - 1, Some(first))?;
348        Ok(Made { by, spacing })
349    }
350
351    /// Adds `more` copies after the copy `last` of the pattern `by`: each
352    /// point moved on from the last copy's, each curve copied. `anchor`:
353    /// the first copy of the anchor, there already (the end of `by`).
354    fn extend_pattern(
355        &mut self,
356        by: CurveId,
357        mut last: Generation,
358        more: usize,
359        mut anchor: Option<PointId>,
360    ) -> GeopResult<()> {
361        let motion = Motion::of(self, by)?;
362        let (by_start, _) = self.curves[&by].endpoints().expect("a line or arc");
363        for _ in 0..more {
364            let class = self.point_classes();
365            let mut points = BTreeMap::new();
366            for (&rep, &p) in &last.points {
367                // A point the motion keeps where it is — a circular
368                // pattern's center — is its own copy.
369                let fixed = self.is_pattern_center(by, p);
370                let q = if fixed {
371                    p
372                } else if let Some(first) = anchor.filter(|_| class[&p] == class[&by_start]) {
373                    first
374                } else {
375                    let q = self.new_point_at(motion.apply(xy(self, p)));
376                    self.constrain(Constraint::Moved { a: p, b: q, by });
377                    q
378                };
379                points.insert(rep, q);
380            }
381            anchor = None;
382            let mut curves = Vec::new();
383            let class = self.point_classes();
384            for &c in &last.curves {
385                let mapped = |p: PointId| points[&class[&p]];
386                curves.push(self.copy_curve(c, &mapped, false));
387            }
388            // From here on, a copy's points are known by their own class.
389            let class = self.point_classes();
390            last = Generation {
391                points: points.values().map(|&q| (class[&q], q)).collect(),
392                curves,
393            };
394        }
395        Ok(())
396    }
397
398    /// Whether `p` is the center of the circular pattern `by`.
399    fn is_pattern_center(&self, by: CurveId, p: PointId) -> bool {
400        let class = self.point_classes();
401        self.constraints.values().any(|c| {
402            matches!(*c, Constraint::Center { point, curve } if curve == by && class[&point] == class[&p])
403        })
404    }
405
406    /// Every point the pattern `by` moves, by how many steps from the
407    /// original: the chain of its [`Constraint::Moved`] constraints. `None`
408    /// if `by` is the construction curve of no pattern.
409    fn pattern_steps(&self, by: CurveId) -> Option<BTreeMap<PointId, usize>> {
410        let moved: Vec<(PointId, PointId)> = self
411            .constraints
412            .values()
413            .filter_map(|c| match *c {
414                Constraint::Moved { a, b, by: m } if m == by => Some((a, b)),
415                _ => None,
416            })
417            .collect();
418        if moved.is_empty() {
419            return None;
420        }
421        let targets: BTreeSet<PointId> = moved.iter().map(|&(_, b)| b).collect();
422        let mut steps: BTreeMap<PointId, usize> = moved
423            .iter()
424            .filter(|(a, _)| !targets.contains(a))
425            .map(|&(a, _)| (a, 0))
426            .collect();
427        // Each copy one step on from the point it is moved from, in passes
428        // while that places more; a constraint whose copy is placed from
429        // elsewhere, or not at all, is left.
430        let _ = repeat::resolve(moved, |&(a, b)| match steps.get(&a) {
431            Some(&k) if !steps.contains_key(&b) => {
432                steps.insert(b, k + 1);
433                Ok(())
434            }
435            _ => Err(()),
436        });
437        Some(steps)
438    }
439
440    /// How many copies the pattern `by` has, the original among them;
441    /// `None` if `by` is the construction curve of no pattern.
442    pub fn pattern_count(&self, by: CurveId) -> Option<usize> {
443        Some(self.pattern_steps(by)?.values().max()? + 1)
444    }
445
446    /// Gives the pattern `by` `count` copies, the original among them:
447    /// removes the copies past the last one kept, or adds copies after its
448    /// last one, as it is now.
449    pub fn set_pattern_count(&mut self, by: CurveId, count: usize) -> GeopResult<()> {
450        let steps = self
451            .pattern_steps(by)
452            .ok_or_else(|| GeopError::new(format!("curve {by} is no pattern's")))?;
453        if count < 2 {
454            return Err(GeopError::new(format!(
455                "a pattern of {count} is no pattern: it takes 2 or more"
456            )));
457        }
458        if let Some(Constraint::Angle { value, .. }) = self
459            .pattern_spacing(by)
460            .and_then(|k| self.constraints.get(&k))
461        {
462            once_round(*value, count)?;
463        }
464        let now = steps.values().max().copied().unwrap_or(0) + 1;
465        if count < now {
466            let gone: Vec<PointId> = steps
467                .iter()
468                .filter(|&(_, &k)| k >= count)
469                .map(|(&p, _)| p)
470                .collect();
471            self.remove(&gone, &[], &[]);
472            return Ok(());
473        }
474        if count == now {
475            return Ok(());
476        }
477        // The last copy: its points, and the curves on them — on them
478        // only, the center of a circular pattern aside, and not the
479        // pattern's own construction.
480        let class = self.point_classes();
481        let mut last: BTreeMap<PointId, PointId> = steps
482            .iter()
483            .filter(|&(_, &k)| k == now - 1)
484            .map(|(&p, _)| (class[&p], p))
485            .collect();
486        let own = self.pattern_own_curves(by);
487        let center = self.constraints.values().find_map(|c| match *c {
488            Constraint::Center { point, curve } if curve == by => Some(point),
489            _ => None,
490        });
491        let curves: Vec<CurveId> = self
492            .curves
493            .iter()
494            .filter(|(id, c)| {
495                let points = c.points();
496                !own.contains(id)
497                    && points.iter().any(|p| last.contains_key(&class[p]))
498                    && points.iter().all(|p| {
499                        last.contains_key(&class[p])
500                            || center.is_some_and(|m| class[&m] == class[p])
501                    })
502            })
503            .map(|(&id, _)| id)
504            .collect();
505        if let Some(m) = center {
506            last.insert(class[&m], m);
507        }
508        self.extend_pattern(
509            by,
510            Generation {
511                points: last,
512                curves,
513            },
514            count - now,
515            None,
516        )
517    }
518
519    /// The dimension of the spacing of the pattern `by`: a
520    /// [`Constraint::Length`] of its line, or the [`Constraint::Angle`]
521    /// between the radii to its arc's ends — if it has one still.
522    pub fn pattern_spacing(&self, by: CurveId) -> Option<ConstraintId> {
523        let (s, e) = self.curves.get(&by)?.endpoints()?;
524        let ends = |l: CurveId| self.curves.get(&l).and_then(|k| k.endpoints());
525        self.constraints.iter().find_map(|(&id, c)| match *c {
526            Constraint::Length { curve, .. } if curve == by => Some(id),
527            Constraint::Angle { a, b, .. }
528                if ends(a).is_some_and(|(_, q)| q == s) && ends(b).is_some_and(|(_, q)| q == e) =>
529            {
530                Some(id)
531            }
532            _ => None,
533        })
534    }
535
536    /// The construction a pattern made for itself: `by`, and a circular
537    /// pattern's radii to its ends.
538    fn pattern_own_curves(&self, by: CurveId) -> Vec<CurveId> {
539        let mut own = vec![by];
540        if let Some(Constraint::Angle { a, b, .. }) = self
541            .pattern_spacing(by)
542            .and_then(|k| self.constraints.get(&k))
543        {
544            own.extend([*a, *b]);
545        }
546        own
547    }
548}
549
550/// Checks that `count` copies `angle` apart turn — they do not stay put —
551/// and go round once at most, the last short of the first.
552fn once_round(angle: Scalar, count: usize) -> GeopResult<()> {
553    let turned = angle.abs().mul(Scalar::from_f64((count - 1) as f64));
554    if angle.could_be_equal(Scalar::ZERO) || !turned.definitely_less(Scalar::TWO.mul(Scalar::PI)) {
555        return Err(GeopError::new(format!(
556            "{count} copies {:.3}° apart do not go round once short of the first: a circular pattern turns, once at most",
557            angle.to_f64().to_degrees()
558        )));
559    }
560    Ok(())
561}
562
563/// One copy of a pattern: its points by class, and its curves.
564struct Generation {
565    points: BTreeMap<PointId, PointId>,
566    curves: Vec<CurveId>,
567}
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572    use crate::plain::Plain;
573    use std::f64::consts::PI;
574
575    fn n(x: f64) -> Scalar {
576        Scalar::from_f64(x)
577    }
578
579    fn close(a: P2, b: P2) -> bool {
580        distance(a, b) < 1e-7
581    }
582
583    /// A unit square at `(x, y)`, its corner fixed and its sides
584    /// dimensioned: fully constrained.
585    fn square(s: &mut Sketch, x: f64, y: f64) -> Vec<CurveId> {
586        let p = [[x, y], [x + 1.0, y], [x + 1.0, y + 1.0], [x, y + 1.0]]
587            .map(|q| s.add_point(n(q[0]), n(q[1])));
588        let l: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
589        s.constrain(Constraint::Fix {
590            point: p[0],
591            x: n(x),
592            y: n(y),
593        });
594        s.constrain(Constraint::Horizontal { line: l[0] });
595        s.constrain(Constraint::Vertical { line: l[1] });
596        s.constrain(Constraint::Horizontal { line: l[2] });
597        s.constrain(Constraint::Vertical { line: l[3] });
598        s.constrain(Constraint::Length {
599            curve: l[0],
600            value: n(1.0),
601        });
602        s.constrain(Constraint::Length {
603            curve: l[1],
604            value: n(1.0),
605        });
606        l
607    }
608
609    fn y_axis(s: &mut Sketch) -> CurveId {
610        let o = s.add_fixed_point(n(0.0), n(0.0));
611        let up = s.add_fixed_point(n(0.0), n(1.0));
612        s.add_line(o, up)
613    }
614
615    /// A half slot right of the y axis, its ends on the axis, mirrored:
616    /// one closed slot whose mirrored half follows the original's radius.
617    #[test]
618    fn a_mirrored_half_closes_on_the_line() {
619        let mut s = Sketch::new();
620        let axis = y_axis(&mut s);
621        let p =
622            [[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])));
623        let bottom = s.add_line(p[0], p[1]);
624        let round = s.add_arc(p[1], p[2], n(PI));
625        let top = s.add_line(p[2], p[3]);
626        for q in [p[0], p[3]] {
627            s.constrain(Constraint::PointOnCurve {
628                point: q,
629                curve: axis,
630            });
631        }
632        s.constrain(Constraint::Tangent {
633            a: bottom,
634            b: round,
635        });
636        s.constrain(Constraint::Tangent { a: round, b: top });
637        let made = s.mirror(&[bottom, round, top, axis], axis).unwrap();
638        assert_eq!(made.len(), 3);
639        assert_eq!(s.regions().unwrap().len(), 1, "one closed region");
640        // Its ends were on the axis, and are shared: two new points.
641        assert_eq!(s.points.len(), 4 + 2 + 2);
642        let report = s.solve().unwrap();
643        assert!(report.converged, "{report:?}");
644        // The original's radius made 0.75: the image's follows.
645        s.constrain(Constraint::Radius {
646            curve: round,
647            value: n(0.75),
648        });
649        assert!(s.solve().unwrap().converged);
650        let image = made[1];
651        let Some(Plain::Round { radius, center, .. }) = Plain::of(&s, image, false) else {
652            panic!("an arc");
653        };
654        let Some(Plain::Round {
655            center: original, ..
656        }) = Plain::of(&s, round, false)
657        else {
658            panic!("an arc");
659        };
660        assert!((radius - 0.75).abs() < 1e-7, "{radius}");
661        assert!(
662            close(center, [-original[0], original[1]]),
663            "{center:?} {original:?}"
664        );
665        s.enclose().unwrap();
666    }
667
668    /// Mirroring a semicircle's arc — where equal radii say nothing about
669    /// the sweep — keeps the copy's sweep by constraint, and the solution
670    /// is proven.
671    #[test]
672    fn mirrored_half_circles_keep_their_sweep() {
673        let mut s = Sketch::new();
674        let axis = y_axis(&mut s);
675        let a = s.add_point(n(1.0), n(0.0));
676        let b = s.add_point(n(3.0), n(0.0));
677        let c = s.add_point(n(3.0), n(-1.0));
678        let arc = s.add_arc(a, b, n(-PI));
679        let down = s.add_line(b, c);
680        for (q, at) in [(a, [1.0, 0.0]), (b, [3.0, 0.0]), (c, [3.0, -1.0])] {
681            s.constrain(Constraint::Fix {
682                point: q,
683                x: n(at[0]),
684                y: n(at[1]),
685            });
686        }
687        // Its end square to the line down: a half circle, by tangency.
688        s.constrain(Constraint::Tangent { a: arc, b: down });
689        let made = s.mirror(&[arc], axis).unwrap();
690        let report = s.solve().unwrap();
691        assert!(report.converged && report.dof == 0, "{report:?}");
692        assert!(
693            matches!(s.curves[&made[0]].kind, CurveKind::Arc { sweep, .. } if (sweep.to_f64() + PI).abs() < 1e-9)
694        );
695        s.enclose().unwrap();
696    }
697
698    /// Three squares along the x axis, 2 apart: the copies follow the
699    /// original and the spacing; more and fewer copies are added and
700    /// removed at the end of the row.
701    #[test]
702    fn linear_patterns_follow_their_spacing_and_count() {
703        let mut s = Sketch::new();
704        let o = s.add_fixed_point(n(0.0), n(0.0));
705        let x = s.add_fixed_point(n(1.0), n(0.0));
706        let x_axis = s.add_line(o, x);
707        let square = square(&mut s, 0.5, 0.5);
708        let made = s
709            .pattern(
710                &square,
711                &Step::Along {
712                    along: x_axis,
713                    backwards: false,
714                    spacing: n(2.0),
715                },
716                3,
717            )
718            .unwrap();
719        assert_eq!(s.pattern_count(made.by), Some(3));
720        assert_eq!(s.pattern_count(x_axis), None);
721        let report = s.solve().unwrap();
722        assert!(report.converged && report.dof == 0, "{report:?}");
723        assert_eq!(s.regions().unwrap().len(), 3);
724        let corners = |s: &Sketch| -> Vec<P2> {
725            let mut c: Vec<P2> = s.points.keys().map(|&p| xy(s, p)).collect();
726            c.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
727            c
728        };
729        assert!(corners(&s).iter().any(|&q| close(q, [5.5, 1.5])));
730        // The spacing made 3: the row stretches.
731        if let Some(Constraint::Length { value, .. }) = s.constraints.get_mut(&made.spacing) {
732            *value = n(3.0);
733        }
734        assert!(s.solve().unwrap().converged);
735        assert!(corners(&s).iter().any(|&q| close(q, [7.5, 1.5])));
736        s.set_pattern_count(made.by, 5).unwrap();
737        assert_eq!(s.pattern_count(made.by), Some(5));
738        let report = s.solve().unwrap();
739        assert!(report.converged && report.dof == 0, "{report:?}");
740        assert_eq!(s.regions().unwrap().len(), 5);
741        assert!(corners(&s).iter().any(|&q| close(q, [13.5, 1.5])));
742        s.set_pattern_count(made.by, 2).unwrap();
743        assert_eq!(s.pattern_count(made.by), Some(2));
744        assert_eq!(s.regions().unwrap().len(), 2);
745        assert!(s.solve().unwrap().converged);
746        s.validate().unwrap();
747        s.enclose().unwrap();
748    }
749
750    /// Six holes round a bolt circle: a circle and a slot copied round the
751    /// origin, the center its own copy where a line ends on it.
752    #[test]
753    fn circular_patterns_turn_round_their_center() {
754        let mut s = Sketch::new();
755        let o = s.add_fixed_point(n(0.0), n(0.0));
756        let c = s.add_point(n(2.0), n(0.0));
757        let hole = s.add_circle(c, n(0.25));
758        s.constrain(Constraint::Fix {
759            point: c,
760            x: n(2.0),
761            y: n(0.0),
762        });
763        s.constrain(Constraint::Radius {
764            curve: hole,
765            value: n(0.25),
766        });
767        let tip = s.add_point(n(1.0), n(0.5));
768        let spoke = s.add_line(o, tip);
769        s.constrain(Constraint::Fix {
770            point: tip,
771            x: n(1.0),
772            y: n(0.5),
773        });
774        let made = s
775            .pattern(
776                &[hole, spoke],
777                &Step::Round {
778                    center: o,
779                    angle: n(PI / 3.0),
780                },
781                6,
782            )
783            .unwrap();
784        assert_eq!(s.pattern_count(made.by), Some(6));
785        let report = s.solve().unwrap();
786        assert!(report.converged && report.dof == 0, "{report:?}");
787        let centers: Vec<P2> = s
788            .curves
789            .values()
790            .filter_map(|k| match k.kind {
791                CurveKind::Circle { center, .. } => Some(xy(&s, center)),
792                _ => None,
793            })
794            .collect();
795        assert_eq!(centers.len(), 6);
796        for k in 0..6 {
797            let a = PI / 3.0 * k as f64;
798            assert!(
799                centers
800                    .iter()
801                    .any(|&q| close(q, [2.0 * a.cos(), 2.0 * a.sin()])),
802                "{k}: {centers:?}"
803            );
804        }
805        // Every spoke starts at the origin itself.
806        let spokes = s
807            .curves
808            .values()
809            .filter(|k| {
810                !k.construction && matches!(k.kind, CurveKind::Line { start, .. } if start == o)
811            })
812            .count();
813        assert_eq!(spokes, 6);
814        // Eight round the circle: 45° apart first, then two more.
815        if let Some(Constraint::Angle { value, .. }) = s.constraints.get_mut(&made.spacing) {
816            *value = n(PI / 4.0);
817        }
818        assert!(s.solve().unwrap().converged);
819        s.set_pattern_count(made.by, 8).unwrap();
820        assert_eq!(s.pattern_count(made.by), Some(8));
821        assert!(s.solve().unwrap().converged);
822        s.enclose().unwrap();
823    }
824
825    #[test]
826    fn copies_refuse_what_they_cannot_copy_by() {
827        let mut s = Sketch::new();
828        let c = s.add_point(n(0.0), n(0.0));
829        let circle = s.add_circle(c, n(1.0));
830        let e = s.mirror(&[circle], circle).unwrap_err();
831        assert!(e.root_message().contains("no line"), "{e}");
832        let e = s
833            .pattern(
834                &[circle],
835                &Step::Round {
836                    center: c,
837                    angle: n(1.0),
838                },
839                3,
840            )
841            .unwrap_err();
842        assert!(e.root_message().contains("at the center"), "{e}");
843        let e = s
844            .pattern(
845                &[circle],
846                &Step::Round {
847                    center: c,
848                    angle: n(1.0),
849                },
850                1,
851            )
852            .unwrap_err();
853        assert!(e.root_message().contains("2 or more"), "{e}");
854        let d = s.add_point(n(3.0), n(0.0));
855        let hole = s.add_circle(d, n(0.5));
856        let e = s
857            .pattern(
858                &[hole],
859                &Step::Round {
860                    center: c,
861                    angle: n(PI / 3.0),
862                },
863                7,
864            )
865            .unwrap_err();
866        assert!(e.root_message().contains("once"), "{e}");
867        let made = s
868            .pattern(
869                &[hole],
870                &Step::Round {
871                    center: c,
872                    angle: n(PI / 3.0),
873                },
874                6,
875            )
876            .unwrap();
877        let e = s.set_pattern_count(made.by, 7).unwrap_err();
878        assert!(e.root_message().contains("once"), "{e}");
879    }
880}