1use ogeom_core::{Exact, OgeomResult, Predicates, Sign, ogeom_bail};
68use ogeom_math::{Point2, Vector2};
69use ogeom_mesh::{Edge2, Loop2, region};
70
71use crate::fit::segments::{Shape, circle_through, deviation, runs};
72
73const SAG: f64 = 32.0;
76
77#[derive(Debug, Clone, Copy, PartialEq)]
79pub struct Outline {
80 pub play: f64,
83 pub tolerance: f64,
86 pub round_below: f64,
89 pub fill_below: f64,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq)]
98pub enum Piece2 {
99 Line {
101 from: Point2,
103 to: Point2,
105 },
106 Arc {
108 centre: Point2,
110 radius: f64,
112 start: f64,
114 sweep: f64,
116 },
117 Circle {
119 centre: Point2,
121 radius: f64,
123 counter_clockwise: bool,
125 },
126}
127
128impl Piece2 {
129 #[must_use]
132 pub fn point_at(&self, t: f64) -> Point2 {
133 match *self {
134 Self::Line { from, to } => from.lerp(to, t),
135 Self::Arc {
136 centre,
137 radius,
138 start,
139 sweep,
140 } => on_circle(centre, radius, sweep.mul_add(t, start)),
141 Self::Circle {
142 centre,
143 radius,
144 counter_clockwise,
145 } => {
146 let turn = if counter_clockwise { 1.0 } else { -1.0 };
147 on_circle(centre, radius, turn * core::f64::consts::TAU * t)
148 }
149 }
150 }
151
152 #[must_use]
154 pub fn start(&self) -> Point2 {
155 self.point_at(0.0)
156 }
157
158 #[must_use]
160 pub fn end(&self) -> Point2 {
161 self.point_at(1.0)
162 }
163
164 #[must_use]
166 pub fn length(&self) -> f64 {
167 match *self {
168 Self::Line { from, to } => from.distance(to),
169 Self::Arc { radius, sweep, .. } => radius * sweep.abs(),
170 Self::Circle { radius, .. } => radius * core::f64::consts::TAU,
171 }
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Default)]
179pub struct Wire2d {
180 pub pieces: Vec<Piece2>,
182}
183
184#[allow(
186 clippy::cast_possible_truncation,
187 clippy::cast_sign_loss,
188 reason = "callers pass whole numbers from zero to below 2^32"
189)]
190const fn whole(v: f64) -> usize {
191 v as usize
192}
193
194#[allow(clippy::cast_precision_loss, reason = "counts below 2^32 are exact")]
196fn fraction(k: usize, n: usize) -> f64 {
197 k as f64 / n as f64
198}
199
200fn on_circle(centre: Point2, radius: f64, angle: f64) -> Point2 {
201 Point2::new(
202 radius.mul_add(angle.cos(), centre.x),
203 radius.mul_add(angle.sin(), centre.y),
204 )
205}
206
207pub fn fit_outline(loops: &[Loop2], outline: &Outline) -> OgeomResult<Vec<Wire2d>> {
228 let Outline {
229 play,
230 tolerance,
231 round_below,
232 fill_below,
233 } = *outline;
234 if !(tolerance > 0.0 && tolerance.is_finite()) {
235 ogeom_bail!(
236 Construction,
237 "an outline tolerance of {tolerance} is no band"
238 );
239 }
240 if !play.is_finite() {
241 ogeom_bail!(Construction, "an outline play of {play} is not a distance");
242 }
243 if !(0.0..=core::f64::consts::PI).contains(&round_below) {
244 ogeom_bail!(
245 Construction,
246 "rounding corners turning less than {round_below} rad is not a turn"
247 );
248 }
249 if !(fill_below >= 0.0 && fill_below.is_finite()) {
250 ogeom_bail!(
251 Construction,
252 "filling below {fill_below} across is not a width"
253 );
254 }
255 let mut silhouette = region(&chain(loops), |w| w[0] > 0)?;
256 if silhouette.is_empty() {
257 return Ok(Vec::new());
258 }
259 if fill_below > 0.0 {
260 silhouette = closed(&silhouette, fill_below / 2.0, tolerance)?;
261 }
262 let snap = snapping(&silhouette);
263 let middle = play + tolerance;
264 let targets = if middle == 0.0 {
265 silhouette.clone()
266 } else {
267 offset(&silhouette, middle, tolerance / SAG)?
268 };
269 let spacing = tolerance / 4.0;
270 let margin = spacing / 2.0 + snap;
273 let band = Band::new(
274 &silhouette,
275 play + margin,
276 2.0f64.mul_add(tolerance, play) - margin,
277 );
278 let fit = Fit {
279 band: &band,
280 within: tolerance
281 - tolerance / (2.0 * SAG)
282 - spacing / 2.0
283 - 2.0 * (snap + snapping(&targets)),
284 round_below,
285 spacing,
286 };
287 let mut wires = Vec::with_capacity(targets.len());
288 for target in &targets {
289 if target.points.len() < 3 {
290 continue;
291 }
292 wires.push(fit.wire(target)?);
293 }
294 Ok(wires)
295}
296
297fn chain(loops: &[Loop2]) -> Vec<Edge2> {
299 let mut edges = Vec::new();
300 for l in loops {
301 let n = l.points.len();
302 for i in 0..n {
303 edges.push(Edge2 {
304 from: l.points[i],
305 to: l.points[(i + 1) % n],
306 weight: [1, 0],
307 });
308 }
309 }
310 edges
311}
312
313fn snapping(loops: &[Loop2]) -> f64 {
316 let mut lo = [f64::INFINITY; 2];
317 let mut hi = [f64::NEG_INFINITY; 2];
318 for p in loops.iter().flat_map(|l| &l.points) {
319 lo = [lo[0].min(p.x), lo[1].min(p.y)];
320 hi = [hi[0].max(p.x), hi[1].max(p.y)];
321 }
322 let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(0.0);
323 extent * 2f64.powi(-36)
324}
325
326fn closed(silhouette: &[Loop2], r: f64, tolerance: f64) -> OgeomResult<Vec<Loop2>> {
329 let sag = tolerance.min(r) / SAG;
330 let dilated = offset(silhouette, r + sag / 2.0, sag)?;
331 offset(&dilated, -(r - sag / 2.0), sag)
332}
333
334fn offset(silhouette: &[Loop2], distance: f64, sag: f64) -> OgeomResult<Vec<Loop2>> {
339 let outward = distance > 0.0;
340 let r = distance.abs() + sag / 2.0;
341 let widest = if sag < r {
344 (2.0 * (1.0 - sag / r).acos()).min(core::f64::consts::FRAC_PI_2)
345 } else {
346 core::f64::consts::FRAC_PI_2
347 };
348 let count = if outward { 0 } else { 1 };
349 let mut edges = chain(silhouette);
350 let mut piece = |ring: &[Point2]| {
351 let n = ring.len();
352 for i in 0..n {
353 let mut weight = [0, 0];
354 weight[count] = 1;
355 edges.push(Edge2 {
356 from: ring[i],
357 to: ring[(i + 1) % n],
358 weight,
359 });
360 }
361 };
362 for l in silhouette {
363 let n = l.points.len();
364 for i in 0..n {
365 let (a, b, c) = (l.points[i], l.points[(i + 1) % n], l.points[(i + 2) % n]);
366 let (u, v) = (b - a, c - b);
367 let (lu, lv) = (u.magnitude(), v.magnitude());
368 if lu == 0.0 || lv == 0.0 {
369 continue;
370 }
371 let (u, v) = (u / lu, v / lv);
372 let side = |d: Vector2| {
374 if outward {
375 -d.perpendicular()
376 } else {
377 d.perpendicular()
378 }
379 };
380 let nu = side(u);
381 if outward {
382 piece(&[a, a + nu * r, b + nu * r, b]);
383 } else {
384 piece(&[a, b, b + nu * r, a + nu * r]);
385 }
386 let turn = u.cross(v).atan2(u.dot(v));
387 if (outward && turn > 0.0) || (!outward && turn < 0.0) {
390 let steps = whole((turn.abs() / widest).ceil()).max(1);
391 let mut ring = vec![b];
392 for k in 0..=steps {
393 let t = turn * fraction(k, steps);
394 let (s, co) = t.sin_cos();
395 let d = Vector2::new(nu.x * co - nu.y * s, nu.x * s + nu.y * co);
396 ring.push(b + d * r);
397 }
398 if !outward {
399 ring[1..].reverse();
400 }
401 piece(&ring);
402 }
403 }
404 }
405 if outward {
406 region(&edges, |w| w[0] > 0)
407 } else {
408 region(&edges, |w| w[0] > 0 && w[1] == 0)
409 }
410}
411
412struct Band {
415 segments: Vec<[Point2; 2]>,
416 lo: f64,
417 hi: f64,
418 reach: f64,
419 origin: Point2,
420 cell: f64,
421 columns: usize,
422 rows: usize,
423 start: Vec<usize>,
424 held: Vec<usize>,
425}
426
427impl Band {
428 fn new(silhouette: &[Loop2], lo: f64, hi: f64) -> Self {
429 let mut segments = Vec::new();
430 for l in silhouette {
431 let n = l.points.len();
432 for i in 0..n {
433 segments.push([l.points[i], l.points[(i + 1) % n]]);
434 }
435 }
436 let mut min = Point2::new(f64::INFINITY, f64::INFINITY);
437 let mut max = Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
438 for p in segments.iter().flatten() {
439 min = Point2::new(min.x.min(p.x), min.y.min(p.y));
440 max = Point2::new(max.x.max(p.x), max.y.max(p.y));
441 }
442 let reach = lo.abs().max(hi.abs());
443 let extent = (max.x - min.x).max(max.y - min.y);
444 let per_side = fraction(segments.len(), 1).sqrt().ceil().max(1.0);
445 let cell = (extent / per_side).max(reach / 2.0).max(f64::MIN_POSITIVE);
446 let columns = whole((max.x - min.x) / cell) + 1;
447 let rows = whole((max.y - min.y) / cell) + 1;
448 let mut band = Self {
449 segments,
450 lo,
451 hi,
452 reach,
453 origin: min,
454 cell,
455 columns,
456 rows,
457 start: Vec::new(),
458 held: Vec::new(),
459 };
460 let mut pairs: Vec<(usize, usize)> = Vec::new();
461 for (k, s) in band.segments.iter().enumerate() {
462 let (c0, r0) = band.cell_of(Point2::new(s[0].x.min(s[1].x), s[0].y.min(s[1].y)));
463 let (c1, r1) = band.cell_of(Point2::new(s[0].x.max(s[1].x), s[0].y.max(s[1].y)));
464 for r in r0..=r1 {
465 for c in c0..=c1 {
466 pairs.push((r * columns + c, k));
467 }
468 }
469 }
470 pairs.sort_unstable();
471 let mut start = vec![0usize; rows * columns + 1];
472 for &(b, _) in &pairs {
473 start[b + 1] += 1;
474 }
475 for k in 0..rows * columns {
476 start[k + 1] += start[k];
477 }
478 band.start = start;
479 band.held = pairs.into_iter().map(|(_, k)| k).collect();
480 band
481 }
482
483 fn cell_of(&self, p: Point2) -> (usize, usize) {
484 let at = |v: f64, o: f64, n: usize| {
485 let k = ((v - o) / self.cell).floor();
486 if k < 0.0 {
487 0
488 } else {
489 whole(k.min(fraction(n - 1, 1)))
490 }
491 };
492 (
493 at(p.x, self.origin.x, self.columns),
494 at(p.y, self.origin.y, self.rows),
495 )
496 }
497
498 fn cell(&self, c: usize, r: usize) -> &[usize] {
499 let b = r * self.columns + c;
500 &self.held[self.start[b]..self.start[b + 1]]
501 }
502
503 fn holds(&self, p: Point2) -> bool {
505 let (c0, r0) = self.cell_of(Point2::new(p.x - self.reach, p.y - self.reach));
506 let (c1, r1) = self.cell_of(Point2::new(p.x + self.reach, p.y + self.reach));
507 let mut nearest = f64::INFINITY;
508 for r in r0..=r1 {
509 for c in c0..=c1 {
510 for &k in self.cell(c, r) {
511 let [a, b] = self.segments[k];
512 nearest = nearest.min(distance_to_segment(p, a, b));
513 }
514 }
515 }
516 if nearest > self.reach {
517 return false;
518 }
519 let signed = if self.inside(p) { -nearest } else { nearest };
520 self.lo <= signed && signed <= self.hi
521 }
522
523 fn inside(&self, p: Point2) -> bool {
526 let (c0, r) = self.cell_of(p);
527 let mut seen: Vec<usize> = (c0..self.columns)
528 .flat_map(|c| self.cell(c, r).iter().copied())
529 .collect();
530 seen.sort_unstable();
531 seen.dedup();
532 let at = [p.x, p.y];
533 let mut winding = 0i32;
534 for k in seen {
535 let [a, b] = self.segments[k];
536 let side = Exact::orient2d([a.x, a.y], [b.x, b.y], at);
537 if a.y <= p.y && p.y < b.y && side == Sign::Positive {
538 winding += 1;
539 } else if b.y <= p.y && p.y < a.y && side == Sign::Negative {
540 winding -= 1;
541 }
542 }
543 winding > 0
544 }
545
546 fn holds_along(&self, point: impl Fn(f64) -> Point2, length: f64, spacing: f64) -> bool {
550 let n = (length / (spacing * 15.0 / 16.0)).ceil().max(1.0);
551 if n > 1e8 {
552 return false;
553 }
554 let n = whole(n);
555 (0..=n).all(|k| self.holds(point(fraction(k, n))))
556 }
557
558 fn holds_piece(&self, piece: &Piece2, spacing: f64) -> bool {
559 self.holds_along(|t| piece.point_at(t), piece.length(), spacing)
560 }
561}
562
563fn distance_to_segment(p: Point2, a: Point2, b: Point2) -> f64 {
564 let d = b - a;
565 let len2 = d.dot(d);
566 let t = if len2 > 0.0 {
567 ((p - a).dot(d) / len2).clamp(0.0, 1.0)
568 } else {
569 0.0
570 };
571 (a + d * t).distance(p)
572}
573
574#[derive(Debug, Clone, Copy)]
576pub(crate) enum Path {
577 Line { at: Point2, along: Vector2 },
579 Circle {
581 centre: Point2,
582 radius: f64,
583 ccw: bool,
584 },
585}
586
587impl Path {
588 fn point(self, t: f64) -> Point2 {
589 match self {
590 Self::Line { at, along } => at + along * t,
591 Self::Circle { centre, radius, .. } => on_circle(centre, radius, t),
592 }
593 }
594
595 fn tangent(self, t: f64) -> Vector2 {
596 match self {
597 Self::Line { along, .. } => along,
598 Self::Circle { ccw, .. } => {
599 let (s, c) = t.sin_cos();
600 if ccw {
601 Vector2::new(-s, c)
602 } else {
603 Vector2::new(s, -c)
604 }
605 }
606 }
607 }
608
609 fn parameter(self, p: Point2, near: f64) -> f64 {
611 match self {
612 Self::Line { at, along } => (p - at).dot(along),
613 Self::Circle { centre, .. } => {
614 let a = (p.y - centre.y).atan2(p.x - centre.x);
615 let tau = core::f64::consts::TAU;
616 a + ((near - a) / tau).round() * tau
617 }
618 }
619 }
620
621 pub(crate) fn foot(self, p: Point2) -> Option<Point2> {
623 match self {
624 Self::Line { at, along } => Some(at + along * (p - at).dot(along)),
625 Self::Circle { centre, radius, .. } => {
626 let d = p - centre;
627 let len = d.magnitude();
628 (len > 0.0).then(|| centre + d * (radius / len))
629 }
630 }
631 }
632
633 fn moved(self, by: f64) -> Option<Self> {
635 match self {
636 Self::Line { at, along } => Some(Self::Line {
637 at: at + along.perpendicular() * by,
638 along,
639 }),
640 Self::Circle {
641 centre,
642 radius,
643 ccw,
644 } => {
645 let radius = if ccw { radius - by } else { radius + by };
646 (radius > 0.0).then_some(Self::Circle {
647 centre,
648 radius,
649 ccw,
650 })
651 }
652 }
653 }
654}
655
656pub(crate) fn meet(a: Path, b: Path) -> Vec<Point2> {
658 match (a, b) {
659 (Path::Line { at: p, along: u }, Path::Line { at: q, along: v }) => {
660 let den = u.cross(v);
661 if den == 0.0 {
662 return Vec::new();
663 }
664 vec![p + u * ((q - p).cross(v) / den)]
665 }
666 (Path::Line { at, along }, Path::Circle { centre, radius, .. })
667 | (Path::Circle { centre, radius, .. }, Path::Line { at, along }) => {
668 let foot = at + along * (centre - at).dot(along);
669 let d2 = radius.mul_add(radius, -foot.square_distance(centre));
670 if d2 < 0.0 {
671 return Vec::new();
672 }
673 let h = d2.sqrt();
674 vec![foot + along * h, foot - along * h]
675 }
676 (
677 Path::Circle {
678 centre: c0,
679 radius: r0,
680 ..
681 },
682 Path::Circle {
683 centre: c1,
684 radius: r1,
685 ..
686 },
687 ) => {
688 let d = c1 - c0;
689 let len = d.magnitude();
690 if len == 0.0 {
691 return Vec::new();
692 }
693 let along = (len.mul_add(len, r0.mul_add(r0, -r1 * r1))) / (2.0 * len);
694 let h2 = r0.mul_add(r0, -along * along);
695 if h2 < 0.0 {
696 return Vec::new();
697 }
698 let u = d / len;
699 let mid = c0 + u * along;
700 let h = h2.sqrt();
701 vec![mid + u.perpendicular() * h, mid - u.perpendicular() * h]
702 }
703 }
704}
705
706fn nearest_to(points: &[Point2], p: Point2) -> Option<Point2> {
707 points
708 .iter()
709 .copied()
710 .min_by(|a, b| a.square_distance(p).total_cmp(&b.square_distance(p)))
711}
712
713#[derive(Debug, Clone, Copy)]
715struct Span {
716 path: Path,
717 from: f64,
718 to: f64,
719}
720
721impl Span {
722 fn progress(&self, t: f64) -> f64 {
725 (t - self.from) / (self.to - self.from)
726 }
727
728 fn length(&self) -> f64 {
729 match self.path {
730 Path::Line { .. } => (self.to - self.from).abs(),
731 Path::Circle { radius, .. } => radius * (self.to - self.from).abs(),
732 }
733 }
734
735 fn piece(&self, from: f64, to: f64) -> Piece2 {
737 match self.path {
738 Path::Line { .. } => Piece2::Line {
739 from: self.path.point(from),
740 to: self.path.point(to),
741 },
742 Path::Circle { centre, radius, .. } => Piece2::Arc {
743 centre,
744 radius,
745 start: from,
746 sweep: to - from,
747 },
748 }
749 }
750}
751
752#[derive(Debug, Clone, Copy)]
754enum Joint {
755 Corner { a: f64, b: f64, at: Point2 },
757 Round { a: f64, b: f64, arc: Piece2 },
759 Bridge,
761}
762
763struct Fit<'a> {
764 band: &'a Band,
765 within: f64,
766 round_below: f64,
767 spacing: f64,
768}
769
770impl Fit<'_> {
771 fn wire(&self, target: &Loop2) -> OgeomResult<Wire2d> {
773 if let Some(w) = self.fitted(target)
774 && w.pieces
775 .iter()
776 .all(|p| self.band.holds_piece(p, self.spacing))
777 {
778 return Ok(w);
779 }
780 let n = target.points.len();
781 let polygon = Wire2d {
782 pieces: (0..n)
783 .map(|i| Piece2::Line {
784 from: target.points[i],
785 to: target.points[(i + 1) % n],
786 })
787 .collect(),
788 };
789 if polygon
790 .pieces
791 .iter()
792 .all(|p| self.band.holds_piece(p, self.spacing))
793 {
794 return Ok(polygon);
795 }
796 ogeom_bail!(
797 Construction,
798 "the middle of the band leaves the band; the outline is not fitted"
799 )
800 }
801
802 fn fitted(&self, target: &Loop2) -> Option<Wire2d> {
804 let points = dense(target, self.within);
805 if points.len() < 4 || self.within.is_nan() || self.within <= 0.0 {
806 return None;
807 }
808 if let Some(Shape::Circle { centre, radius }) = circle_through(&points[..points.len() - 1])
809 && deviation(&points, Shape::Circle { centre, radius }) <= self.within
810 {
811 let circle = Piece2::Circle {
812 centre,
813 radius,
814 counter_clockwise: target.area() > 0.0,
815 };
816 if self.band.holds_piece(&circle, self.spacing) {
817 return Some(Wire2d {
818 pieces: vec![circle],
819 });
820 }
821 }
822 let runs = runs(&points, self.within);
823 if runs.len() < 2 {
824 return None;
825 }
826 let mut spans = Vec::with_capacity(runs.len());
827 for run in &runs {
828 let (p, q) = (points[run.first], points[run.last]);
829 let span = match run.shape {
830 Shape::Line { at, along } => Span {
831 path: Path::Line { at, along },
832 from: (p - at).dot(along),
833 to: (q - at).dot(along),
834 },
835 Shape::Circle { centre, radius } => {
836 let angle = |x: Point2| (x.y - centre.y).atan2(x.x - centre.x);
837 let mut sweep = 0.0;
838 for k in run.first..run.last {
839 let (u, v) = (points[k] - centre, points[k + 1] - centre);
840 sweep += u.cross(v).atan2(u.dot(v));
841 }
842 let from = angle(p);
843 Span {
844 path: Path::Circle {
845 centre,
846 radius,
847 ccw: sweep > 0.0,
848 },
849 from,
850 to: from + sweep,
851 }
852 }
853 };
854 let forward = match span.path {
855 Path::Line { .. } => span.to > span.from,
856 Path::Circle { .. } => span.to != span.from,
857 };
858 if !forward {
859 return None;
860 }
861 spans.push(span);
862 }
863 let mut chain: Vec<(Span, Point2)> = spans
865 .into_iter()
866 .zip(runs.iter().map(|r| points[r.last]))
867 .collect();
868 let mut joints: Vec<Option<Joint>> = vec![None; chain.len()];
869 let mut k = 0;
874 while chain.len() > 3 && k < chain.len() {
875 let n = chain.len();
876 let (before, after) = ((k + n - 1) % n, (k + 1) % n);
877 let is_line = |s: &Span| matches!(s.path, Path::Line { .. });
878 let shorter =
879 chain[k].0.length() < chain[before].0.length().min(chain[after].0.length());
880 if shorter && is_line(&chain[before].0) && is_line(&chain[after].0) {
881 let arc = &chain[k].0;
882 let middle = arc.path.point(0.5 * (arc.from + arc.to));
883 let joint = self.joint(&chain[before].0, &chain[after].0, middle);
884 if !matches!(joint, Joint::Bridge) {
885 chain.remove(k);
886 joints.remove(k);
887 let before = if k == 0 { chain.len() - 1 } else { k - 1 };
888 joints[before] = Some(joint);
889 continue;
890 }
891 }
892 k += 1;
893 }
894 let count = chain.len();
895 let joints: Vec<Joint> = (0..count)
896 .map(|k| {
897 joints[k].unwrap_or_else(|| {
898 self.joint(&chain[k].0, &chain[(k + 1) % count].0, chain[k].1)
899 })
900 })
901 .collect();
902 let spans: Vec<Span> = chain.into_iter().map(|(s, _)| s).collect();
903 let mut pieces = Vec::with_capacity(2 * count);
904 for k in 0..count {
905 let span = &spans[k];
906 let from = match joints[(k + count - 1) % count] {
907 Joint::Corner { b, .. } | Joint::Round { b, .. } => b,
908 Joint::Bridge => span.from,
909 };
910 let (to, after) = match joints[k] {
911 Joint::Corner { a, .. } => (a, None),
912 Joint::Round { a, arc, .. } => (a, Some(arc)),
913 Joint::Bridge => {
914 let next = &spans[(k + 1) % count];
915 let line = Piece2::Line {
916 from: span.path.point(span.to),
917 to: next.path.point(next.from),
918 };
919 (span.to, Some(line))
920 }
921 };
922 let mut piece = span.piece(from, to);
923 if let Piece2::Line {
925 from: start,
926 to: end,
927 } = &mut piece
928 {
929 if let Joint::Corner { at, .. } = joints[(k + count - 1) % count] {
930 *start = at;
931 }
932 if let Joint::Corner { at, .. } = joints[k] {
933 *end = at;
934 }
935 }
936 if piece.length() > 0.0 {
937 pieces.push(piece);
938 }
939 if let Some(p) = after
940 && p.length() > 0.0
941 {
942 pieces.push(p);
943 }
944 }
945 Some(Wire2d { pieces })
946 }
947
948 fn joint(&self, a: &Span, b: &Span, q: Point2) -> Joint {
951 let corner = nearest_to(&meet(a.path, b.path), q).and_then(|x| {
952 let (ta, tb) = (a.path.parameter(x, a.to), b.path.parameter(x, b.from));
953 (a.progress(ta) >= 0.5 && b.progress(tb) <= 0.5).then_some((ta, tb, x))
954 });
955 let (ta, tb) = corner.map_or((a.to, b.from), |(ta, tb, _)| (ta, tb));
956 let (u, v) = (a.path.tangent(ta), b.path.tangent(tb));
957 let turn = u.cross(v).atan2(u.dot(v));
958 let sharp = corner.filter(|&(ta, tb, _)| self.extension_holds(a, b, ta, tb));
959 let at_corner = sharp.map(|(a, b, at)| Joint::Corner { a, b, at });
960 if turn.abs() >= self.round_below
961 && let Some(j) = at_corner
962 {
963 return j;
964 }
965 if turn != 0.0
966 && let Some(j) = self.round(a, b, turn, corner.map_or(q, |c| c.2))
967 {
968 return j;
969 }
970 at_corner.unwrap_or(Joint::Bridge)
971 }
972
973 fn extension_holds(&self, a: &Span, b: &Span, ta: f64, tb: f64) -> bool {
976 let stretch = |s: &Span, t0: f64, t1: f64| {
977 let length = s.piece(t0, t1).length();
978 self.band
979 .holds_along(|t| s.path.point(t0 + (t1 - t0) * t), length, self.spacing)
980 };
981 (a.progress(ta) <= 1.0 || stretch(a, a.to, ta))
982 && (b.progress(tb) >= 0.0 || stretch(b, tb, b.from))
983 }
984
985 fn arc(&self, a: &Span, b: &Span, r: f64, turn: f64, near: Point2) -> Option<Joint> {
988 let side = turn.signum();
989 let centre = nearest_to(
990 &meet(a.path.moved(side * r)?, b.path.moved(side * r)?),
991 near,
992 )?;
993 let (pa, pb) = (a.path.foot(centre)?, b.path.foot(centre)?);
994 let (ta, tb) = (a.path.parameter(pa, a.to), b.path.parameter(pb, b.from));
995 if a.progress(ta) < 0.5 || b.progress(tb) > 0.5 {
996 return None;
997 }
998 let (u, v) = (pa - centre, pb - centre);
999 let sweep = u.cross(v).atan2(u.dot(v));
1000 if sweep.signum() != side {
1001 return None;
1002 }
1003 let start = u.y.atan2(u.x);
1005 let arc = Piece2::Arc {
1006 centre,
1007 radius: r,
1008 start,
1009 sweep,
1010 };
1011 let along = |t: f64| {
1012 let d = on_circle(Point2::ORIGIN, 1.0, sweep.mul_add(t, start)).to_vector();
1013 d.perpendicular() * side
1014 };
1015 if along(0.0).dot(a.path.tangent(ta)) <= 0.0 || along(1.0).dot(b.path.tangent(tb)) <= 0.0 {
1016 return None;
1017 }
1018 Some(Joint::Round { a: ta, b: tb, arc })
1019 }
1020
1021 fn arc_holds(&self, a: &Span, b: &Span, joint: &Joint) -> bool {
1022 match joint {
1023 Joint::Round { a: ta, b: tb, arc } => {
1024 self.band.holds_piece(arc, self.spacing) && self.extension_holds(a, b, *ta, *tb)
1025 }
1026 _ => false,
1027 }
1028 }
1029
1030 fn round(&self, a: &Span, b: &Span, turn: f64, near: Point2) -> Option<Joint> {
1034 let reach = 0.5 * a.length().min(b.length());
1035 let largest = reach / (turn.abs() / 2.0).tan().max(1e-9);
1036 let shrink: f64 = 0.7;
1037 let mut failed = None;
1038 for k in 0..80 {
1039 let r = largest * shrink.powi(k);
1040 let Some(joint) = self.arc(a, b, r, turn, near) else {
1041 failed = Some(r);
1042 continue;
1043 };
1044 if !self.arc_holds(a, b, &joint) {
1045 failed = Some(r);
1046 continue;
1047 }
1048 let Some(mut above) = failed else {
1049 return Some(joint);
1050 };
1051 let (mut below, mut best) = (r, joint);
1052 for _ in 0..12 {
1053 let mid = (above * below).sqrt();
1054 match self.arc(a, b, mid, turn, near) {
1055 Some(j) if self.arc_holds(a, b, &j) => {
1056 below = mid;
1057 best = j;
1058 }
1059 _ => above = mid,
1060 }
1061 }
1062 return Some(best);
1063 }
1064 None
1065 }
1066}
1067
1068fn dense(target: &Loop2, spacing: f64) -> Vec<Point2> {
1072 let p = &target.points;
1073 let n = p.len();
1074 let turn = |i: usize| {
1075 let (u, v) = (p[i] - p[(i + n - 1) % n], p[(i + 1) % n] - p[i]);
1076 u.cross(v).atan2(u.dot(v)).abs()
1077 };
1078 let first = (0..n)
1079 .max_by(|&i, &j| turn(i).total_cmp(&turn(j)))
1080 .unwrap_or(0);
1081 let perimeter: f64 = (0..n).map(|i| p[i].distance(p[(i + 1) % n])).sum();
1082 let spacing = spacing.max(perimeter / 2e6);
1084 let mut out = Vec::new();
1085 for k in 0..n {
1086 let (a, b) = (p[(first + k) % n], p[(first + k + 1) % n]);
1087 let steps = whole((a.distance(b) / spacing).ceil()).max(1);
1088 for s in 0..steps {
1089 out.push(a.lerp(b, fraction(s, steps)));
1090 }
1091 }
1092 out.push(p[first]);
1093 out
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098 #![allow(clippy::unwrap_used, reason = "test code")]
1099 use super::*;
1100
1101 fn square(s: f64) -> Loop2 {
1102 Loop2 {
1103 points: vec![
1104 Point2::new(0.0, 0.0),
1105 Point2::new(s, 0.0),
1106 Point2::new(s, s),
1107 Point2::new(0.0, s),
1108 ],
1109 }
1110 }
1111
1112 #[test]
1115 fn a_square_keeps_its_corners() {
1116 let o = Outline {
1117 play: 0.0,
1118 tolerance: 0.1,
1119 round_below: 0.5,
1120 fill_below: 0.0,
1121 };
1122 let wires = fit_outline(&[square(10.0)], &o).unwrap();
1123 assert_eq!(wires.len(), 1);
1124 let w = &wires[0];
1125 assert_eq!(w.pieces.len(), 4, "{w:?}");
1126 for p in &w.pieces {
1127 assert!(matches!(p, Piece2::Line { .. }), "{w:?}");
1128 }
1129 }
1130
1131 #[test]
1134 fn a_large_play_rounds_the_corners() {
1135 let o = Outline {
1136 play: 1.0,
1137 tolerance: 0.1,
1138 round_below: 0.5,
1139 fill_below: 0.0,
1140 };
1141 let wires = fit_outline(&[square(10.0)], &o).unwrap();
1142 assert_eq!(wires.len(), 1);
1143 let arcs = wires[0]
1144 .pieces
1145 .iter()
1146 .filter(|p| matches!(p, Piece2::Arc { .. }))
1147 .count();
1148 assert_eq!(arcs, 4, "{:?}", wires[0]);
1149 }
1150
1151 fn plate(width: f64, diameter: f64) -> Vec<Loop2> {
1155 let h = width / 2.0;
1156 let outer = Loop2 {
1157 points: [
1158 (0.0, 0.0),
1159 (40.0, 0.0),
1160 (40.0, 30.0),
1161 (20.0 + h, 30.0),
1162 (20.0 + h, 25.0),
1163 (20.0 - h, 25.0),
1164 (20.0 - h, 30.0),
1165 (0.0, 30.0),
1166 ]
1167 .map(|(x, y)| Point2::new(x, y))
1168 .to_vec(),
1169 };
1170 let hole = Loop2 {
1171 points: (0..64)
1172 .map(|k| {
1173 let a = -fraction(k, 64) * core::f64::consts::TAU;
1174 on_circle(Point2::new(10.0, 10.0), diameter / 2.0, a)
1175 })
1176 .collect(),
1177 };
1178 vec![outer, hole]
1179 }
1180
1181 fn signed_distance(loops: &[Loop2], p: Point2) -> f64 {
1184 let mut nearest = f64::INFINITY;
1185 let mut winding = 0;
1186 for l in loops {
1187 let n = l.points.len();
1188 for i in 0..n {
1189 let (a, b) = (l.points[i], l.points[(i + 1) % n]);
1190 nearest = nearest.min(distance_to_segment(p, a, b));
1191 let side = (b - a).cross(p - a);
1192 if a.y <= p.y && p.y < b.y && side > 0.0 {
1193 winding += 1;
1194 } else if b.y <= p.y && p.y < a.y && side < 0.0 {
1195 winding -= 1;
1196 }
1197 }
1198 }
1199 if winding > 0 { -nearest } else { nearest }
1200 }
1201
1202 fn samples(wires: &[Wire2d], step: f64) -> Vec<Point2> {
1203 let mut out = Vec::new();
1204 for p in wires.iter().flat_map(|w| &w.pieces) {
1205 let n = whole((p.length() / step).ceil().max(1.0));
1206 out.extend((0..=n).map(|k| p.point_at(fraction(k, n))));
1207 }
1208 out
1209 }
1210
1211 fn filling(fill_below: f64) -> Outline {
1212 Outline {
1213 play: 0.05,
1214 tolerance: 0.05,
1215 round_below: 0.5,
1216 fill_below,
1217 }
1218 }
1219
1220 #[test]
1228 fn fill_below_fills_a_narrow_slot_and_a_small_hole() {
1229 let loops = plate(0.5, 0.4);
1230 let o = filling(1.0);
1231 let (r, s) = (0.5, 0.5);
1232 let sigma = o.tolerance.min(r) / SAG;
1233 let silhouette = region(&chain(&loops), |w| w[0] > 0).unwrap();
1234 let filled = closed(&silhouette, r, o.tolerance).unwrap();
1235 assert_eq!(filled.len(), 1, "{filled:?}");
1236 assert!(!filled[0].is_hole());
1237 let inside = |x: f64, y: f64| signed_distance(&filled, Point2::new(x, y)) < 0.0;
1238 assert!(inside(10.0, 10.0) && inside(20.0, 27.5));
1239 let dip = |r: f64, s: f64| r - (r * r - s * s / 4.0).sqrt();
1240 let deepest = 30.0 - dip(r, s);
1241 let shallowest = 2.0f64.mul_add(sigma, 30.0) - dip(r - sigma, 4.0f64.mul_add(-sigma, s));
1242 assert!(inside(20.0, deepest - 1e-6));
1243 assert!(!inside(20.0, shallowest + 1e-6));
1244 assert!(inside(40.0 - 1e-6, 30.0 - 1e-6) && inside(1e-6, 1e-6));
1245 assert!(!inside(40.0 + sigma + 1e-6, 15.0) && !inside(15.0, -sigma - 1e-6));
1246
1247 let wires = fit_outline(&loops, &o).unwrap();
1248 assert_eq!(wires.len(), 1, "{wires:?}");
1249 for q in samples(&wires, o.tolerance / 100.0) {
1250 let d = signed_distance(&filled, q);
1251 assert!(
1252 o.play <= d && d <= 2.0f64.mul_add(o.tolerance, o.play),
1253 "{q:?} at {d} from the filled plate"
1254 );
1255 assert!(signed_distance(&loops, q) >= o.play, "{q:?}");
1256 }
1257 }
1258
1259 #[test]
1262 fn no_filling_keeps_the_slot_and_the_hole() {
1263 let wires = fit_outline(&plate(0.5, 0.4), &filling(0.0)).unwrap();
1264 assert_eq!(wires.len(), 2, "{wires:?}");
1265 let points = samples(&wires, 0.01);
1266 assert!(
1267 points
1268 .iter()
1269 .any(|q| (q.x - 20.0).abs() < 0.25 && q.y < 26.0)
1270 );
1271 assert!(
1272 points
1273 .iter()
1274 .any(|q| q.distance(Point2::new(10.0, 10.0)) < 0.2)
1275 );
1276 }
1277
1278 #[test]
1283 fn fill_below_keeps_what_is_wider() {
1284 let o = filling(1.0);
1285 let sigma = o.tolerance.min(0.5) / SAG;
1286 let mut loops = plate(1.2, 1.01);
1287 loops.push(Loop2 {
1288 points: plate(1.2, 1.0)[1]
1289 .points
1290 .iter()
1291 .map(|&p| Point2::new(p.x + 20.0, p.y))
1292 .collect(),
1293 });
1294 let silhouette = region(&chain(&loops), |w| w[0] > 0).unwrap();
1295 let filled = closed(&silhouette, 0.5, o.tolerance).unwrap();
1296 assert_eq!(filled.len(), 2, "{filled:?}");
1297 let at = |x: f64, y: f64| signed_distance(&filled, Point2::new(x, y));
1298 assert!(at(20.0, 27.5) > 0.0);
1299 assert!(at(30.0, 10.0) < 0.0);
1300 let rim = 0.505 * (core::f64::consts::PI / 64.0).cos();
1301 assert!(
1302 at(10.0, 10.0) > rim - 2.0 * sigma - 1e-6,
1303 "{}",
1304 at(10.0, 10.0)
1305 );
1306
1307 let wires = fit_outline(&loops, &o).unwrap();
1308 assert_eq!(wires.len(), 2, "{wires:?}");
1309 assert!(
1310 samples(&wires, 0.01)
1311 .iter()
1312 .any(|q| (q.x - 20.0).abs() < 0.6 && q.y < 26.0)
1313 );
1314 }
1315
1316 #[test]
1318 fn fill_below_must_be_a_width() {
1319 for w in [-1.0, f64::NAN, f64::INFINITY] {
1320 assert!(fit_outline(&plate(0.5, 0.4), &filling(w)).is_err());
1321 }
1322 }
1323}