1use 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#[derive(Clone, Copy, Debug, PartialEq)]
37pub enum Step {
38 Along {
41 along: CurveId,
42 backwards: bool,
43 spacing: Scalar,
44 },
45 Round { center: PointId, angle: Scalar },
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct Made {
54 pub by: CurveId,
55 pub spacing: ConstraintId,
56}
57
58#[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 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 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 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 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[©].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 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 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 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 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[¢er])
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 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[¢er], 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 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 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 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 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 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 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 pub fn pattern_count(&self, by: CurveId) -> Option<usize> {
443 Some(self.pattern_steps(by)?.values().max()? + 1)
444 }
445
446 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 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 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 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
550fn 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
563struct 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 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 #[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 assert_eq!(s.points.len(), 4 + 2 + 2);
642 let report = s.solve().unwrap();
643 assert!(report.converged, "{report:?}");
644 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 #[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 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 #[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 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 #[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 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 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}