1use std::collections::HashMap;
34
35use ogeom_core::{OgeomResult, Tolerance, Tolerances, ogeom_bail};
36use ogeom_geom::{Curve, LineCurve, PlanarCurve, PlaneSurface};
37use ogeom_math::{Cone, Cylinder, Direction, Frame, Plane, Point, Point2, Sphere, Torus, Vector};
38
39use crate::recognize::{Canonical, recognize_curved, worst_deviation};
40use ogeom_topo::{EdgeData, EdgeRepr, FaceData, Location, Model, Shape, Triangulation, VertexData};
41
42#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct MeshSolidOptions {
45 pub merge_coplanar: bool,
49 pub coplanar_angle: f64,
52 pub coplanar_distance: Option<f64>,
59 pub weld: Option<f64>,
63 pub recognize: bool,
66 pub crease: f64,
71}
72
73impl Default for MeshSolidOptions {
74 fn default() -> Self {
75 Self {
76 merge_coplanar: true,
77 coplanar_angle: 1e-3,
78 coplanar_distance: None,
79 weld: None,
80 recognize: true,
81 crease: core::f64::consts::FRAC_PI_6,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Default, PartialEq, Eq)]
88pub struct MeshSolidReport {
89 pub triangles: usize,
91 pub faces: usize,
94 pub curved_faces: usize,
96 pub curved_faceted: usize,
99 pub vertices_welded: usize,
101 pub degenerate_dropped: usize,
103 pub duplicates_dropped: usize,
105 pub windings_flipped: usize,
108 pub edges_used_once: usize,
110 pub edges_used_more: usize,
112 pub orientation_conflicts: usize,
115 pub shells: usize,
117}
118
119#[derive(Debug, Clone)]
121pub struct MeshSolid {
122 pub shape: Shape,
126 pub closed: bool,
128 pub report: MeshSolidReport,
130}
131
132fn split_across_slivers(points: &[Point], triangles: &mut Vec<[u32; 3]>, slivers: &[[u32; 3]]) {
142 let mut pending: Vec<[u32; 3]> = slivers
143 .iter()
144 .map(|&[a, b, c]| {
145 let long = |x: u32, y: u32| points[x as usize].distance(points[y as usize]);
147 let sides = [
148 (long(b, c), a, [b, c]),
149 (long(c, a), b, [c, a]),
150 (long(a, b), c, [a, b]),
151 ];
152 let (_, middle, [p, q]) =
153 sides.into_iter().fold(
154 sides[0],
155 |best, side| if side.0 > best.0 { side } else { best },
156 );
157 [p, middle, q]
158 })
159 .collect();
160 loop {
161 let mut progress = false;
162 let mut left = Vec::new();
163 for [p, middle, q] in pending {
164 let across = triangles.iter().position(|t| {
165 (0..3).any(|k| {
166 let (x, y) = (t[k], t[(k + 1) % 3]);
167 (x == p && y == q) || (x == q && y == p)
168 }) && !t.contains(&middle)
169 });
170 let Some(index) = across else {
171 left.push([p, middle, q]);
172 continue;
173 };
174 let t = triangles[index];
175 let Some(k) = (0..3).find(|&k| {
176 let (x, y) = (t[k], t[(k + 1) % 3]);
177 (x == p && y == q) || (x == q && y == p)
178 }) else {
179 continue;
180 };
181 let (x, y, z) = (t[k], t[(k + 1) % 3], t[(k + 2) % 3]);
182 triangles[index] = [x, middle, z];
183 triangles.push([middle, y, z]);
184 progress = true;
185 }
186 pending = left;
187 if pending.is_empty() || !progress {
188 break;
189 }
190 }
191}
192
193pub fn solid_from_mesh(
204 model: &mut Model,
205 mesh: &Triangulation,
206 options: &MeshSolidOptions,
207 tol: Tolerances,
208) -> OgeomResult<MeshSolid> {
209 let weld = options.weld.unwrap_or_else(|| tol.confusion());
210 if !(weld.is_finite() && weld > 0.0) {
211 ogeom_bail!(
212 Construction,
213 "the weld distance {weld} is not finite and positive"
214 );
215 }
216 if !(options.coplanar_angle.is_finite() && options.coplanar_angle >= 0.0) {
217 ogeom_bail!(Construction, "the coplanar angle is not finite");
218 }
219 let count = mesh.positions.len();
220 if mesh
221 .triangles
222 .iter()
223 .flatten()
224 .any(|&v| v as usize >= count)
225 {
226 ogeom_bail!(
227 Construction,
228 "a triangle names a vertex past the {count} the mesh has"
229 );
230 }
231 let mut report = MeshSolidReport::default();
232
233 let (points, remap) = weld_points(&mesh.positions, weld);
234 report.vertices_welded = count - points.len();
235 let mut triangles = Vec::with_capacity(mesh.triangles.len());
236 let mut seen: HashMap<[u32; 3], ()> = HashMap::with_capacity(mesh.triangles.len());
237 let mut flat_slivers: Vec<[u32; 3]> = Vec::new();
238 for t in &mesh.triangles {
239 let [a, b, c] = t.map(|v| remap[v as usize]);
240 if a == b || b == c || c == a || !has_area(&points, [a, b, c], weld) {
241 report.degenerate_dropped += 1;
242 if a != b && b != c && c != a {
243 flat_slivers.push([a, b, c]);
244 }
245 continue;
246 }
247 let mut key = [a, b, c];
248 key.sort_unstable();
249 if seen.insert(key, ()).is_some() {
250 report.duplicates_dropped += 1;
251 continue;
252 }
253 triangles.push([a, b, c]);
254 }
255 if triangles.is_empty() {
256 ogeom_bail!(Construction, "the mesh has no triangle with area");
257 }
258 split_across_slivers(&points, &mut triangles, &flat_slivers);
259 report.triangles = triangles.len();
260
261 let adjacency = Adjacency::new(&triangles);
263 report.edges_used_once = adjacency.used_once;
264 report.edges_used_more = adjacency.used_more;
265 let pieces = orient(&points, &mut triangles, &adjacency, &mut report);
266 let all_closed = pieces.iter().all(|p| p.closed);
269 let depth: Vec<usize> = if all_closed {
270 (0..pieces.len())
271 .map(|i| {
272 (0..pieces.len())
273 .filter(|&j| j != i && inside(&points, &triangles, &pieces[j], &pieces[i]))
274 .count()
275 })
276 .collect()
277 } else {
278 vec![0; pieces.len()]
279 };
280 for (piece, d) in pieces.iter().zip(&depth) {
281 if d % 2 == 1 {
282 for &t in &piece.triangles {
283 triangles[t as usize].swap(1, 2);
284 report.windings_flipped += 1;
285 }
286 }
287 }
288 let adjacency = Adjacency::new(&triangles);
290 report.shells = pieces.len();
291
292 let diagonal = diagonal(&points);
293 let flat = options
294 .coplanar_distance
295 .unwrap_or(1e-6 * diagonal)
296 .max(weld);
297 let mut groups = segment(&points, &triangles, &adjacency, options, flat, tol)?;
298 let mut pinned: std::collections::HashSet<u32> = std::collections::HashSet::new();
301 let plan = loop {
302 let planner = Planner {
303 points: &points,
304 triangles: &triangles,
305 adjacency: &adjacency,
306 groups: &groups,
307 merge: options.merge_coplanar,
308 pinned: &pinned,
309 flat,
310 tol,
311 };
312 let planned = planner.plan()?;
313
314 match planned {
315 Ok(plan) => break plan,
316 Err(Replan::Pin(vertices)) => pinned.extend(vertices),
317 Err(Replan::Facet(failed)) => {
318 for g in failed {
319 groups.carriers[g] = Carrier::Gone;
320 report.curved_faceted += 1;
321 for of in &mut groups.of {
322 if *of == g {
323 *of = usize::MAX;
324 }
325 }
326 }
327 coplanar_groups(
328 &points,
329 &triangles,
330 &adjacency,
331 options.coplanar_angle,
332 flat,
333 &mut groups,
334 tol,
335 )?;
336 }
337 }
338 };
339
340 model.begin_operation();
341 let built = Builder {
342 model,
343 points: &points,
344 triangles: &triangles,
345 groups: &groups,
346 plan: &plan,
347 tol,
348 }
349 .build()?;
350 report.faces = built.iter().flatten().count();
351 report.curved_faces = groups
352 .carriers
353 .iter()
354 .zip(&built)
355 .filter(|(c, b)| matches!(c, Carrier::Curved(_)) && b.is_some())
356 .count();
357
358 let mut shells = Vec::with_capacity(pieces.len());
361 for piece in &pieces {
362 let mut faces: Vec<Shape> = Vec::new();
363 let mut taken = vec![false; groups.carriers.len()];
364 for &t in &piece.triangles {
365 let g = groups.of[t as usize];
366 if !taken[g] {
367 taken[g] = true;
368 if let Some(face) = &built[g] {
369 faces.push(face.clone());
370 }
371 }
372 }
373 shells.push(model.add_shell(&faces)?);
374 }
375 let shape = if all_closed {
376 let mut solids = Vec::new();
377 for (i, shell) in shells.iter().enumerate() {
378 if depth[i] % 2 == 1 {
379 continue;
380 }
381 let mut members = vec![shell.clone()];
382 for (j, void) in shells.iter().enumerate() {
383 if depth[j] == depth[i] + 1 && inside(&points, &triangles, &pieces[i], &pieces[j]) {
384 members.push(void.clone());
385 }
386 }
387 solids.push(model.add_solid(&members)?);
388 }
389 if solids.len() == 1 {
390 solids.swap_remove(0)
391 } else {
392 model.add_compound(&solids)?
393 }
394 } else if shells.len() == 1 {
395 shells.swap_remove(0)
396 } else {
397 model.add_compound(&shells)?
398 };
399 Ok(MeshSolid {
400 shape,
401 closed: all_closed,
402 report,
403 })
404}
405
406fn weld_points(positions: &[Point], weld: f64) -> (Vec<Point>, Vec<u32>) {
409 #[allow(clippy::cast_possible_truncation, reason = "saturating")]
412 let cell = |p: Point| {
413 (
414 (p.x / weld).floor() as i64,
415 (p.y / weld).floor() as i64,
416 (p.z / weld).floor() as i64,
417 )
418 };
419 let mut grid: HashMap<(i64, i64, i64), Vec<u32>> = HashMap::with_capacity(positions.len());
420 let mut kept: Vec<Point> = Vec::with_capacity(positions.len());
421 let mut remap = Vec::with_capacity(positions.len());
422 for p in positions {
423 let (x, y, z) = cell(*p);
424 let mut found = None;
425 'search: for dx in -1..=1_i64 {
426 for dy in -1..=1_i64 {
427 for dz in -1..=1_i64 {
428 let key = (
429 x.saturating_add(dx),
430 y.saturating_add(dy),
431 z.saturating_add(dz),
432 );
433 if let Some(list) = grid.get(&key)
434 && let Some(&k) = list
435 .iter()
436 .find(|&&k| kept[k as usize].distance(*p) <= weld)
437 {
438 found = Some(k);
439 break 'search;
440 }
441 }
442 }
443 }
444 let index = found.unwrap_or_else(|| {
445 let k = u32::try_from(kept.len()).unwrap_or(u32::MAX);
446 kept.push(*p);
447 grid.entry((x, y, z)).or_default().push(k);
448 k
449 });
450 remap.push(index);
451 }
452 (kept, remap)
453}
454
455fn has_area(points: &[Point], [a, b, c]: [u32; 3], weld: f64) -> bool {
458 let [a, b, c] = [a, b, c].map(|i| points[i as usize]);
459 let twice_area = (b - a).cross(c - a).magnitude();
460 let longest = a.distance(b).max(b.distance(c)).max(c.distance(a));
461 twice_area > weld * longest
462}
463
464fn diagonal(points: &[Point]) -> f64 {
465 let (lo, hi) = points
466 .iter()
467 .fold(([f64::MAX; 3], [f64::MIN; 3]), |(lo, hi), p| {
468 (
469 [lo[0].min(p.x), lo[1].min(p.y), lo[2].min(p.z)],
470 [hi[0].max(p.x), hi[1].max(p.y), hi[2].max(p.z)],
471 )
472 });
473 Point::new(lo[0], lo[1], lo[2]).distance(Point::new(hi[0], hi[1], hi[2]))
474}
475
476type Half = usize;
479
480fn from_to(triangles: &[[u32; 3]], h: Half) -> (u32, u32) {
481 let t = triangles[h / 3];
482 (t[h % 3], t[(h % 3 + 1) % 3])
483}
484
485fn next(h: Half) -> Half {
486 h - h % 3 + (h % 3 + 1) % 3
487}
488
489struct Adjacency {
491 twin: Vec<Option<Half>>,
494 used_once: usize,
495 used_more: usize,
496}
497
498impl Adjacency {
499 fn new(triangles: &[[u32; 3]]) -> Self {
500 let mut keyed: Vec<(u64, Half)> = (0..triangles.len() * 3)
501 .map(|h| {
502 let (a, b) = from_to(triangles, h);
503 ((u64::from(a.min(b)) << 32) | u64::from(a.max(b)), h)
504 })
505 .collect();
506 keyed.sort_unstable();
507 let mut twin = vec![None; keyed.len()];
508 let (mut used_once, mut used_more) = (0, 0);
509 let mut i = 0;
510 while i < keyed.len() {
511 let mut j = i;
512 while j < keyed.len() && keyed[j].0 == keyed[i].0 {
513 j += 1;
514 }
515 let n = j - i;
516 match n {
517 1 => used_once += 1,
518 2 => {
519 twin[keyed[i].1] = Some(keyed[i + 1].1);
520 twin[keyed[i + 1].1] = Some(keyed[i].1);
521 }
522 _ => used_more += 1,
523 }
524 i = j;
525 }
526 Self {
527 twin,
528 used_once,
529 used_more,
530 }
531 }
532}
533
534struct Piece {
536 triangles: Vec<u32>,
537 closed: bool,
538}
539
540fn orient(
544 points: &[Point],
545 triangles: &mut [[u32; 3]],
546 adjacency: &Adjacency,
547 report: &mut MeshSolidReport,
548) -> Vec<Piece> {
549 let n = triangles.len();
550 let mut flip: Vec<Option<bool>> = vec![None; n];
551 let mut pieces = Vec::new();
552 for seed in 0..n {
553 if flip[seed].is_some() {
554 continue;
555 }
556 flip[seed] = Some(false);
557 let mut members = vec![seed];
558 let mut stack = vec![seed];
559 let mut conflicts = 0;
560 let mut open = false;
561 while let Some(t) = stack.pop() {
562 let mine = flip[t].unwrap_or(false);
563 for h in 3 * t..3 * t + 3 {
564 let Some(g) = adjacency.twin[h] else {
565 open = true;
566 continue;
567 };
568 let other = g / 3;
569 let same_way = from_to(triangles, h) == from_to(triangles, g);
571 let wanted = mine ^ same_way;
572 match flip[other] {
573 None => {
574 flip[other] = Some(wanted);
575 members.push(other);
576 stack.push(other);
577 }
578 Some(have) if have != wanted => conflicts += 1,
579 Some(_) => {}
580 }
581 }
582 }
583 conflicts /= 2;
585 report.orientation_conflicts += conflicts;
586 let flipped = members.iter().filter(|&&t| flip[t] == Some(true)).count();
587 let closed = !open && conflicts == 0;
588 let turn_all = if closed {
589 let volume: f64 = members
590 .iter()
591 .map(|&t| {
592 let mut tri = triangles[t];
593 if flip[t] == Some(true) {
594 tri.swap(1, 2);
595 }
596 signed_volume(points, tri)
597 })
598 .sum();
599 volume < 0.0
600 } else {
601 flipped * 2 > members.len()
602 };
603 for &t in &members {
604 if flip[t].unwrap_or(false) ^ turn_all {
605 triangles[t].swap(1, 2);
606 report.windings_flipped += 1;
607 }
608 }
609 let mut members: Vec<u32> = members
610 .into_iter()
611 .map(|t| u32::try_from(t).unwrap_or(u32::MAX))
612 .collect();
613 members.sort_unstable();
614 pieces.push(Piece {
615 triangles: members,
616 closed,
617 });
618 }
619 pieces
620}
621
622fn signed_volume(points: &[Point], [a, b, c]: [u32; 3]) -> f64 {
623 let [a, b, c] = [a, b, c].map(|i| points[i as usize] - Point::ORIGIN);
624 a.dot(b.cross(c)) / 6.0
625}
626
627fn inside(points: &[Point], triangles: &[[u32; 3]], outer: &Piece, inner: &Piece) -> bool {
630 let Some(&first) = inner.triangles.first() else {
631 return false;
632 };
633 let origin = points[triangles[first as usize][0] as usize];
634 let direction = Vector::new(0.577_215_664_9, 0.618_033_988_7, 0.533_751_168_7);
636 let mut crossings = 0;
637 for &t in &outer.triangles {
638 let [a, b, c] = triangles[t as usize].map(|i| points[i as usize]);
639 let (e1, e2) = (b - a, c - a);
640 let p = direction.cross(e2);
641 let det = e1.dot(p);
642 if det.abs() < 1e-300 {
643 continue;
644 }
645 let s = origin - a;
646 let u = s.dot(p) / det;
647 if !(0.0..=1.0).contains(&u) {
648 continue;
649 }
650 let q = s.cross(e1);
651 let v = direction.dot(q) / det;
652 if v < 0.0 || u + v > 1.0 {
653 continue;
654 }
655 if e2.dot(q) / det > 0.0 {
656 crossings += 1;
657 }
658 }
659 crossings % 2 == 1
660}
661
662#[derive(Debug, Clone)]
664enum Carrier {
665 Plane(Plane),
667 Curved(Curved),
669 Gone,
671}
672
673#[derive(Debug, Clone)]
674struct Curved {
675 shape: Canonical,
676 deviation: f64,
678 centre: (f64, f64),
681 wraps: bool,
684 wraps_v: bool,
687 fixed: bool,
691 vertices: Vec<u32>,
693}
694
695#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697enum Layout {
698 Open,
700 Band { round_tube: bool },
703 Cap,
705 Whole,
707 Wrapped,
711 Holed,
714}
715
716struct Groups {
719 of: Vec<usize>,
720 carriers: Vec<Carrier>,
721}
722
723fn plane_of(points: &[Point], [a, b, c]: [u32; 3], tol: Tolerances) -> OgeomResult<Plane> {
726 let [a, b, c] = [a, b, c].map(|i| points[i as usize]);
727 let normal = (b - a).cross(c - a);
731 let z = Direction::new(normal / normal.magnitude(), tol)?;
732 let x = Direction::new((b - a) / (b - a).magnitude(), tol)?;
733 Ok(Plane::new(Frame::new(a, z, x, tol)?))
734}
735
736fn unit_normal(points: &[Point], [a, b, c]: [u32; 3]) -> Vector {
737 let [a, b, c] = [a, b, c].map(|i| points[i as usize]);
738 let n = (b - a).cross(c - a);
739 n / n.magnitude()
740}
741
742fn coplanar_groups(
749 points: &[Point],
750 triangles: &[[u32; 3]],
751 adjacency: &Adjacency,
752 angle: f64,
753 flat: f64,
754 groups: &mut Groups,
755 tol: Tolerances,
756) -> OgeomResult<()> {
757 let area = |t: usize| {
758 let [a, b, c] = triangles[t].map(|i| points[i as usize]);
759 (b - a).cross(c - a).magnitude()
760 };
761 let mut order: Vec<usize> = (0..triangles.len())
762 .filter(|&t| groups.of[t] == usize::MAX)
763 .collect();
764 order.sort_by(|&x, &y| area(y).total_cmp(&area(x)));
765 let cos = angle.cos();
766 let mut stack = Vec::new();
767 for seed in order {
768 if groups.of[seed] != usize::MAX {
769 continue;
770 }
771 let g = groups.carriers.len();
772 let plane = plane_of(points, triangles[seed], tol)?;
773 let (origin, normal) = (plane.frame().origin(), plane.frame().z().vector());
774 groups.carriers.push(Carrier::Plane(plane));
775 groups.of[seed] = g;
776 stack.push(seed);
777 while let Some(t) = stack.pop() {
778 for h in 3 * t..3 * t + 3 {
779 let Some(twin) = adjacency.twin[h] else {
780 continue;
781 };
782 let other = twin / 3;
783 if groups.of[other] != usize::MAX {
784 continue;
785 }
786 let [a, b, c] = triangles[other].map(|i| points[i as usize]);
787 let n = (b - a).cross(c - a);
788 if n.dot(normal) < cos * n.magnitude() {
789 continue;
790 }
791 if [a, b, c]
792 .iter()
793 .all(|p| (*p - origin).dot(normal).abs() <= flat)
794 {
795 groups.of[other] = g;
796 stack.push(other);
797 }
798 }
799 }
800 }
801 Ok(())
802}
803
804fn one_each(
806 points: &[Point],
807 triangles: &[[u32; 3]],
808 groups: &mut Groups,
809 tol: Tolerances,
810) -> OgeomResult<()> {
811 for (t, triangle) in triangles.iter().enumerate() {
812 if groups.of[t] == usize::MAX {
813 groups.of[t] = groups.carriers.len();
814 groups
815 .carriers
816 .push(Carrier::Plane(plane_of(points, *triangle, tol)?));
817 }
818 }
819 Ok(())
820}
821
822const SMALL_STAGE_TURN: f64 = 0.35;
827
828fn turn_of(normals: &[Vector]) -> f64 {
830 let mut widest: f64 = 0.0;
831 for (i, a) in normals.iter().enumerate() {
832 for b in &normals[i + 1..] {
833 widest = widest.max(a.dot(*b).clamp(-1.0, 1.0).acos());
834 }
835 }
836 widest
837}
838
839fn leans_as_the_surface(shape: &Canonical, corners: [Point; 3], normal: Vector) -> bool {
847 let mut at = [Vector::ZERO; 3];
848 for (n, p) in at.iter_mut().zip(corners) {
849 let g = gradient(shape, p);
850 let m = g.magnitude();
851 if m == 0.0 {
852 return false;
853 }
854 *n = if g.dot(normal) < 0.0 { -g / m } else { g / m };
855 }
856 let angle = |a: Vector, b: Vector| a.dot(b).clamp(-1.0, 1.0).acos();
857 let spread = angle(at[0], at[1])
858 .max(angle(at[1], at[2]))
859 .max(angle(at[0], at[2]));
860 if spread > FACET_TURN {
863 return false;
864 }
865 let mean = at[0] + at[1] + at[2];
866 let m = mean.magnitude();
867 m > 0.0 && angle(mean / m, normal) <= spread + FACET_LEAN
868}
869
870const FACET_LEAN: f64 = 0.2;
875
876const FACET_TURN: f64 = core::f64::consts::FRAC_PI_3;
878
879fn gradient(shape: &Canonical, p: Point) -> Vector {
880 let radial = |o: Point, z: Vector| {
881 let w = p - o;
882 let r = w - z * w.dot(z);
883 let m = r.magnitude();
884 (if m > 0.0 { r / m } else { Vector::ZERO }, w.dot(z))
885 };
886 match shape {
887 Canonical::Plane(plane) => plane.frame().z().vector(),
888 Canonical::Cylinder(c) => radial(c.frame().origin(), c.frame().z().vector()).0,
889 Canonical::Cone(c) => {
890 let z = c.frame().z().vector();
891 let (out, _) = radial(c.frame().origin(), z);
892 out - z * c.half_angle().tan()
893 }
894 Canonical::Sphere(s) => p - s.centre(),
895 Canonical::Torus(t) => {
896 let z = t.frame().z().vector();
897 let (out, _) = radial(t.frame().origin(), z);
898 p - (t.frame().origin() + out * t.major_radius())
899 }
900 }
901}
902
903fn axis_frame(shape: &Canonical) -> Option<Frame> {
905 match shape {
906 Canonical::Cylinder(c) => Some(c.frame()),
907 Canonical::Cone(c) => Some(c.frame()),
908 Canonical::Torus(t) => Some(t.frame()),
909 Canonical::Sphere(s) => Some(s.frame()),
910 Canonical::Plane(_) => None,
911 }
912}
913
914fn on_frame(shape: &Canonical, frame: Frame, tol: Tolerances) -> Option<Canonical> {
917 Some(match shape {
918 Canonical::Cylinder(c) => Canonical::Cylinder(Cylinder::new(frame, c.radius(), tol).ok()?),
919 Canonical::Cone(c) => {
920 let old = c.frame();
923 let shift = (frame.origin() - old.origin()).dot(old.z().vector());
924 let same = frame.z().vector().dot(old.z().vector()) > 0.0;
925 if !same {
926 return None;
927 }
928 let r0 = c.radius_at(shift);
929 Canonical::Cone(Cone::new(frame, r0.max(tol.confusion()), c.half_angle(), tol).ok()?)
930 }
931 Canonical::Torus(t) => {
932 Canonical::Torus(Torus::new(frame, t.major_radius(), t.minor_radius(), tol).ok()?)
933 }
934 Canonical::Sphere(s) => Canonical::Sphere(Sphere::new(frame, s.radius(), tol).ok()?),
935 Canonical::Plane(_) => return None,
936 })
937}
938
939fn chart(shape: &Canonical, p: Point, tol: Tolerances) -> Option<(f64, f64)> {
941 use ogeom_math::elementary as e;
942 match shape {
943 Canonical::Plane(plane) => Some(e::plane_parameters(plane, p)),
944 Canonical::Cylinder(c) => e::cylinder_parameters(c, p, tol).ok(),
945 Canonical::Cone(c) => e::cone_parameters(c, p, tol).ok(),
946 Canonical::Sphere(s) => e::sphere_parameters(s, p, tol).ok(),
947 Canonical::Torus(t) => e::torus_parameters(t, p, tol).ok(),
948 }
949}
950
951fn evaluate(shape: &Canonical, (u, v): (f64, f64)) -> Point {
952 use ogeom_math::elementary as e;
953 match shape {
954 Canonical::Plane(plane) => e::plane_at(plane, u, v).point,
955 Canonical::Cylinder(c) => e::cylinder_at(c, u, v).point,
956 Canonical::Cone(c) => e::cone_at(c, u, v).point,
957 Canonical::Sphere(s) => e::sphere_at(s, u, v).point,
958 Canonical::Torus(t) => e::torus_at(t, u, v).point,
959 }
960}
961
962fn periodic(shape: &Canonical) -> (bool, bool) {
964 match shape {
965 Canonical::Plane(_) => (false, false),
966 Canonical::Cylinder(_) | Canonical::Cone(_) | Canonical::Sphere(_) => (true, false),
967 Canonical::Torus(_) => (true, true),
968 }
969}
970
971fn unwrapped(curved: &Curved, p: Point, tol: Tolerances) -> Option<(f64, f64)> {
973 let (u, v) = chart(&curved.shape, p, tol)?;
974 let (pu, pv) = periodic(&curved.shape);
975 let near = |x: f64, c: f64, wraps: bool| {
976 if wraps {
977 c + ogeom_math::elementary::wrap_signed_angle(x - c)
978 } else {
979 x
980 }
981 };
982 Some((near(u, curved.centre.0, pu), near(v, curved.centre.1, pv)))
983}
984
985fn angular_spread(angles: &mut [f64]) -> (f64, f64) {
987 let (s, c) = angles
988 .iter()
989 .fold((0.0, 0.0), |(s, c), a| (s + a.sin(), c + a.cos()));
990 angles.sort_by(f64::total_cmp);
991 let mut gap: f64 = 0.0;
992 for w in angles.windows(2) {
993 gap = gap.max(w[1] - w[0]);
994 }
995 if let (Some(first), Some(last)) = (angles.first(), angles.last()) {
996 gap = gap.max(first + core::f64::consts::TAU - last);
997 }
998 (s.atan2(c), gap)
999}
1000
1001#[allow(clippy::too_many_arguments, reason = "the segmentation's inputs")]
1014fn segment(
1015 points: &[Point],
1016 triangles: &[[u32; 3]],
1017 adjacency: &Adjacency,
1018 options: &MeshSolidOptions,
1019 flat: f64,
1020 tol: Tolerances,
1021) -> OgeomResult<Groups> {
1022 let n = triangles.len();
1023 let mut groups = Groups {
1024 of: vec![usize::MAX; n],
1025 carriers: Vec::new(),
1026 };
1027 if !options.merge_coplanar {
1028 one_each(points, triangles, &mut groups, tol)?;
1029 return Ok(groups);
1030 }
1031 if options.recognize {
1032 recognized_regions(
1033 points,
1034 triangles,
1035 adjacency,
1036 options,
1037 flat,
1038 &mut groups,
1039 tol,
1040 );
1041 sphere_axes(points, triangles, adjacency, &mut groups, flat, tol);
1042 align_axes(points, &mut groups, flat, tol);
1043 hole_frames(points, triangles, adjacency, &mut groups, tol);
1044 slit_bands(points, triangles, adjacency, &mut groups, tol);
1045 }
1046 coplanar_groups(
1047 points,
1048 triangles,
1049 adjacency,
1050 options.coplanar_angle,
1051 flat,
1052 &mut groups,
1053 tol,
1054 )?;
1055 Ok(groups)
1056}
1057
1058struct FirstFit {
1062 region: Vec<usize>,
1063 vertices: Vec<u32>,
1064 shared: Vec<u32>,
1065 first_sample: usize,
1066 wide: bool,
1070 found: Option<(crate::recognize::Recognized, Vec<bool>)>,
1071}
1072
1073struct Surfaces<'a> {
1075 points: &'a [Point],
1076 triangles: &'a [[u32; 3]],
1077 adjacency: &'a Adjacency,
1078 normals: Vec<Vector>,
1079 cos_crease: f64,
1080 cos_flat: f64,
1081 flat: f64,
1082 tol: Tolerances,
1083}
1084
1085impl Surfaces<'_> {
1086 fn turn(&self, h: Half) -> Option<f64> {
1087 self.adjacency.twin[h].map(|g| self.normals[h / 3].dot(self.normals[g / 3]))
1088 }
1089
1090 fn curved(&self, h: Half) -> bool {
1091 self.turn(h)
1092 .is_some_and(|c| c >= self.cos_crease && c < self.cos_flat)
1093 }
1094
1095 fn smooth(&self, h: Half) -> bool {
1096 self.turn(h).is_some_and(|c| c >= self.cos_crease)
1097 }
1098
1099 fn bends(&self, t: usize) -> bool {
1100 (3 * t..3 * t + 3).any(|h| self.curved(h))
1101 }
1102
1103 fn samples(&self, vertices: &[u32], region: &[usize]) -> (Vec<Point>, Vec<Vector>) {
1105 let mut sum: HashMap<u32, Vector> = HashMap::with_capacity(vertices.len());
1106 for &t in region {
1107 for &v in &self.triangles[t] {
1108 *sum.entry(v).or_insert(Vector::ZERO) += self.normals[t];
1109 }
1110 }
1111 let pts = vertices.iter().map(|&v| self.points[v as usize]).collect();
1112 let nrm = vertices
1113 .iter()
1114 .map(|v| {
1115 let s = sum.get(v).copied().unwrap_or(Vector::Z);
1116 let m = s.magnitude();
1117 if m > 0.0 { s / m } else { Vector::Z }
1118 })
1119 .collect();
1120 (pts, nrm)
1121 }
1122
1123 fn chords(&self, region: &[usize]) -> Vec<(Point, Point)> {
1126 let stride = region.len().div_ceil(100).max(1);
1127 region
1128 .iter()
1129 .step_by(stride)
1130 .flat_map(|&t| {
1131 let [a, b, c] = self.triangles[t].map(|v| self.points[v as usize]);
1132 [(a, b), (b, c), (c, a)]
1133 })
1134 .collect()
1135 }
1136
1137 fn first_fit(&self, seed: usize, of: &[usize], tried: &[bool]) -> FirstFit {
1150 const STAGES: [usize; 4] = [12, 24, 60, 150];
1151 let mut held: std::collections::HashSet<usize> = std::collections::HashSet::from([seed]);
1152 let mut seen: std::collections::HashSet<u32> = std::collections::HashSet::new();
1153 let mut region = vec![seed];
1154 let mut vertices: Vec<u32> = Vec::new();
1155 let take =
1156 |t: usize, vertices: &mut Vec<u32>, seen: &mut std::collections::HashSet<u32>| {
1157 for &v in &self.triangles[t] {
1158 if seen.insert(v) {
1159 vertices.push(v);
1160 }
1161 }
1162 };
1163 take(seed, &mut vertices, &mut seen);
1164 let mut queue: std::collections::VecDeque<usize> = std::collections::VecDeque::from([seed]);
1165 let mut found = None;
1166 let mut shared = Vec::new();
1167 let mut first_sample = usize::MAX;
1168 let mut wide = false;
1169 for target in STAGES {
1170 while vertices.len() < target {
1171 let Some(next) = queue.pop_front() else {
1172 break;
1173 };
1174 for h in 3 * next..3 * next + 3 {
1175 let Some(g) = self.adjacency.twin[h] else {
1176 continue;
1177 };
1178 let other = g / 3;
1179 if !self.smooth(h) || held.contains(&other) {
1180 continue;
1181 }
1182 if of[other] != usize::MAX
1183 || tried[other]
1184 || !(self.curved(h) || self.bends(other))
1185 {
1186 continue;
1187 }
1188 held.insert(other);
1189 region.push(other);
1190 take(other, &mut vertices, &mut seen);
1191 queue.push_back(other);
1192 }
1193 }
1194 shared = {
1199 let mut count: HashMap<u32, u32> = HashMap::with_capacity(vertices.len());
1200 for &t in ®ion {
1201 for &v in &self.triangles[t] {
1202 *count.entry(v).or_insert(0) += 1;
1203 }
1204 }
1205 vertices
1206 .iter()
1207 .copied()
1208 .filter(|v| count[v] >= 2)
1209 .collect::<Vec<u32>>()
1210 };
1211 first_sample = first_sample.min(region.len());
1212 if target == STAGES[0] {
1213 let normals: Vec<Vector> = region.iter().map(|&t| self.normals[t]).collect();
1214 wide = turn_of(&normals) >= SMALL_STAGE_TURN;
1215 }
1216 if shared.len() < 8 {
1217 if queue.is_empty() {
1218 break;
1219 }
1220 continue;
1221 }
1222 let (pts, nrm) = self.samples(&shared, ®ion);
1223 if target == STAGES[0] && !wide {
1227 continue;
1228 }
1229 let chords = self.chords(®ion);
1230 match crate::recognize::recognize_trimmed(&pts, &nrm, &chords, self.flat, self.tol) {
1231 Ok(fit) => {
1232 found = Some(fit);
1233 break;
1234 }
1235 Err(closest) if closest > span(&pts) * 1e-2 || queue.is_empty() => break,
1240 Err(_) => {}
1241 }
1242 }
1243 FirstFit {
1244 region,
1245 vertices,
1246 shared,
1247 first_sample,
1248 wide,
1249 found,
1250 }
1251 }
1252}
1253
1254#[allow(clippy::too_many_lines, reason = "one growth, read in one place")]
1262fn recognized_regions(
1263 points: &[Point],
1264 triangles: &[[u32; 3]],
1265 adjacency: &Adjacency,
1266 options: &MeshSolidOptions,
1267 flat: f64,
1268 groups: &mut Groups,
1269 tol: Tolerances,
1270) {
1271 let n = triangles.len();
1272 let mesh = Surfaces {
1273 points,
1274 triangles,
1275 adjacency,
1276 normals: triangles.iter().map(|t| unit_normal(points, *t)).collect(),
1277 cos_crease: options.crease.cos(),
1278 cos_flat: options.coplanar_angle.cos(),
1279 flat,
1280 tol,
1281 };
1282 let agree = options.crease.cos();
1285 let mut tried = vec![false; n];
1286 let mut changed = vec![0_u32; n];
1288 let batch_size = ogeom_core::parallel::threads().max(1) * 4;
1289 let mut batch = 0_u32;
1290 let eligible =
1291 |t: usize, of: &[usize], tried: &[bool]| of[t] == usize::MAX && !tried[t] && mesh.bends(t);
1292 let stride = [7919_usize, 7907, 7901]
1298 .into_iter()
1299 .find(|p| !n.is_multiple_of(*p))
1300 .unwrap_or(1);
1301 let order: Vec<usize> = (0..n).map(|i| (i * stride) % n).collect();
1302 let mut next = 0;
1303 while next < n {
1304 batch += 1;
1305 let mut seeds = Vec::with_capacity(batch_size);
1306 while next < n && seeds.len() < batch_size {
1307 if eligible(order[next], &groups.of, &tried) {
1308 seeds.push(order[next]);
1309 }
1310 next += 1;
1311 }
1312 let fits = {
1313 let (of, tried) = (&groups.of, &tried);
1314 ogeom_core::parallel::map_ordered(&seeds, |_, &seed| mesh.first_fit(seed, of, tried))
1315 };
1316 for (seed, fit) in seeds.into_iter().zip(fits) {
1317 if !eligible(seed, &groups.of, &tried) {
1318 continue;
1319 }
1320 let fit = if fit.region.iter().any(|&t| changed[t] == batch) {
1324 mesh.first_fit(seed, &groups.of, &tried)
1325 } else {
1326 fit
1327 };
1328 let FirstFit {
1329 mut region,
1330 mut vertices,
1331 shared,
1332 first_sample,
1333 wide,
1334 found,
1335 ..
1336 } = fit;
1337 let Some((found, keep)) = found else {
1338 let retired = if wide { 1 } else { first_sample };
1343 for &t in ®ion[..retired.min(region.len())] {
1344 tried[t] = true;
1345 changed[t] = batch;
1346 }
1347 continue;
1348 };
1349 let kept: HashMap<u32, bool> = shared.iter().copied().zip(keep).collect();
1352 let dropped: std::collections::HashSet<u32> = vertices
1353 .iter()
1354 .copied()
1355 .filter(|v| {
1356 !kept
1357 .get(v)
1358 .copied()
1359 .unwrap_or_else(|| found.surface.distance_to(points[*v as usize]) <= flat)
1360 })
1361 .collect();
1362 if !dropped.is_empty() {
1363 let gathered = region.clone();
1364 region.retain(|&t| triangles[t].iter().all(|v| !dropped.contains(v)));
1365 if region.is_empty() {
1366 for &t in &gathered[..first_sample.min(gathered.len())] {
1367 tried[t] = true;
1368 changed[t] = batch;
1369 }
1370 continue;
1371 }
1372 let mut seen = std::collections::HashSet::new();
1373 vertices.clear();
1374 for &t in ®ion {
1375 for &v in &triangles[t] {
1376 if seen.insert(v) {
1377 vertices.push(v);
1378 }
1379 }
1380 }
1381 }
1382 let mut shape = found.surface;
1383 let mut mine: std::collections::HashSet<usize> = region.iter().copied().collect();
1384 let mut seen: std::collections::HashSet<u32> = vertices.iter().copied().collect();
1385
1386 let mut fitted_at = vertices.len();
1391 loop {
1392 let mut i = 0;
1393 while i < region.len() {
1394 let t = region[i];
1395 i += 1;
1396 for h in 3 * t..3 * t + 3 {
1397 let Some(g) = adjacency.twin[h] else {
1398 continue;
1399 };
1400 let other = g / 3;
1401 if mine.contains(&other) || groups.of[other] != usize::MAX {
1402 continue;
1403 }
1404 let corners = triangles[other].map(|v| points[v as usize]);
1405 if corners.iter().any(|p| shape.distance_to(*p) > flat) {
1406 continue;
1407 }
1408 if mesh.smooth(h) {
1414 let centroid = Point::from_vector(
1415 (corners[0].to_vector()
1416 + corners[1].to_vector()
1417 + corners[2].to_vector())
1418 / 3.0,
1419 );
1420 let direction = gradient(&shape, centroid);
1421 let m = direction.magnitude();
1422 if m == 0.0 || (direction.dot(mesh.normals[other]) / m).abs() < agree {
1423 continue;
1424 }
1425 } else if !leans_as_the_surface(&shape, corners, mesh.normals[other])
1426 || !sags_as_the_surface(&shape, corners, flat)
1427 {
1428 continue;
1429 }
1430 mine.insert(other);
1431 region.push(other);
1432 for &v in &triangles[other] {
1433 if seen.insert(v) {
1434 vertices.push(v);
1435 }
1436 }
1437 }
1438 }
1439 if vertices.len() <= fitted_at {
1440 break;
1441 }
1442 fitted_at = vertices.len();
1443 let (pts, nrm) = mesh.samples(&vertices, ®ion);
1444 match recognize_curved(&pts, &nrm, &mesh.chords(®ion), flat, tol) {
1445 Some(better) => shape = better.surface,
1446 None => break,
1447 }
1448 }
1449 let mut claimed: std::collections::HashSet<usize> = std::collections::HashSet::new();
1455 let rim: Vec<usize> = region
1456 .iter()
1457 .flat_map(|&t| (3 * t..3 * t + 3).filter_map(|h| adjacency.twin[h]))
1458 .map(|g| g / 3)
1459 .filter(|&other| !mine.contains(&other) && groups.of[other] == usize::MAX)
1460 .collect();
1461 for start in rim {
1462 if claimed.contains(&start) {
1463 continue;
1464 }
1465 let mut cluster = vec![start];
1466 let mut inside: std::collections::HashSet<usize> =
1467 std::collections::HashSet::from([start]);
1468 let mut surrounded = true;
1469 let mut i = 0;
1470 while i < cluster.len() && surrounded {
1471 let t = cluster[i];
1472 i += 1;
1473 for h in 3 * t..3 * t + 3 {
1474 let Some(g) = adjacency.twin[h] else {
1475 surrounded = false;
1476 break;
1477 };
1478 let next = g / 3;
1479 if mine.contains(&next) || inside.contains(&next) {
1480 continue;
1481 }
1482 if groups.of[next] != usize::MAX || cluster.len() >= ENCLOSED_CLUSTER {
1483 surrounded = false;
1484 break;
1485 }
1486 inside.insert(next);
1487 cluster.push(next);
1488 }
1489 }
1490 let on_surface = cluster.iter().all(|&t| {
1491 let corners = triangles[t].map(|v| points[v as usize]);
1492 corners.iter().all(|p| shape.distance_to(*p) <= flat)
1493 && sags_as_the_surface(&shape, corners, flat)
1494 && (is_sliver(corners)
1495 || leans_as_the_surface(&shape, corners, mesh.normals[t]))
1496 });
1497 if surrounded && on_surface {
1498 for t in cluster {
1499 claimed.insert(t);
1500 if mine.insert(t) {
1501 region.push(t);
1502 }
1503 }
1504 }
1505 }
1506 let pts: Vec<Point> = vertices.iter().map(|&v| points[v as usize]).collect();
1507 let deviation = worst_deviation(&shape, &pts);
1508 let flat_too = crate::recognize::is_flat(&pts, flat, tol);
1509 if deviation > flat || flat_too || region.len() < 2 {
1510 for &t in ®ion {
1511 tried[t] = true;
1512 changed[t] = batch;
1513 }
1514 continue;
1515 }
1516 let g = groups.carriers.len();
1517 for &t in ®ion {
1518 groups.of[t] = g;
1519 changed[t] = batch;
1520 }
1521 groups.carriers.push(Carrier::Curved(Curved {
1522 shape,
1523 deviation,
1524 centre: (0.0, 0.0),
1525 wraps: false,
1526 wraps_v: false,
1527 fixed: false,
1528 vertices,
1529 }));
1530 }
1531 }
1532}
1533
1534fn span(points: &[Point]) -> f64 {
1536 points.first().map_or(0.0, |a| {
1537 points.iter().map(|p| p.distance(*a)).fold(0.0, f64::max)
1538 })
1539}
1540
1541fn sphere_axes(
1548 points: &[Point],
1549 triangles: &[[u32; 3]],
1550 adjacency: &Adjacency,
1551 groups: &mut Groups,
1552 flat: f64,
1553 tol: Tolerances,
1554) {
1555 let reach = flat * REACH;
1556 for g in 0..groups.carriers.len() {
1557 let Carrier::Curved(curved) = &groups.carriers[g] else {
1558 continue;
1559 };
1560 let Canonical::Sphere(sphere) = curved.shape else {
1561 continue;
1562 };
1563 let Some(loops) = border_loops(triangles, adjacency, &groups.of, g) else {
1564 continue;
1565 };
1566 let mut axis: Option<Vector> = None;
1567 let mut planar = true;
1568 for ring in &loops {
1569 let pts: Vec<Point> = ring.iter().map(|&v| points[v as usize]).collect();
1570 let Some((through, normal)) =
1571 (pts.len() >= 3).then(|| plane_through(&pts, tol)).flatten()
1572 else {
1573 planar = false;
1574 break;
1575 };
1576 let n = normal.vector();
1577 if pts.iter().any(|p| (*p - through).dot(n).abs() > reach) {
1578 planar = false;
1579 break;
1580 }
1581 match axis {
1582 None => axis = Some(n),
1583 Some(a) if a.cross(n).magnitude() <= 1e-3 => {}
1584 Some(_) => {
1585 planar = false;
1586 break;
1587 }
1588 }
1589 }
1590 let (true, Some(mut z)) = (planar, axis) else {
1591 continue;
1592 };
1593 let side: f64 = curved
1595 .vertices
1596 .iter()
1597 .map(|&v| (points[v as usize] - sphere.centre()).dot(z))
1598 .sum();
1599 if side < 0.0 {
1600 z = -z;
1601 }
1602 let Ok(z) = Direction::new(z, tol) else {
1603 continue;
1604 };
1605 let Ok(frame) = Frame::new(sphere.centre(), z, z.any_perpendicular(), tol) else {
1606 continue;
1607 };
1608 let Ok(turned) = Sphere::new(frame, sphere.radius(), tol) else {
1609 continue;
1610 };
1611 if let Carrier::Curved(curved) = &mut groups.carriers[g] {
1612 curved.shape = Canonical::Sphere(turned);
1613 curved.fixed = true;
1614 }
1615 }
1616}
1617
1618fn border_loops(
1622 triangles: &[[u32; 3]],
1623 adjacency: &Adjacency,
1624 of: &[usize],
1625 g: usize,
1626) -> Option<Vec<Vec<u32>>> {
1627 let mut leaving: HashMap<u32, Vec<u32>> = HashMap::new();
1628 for (t, tri) in triangles.iter().enumerate() {
1629 if of[t] != g {
1630 continue;
1631 }
1632 for k in 0..3 {
1633 let inside = adjacency.twin[3 * t + k].is_some_and(|o| of[o / 3] == g);
1634 if !inside {
1635 leaving.entry(tri[k]).or_default().push(tri[(k + 1) % 3]);
1636 }
1637 }
1638 }
1639 if leaving.is_empty() || leaving.values().any(|to| to.len() != 1) {
1640 return None;
1641 }
1642 let mut loops: Vec<Vec<u32>> = Vec::new();
1643 let mut done: std::collections::HashSet<u32> = std::collections::HashSet::new();
1644 let mut starts: Vec<u32> = leaving.keys().copied().collect();
1645 starts.sort_unstable();
1646 for start in starts {
1647 if !done.insert(start) {
1648 continue;
1649 }
1650 let mut ring = vec![start];
1651 let mut at = leaving[&start][0];
1652 while at != start && ring.len() <= leaving.len() {
1653 done.insert(at);
1654 ring.push(at);
1655 at = leaving.get(&at).map_or(start, |to| to[0]);
1656 }
1657 loops.push(ring);
1658 }
1659 Some(loops)
1660}
1661
1662fn hole_frames(
1668 points: &[Point],
1669 triangles: &[[u32; 3]],
1670 adjacency: &Adjacency,
1671 groups: &mut Groups,
1672 tol: Tolerances,
1673) {
1674 for g in 0..groups.carriers.len() {
1675 let Carrier::Curved(curved) = &groups.carriers[g] else {
1676 continue;
1677 };
1678 let wanted = match curved.shape {
1679 Canonical::Sphere(_) => curved.wraps && !curved.fixed,
1680 Canonical::Torus(_) => curved.wraps && curved.wraps_v,
1681 _ => false,
1682 };
1683 if !wanted {
1684 continue;
1685 }
1686 let Some(loops) = border_loops(triangles, adjacency, &groups.of, g) else {
1687 continue;
1688 };
1689 let ring_points: Vec<Point> = loops
1690 .iter()
1691 .flatten()
1692 .map(|&v| points[v as usize])
1693 .collect();
1694 let shape = match curved.shape {
1695 Canonical::Sphere(sphere) => {
1696 let unit = |p: Point| {
1700 let d = p - sphere.centre();
1701 let m = d.magnitude();
1702 (m > 0.0).then(|| d / m)
1703 };
1704 let directions: Vec<Vector> = ring_points.iter().filter_map(|p| unit(*p)).collect();
1705 let rings: Vec<Vec<Vector>> = loops
1706 .iter()
1707 .map(|ring| {
1708 ring.iter()
1709 .filter_map(|&v| unit(points[v as usize]))
1710 .collect()
1711 })
1712 .collect();
1713 let mut ranked: Vec<(f64, Vector)> = spread_directions(POLE_CANDIDATES)
1714 .into_iter()
1715 .map(|z| {
1716 let nearest = directions
1717 .iter()
1718 .map(|d| d.dot(z).abs())
1719 .fold(0.0_f64, f64::max);
1720 (nearest, z)
1721 })
1722 .collect();
1723 ranked.sort_by(|a, b| a.0.total_cmp(&b.0));
1724 let on_region = |p: Point| {
1728 triangles
1729 .iter()
1730 .enumerate()
1731 .map(|(t, tri)| {
1732 let c = Point::from_vector(
1733 (points[tri[0] as usize].to_vector()
1734 + points[tri[1] as usize].to_vector()
1735 + points[tri[2] as usize].to_vector())
1736 / 3.0,
1737 );
1738 (c.distance(p), t)
1739 })
1740 .min_by(|a, b| a.0.total_cmp(&b.0))
1741 .is_some_and(|(_, t)| groups.of[t] == g)
1742 };
1743 let best = ranked.into_iter().find(|(_, z)| {
1744 rings.iter().all(|ring| turns_about(ring, *z) == 0)
1745 && on_region(sphere.centre() + *z * sphere.radius())
1746 && on_region(sphere.centre() - *z * sphere.radius())
1747 });
1748 let Some((nearest, z)) = best else {
1749 continue;
1750 };
1751 if nearest > POLE_CLEARANCE.cos() {
1753 continue;
1754 }
1755 let Ok(z) = Direction::new(z, tol) else {
1756 continue;
1757 };
1758 let Ok(frame) = Frame::new(sphere.centre(), z, z.any_perpendicular(), tol) else {
1759 continue;
1760 };
1761 let Ok(turned) = Sphere::new(frame, sphere.radius(), tol) else {
1762 continue;
1763 };
1764 Canonical::Sphere(turned)
1765 }
1766 other => other,
1767 };
1768 let angles: Vec<Vec<(f64, f64)>> = vec![
1771 ring_points
1772 .iter()
1773 .filter_map(|p| chart(&shape, *p, tol))
1774 .collect(),
1775 ];
1776 let Some(free) = free_angle(&angles) else {
1777 continue;
1778 };
1779 let Some(frame) = axis_frame(&shape) else {
1780 continue;
1781 };
1782 let (x, y) = (frame.x().vector(), frame.y().vector());
1783 let Ok(x) = Direction::new(x * free.cos() + y * free.sin(), tol) else {
1784 continue;
1785 };
1786 let Ok(turned) = Frame::new(frame.origin(), frame.z(), x, tol) else {
1787 continue;
1788 };
1789 let Some(shape) = on_frame(&shape, turned, tol) else {
1790 continue;
1791 };
1792 if let Carrier::Curved(curved) = &mut groups.carriers[g] {
1793 curved.shape = shape;
1794 curved.fixed = true;
1795 curved.centre = (core::f64::consts::PI, curved.centre.1);
1796 }
1797 }
1798}
1799
1800fn turns_about(ring: &[Vector], z: Vector) -> i32 {
1802 let x = if z.x.abs() < 0.9 {
1803 Vector::X
1804 } else {
1805 Vector::Y
1806 };
1807 let x = x - z * x.dot(z);
1808 let y = z.cross(x);
1809 let angle = |d: &Vector| d.dot(y).atan2(d.dot(x));
1810 let mut turned = 0.0;
1811 for (i, d) in ring.iter().enumerate() {
1812 let next = &ring[(i + 1) % ring.len()];
1813 turned += ogeom_math::elementary::wrap_signed_angle(angle(next) - angle(d));
1814 }
1815 #[allow(clippy::cast_possible_truncation, reason = "a handful of turns")]
1816 let turns = (turned / core::f64::consts::TAU).round() as i32;
1817 turns
1818}
1819
1820fn slit_bands(
1826 points: &[Point],
1827 triangles: &[[u32; 3]],
1828 adjacency: &Adjacency,
1829 groups: &mut Groups,
1830 tol: Tolerances,
1831) {
1832 for g in 0..groups.carriers.len() {
1833 let Carrier::Curved(curved) = &groups.carriers[g] else {
1834 continue;
1835 };
1836 if !curved.wraps
1837 || curved.wraps_v
1838 || !matches!(curved.shape, Canonical::Cylinder(_) | Canonical::Cone(_))
1839 {
1840 continue;
1841 }
1842 let Some(loops) = border_loops(triangles, adjacency, &groups.of, g) else {
1843 continue;
1844 };
1845 let [ring] = &loops[..] else {
1846 continue;
1847 };
1848 let Some(frame) = axis_frame(&curved.shape) else {
1849 continue;
1850 };
1851 let directions: Vec<Vector> = ring
1852 .iter()
1853 .map(|&v| points[v as usize] - frame.origin())
1854 .collect();
1855 if turns_about(&directions, frame.z().vector()) != 0 {
1856 continue;
1857 }
1858 let mut angles: Vec<f64> = curved
1859 .vertices
1860 .iter()
1861 .filter_map(|&v| chart(&curved.shape, points[v as usize], tol).map(|c| c.0))
1862 .collect();
1863 let Some(gap) = widest_gap(&mut angles) else {
1864 continue;
1865 };
1866 if let Carrier::Curved(curved) = &mut groups.carriers[g] {
1867 curved.wraps = false;
1868 curved.centre = (
1869 ogeom_math::elementary::wrap_angle(gap + core::f64::consts::PI),
1870 curved.centre.1,
1871 );
1872 }
1873 }
1874}
1875
1876fn widest_gap(angles: &mut [f64]) -> Option<f64> {
1878 let tau = core::f64::consts::TAU;
1879 if angles.is_empty() {
1880 return None;
1881 }
1882 for a in angles.iter_mut() {
1883 *a = a.rem_euclid(tau);
1884 }
1885 angles.sort_by(f64::total_cmp);
1886 let mut best = (
1887 angles[0] + tau - angles[angles.len() - 1],
1888 angles[angles.len() - 1],
1889 );
1890 for pair in angles.windows(2) {
1891 if pair[1] - pair[0] > best.0 {
1892 best = (pair[1] - pair[0], pair[0]);
1893 }
1894 }
1895 Some(best.1 + best.0 / 2.0)
1896}
1897
1898const POLE_CANDIDATES: usize = 400;
1900
1901const POLE_CLEARANCE: f64 = 0.087;
1903
1904fn spread_directions(count: usize) -> Vec<Vector> {
1906 let golden = core::f64::consts::PI * (3.0 - 5.0_f64.sqrt());
1907 (0..count)
1908 .map(|i| {
1909 #[allow(clippy::cast_precision_loss, reason = "a few hundred directions")]
1910 let (i, n) = (i as f64, count as f64);
1911 let z = 1.0 - 2.0 * (i + 0.5) / n;
1912 let r = (1.0 - z * z).max(0.0).sqrt();
1913 let a = golden * i;
1914 Vector::new(r * a.cos(), r * a.sin(), z)
1915 })
1916 .collect()
1917}
1918
1919fn align_axes(points: &[Point], groups: &mut Groups, flat: f64, tol: Tolerances) {
1923 let mut leaders: Vec<Frame> = Vec::new();
1924 for carrier in &mut groups.carriers {
1925 let Carrier::Curved(curved) = carrier else {
1926 continue;
1927 };
1928 let Some(frame) = axis_frame(&curved.shape) else {
1929 continue;
1930 };
1931 let sphere = matches!(curved.shape, Canonical::Sphere(_));
1932 let lead = leaders.iter().find(|l| {
1933 let parallel = l.z().vector().cross(frame.z().vector()).magnitude() <= 1e-3;
1934 let w = frame.origin() - l.origin();
1935 let off = (w - l.z().vector() * w.dot(l.z().vector())).magnitude();
1936 !sphere && parallel && off <= flat * 10.0
1937 });
1938 if let Some(lead) = lead {
1939 let z = lead.z().vector();
1940 let w = frame.origin() - lead.origin();
1941 let origin = lead.origin() + z * w.dot(z);
1942 let axis = if frame.z().vector().dot(z) >= 0.0 {
1943 lead.z()
1944 } else {
1945 -lead.z()
1946 };
1947 if let Ok(snapped) = Frame::new(origin, axis, lead.x(), tol)
1948 && let Some(shape) = on_frame(&curved.shape, snapped, tol)
1949 {
1950 let pts: Vec<Point> = curved
1951 .vertices
1952 .iter()
1953 .map(|&v| points[v as usize])
1954 .collect();
1955 let deviation = worst_deviation(&shape, &pts);
1956 if deviation <= flat {
1957 curved.shape = shape;
1958 curved.deviation = deviation;
1959 }
1960 }
1961 } else if !sphere {
1962 leaders.push(frame);
1963 }
1964 let charts: Vec<(f64, f64)> = curved
1966 .vertices
1967 .iter()
1968 .filter_map(|&v| chart(&curved.shape, points[v as usize], tol))
1969 .collect();
1970 let mut us: Vec<f64> = charts.iter().map(|c| c.0).collect();
1971 let (_, gap_u) = angular_spread(&mut us);
1972 let (_, pv) = periodic(&curved.shape);
1973 if pv {
1974 let mut vs: Vec<f64> = charts.iter().map(|c| c.1).collect();
1975 curved.wraps_v = angular_spread(&mut vs).1 < core::f64::consts::FRAC_PI_2;
1976 }
1977 let wraps_u = gap_u < core::f64::consts::FRAC_PI_2;
1978 curved.wraps = wraps_u;
1979 if !curved.wraps
1980 && !curved.wraps_v
1981 && !curved.fixed
1982 && let Some(reframed) = away_from(curved, points, tol)
1983 {
1984 curved.shape = reframed;
1985 }
1986 let charts: Vec<(f64, f64)> = curved
1989 .vertices
1990 .iter()
1991 .filter_map(|&v| chart(&curved.shape, points[v as usize], tol))
1992 .collect();
1993 let mut us: Vec<f64> = charts.iter().map(|c| c.0).collect();
1994 let (mean_u, _) = angular_spread(&mut us);
1995 let mean_v = if curved.wraps_v {
1996 core::f64::consts::PI
1997 } else if pv {
1998 let mut vs: Vec<f64> = charts.iter().map(|c| c.1).collect();
1999 angular_spread(&mut vs).0
2000 } else {
2001 #[allow(
2002 clippy::cast_precision_loss,
2003 reason = "vertex counts are far below 2^52"
2004 )]
2005 let count = charts.len().max(1) as f64;
2006 charts.iter().map(|c| c.1).sum::<f64>() / count
2007 };
2008 let centre_u = if wraps_u {
2009 core::f64::consts::PI
2010 } else {
2011 ogeom_math::elementary::wrap_angle(mean_u)
2012 };
2013 curved.centre = (centre_u, mean_v);
2014 }
2015}
2016
2017fn away_from(curved: &Curved, points: &[Point], tol: Tolerances) -> Option<Canonical> {
2021 let mut mean = Vector::ZERO;
2022 match curved.shape {
2023 Canonical::Sphere(s) => {
2024 for &v in &curved.vertices {
2025 let d = points[v as usize] - s.centre();
2026 let m = d.magnitude();
2027 if m > 0.0 {
2028 mean += d / m;
2029 }
2030 }
2031 let facing = Direction::new(mean, tol).ok()?;
2032 let frame = Frame::new(s.centre(), facing.any_perpendicular(), -facing, tol).ok()?;
2033 Some(Canonical::Sphere(Sphere::new(frame, s.radius(), tol).ok()?))
2034 }
2035 _ => {
2036 let frame = axis_frame(&curved.shape)?;
2037 let z = frame.z().vector();
2038 for &v in &curved.vertices {
2039 let w = points[v as usize] - frame.origin();
2040 let r = w - z * w.dot(z);
2041 let m = r.magnitude();
2042 if m > 0.0 {
2043 mean += r / m;
2044 }
2045 }
2046 let facing = Direction::new(mean, tol).ok()?;
2047 on_frame(
2048 &curved.shape,
2049 Frame::new(frame.origin(), frame.z(), -facing, tol).ok()?,
2050 tol,
2051 )
2052 }
2053 }
2054}
2055
2056#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2059enum Corner {
2060 Mesh(u32),
2061 Placed(usize),
2062}
2063
2064struct EdgeSpec {
2066 curve: Curve,
2067 range: (f64, f64),
2068 ends: [Corner; 2],
2069 tolerance: f64,
2070 closed_circle: bool,
2071}
2072
2073struct Plan {
2075 edges: Vec<EdgeSpec>,
2076 edge_of: HashMap<(u32, u32), (usize, bool)>,
2080 loops: Vec<Vec<Vec<Half>>>,
2082 placed: Vec<Point>,
2083 surfaces: Vec<Option<ogeom_geom::SurfaceGeometry>>,
2085 pcurves: HashMap<(usize, usize), (PlanarCurve, f64)>,
2088 layouts: Vec<Layout>,
2090}
2091
2092struct Planner<'a> {
2094 points: &'a [Point],
2095 triangles: &'a [[u32; 3]],
2096 adjacency: &'a Adjacency,
2097 groups: &'a Groups,
2098 merge: bool,
2099 pinned: &'a std::collections::HashSet<u32>,
2101 flat: f64,
2102 tol: Tolerances,
2103}
2104
2105enum Replan {
2107 Facet(Vec<usize>),
2108 Pin(Vec<u32>),
2109}
2110
2111fn sags_as_the_surface(shape: &Canonical, corners: [Point; 3], flat: f64) -> bool {
2117 let unit = |p: Point| {
2118 let g = gradient(shape, p);
2119 let m = g.magnitude();
2120 (m > 0.0).then(|| g / m)
2121 };
2122 let (Some(a), Some(b), Some(c)) = (unit(corners[0]), unit(corners[1]), unit(corners[2])) else {
2123 return false;
2124 };
2125 let angle = |x: Vector, y: Vector| x.dot(y).clamp(-1.0, 1.0).acos();
2126 let turn = angle(a, b).max(angle(b, c)).max(angle(a, c));
2127 let longest = corners[0]
2128 .distance(corners[1])
2129 .max(corners[1].distance(corners[2]))
2130 .max(corners[0].distance(corners[2]));
2131 let centroid = Point::from_vector(
2132 (corners[0].to_vector() + corners[1].to_vector() + corners[2].to_vector()) / 3.0,
2133 );
2134 shape.distance_to(centroid) <= longest * turn / 6.0 + flat
2135}
2136
2137fn is_sliver(corners: [Point; 3]) -> bool {
2140 let [a, b, c] = corners;
2141 let sides = [(a, b, c), (b, c, a), (c, a, b)];
2142 let (p, q, r) = sides
2143 .into_iter()
2144 .max_by(|x, y| x.0.distance(x.1).total_cmp(&y.0.distance(y.1)))
2145 .unwrap_or((a, b, c));
2146 let base = p.distance(q);
2147 base > 0.0 && distance_to_line(r, p, q) <= base * 0.1
2148}
2149
2150const CHORD_SAG: f64 = 0.05;
2153
2154const TOLERANCE_MARGIN: f64 = 1e-6;
2157
2158const ENCLOSED_CLUSTER: usize = 16;
2161
2162const REACH: f64 = 20.0;
2164
2165impl Planner<'_> {
2166 fn border(&self, h: Half) -> bool {
2167 match self.adjacency.twin[h] {
2168 None => true,
2169 Some(g) => self.groups.of[g / 3] != self.groups.of[h / 3],
2170 }
2171 }
2172
2173 fn curved(&self, g: usize) -> Option<&Curved> {
2174 match &self.groups.carriers[g] {
2175 Carrier::Curved(c) => Some(c),
2176 _ => None,
2177 }
2178 }
2179
2180 #[allow(clippy::too_many_lines, reason = "one pass over the boundary")]
2183 fn plan(&self) -> OgeomResult<Result<Plan, Replan>> {
2184 let halves = self.triangles.len() * 3;
2185 let mut edge_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
2186 for h in 0..halves {
2187 if self.border(h) {
2188 let (a, b) = from_to(self.triangles, h);
2189 edge_faces
2190 .entry((a.min(b), a.max(b)))
2191 .or_default()
2192 .push(self.groups.of[h / 3]);
2193 }
2194 }
2195 for faces in edge_faces.values_mut() {
2196 faces.sort_unstable();
2197 }
2198 let mut incident: HashMap<u32, Vec<(u32, u32)>> = HashMap::new();
2199 for &(a, b) in edge_faces.keys() {
2200 incident.entry(a).or_default().push((a, b));
2201 incident.entry(b).or_default().push((a, b));
2202 }
2203 let any_curved = |faces: &[usize]| faces.iter().any(|&g| self.curved(g).is_some());
2204 let removable = |v: u32| -> bool {
2208 if !self.merge || self.pinned.contains(&v) {
2209 return false;
2210 }
2211 let Some(list) = incident.get(&v) else {
2212 return false;
2213 };
2214 let [e1, e2] = list[..] else {
2215 return false;
2216 };
2217 let faces = &edge_faces[&e1];
2218 if *faces != edge_faces[&e2] {
2219 return false;
2220 }
2221 if any_curved(faces) {
2222 return true;
2223 }
2224 let far = |(a, b): (u32, u32)| if a == v { b } else { a };
2225 let (p, q) = (self.points[far(e1) as usize], self.points[far(e2) as usize]);
2226 let at = self.points[v as usize];
2227 (at - p).dot(q - at) > 0.0 && distance_to_line(at, p, q) <= self.flat
2228 };
2229 let is_kept: HashMap<u32, bool> = incident.keys().map(|&v| (v, !removable(v))).collect();
2230
2231 let mut plan = Plan {
2232 edges: Vec::new(),
2233 edge_of: HashMap::new(),
2234 loops: vec![Vec::new(); self.groups.carriers.len()],
2235 placed: Vec::new(),
2236 surfaces: vec![None; self.groups.carriers.len()],
2237 pcurves: HashMap::new(),
2238 layouts: vec![Layout::Open; self.groups.carriers.len()],
2239 };
2240 let mut failed: Vec<usize> = Vec::new();
2241 let mut keys: Vec<(u32, u32)> = edge_faces.keys().copied().collect();
2242 keys.sort_unstable();
2243 for pass in 0..2 {
2244 for &key in &keys {
2245 if plan.edge_of.contains_key(&key) {
2246 continue;
2247 }
2248 let (a, b) = key;
2249 let start = if is_kept[&a] {
2250 a
2251 } else if is_kept[&b] {
2252 b
2253 } else if pass == 1 {
2254 a
2255 } else {
2256 continue;
2257 };
2258 let mut chain = vec![start];
2259 let mut edges = vec![key];
2260 let mut at = if start == a { b } else { a };
2261 chain.push(at);
2262 while !is_kept[&at] && at != start {
2263 let Some(&following) = incident[&at].iter().find(|e| !edges.contains(e)) else {
2264 break;
2265 };
2266 edges.push(following);
2267 at = if following.0 == at {
2268 following.1
2269 } else {
2270 following.0
2271 };
2272 chain.push(at);
2273 }
2274 let faces = edge_faces[&key].clone();
2275 if any_curved(&faces) {
2276 match self.snapped(&chain, &faces) {
2277 Some(spec) => {
2278 let index = plan.edges.len();
2279 let (spec, forward, images) = spec;
2280 for (g, pcurve, deviation) in images {
2281 plan.pcurves.insert((index, g), (pcurve, deviation));
2282 }
2283 let spec = match spec {
2284 Snapped::Open(curve, range, tolerance) => EdgeSpec {
2285 curve,
2286 range,
2287 ends: if forward {
2288 [
2289 Corner::Mesh(chain[0]),
2290 Corner::Mesh(*chain.last().unwrap_or(&chain[0])),
2291 ]
2292 } else {
2293 [
2294 Corner::Mesh(*chain.last().unwrap_or(&chain[0])),
2295 Corner::Mesh(chain[0]),
2296 ]
2297 },
2298 tolerance,
2299 closed_circle: false,
2300 },
2301 Snapped::Loop(curve, range, tolerance) => EdgeSpec {
2302 curve,
2303 range,
2304 ends: [Corner::Mesh(chain[0]), Corner::Mesh(chain[0])],
2305 tolerance,
2306 closed_circle: false,
2307 },
2308 Snapped::Closed(curve, tolerance) => {
2309 use ogeom_geom::Curve3d as _;
2310 let at = curve.point_at(0.0, self.tol)?;
2311 plan.placed.push(at);
2312 let corner = Corner::Placed(plan.placed.len() - 1);
2313 EdgeSpec {
2314 curve,
2315 range: (0.0, core::f64::consts::TAU),
2316 ends: [corner, corner],
2317 tolerance,
2318 closed_circle: true,
2319 }
2320 }
2321 };
2322 plan.edges.push(spec);
2323 for (i, e) in edges.iter().enumerate() {
2324 plan.edge_of
2325 .insert(*e, (index, (chain[i] == e.0) == forward));
2326 }
2327 }
2328 None => {
2329 for g in faces {
2330 if self.curved(g).is_some() && !failed.contains(&g) {
2331 failed.push(g);
2332 }
2333 }
2334 for e in edges {
2335 plan.edge_of.insert(e, (usize::MAX, true));
2336 }
2337 }
2338 }
2339 continue;
2340 }
2341 let end = at;
2342 let (p, q) = (self.points[start as usize], self.points[end as usize]);
2343 let straight = start != end
2344 && chain[1..chain.len() - 1]
2345 .iter()
2346 .all(|&v| distance_to_line(self.points[v as usize], p, q) <= self.flat);
2347 type Piece = (Vec<u32>, Vec<(u32, u32)>);
2349 let pieces: Vec<Piece> = if straight {
2350 vec![(chain.clone(), edges.clone())]
2351 } else {
2352 edges.iter().map(|&e| (vec![e.0, e.1], vec![e])).collect()
2353 };
2354 for (piece, piece_edges) in pieces {
2355 let (from, to) = (piece[0], piece[piece.len() - 1]);
2356 let (p, q) = (self.points[from as usize], self.points[to as usize]);
2357 let mut reach = self.tol.confusion();
2358 for &v in &piece {
2359 let at = self.points[v as usize];
2360 reach = reach.max(distance_to_line(at, p, q));
2361 for &g in &faces {
2362 if let Carrier::Plane(plane) = &self.groups.carriers[g] {
2363 reach = reach.max(plane.signed_distance_to(at).abs());
2364 }
2365 }
2366 }
2367 let index = plan.edges.len();
2368 plan.edges.push(EdgeSpec {
2369 curve: LineCurve::segment(p, q, self.tol)?.into(),
2370 range: (0.0, p.distance(q)),
2371 ends: [Corner::Mesh(from), Corner::Mesh(to)],
2372 tolerance: reach,
2373 closed_circle: false,
2374 });
2375 for (i, e) in piece_edges.iter().enumerate() {
2376 plan.edge_of.insert(*e, (index, piece[i] == e.0));
2377 }
2378 }
2379 }
2380 }
2381
2382 let mut walked = vec![false; halves];
2387 for h in 0..halves {
2388 if walked[h] || !self.border(h) {
2389 continue;
2390 }
2391 let mut ring = Vec::new();
2392 let mut at = h;
2393 loop {
2394 walked[at] = true;
2395 ring.push(at);
2396 let mut step = next(at);
2397 let mut guard = 0;
2398 while !self.border(step) {
2399 let Some(twin) = self.adjacency.twin[step] else {
2400 break;
2401 };
2402 step = next(twin);
2403 guard += 1;
2404 if guard > halves {
2405 ogeom_bail!(Construction, "a face's boundary does not close");
2406 }
2407 }
2408 if step == h {
2409 break;
2410 }
2411 if walked[step] {
2412 ogeom_bail!(Construction, "a face's boundary runs into itself");
2413 }
2414 at = step;
2415 }
2416 plan.loops[self.groups.of[h / 3]].push(ring);
2417 }
2418
2419 let mut pin: Vec<u32> = Vec::new();
2425 for (g, rings) in plan.loops.iter().enumerate() {
2426 if !matches!(self.groups.carriers[g], Carrier::Plane(_)) {
2427 continue;
2428 }
2429 for ring in rings {
2430 let mut entries: Vec<usize> = Vec::new();
2431 for &h in ring {
2432 let (edge, _) = self.entry(&plan, h);
2433 if entries.last() != Some(&edge) {
2434 entries.push(edge);
2435 }
2436 }
2437 if entries.len() > 1 && entries.first() == entries.last() {
2438 entries.pop();
2439 }
2440 if entries.len() < 3
2441 && entries.iter().all(|&e| {
2442 e != usize::MAX
2443 && !plan.edges[e].closed_circle
2444 && matches!(plan.edges[e].curve, Curve::Line(_))
2445 })
2446 {
2447 for &h in ring {
2448 let (a, b) = from_to(self.triangles, h);
2449 for v in [a, b] {
2450 if !pin.contains(&v) && !self.pinned.contains(&v) {
2451 pin.push(v);
2452 }
2453 }
2454 }
2455 }
2456 }
2457 }
2458 if !pin.is_empty() && failed.is_empty() {
2459 return Ok(Err(Replan::Pin(pin)));
2460 }
2461
2462 for (g, carrier) in self.groups.carriers.iter().enumerate() {
2468 let Carrier::Curved(curved) = carrier else {
2469 continue;
2470 };
2471 if failed.contains(&g) || !(curved.wraps || curved.wraps_v) {
2472 continue;
2473 }
2474 let sphere = matches!(curved.shape, Canonical::Sphere(_));
2475 let torus = matches!(curved.shape, Canonical::Torus(_));
2476 let rings = &plan.loops[g];
2477 let circles = rings.iter().all(|ring| {
2478 let first = self.entry(&plan, ring[0]);
2479 first.0 != usize::MAX
2480 && plan.edges[first.0].closed_circle
2481 && ring.iter().all(|&h| self.entry(&plan, h).0 == first.0)
2482 });
2483 let wrapped = || {
2484 let resolved = rings
2485 .iter()
2486 .all(|ring| ring.iter().all(|&h| self.entry(&plan, h).0 != usize::MAX));
2487 if sphere || !curved.wraps || curved.wraps_v || !resolved {
2488 return None;
2489 }
2490 let windings: Option<Vec<i32>> = rings
2491 .iter()
2492 .map(|ring| winding(&curved.shape, ring, self.triangles, self.points, self.tol))
2493 .collect();
2494 let windings = windings?;
2495 let rims = windings.iter().filter(|w| w.abs() == 1).count();
2496 let holes = windings.iter().filter(|w| **w == 0).count();
2497 (rims == 2 && rims + holes == windings.len()).then_some(Layout::Wrapped)
2498 };
2499 let holed = || {
2500 let closed_round = sphere || (torus && curved.wraps && curved.wraps_v);
2501 let resolved = rings
2502 .iter()
2503 .all(|ring| ring.iter().all(|&h| self.entry(&plan, h).0 != usize::MAX));
2504 if !closed_round || !resolved {
2505 return false;
2506 }
2507 let tau = core::f64::consts::TAU;
2508 let (_, wraps_v) = periodic(&curved.shape);
2509 rings.iter().all(|ring| {
2510 let turns =
2511 windings(&curved.shape, ring, self.triangles, self.points, self.tol);
2512 let Some((0, 0)) = turns else {
2514 return false;
2515 };
2516 let Some(polygon) = hole_polygons(
2517 &curved.shape,
2518 &[ring.as_slice()],
2519 self.triangles,
2520 self.points,
2521 self.tol,
2522 )
2523 .pop() else {
2524 return false;
2525 };
2526 let clear = |values: &mut dyn Iterator<Item = f64>| {
2527 let (lo, hi) = values
2528 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), x| {
2529 (a.min(x), b.max(x))
2530 });
2531 (lo / tau).floor() == (hi / tau).floor()
2532 };
2533 clear(&mut polygon.iter().map(|p| p.0))
2534 && (!wraps_v || clear(&mut polygon.iter().map(|p| p.1)))
2535 })
2536 };
2537 let layout = if rings.is_empty() {
2538 (sphere || torus).then_some(Layout::Whole)
2539 } else if holed() {
2540 Some(Layout::Holed)
2541 } else if (curved.wraps && curved.wraps_v) || !circles {
2542 wrapped()
2543 } else if sphere && rings.len() == 1 && curved.fixed {
2544 Some(Layout::Cap)
2545 } else if rings.len() == 2 && (!sphere || curved.fixed) {
2546 Some(Layout::Band {
2547 round_tube: curved.wraps_v,
2548 })
2549 } else {
2550 wrapped()
2551 };
2552 let layout = match layout {
2553 Some(Layout::Wrapped) => {
2554 let rings = plan.loops[g].clone();
2555 self.seat_seam(&mut plan, curved, &rings)
2556 .then_some(Layout::Wrapped)
2557 }
2558 other => other,
2559 };
2560 match layout {
2561 Some(layout) => plan.layouts[g] = layout,
2562 None => failed.push(g),
2563 }
2564 }
2565
2566 let reach = self.flat * REACH;
2570 for (g, carrier) in self.groups.carriers.iter().enumerate() {
2571 let Carrier::Curved(curved) = carrier else {
2572 continue;
2573 };
2574 if failed.contains(&g) || plan.loops[g].is_empty() {
2575 continue;
2576 }
2577 let surface = surface_of(curved, self.points, self.tol)?;
2578 if matches!(
2579 plan.layouts[g],
2580 Layout::Open | Layout::Wrapped | Layout::Holed
2581 ) {
2582 let mut held = true;
2583 'rings: for ring in &plan.loops[g] {
2584 for &h in ring {
2585 let (edge, _) = self.entry(&plan, h);
2586 if edge == usize::MAX {
2587 held = false;
2588 break 'rings;
2589 }
2590 if plan.pcurves.contains_key(&(edge, g)) {
2591 continue;
2592 }
2593 let spec = &plan.edges[edge];
2594 let reach = reach.max(spec.tolerance * 2.0);
2597 let image =
2598 image_on(curved, &surface, &spec.curve, spec.range, reach, self.tol);
2599 match image {
2600 Some(found) => {
2601 plan.pcurves.insert((edge, g), found);
2602 }
2603 None => {
2604 held = false;
2605 break 'rings;
2606 }
2607 }
2608 }
2609 }
2610 if !held {
2611 failed.push(g);
2612 continue;
2613 }
2614 }
2615 plan.surfaces[g] = Some(surface);
2616 }
2617 if failed.is_empty() {
2618 Ok(Ok(plan))
2619 } else {
2620 Ok(Err(Replan::Facet(failed)))
2621 }
2622 }
2623
2624 fn seat_seam(&self, plan: &mut Plan, curved: &Curved, rings: &[Vec<Half>]) -> bool {
2628 use ogeom_geom::Curve3d as _;
2629 let shape = &curved.shape;
2630 let windings: Vec<i32> = rings
2631 .iter()
2632 .map(|ring| winding(shape, ring, self.triangles, self.points, self.tol).unwrap_or(0))
2633 .collect();
2634 let rims: Vec<usize> = (0..rings.len())
2635 .filter(|&k| windings[k].abs() == 1)
2636 .collect();
2637 let [low, high] = rims[..] else {
2638 return false;
2639 };
2640 let holes: Vec<&[Half]> = (0..rings.len())
2641 .filter(|&k| windings[k] == 0)
2642 .map(|k| rings[k].as_slice())
2643 .collect();
2644 let hole_rings = hole_polygons(shape, &holes, self.triangles, self.points, self.tol);
2645 let entries = |plan: &Plan, ring: &[Half]| -> Vec<(usize, bool)> {
2646 let mut out: Vec<(usize, bool)> = Vec::new();
2647 for &h in ring {
2648 let entry = self.entry(plan, h);
2649 if out.last() != Some(&entry) {
2650 out.push(entry);
2651 }
2652 }
2653 if out.len() > 1 && out.first() == out.last() {
2654 out.pop();
2655 }
2656 out
2657 };
2658 let starts = |plan: &Plan, ring: &[Half]| -> Vec<Point> {
2659 entries(plan, ring)
2660 .into_iter()
2661 .map(
2662 |(edge, forward)| match plan.edges[edge].ends[usize::from(!forward)] {
2663 Corner::Mesh(v) => self.points[v as usize],
2664 Corner::Placed(k) => plan.placed[k],
2665 },
2666 )
2667 .collect()
2668 };
2669 let clear = |plan: &Plan| {
2670 choose_seam(
2671 shape,
2672 &starts(plan, &rings[low]),
2673 &starts(plan, &rings[high]),
2674 &hole_rings,
2675 self.tol,
2676 )
2677 .is_some()
2678 };
2679 if clear(plan) {
2680 return true;
2681 }
2682 let Some(angle) = free_angle(&hole_rings) else {
2683 return false;
2684 };
2685 for rim in [low, high] {
2686 let list = entries(plan, &rings[rim]);
2687 let [(edge, _)] = list[..] else {
2688 continue;
2689 };
2690 let spec = &plan.edges[edge];
2691 let (Curve::Circle(c), Corner::Placed(k)) = (&spec.curve, spec.ends[0]) else {
2692 continue;
2693 };
2694 let circle = c.circle();
2695 let Some((_, v)) = chart(shape, plan.placed[k], self.tol) else {
2696 continue;
2697 };
2698 let target = evaluate(shape, (angle, v));
2699 let Ok(x) = Direction::new(target - circle.centre(), self.tol) else {
2700 continue;
2701 };
2702 let Ok(frame) = Frame::new(circle.centre(), circle.frame().z(), x, self.tol) else {
2703 continue;
2704 };
2705 let Ok(turned) = ogeom_math::Circle::new(frame, circle.radius(), self.tol) else {
2706 continue;
2707 };
2708 let curve: Curve = ogeom_geom::CircleCurve::new(turned).into();
2709 let Ok(at) = curve.point_at(0.0, self.tol) else {
2710 continue;
2711 };
2712 plan.edges[edge].curve = curve;
2713 plan.placed[k] = at;
2714 }
2715 clear(plan)
2716 }
2717
2718 fn entry(&self, plan: &Plan, h: Half) -> (usize, bool) {
2721 let (a, b) = from_to(self.triangles, h);
2722 let (edge, along) = plan.edge_of[&(a.min(b), a.max(b))];
2723 (edge, along == (a < b))
2724 }
2725
2726 fn snapped(&self, chain: &[u32], faces: &[usize]) -> Option<(Snapped, bool, Images)> {
2731 if faces.len() > 2 {
2732 return None;
2733 }
2734 let closed = chain.len() > 2 && chain[0] == chain[chain.len() - 1];
2735 let pts: Vec<Point> = chain[..chain.len() - usize::from(closed)]
2736 .iter()
2737 .map(|&v| self.points[v as usize])
2738 .collect();
2739 let reach = self.flat * REACH;
2740 let mut order: Vec<usize> = faces.to_vec();
2743 order.sort_by_key(|&g| !self.curved(g).is_some_and(|c| c.wraps || c.wraps_v));
2744 let across = faces.iter().find_map(|&g| match &self.groups.carriers[g] {
2747 Carrier::Plane(plane) => Some(*plane),
2748 _ => None,
2749 });
2750 for &g in &order {
2751 let Some(curved) = self.curved(g) else {
2752 continue;
2753 };
2754 for candidate in candidates(&curved.shape, &pts, across, reach, self.tol) {
2755 if let Some((snapped, forward)) = self.fitted(candidate, &pts, closed, faces, reach)
2756 {
2757 return Some((snapped, forward, Vec::new()));
2758 }
2759 }
2760 }
2761 self.section(&pts, closed, faces, reach)
2762 .or_else(|| self.chord(&pts, closed, faces, reach))
2763 }
2764
2765 fn chord(
2775 &self,
2776 pts: &[Point],
2777 closed: bool,
2778 faces: &[usize],
2779 reach: f64,
2780 ) -> Option<(Snapped, bool, Images)> {
2781 use ogeom_geom::Curve3d as _;
2782 let [a, b] = faces[..] else {
2783 return None;
2784 };
2785 let (fa, fb) = (self.signed(a)?, self.signed(b)?);
2786 let mut on: Vec<Point> = pts.to_vec();
2787 if closed {
2788 on.push(pts[0]);
2789 }
2790 let longest = on
2791 .windows(2)
2792 .map(|w| w[0].distance(w[1]))
2793 .fold(0.0_f64, f64::max);
2794 if longest <= self.tol.confusion() {
2795 return None;
2796 }
2797 let (curve, samples): (Curve, Vec<f64>) = if on.len() == 2 {
2798 let length = on[0].distance(on[1]);
2799 let line: Curve = LineCurve::segment(on[0], on[1], self.tol).ok()?.into();
2800 (
2801 line,
2802 (0..=16).map(|k| length * f64::from(k) / 16.0).collect(),
2803 )
2804 } else {
2805 let n = on.len();
2808 let pad = if closed { 3.min(n / 3) } else { 0 };
2809 let padded: Vec<Point> = if pad > 0 {
2810 on[n - 1 - pad..n - 1]
2811 .iter()
2812 .chain(&on)
2813 .chain(&on[1..=pad])
2814 .copied()
2815 .collect()
2816 } else {
2817 on.clone()
2818 };
2819 let parameters =
2820 crate::fit::spaced(&padded, crate::fit::Spacing::Centripetal, self.tol).ok()?;
2821 let mut spline = crate::fit::interpolate_at(&padded, ¶meters, 3, self.tol).ok()?;
2822 if pad > 0 {
2823 spline = spline.split_at(parameters[pad], self.tol).ok()?.1;
2824 spline = spline.split_at(parameters[pad + n - 1], self.tol).ok()?.0;
2825 }
2826 let own = ¶meters[pad..pad + n];
2827 let samples = own
2828 .windows(2)
2829 .flat_map(|w| {
2830 [
2831 w[0],
2832 w[0] + (w[1] - w[0]) * 0.25,
2833 w[0] + (w[1] - w[0]) * 0.5,
2834 w[0] + (w[1] - w[0]) * 0.75,
2835 ]
2836 })
2837 .chain(std::iter::once(own[n - 1]))
2838 .collect();
2839 (spline.into(), samples)
2840 };
2841 let range = curve.domain();
2842 let mut tolerance = self.tol.confusion();
2843 for t in samples {
2844 let p = curve.point_at(t.clamp(range.0, range.1), self.tol).ok()?;
2845 tolerance = tolerance.max(fa(p).abs()).max(fb(p).abs());
2846 }
2847 if tolerance > reach.max(longest * CHORD_SAG) {
2848 return None;
2849 }
2850 Some((
2851 if closed {
2852 Snapped::Loop(curve, range, tolerance)
2853 } else {
2854 Snapped::Open(curve, range, tolerance)
2855 },
2856 true,
2857 Vec::new(),
2858 ))
2859 }
2860
2861 fn section(
2869 &self,
2870 pts: &[Point],
2871 closed: bool,
2872 faces: &[usize],
2873 reach: f64,
2874 ) -> Option<(Snapped, bool, Images)> {
2875 let close = self.tol.confusion() * 50.0;
2879 let mut best: Option<(f64, (Snapped, bool, Images))> = None;
2880 let mut count = SECTION_POINTS;
2881 while count <= SECTION_POINTS * 8 {
2882 let Some((worst, found)) = self.section_through(pts, closed, faces, reach, count)
2883 else {
2884 break;
2885 };
2886 let done = worst <= close;
2887 if best.as_ref().is_none_or(|(held, _)| worst < *held) {
2888 best = Some((worst, found));
2889 }
2890 if done {
2891 break;
2892 }
2893 count *= 2;
2894 }
2895 best.map(|(_, found)| found)
2896 }
2897
2898 fn section_through(
2901 &self,
2902 pts: &[Point],
2903 closed: bool,
2904 faces: &[usize],
2905 reach: f64,
2906 count: usize,
2907 ) -> Option<(f64, (Snapped, bool, Images))> {
2908 use ogeom_geom::Curve3d as _;
2909 let [a, b] = faces[..] else {
2910 return None;
2911 };
2912 let (fa, fb) = (self.signed(a)?, self.signed(b)?);
2913 let steps = if closed { pts.len() } else { pts.len() - 1 };
2914 let stride = steps.div_ceil(SECTION_SPANS).max(1);
2917 let split = count.div_ceil(steps.div_ceil(stride)).max(SECTION_SPLIT);
2918 let mut on = Vec::new();
2919 let mut i = 0;
2920 while i < steps {
2921 let next = (i + stride).min(steps);
2922 let (p, q) = (pts[i], pts[next % pts.len()]);
2923 for k in 0..split {
2924 #[allow(clippy::cast_precision_loss, reason = "a handful of splits")]
2925 let f = k as f64 / split as f64;
2926 let limit = if k == 0 { reach } else { p.distance(q) + reach };
2927 on.push(onto_both(&fa, &fb, p + (q - p) * f, reach, limit)?);
2928 }
2929 i = next;
2930 }
2931 on.push(if closed {
2932 on[0]
2933 } else {
2934 onto_both(&fa, &fb, pts[pts.len() - 1], reach, reach)?
2935 });
2936 let pad = if closed {
2940 SECTION_PAD.min(on.len() / 4)
2941 } else {
2942 0
2943 };
2944 let n = on.len();
2945 let padded: Vec<Point> = if pad > 0 {
2946 on[n - 1 - pad..n - 1]
2947 .iter()
2948 .chain(&on)
2949 .chain(&on[1..=pad])
2950 .copied()
2951 .collect()
2952 } else {
2953 on.clone()
2954 };
2955 let parameters =
2956 crate::fit::spaced(&padded, crate::fit::Spacing::Centripetal, self.tol).ok()?;
2957 let cut = (parameters[pad], parameters[pad + n - 1]);
2958 let trimmed = |spline: ogeom_geom::BSplineCurve| -> Option<ogeom_geom::BSplineCurve> {
2959 if pad == 0 {
2960 return Some(spline);
2961 }
2962 let (_, after) = spline.split_at(cut.0, self.tol).ok()?;
2963 Some(after.split_at(cut.1, self.tol).ok()?.0)
2964 };
2965 let curve: Curve =
2966 trimmed(crate::fit::interpolate_at(&padded, ¶meters, 3, self.tol).ok()?)?.into();
2967 let range = curve.domain();
2968 let parameters = parameters[pad..pad + n].to_vec();
2969 let between = |k: usize, f: f64| parameters[k] + (parameters[k + 1] - parameters[k]) * f;
2972 let last = if closed { pts[0] } else { pts[pts.len() - 1] };
2975 let mut tolerance = self
2976 .tol
2977 .confusion()
2978 .max(on[0].distance(pts[0]))
2979 .max(on[on.len() - 1].distance(last));
2980 for k in 0..on.len() - 1 {
2981 for f in [0.25, 0.5, 0.75] {
2982 let p = curve.point_at(between(k, f), self.tol).ok()?;
2983 tolerance = tolerance.max(fa(p).abs()).max(fb(p).abs());
2984 }
2985 }
2986 if tolerance > reach {
2987 return None;
2988 }
2989 let mut images = Vec::new();
2990 for &g in faces {
2991 let Some(curved) = self.curved(g) else {
2992 continue;
2993 };
2994 let (pu, pv) = periodic(&curved.shape);
2995 let mut uv: Vec<Point> = Vec::with_capacity(padded.len());
2996 for p in &padded {
2997 let (u, v) = match uv.last() {
2998 None => unwrapped(curved, *p, self.tol)?,
2999 Some(last) => {
3000 let (u, v) = chart(&curved.shape, *p, self.tol)?;
3001 let near = |x: f64, c: f64, wraps: bool| {
3002 if wraps {
3003 c + ogeom_math::elementary::wrap_signed_angle(x - c)
3004 } else {
3005 x
3006 }
3007 };
3008 (near(u, last.x, pu), near(v, last.y, pv))
3009 }
3010 };
3011 uv.push(Point::new(u, v, 0.0));
3012 }
3013 let all =
3014 crate::fit::spaced(&padded, crate::fit::Spacing::Centripetal, self.tol).ok()?;
3015 let flat = trimmed(crate::fit::interpolate_at(&uv, &all, 3, self.tol).ok()?)?;
3016 let control: Vec<Point2> = flat
3017 .control_points()
3018 .iter()
3019 .map(|c| Point2::new(c.scaled.x, c.scaled.y))
3020 .collect();
3021 let pcurve: PlanarCurve =
3022 ogeom_geom::BSpline2d::new(flat.knots().clone(), control, self.tol)
3023 .ok()?
3024 .into();
3025 let mut deviation = self.tol.confusion();
3026 for k in 0..on.len() - 1 {
3027 for f in [0.0, 0.25, 0.5, 0.75] {
3028 use ogeom_geom::Curve2d as _;
3029 let t = between(k, f);
3030 let at = pcurve.point_at(t, self.tol).ok()?;
3031 let lifted = evaluate(&curved.shape, (at.x, at.y));
3032 deviation = deviation.max(lifted.distance(curve.point_at(t, self.tol).ok()?));
3033 }
3034 }
3035 if deviation > reach {
3036 return None;
3037 }
3038 images.push((g, pcurve, deviation));
3039 }
3040 let worst = images.iter().map(|(_, _, d)| *d).fold(tolerance, f64::max);
3041 Some((
3042 worst,
3043 (
3044 if closed {
3045 Snapped::Loop(curve, range, tolerance)
3046 } else {
3047 Snapped::Open(curve, range, tolerance)
3048 },
3049 true,
3050 images,
3051 ),
3052 ))
3053 }
3054
3055 fn signed(&self, g: usize) -> Option<Box<dyn Fn(Point) -> f64>> {
3057 match &self.groups.carriers[g] {
3058 Carrier::Plane(plane) => {
3059 let plane = *plane;
3060 Some(Box::new(move |p: Point| plane.signed_distance_to(p)))
3061 }
3062 Carrier::Curved(c) => {
3063 let shape = c.shape;
3064 Some(Box::new(move |p: Point| shape.signed_distance_to(p)))
3065 }
3066 Carrier::Gone => None,
3067 }
3068 }
3069
3070 fn fitted(
3073 &self,
3074 curve: Curve,
3075 pts: &[Point],
3076 closed: bool,
3077 faces: &[usize],
3078 reach: f64,
3079 ) -> Option<(Snapped, bool)> {
3080 use ogeom_geom::Curve3d as _;
3081 let tau = core::f64::consts::TAU;
3082 let parameter = |p: Point| -> Option<f64> {
3083 match &curve {
3084 Curve::Line(l) => {
3085 let axis = l.axis();
3086 Some((p - axis.location).dot(axis.direction.vector()))
3087 }
3088 Curve::Circle(c) => {
3089 ogeom_math::elementary::circle_parameter(&c.circle(), p, self.tol).ok()
3090 }
3091 _ => None,
3092 }
3093 };
3094 let mut tolerance = self.tol.confusion();
3095 let mut ts = Vec::with_capacity(pts.len());
3096 for p in pts {
3097 let t = parameter(*p)?;
3098 let on = curve.point_at(t, self.tol).ok()?;
3099 tolerance = tolerance.max(on.distance(*p));
3100 ts.push(t);
3101 }
3102 let mut sweep = 0.0;
3104 let steps = if closed { ts.len() } else { ts.len() - 1 };
3105 for i in 0..steps {
3106 let (a, b) = (ts[i], ts[(i + 1) % ts.len()]);
3107 let d = b - a;
3108 sweep += if matches!(curve, Curve::Circle(_)) {
3109 ogeom_math::elementary::wrap_signed_angle(d)
3110 } else {
3111 d
3112 };
3113 }
3114 let (snapped, forward) = if closed {
3115 if !matches!(curve, Curve::Circle(_)) || (sweep.abs() - tau).abs() > 1e-3 {
3116 return None;
3117 }
3118 (None, sweep > 0.0)
3119 } else if sweep > 0.0 {
3120 (Some((ts[0], ts[0] + sweep)), true)
3121 } else {
3122 let last = ts[ts.len() - 1];
3123 (Some((last, last - sweep)), false)
3124 };
3125 let range = snapped.unwrap_or((0.0, tau));
3126 if range.1 - range.0 <= self.tol.parametric() {
3127 return None;
3128 }
3129 for &g in faces {
3132 if let Carrier::Plane(plane) = &self.groups.carriers[g] {
3133 let surface: ogeom_geom::SurfaceGeometry = PlaneSurface::new(*plane).into();
3134 ogeom_intersect::exact_pcurve_of(&curve, &surface, self.tol)?;
3135 }
3136 }
3137 for k in 0..=16 {
3139 let t = range.0 + (range.1 - range.0) * f64::from(k) / 16.0;
3140 let p = curve.point_at(t, self.tol).ok()?;
3141 for &g in faces {
3142 let off = match &self.groups.carriers[g] {
3143 Carrier::Plane(plane) => plane.signed_distance_to(p).abs(),
3144 Carrier::Curved(c) => c.shape.distance_to(p),
3145 Carrier::Gone => return None,
3146 };
3147 tolerance = tolerance.max(off);
3148 }
3149 }
3150 if tolerance > reach {
3151 return None;
3152 }
3153 Some((
3154 match snapped {
3155 Some(range) => Snapped::Open(curve, range, tolerance),
3156 None => Snapped::Closed(curve, tolerance),
3157 },
3158 forward,
3159 ))
3160 }
3161}
3162
3163enum Snapped {
3164 Open(Curve, (f64, f64), f64),
3165 Closed(Curve, f64),
3166 Loop(Curve, (f64, f64), f64),
3169}
3170
3171const SECTION_SPLIT: usize = 4;
3175
3176const SECTION_SPANS: usize = 40;
3178
3179const SECTION_PAD: usize = 8;
3182
3183const SECTION_POINTS: usize = 160;
3186
3187type Images = Vec<(usize, PlanarCurve, f64)>;
3190
3191fn onto_both(
3197 fa: &dyn Fn(Point) -> f64,
3198 fb: &dyn Fn(Point) -> f64,
3199 start: Point,
3200 reach: f64,
3201 limit: f64,
3202) -> Option<Point> {
3203 let mut p = start;
3204 let h = 1e-7 * (1.0 + p.to_vector().magnitude());
3205 let gradient = |f: &dyn Fn(Point) -> f64, p: Point| {
3206 let d = |v: Vector| (f(p + v * h) - f(p - v * h)) / (2.0 * h);
3207 Vector::new(d(Vector::X), d(Vector::Y), d(Vector::Z))
3208 };
3209 for _ in 0..40 {
3210 let (va, vb) = (fa(p), fb(p));
3211 if va.abs().max(vb.abs()) <= 1e-13 * (1.0 + p.to_vector().magnitude()) {
3212 break;
3213 }
3214 let (ga, gb) = (gradient(fa, p), gradient(fb, p));
3215 let (aa, ab, bb) = (ga.dot(ga), ga.dot(gb), gb.dot(gb));
3216 let det = aa.mul_add(bb, -(ab * ab));
3217 if det <= 1e-12 * aa * bb {
3218 return None;
3219 }
3220 let la = (va * bb - vb * ab) / det;
3221 let lb = (vb * aa - va * ab) / det;
3222 p = p - ga * la - gb * lb;
3223 }
3224 (fa(p).abs().max(fb(p).abs()) <= reach * 1e-3 && p.distance(start) <= limit).then_some(p)
3225}
3226
3227fn candidates(
3234 shape: &Canonical,
3235 pts: &[Point],
3236 across: Option<Plane>,
3237 reach: f64,
3238 tol: Tolerances,
3239) -> Vec<Curve> {
3240 let mut out = Vec::new();
3241 let plane_of = |pts: &[Point]| match across {
3242 Some(plane) => Some((plane.project(pts[0]), plane.normal())),
3243 None => plane_through(pts, tol),
3244 };
3245 if let Canonical::Sphere(sphere) = shape {
3246 let axis = sphere.frame().z();
3251 if pts.len() >= 3
3252 && let Some((centre, normal)) = plane_of(pts)
3253 && normal.vector().cross(axis.vector()).magnitude()
3254 <= if across.is_some() { 1e-12 } else { 1e-3 }
3255 {
3256 let h = (centre - sphere.centre()).dot(axis.vector());
3257 let r2 = sphere.radius().powi(2) - h * h;
3258 if r2 > 0.0
3259 && let Ok(frame) = Frame::new(
3260 sphere.centre() + axis.vector() * h,
3261 axis,
3262 sphere.frame().x(),
3263 tol,
3264 )
3265 && let Ok(circle) = ogeom_math::Circle::new(frame, r2.sqrt(), tol)
3266 {
3267 out.push(ogeom_geom::CircleCurve::new(circle).into());
3268 return out;
3269 }
3270 }
3271 if pts.len() >= 3
3272 && let Some((centre, normal)) = plane_of(pts)
3273 {
3274 let n = normal.vector();
3275 let d = (centre - sphere.centre()).dot(n);
3276 let r2 = sphere.radius().powi(2) - d * d;
3277 if r2 > 0.0
3278 && let Ok(frame) = Frame::new(
3279 sphere.centre() + n * d,
3280 normal,
3281 normal.any_perpendicular(),
3282 tol,
3283 )
3284 && let Ok(circle) = ogeom_math::Circle::new(frame, r2.sqrt(), tol)
3285 {
3286 out.push(ogeom_geom::CircleCurve::new(circle).into());
3287 }
3288 }
3289 return out;
3290 }
3291 let Some(frame) = axis_frame(shape) else {
3292 return out;
3293 };
3294 let (o, z) = (frame.origin(), frame.z().vector());
3295 let heights: Vec<f64> = pts.iter().map(|p| (*p - o).dot(z)).collect();
3296 let radii: Vec<f64> = pts
3297 .iter()
3298 .zip(&heights)
3299 .map(|(p, h)| ((*p - o) - z * *h).magnitude())
3300 .collect();
3301 #[allow(
3302 clippy::cast_precision_loss,
3303 reason = "chain lengths are far below 2^52"
3304 )]
3305 let count = pts.len() as f64;
3306 let mean_h = heights.iter().sum::<f64>() / count;
3307 let mean_r = radii.iter().sum::<f64>() / count;
3308 let level = heights.iter().all(|h| (h - mean_h).abs() <= reach)
3309 && radii.iter().all(|r| (r - mean_r).abs() <= reach);
3310 if level {
3311 let radius = match shape {
3312 Canonical::Cylinder(c) => c.radius(),
3313 Canonical::Cone(c) => c.radius_at(mean_h),
3314 Canonical::Torus(t) => {
3315 let (big, small) = (t.major_radius(), t.minor_radius());
3316 let off = (small * small - mean_h * mean_h).max(0.0).sqrt();
3317 if (big + off - mean_r).abs() <= (big - off - mean_r).abs() {
3318 big + off
3319 } else {
3320 big - off
3321 }
3322 }
3323 _ => mean_r,
3324 };
3325 if let Ok(at) = Frame::new(o + z * mean_h, frame.z(), frame.x(), tol)
3326 && let Ok(circle) = ogeom_math::Circle::new(at, radius, tol)
3327 {
3328 out.push(ogeom_geom::CircleCurve::new(circle).into());
3329 }
3330 }
3331 if let Canonical::Torus(torus) = shape
3335 && pts.len() >= 2
3336 {
3337 let mut angles: Vec<f64> = pts
3338 .iter()
3339 .filter_map(|x| chart(shape, *x, tol).map(|c| c.0))
3340 .collect();
3341 if !angles.is_empty() {
3342 let (u, _) = angular_spread(&mut angles);
3343 let (x, y) = (frame.x().vector(), frame.y().vector());
3344 let out_u = x * u.cos() + y * u.sin();
3345 let across = y * u.cos() - x * u.sin();
3346 let in_plane = pts.iter().all(|p| (*p - o).dot(across).abs() <= reach);
3347 if in_plane
3348 && let Ok(radial) = Direction::new(out_u, tol)
3349 && let Ok(normal) = Direction::new(out_u.cross(z), tol)
3350 && let Ok(at) = Frame::new(o + out_u * torus.major_radius(), normal, radial, tol)
3351 && let Ok(circle) = ogeom_math::Circle::new(at, torus.minor_radius(), tol)
3352 {
3353 out.push(ogeom_geom::CircleCurve::new(circle).into());
3354 }
3355 }
3356 }
3357 if matches!(shape, Canonical::Cylinder(_) | Canonical::Cone(_)) && pts.len() >= 2 {
3358 let (p, q) = (pts[0], pts[pts.len() - 1]);
3359 let straight = pts.iter().all(|x| distance_to_line(*x, p, q) <= reach);
3360 let mut angles: Vec<f64> = pts
3361 .iter()
3362 .filter_map(|x| chart(shape, *x, tol).map(|c| c.0))
3363 .collect();
3364 if straight && !angles.is_empty() {
3365 let (u, _) = angular_spread(&mut angles);
3366 let (lo, hi) = (
3367 heights.iter().copied().fold(f64::INFINITY, f64::min),
3368 heights.iter().copied().fold(f64::NEG_INFINITY, f64::max),
3369 );
3370 let (a, b) = (evaluate(shape, (u, lo)), evaluate(shape, (u, hi)));
3371 if let Ok(line) = LineCurve::segment(a, b, tol) {
3372 let _ = line;
3375 let (a, b) = if (pts[0] - a).magnitude() <= (pts[0] - b).magnitude() {
3376 (a, b)
3377 } else {
3378 (b, a)
3379 };
3380 if let Ok(line) = LineCurve::segment(a, b, tol) {
3381 out.push(line.into());
3382 }
3383 }
3384 }
3385 }
3386 out
3387}
3388
3389fn plane_through(points: &[Point], tol: Tolerances) -> Option<(Point, Direction)> {
3392 #[allow(
3393 clippy::cast_precision_loss,
3394 reason = "chain lengths are far below 2^52"
3395 )]
3396 let count = points.len() as f64;
3397 let c = points.iter().fold(Vector::ZERO, |s, p| s + p.to_vector()) / count;
3398 let mut m = nalgebra::Matrix3::<f64>::zeros();
3399 for p in points {
3400 let d = p.to_vector() - c;
3401 let v = nalgebra::Vector3::new(d.x, d.y, d.z);
3402 m += v * v.transpose();
3403 }
3404 let eigen = nalgebra::SymmetricEigen::new(m);
3405 let mut best = 0;
3406 for i in 1..3 {
3407 if eigen.eigenvalues[i] < eigen.eigenvalues[best] {
3408 best = i;
3409 }
3410 }
3411 let v = eigen.eigenvectors.column(best);
3412 Some((
3413 Point::from_vector(c),
3414 Direction::new(Vector::new(v[0], v[1], v[2]), tol).ok()?,
3415 ))
3416}
3417
3418fn surface_of(
3421 curved: &Curved,
3422 points: &[Point],
3423 tol: Tolerances,
3424) -> OgeomResult<ogeom_geom::SurfaceGeometry> {
3425 use ogeom_geom::{ConeSurface, CylinderSurface, SphereSurface, TorusSurface};
3426 let heights = || {
3427 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
3428 for &v in &curved.vertices {
3429 if let Some((_, h)) = chart(&curved.shape, points[v as usize], tol) {
3430 lo = lo.min(h);
3431 hi = hi.max(h);
3432 }
3433 }
3434 let margin = (hi - lo).mul_add(0.25, tol.confusion() * 10.0);
3435 (lo - margin, hi + margin)
3436 };
3437 Ok(match curved.shape {
3438 Canonical::Cylinder(c) => CylinderSurface::new(c, heights())?.into(),
3439 Canonical::Cone(c) => {
3440 let (lo, hi) = heights();
3442 let apex = -c.reference_radius() / c.half_angle().tan();
3443 let lo = lo.max(apex + (hi - apex) * 1e-6);
3444 ConeSurface::new(c, (lo, hi))?.into()
3445 }
3446 Canonical::Sphere(s) => SphereSurface::new(s).into(),
3447 Canonical::Torus(t) => TorusSurface::new(t).into(),
3448 Canonical::Plane(p) => PlaneSurface::new(p).into(),
3449 })
3450}
3451
3452fn image_on(
3456 curved: &Curved,
3457 surface: &ogeom_geom::SurfaceGeometry,
3458 curve: &Curve,
3459 range: (f64, f64),
3460 reach: f64,
3461 tol: Tolerances,
3462) -> Option<(PlanarCurve, f64)> {
3463 if let Some((pcurve, deviation)) = straight_image(curved, curve, range, tol)
3464 && deviation <= reach
3465 {
3466 return Some((pcurve, deviation));
3467 }
3468 if let Some((pcurve, deviation)) = interpolated_image(curved, curve, range, tol)
3469 && deviation <= reach
3470 {
3471 return Some((pcurve, deviation));
3472 }
3473 let (pcurve, error, _, off, _) =
3474 crate::pcurve_fit::fit_projected_pcurve_capped(curve, range, surface, reach, tol).ok()?;
3475 let deviation = error.max(off);
3476 (deviation <= reach).then_some((pcurve, deviation))
3477}
3478
3479fn interpolated_image(
3486 curved: &Curved,
3487 curve: &Curve,
3488 range: (f64, f64),
3489 tol: Tolerances,
3490) -> Option<(PlanarCurve, f64)> {
3491 use ogeom_geom::{Curve2d as _, Curve3d as _};
3492 let (pu, pv) = periodic(&curved.shape);
3493 let near = |x: f64, c: f64, wraps: bool| {
3494 if wraps {
3495 c + ogeom_math::elementary::wrap_signed_angle(x - c)
3496 } else {
3497 x
3498 }
3499 };
3500 let mut best: Option<(PlanarCurve, f64)> = None;
3501 let mut count: u32 = 32;
3502 while count <= 1024 {
3503 let parameters: Vec<f64> = (0..=count)
3504 .map(|k| range.0 + (range.1 - range.0) * f64::from(k) / f64::from(count))
3505 .collect();
3506 let mut uv: Vec<Point> = Vec::with_capacity(parameters.len());
3507 for &t in ¶meters {
3508 let p = curve.point_at(t, tol).ok()?;
3509 let (u, v) = match uv.last() {
3510 None => unwrapped(curved, p, tol)?,
3511 Some(last) => {
3512 let (u, v) = chart(&curved.shape, p, tol)?;
3513 (near(u, last.x, pu), near(v, last.y, pv))
3514 }
3515 };
3516 uv.push(Point::new(u, v, 0.0));
3517 }
3518 let flat = crate::fit::interpolate_at(&uv, ¶meters, 3, tol).ok()?;
3519 let control: Vec<Point2> = flat
3520 .control_points()
3521 .iter()
3522 .map(|c| Point2::new(c.scaled.x, c.scaled.y))
3523 .collect();
3524 let pcurve: PlanarCurve = ogeom_geom::BSpline2d::new(flat.knots().clone(), control, tol)
3525 .ok()?
3526 .into();
3527 let mut deviation = tol.confusion() * 1e-2;
3528 for pair in parameters.windows(2) {
3529 for f in [0.25, 0.5, 0.75] {
3530 let t = pair[0] + (pair[1] - pair[0]) * f;
3531 let at = pcurve.point_at(t, tol).ok()?;
3532 let lifted = evaluate(&curved.shape, (at.x, at.y));
3533 deviation = deviation.max(lifted.distance(curve.point_at(t, tol).ok()?));
3534 }
3535 }
3536 let done = deviation <= tol.confusion() * 1e-2;
3537 if best.as_ref().is_none_or(|(_, held)| deviation < *held) {
3538 best = Some((pcurve, deviation));
3539 }
3540 if done {
3541 break;
3542 }
3543 count *= 2;
3544 }
3545 best
3546}
3547
3548fn straight_image(
3551 curved: &Curved,
3552 curve: &Curve,
3553 range: (f64, f64),
3554 tol: Tolerances,
3555) -> Option<(PlanarCurve, f64)> {
3556 use ogeom_geom::Curve3d as _;
3557 let at =
3558 |t: f64| -> Option<(f64, f64)> { unwrapped(curved, curve.point_at(t, tol).ok()?, tol) };
3559 let start = at(range.0)?;
3560 let (pu, pv) = periodic(&curved.shape);
3563 let step = |a: f64, b: f64, wraps: bool| {
3564 if wraps {
3565 a + ogeom_math::elementary::wrap_signed_angle(b - a)
3566 } else {
3567 b
3568 }
3569 };
3570 let mut end = start;
3571 for k in 1..=4 {
3572 let next = at(range.0 + (range.1 - range.0) * f64::from(k) / 4.0)?;
3573 end = (step(end.0, next.0, pu), step(end.1, next.1, pv));
3574 }
3575 let pcurve = linear(start, end, range, tol).ok()?;
3576 let mut deviation: f64 = 0.0;
3577 for k in 0..=16 {
3578 let f = f64::from(k) / 16.0;
3579 let t = range.0 + (range.1 - range.0) * f;
3580 let uv = (
3581 (end.0 - start.0).mul_add(f, start.0),
3582 (end.1 - start.1).mul_add(f, start.1),
3583 );
3584 deviation =
3585 deviation.max(evaluate(&curved.shape, uv).distance(curve.point_at(t, tol).ok()?));
3586 }
3587 Some((pcurve, deviation))
3588}
3589
3590struct Builder<'a> {
3592 model: &'a mut Model,
3593 points: &'a [Point],
3594 triangles: &'a [[u32; 3]],
3595 groups: &'a Groups,
3596 plan: &'a Plan,
3597 tol: Tolerances,
3598}
3599
3600impl Builder<'_> {
3601 fn entry(&self, h: Half) -> (usize, bool) {
3602 let (a, b) = from_to(self.triangles, h);
3603 let (edge, along) = self.plan.edge_of[&(a.min(b), a.max(b))];
3604 (edge, along == (a < b))
3605 }
3606
3607 fn build(mut self) -> OgeomResult<Vec<Option<Shape>>> {
3609 let mut corners: HashMap<Corner, Shape> = HashMap::new();
3610 let mut edges: Vec<Shape> = Vec::with_capacity(self.plan.edges.len());
3611 for spec in &self.plan.edges {
3612 for corner in spec.ends {
3613 corners.entry(corner).or_insert_with(|| {
3614 let at = match corner {
3615 Corner::Mesh(v) => self.points[v as usize],
3616 Corner::Placed(i) => self.plan.placed[i],
3617 };
3618 self.model.add_vertex(VertexData::new(at))
3619 });
3620 }
3621 let id = self.model.geometry_mut().add_curve(spec.curve.clone());
3622 let mut data = EdgeData::on_curve(id, Location::identity(), spec.range);
3623 data.tolerance = Tolerance::new(spec.tolerance * (1.0 + TOLERANCE_MARGIN))?;
3626 let bounds = if spec.ends[0] == spec.ends[1] {
3627 vec![
3628 corners[&spec.ends[0]].clone(),
3629 corners[&spec.ends[0]].clone(),
3630 ]
3631 } else {
3632 vec![
3633 corners[&spec.ends[0]].clone(),
3634 corners[&spec.ends[1]].clone(),
3635 ]
3636 };
3637 edges.push(self.model.add_edge(data, &bounds)?);
3638 }
3639
3640 let mut faces = Vec::with_capacity(self.groups.carriers.len());
3641 for (g, carrier) in self.groups.carriers.iter().enumerate() {
3642 let rings = &self.plan.loops[g];
3643 let face = match carrier {
3644 Carrier::Gone => None,
3645 Carrier::Curved(curved) if self.plan.layouts[g] == Layout::Whole => {
3646 Some(self.whole_face(curved, g)?)
3647 }
3648 _ if rings.is_empty() => None,
3649 Carrier::Plane(plane) => Some(self.plane_face(*plane, rings, &edges)?),
3650 Carrier::Curved(curved) => match self.plan.layouts[g] {
3651 Layout::Band { round_tube } => {
3652 Some(self.band_face(curved, rings, &edges, round_tube)?)
3653 }
3654 Layout::Cap => Some(self.cap_face(curved, rings, &edges)?),
3655 Layout::Wrapped => Some(self.wrapped_face(curved, g, rings, &edges)?),
3656 Layout::Holed => Some(self.holed_face(curved, g, rings, &edges)?),
3657 Layout::Open | Layout::Whole => {
3658 Some(self.curved_face(curved, g, rings, &edges)?)
3659 }
3660 },
3661 };
3662 faces.push(face);
3663 }
3664 Ok(faces)
3665 }
3666
3667 fn entries(&self, ring: &[Half]) -> Vec<(usize, bool)> {
3669 let mut entries: Vec<(usize, bool)> = Vec::new();
3670 for &h in ring {
3671 let entry = self.entry(h);
3672 if entries.last() != Some(&entry) {
3673 entries.push(entry);
3674 }
3675 }
3676 if entries.len() > 1 && entries.first() == entries.last() {
3677 entries.pop();
3678 }
3679 entries
3680 }
3681
3682 fn has_pcurve(&self, edge: &Shape, surface: ogeom_topo::SurfaceId) -> bool {
3683 self.model.node(edge).and_then(|n| n.data().as_edge()).is_some_and(|d| {
3684 d.representations.iter().any(|rep| {
3685 matches!(rep, EdgeRepr::PCurve { surface: s, .. } | EdgeRepr::Seam { surface: s, .. } if *s == surface)
3686 })
3687 })
3688 }
3689
3690 fn plane_face(
3691 &mut self,
3692 plane: Plane,
3693 rings: &[Vec<Half>],
3694 edges: &[Shape],
3695 ) -> OgeomResult<Shape> {
3696 let geometry: ogeom_geom::SurfaceGeometry = PlaneSurface::new(plane).into();
3697 let surface = self.model.geometry_mut().add_surface(geometry.clone());
3698 let local = |p: Point| {
3699 let l = plane.frame().to_local(p);
3700 Point2::new(l.x, l.y)
3701 };
3702 let mut wires: Vec<(f64, Shape)> = Vec::with_capacity(rings.len());
3703 for ring in rings {
3704 let area = ring_area(ring, self.triangles, |p| Some(local(p)), self.points);
3705 let mut ring_edges = Vec::new();
3706 for (edge, forward) in self.entries(ring) {
3707 let spec = &self.plan.edges[edge];
3708 if !self.has_pcurve(&edges[edge], surface) {
3709 let Some(pcurve) =
3710 ogeom_intersect::exact_pcurve_of(&spec.curve, &geometry, self.tol)
3711 else {
3712 ogeom_bail!(Construction, "an edge has no image in its face's plane");
3713 };
3714 crate::build::attach_pcurve(
3715 self.model,
3716 &edges[edge],
3717 pcurve,
3718 surface,
3719 Location::identity(),
3720 spec.range,
3721 )?;
3722 }
3723 ring_edges.push(oriented(&edges[edge], forward));
3724 }
3725 wires.push((area, self.model.add_wire(&ring_edges)?));
3726 }
3727 wires.sort_by(|a, b| b.0.total_cmp(&a.0));
3728 let wires: Vec<Shape> = wires.into_iter().map(|(_, w)| w).collect();
3729 self.model
3730 .add_face(FaceData::new(surface, Location::identity()), &wires)
3731 }
3732
3733 fn outward(&self, curved: &Curved, g: usize) -> bool {
3735 let mut vote = 0.0;
3736 for (t, tri) in self.triangles.iter().enumerate() {
3737 if self.groups.of[t] != g {
3738 continue;
3739 }
3740 let corners = tri.map(|v| self.points[v as usize]);
3741 let centroid = Point::from_vector(
3742 (corners[0].to_vector() + corners[1].to_vector() + corners[2].to_vector()) / 3.0,
3743 );
3744 let n = unit_normal(self.points, *tri);
3745 vote += n.dot(self.surface_normal(curved, centroid));
3746 }
3747 vote >= 0.0
3748 }
3749
3750 fn surface_normal(&self, curved: &Curved, p: Point) -> Vector {
3752 let Some(at) = chart(&curved.shape, p, self.tol) else {
3753 return Vector::ZERO;
3754 };
3755 let e = 1e-6;
3756 let o = evaluate(&curved.shape, at);
3757 let du = evaluate(&curved.shape, (at.0 + e, at.1)) - o;
3758 let dv = evaluate(&curved.shape, (at.0, at.1 + e)) - o;
3759 let n = du.cross(dv);
3760 let m = n.magnitude();
3761 if m > 0.0 { n / m } else { Vector::ZERO }
3762 }
3763
3764 fn curved_face(
3765 &mut self,
3766 curved: &Curved,
3767 g: usize,
3768 rings: &[Vec<Half>],
3769 edges: &[Shape],
3770 ) -> OgeomResult<Shape> {
3771 let Some(geometry) = self.plan.surfaces[g].clone() else {
3772 ogeom_bail!(
3773 Construction,
3774 "a curved face was planned without its surface"
3775 );
3776 };
3777 let surface = self.model.geometry_mut().add_surface(geometry);
3778 let outward = self.outward(curved, g);
3779 let mut wires: Vec<(f64, Shape)> = Vec::with_capacity(rings.len());
3780 for ring in rings {
3781 let area = ring_area(
3782 ring,
3783 self.triangles,
3784 |p| unwrapped(curved, p, self.tol).map(|(u, v)| Point2::new(u, v)),
3785 self.points,
3786 );
3787 let mut ring_edges = Vec::new();
3788 for (edge, forward) in self.entries(ring) {
3789 let spec = &self.plan.edges[edge];
3790 if !self.has_pcurve(&edges[edge], surface) {
3791 let Some((pcurve, deviation)) = self.plan.pcurves.get(&(edge, g)).cloned()
3792 else {
3793 ogeom_bail!(
3794 Construction,
3795 "an edge was planned without its image on a face"
3796 );
3797 };
3798 self.model.widen(
3799 &edges[edge],
3800 Tolerance::new(deviation.max(self.tol.confusion()))?,
3801 )?;
3802 crate::build::attach_pcurve(
3803 self.model,
3804 &edges[edge],
3805 pcurve,
3806 surface,
3807 Location::identity(),
3808 spec.range,
3809 )?;
3810 }
3811 ring_edges.push(oriented(&edges[edge], forward));
3812 }
3813 let sign = if outward { 1.0 } else { -1.0 };
3814 wires.push((area * sign, self.model.add_wire(&ring_edges)?));
3815 }
3816 wires.sort_by(|a, b| b.0.total_cmp(&a.0));
3817 let wires: Vec<Shape> = wires.into_iter().map(|(_, w)| w).collect();
3818 crate::build::chain_wire_branches(self.model, surface, &wires, self.tol)?;
3820 let mut data = FaceData::new(surface, Location::identity());
3821 data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
3822 let face = self.model.add_face(data, &wires)?;
3823 Ok(if outward { face } else { face.reversed() })
3824 }
3825
3826 fn wrapped_face(
3836 &mut self,
3837 curved: &Curved,
3838 g: usize,
3839 rings: &[Vec<Half>],
3840 edges: &[Shape],
3841 ) -> OgeomResult<Shape> {
3842 use ogeom_geom::Curve3d as _;
3843 let tau = core::f64::consts::TAU;
3844 let Some(geometry) = self.plan.surfaces[g].clone() else {
3845 ogeom_bail!(
3846 Construction,
3847 "a face round its axis was planned without its surface"
3848 );
3849 };
3850 let surface = self.model.geometry_mut().add_surface(geometry);
3851 let outward = self.outward(curved, g);
3852 for ring in rings {
3854 for (edge, _) in self.entries(ring) {
3855 if self.has_pcurve(&edges[edge], surface) {
3856 continue;
3857 }
3858 let Some((pcurve, deviation)) = self.plan.pcurves.get(&(edge, g)).cloned() else {
3859 ogeom_bail!(
3860 Construction,
3861 "an edge was planned without its image on a face"
3862 );
3863 };
3864 self.model.widen(
3865 &edges[edge],
3866 Tolerance::new(deviation.max(self.tol.confusion()))?,
3867 )?;
3868 crate::build::attach_pcurve(
3869 self.model,
3870 &edges[edge],
3871 pcurve,
3872 surface,
3873 Location::identity(),
3874 self.plan.edges[edge].range,
3875 )?;
3876 }
3877 }
3878 let windings: Vec<i32> = rings
3879 .iter()
3880 .map(|ring| {
3881 winding(&curved.shape, ring, self.triangles, self.points, self.tol).unwrap_or(0)
3882 })
3883 .collect();
3884 let rims: Vec<usize> = (0..rings.len())
3885 .filter(|&k| windings[k].abs() == 1)
3886 .collect();
3887 let [low, high] = rims[..] else {
3888 ogeom_bail!(Construction, "a face round its axis has two rims");
3889 };
3890 if windings[low] != -windings[high] {
3891 ogeom_bail!(
3892 Construction,
3893 "a face round its axis has its rims turning one way"
3894 );
3895 }
3896 let holes: Vec<usize> = (0..rings.len()).filter(|&k| windings[k] == 0).collect();
3897
3898 let starts = |this: &Self, ring: &[Half]| -> OgeomResult<Vec<RimStart>> {
3900 let mut out = Vec::new();
3901 for (edge, forward) in this.entries(ring) {
3902 let ends = this.model.children_of(&edges[edge])?;
3903 let vertex = if forward { ends.first() } else { ends.last() };
3904 let Some(vertex) = vertex.cloned() else {
3905 ogeom_bail!(Construction, "a rim edge has no vertex");
3906 };
3907 let Some(ogeom_topo::NodeData::Vertex(data)) =
3908 this.model.node(&vertex).map(|n| n.data())
3909 else {
3910 ogeom_bail!(Construction, "a rim vertex has no position");
3911 };
3912 out.push(((edge, forward), vertex, data.point));
3913 }
3914 Ok(out)
3915 };
3916 let from = starts(self, &rings[low])?;
3917 let to = starts(self, &rings[high])?;
3918 let hole_rings = hole_polygons(
3919 &curved.shape,
3920 &holes
3921 .iter()
3922 .map(|&k| rings[k].as_slice())
3923 .collect::<Vec<_>>(),
3924 self.triangles,
3925 self.points,
3926 self.tol,
3927 );
3928 let from_at: Vec<Point> = from.iter().map(|x| x.2).collect();
3929 let to_at: Vec<Point> = to.iter().map(|x| x.2).collect();
3930 let Some((i, j, a, b)) =
3931 choose_seam(&curved.shape, &from_at, &to_at, &hole_rings, self.tol)
3932 else {
3933 ogeom_bail!(Construction, "no seam joins the rims clear of the holes");
3934 };
3935 let (pa, pb) = (from[i].2, to[j].2);
3936 let straight = (b.0 - a.0).abs() <= 1e-12
3937 && matches!(curved.shape, Canonical::Cylinder(_) | Canonical::Cone(_));
3938 let (seam_curve, range, deviation): (Curve, (f64, f64), f64) = if straight {
3939 let line = LineCurve::segment(pa, pb, self.tol)?;
3940 (line.into(), (0.0, pa.distance(pb)), self.tol.confusion())
3941 } else {
3942 const SAMPLES: u32 = 96;
3943 let along = |f: f64| (a.0 + (b.0 - a.0) * f, a.1 + (b.1 - a.1) * f);
3944 let mut pts: Vec<Point> = (0..=SAMPLES)
3945 .map(|k| evaluate(&curved.shape, along(f64::from(k) / f64::from(SAMPLES))))
3946 .collect();
3947 pts[0] = pa;
3948 pts[SAMPLES as usize] = pb;
3949 let curve: Curve =
3950 crate::fit::interpolate(&pts, 3, crate::fit::Spacing::Uniform, self.tol)?.into();
3951 let range = curve.domain();
3952 let mut deviation = self.tol.confusion();
3953 for k in 0..=(SAMPLES * 4) {
3954 let f = f64::from(k) / f64::from(SAMPLES * 4);
3955 let t = range.0 + (range.1 - range.0) * f;
3956 deviation = deviation.max(
3957 curve
3958 .point_at(t, self.tol)?
3959 .distance(evaluate(&curved.shape, along(f))),
3960 );
3961 }
3962 (curve, range, deviation)
3963 };
3964 let id = self.model.geometry_mut().add_curve(seam_curve);
3965 let mut data = EdgeData::on_curve(id, Location::identity(), range);
3966 data.tolerance = Tolerance::new(deviation)?;
3967 let seam = self
3968 .model
3969 .add_edge(data, &[from[i].1.clone(), to[j].1.clone()])?;
3970 let over = tau * f64::from(windings[low]);
3973 let back = linear(a, b, range, self.tol)?;
3974 let forward = linear((a.0 + over, a.1), (b.0 + over, b.1), range, self.tol)?;
3975 crate::build::attach_seam(
3976 self.model,
3977 &seam,
3978 forward,
3979 back,
3980 surface,
3981 Location::identity(),
3982 range,
3983 )?;
3984 let rotated = |list: &[RimStart], at: usize| -> Vec<Shape> {
3985 (0..list.len())
3986 .map(|k| {
3987 let ((edge, forward), _, _) = list[(at + k) % list.len()];
3988 oriented(&edges[edge], forward)
3989 })
3990 .collect()
3991 };
3992 let mut outer = vec![seam.reversed()];
3993 outer.extend(rotated(&from, i));
3994 outer.push(seam.clone());
3995 outer.extend(rotated(&to, j));
3996 let mut wires = vec![self.model.add_wire(&outer)?];
3997 for &k in &holes {
3998 let ring_edges: Vec<Shape> = self
3999 .entries(&rings[k])
4000 .into_iter()
4001 .map(|(edge, forward)| oriented(&edges[edge], forward))
4002 .collect();
4003 wires.push(self.model.add_wire(&ring_edges)?);
4004 }
4005 crate::build::chain_wire_branches(self.model, surface, &wires, self.tol)?;
4006 let mut data = FaceData::new(surface, Location::identity());
4007 data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
4008 let face = self.model.add_face(data, &wires)?;
4009 Ok(if outward { face } else { face.reversed() })
4010 }
4011
4012 fn band_face(
4015 &mut self,
4016 curved: &Curved,
4017 rings: &[Vec<Half>],
4018 edges: &[Shape],
4019 round_tube: bool,
4020 ) -> OgeomResult<Shape> {
4021 use ogeom_geom::Curve3d as _;
4022 let tau = core::f64::consts::TAU;
4023 let g = self.groups.of[rings[0][0] / 3];
4024 let Some(geometry) = self.plan.surfaces[g].clone() else {
4025 ogeom_bail!(Construction, "a band was planned without its surface");
4026 };
4027 let surface = self.model.geometry_mut().add_surface(geometry);
4028 let outward = self.outward(curved, g);
4029 let Some(frame) = axis_frame(&curved.shape) else {
4030 ogeom_bail!(Construction, "a band has no axis");
4031 };
4032 let mut rims = Vec::with_capacity(2);
4035 for ring in rings {
4036 let (edge, _) = self.entry(ring[0]);
4037 let spec = &self.plan.edges[edge];
4038 let Curve::Circle(c) = &spec.curve else {
4039 ogeom_bail!(Construction, "a band's rim is not a circle");
4040 };
4041 let circle = c.circle();
4042 let start = spec.curve.point_at(0.0, self.tol)?;
4043 let Some((u, v)) = unwrapped(curved, start, self.tol) else {
4044 ogeom_bail!(Construction, "a band's rim has no chart position");
4045 };
4046 let turning = if round_tube {
4050 let radial = circle.centre() - frame.origin();
4051 radial.cross(frame.z().vector())
4052 } else {
4053 frame.z().vector()
4054 };
4055 let with = circle.frame().z().vector().dot(turning) > 0.0;
4056 rims.push((edge, if round_tube { u } else { v }, with, start));
4057 }
4058 rims.sort_by(|a, b| a.1.total_cmp(&b.1));
4059 let [
4060 (low, v_low, low_with, low_at),
4061 (high, v_high, high_with, high_at),
4062 ] = rims[..]
4063 else {
4064 ogeom_bail!(Construction, "a band has two rims");
4065 };
4066 for (edge, at, with) in [(low, v_low, low_with), (high, v_high, high_with)] {
4067 let (a, b) = if with { (0.0, tau) } else { (tau, 0.0) };
4068 let (from, to) = if round_tube {
4069 ((at, a), (at, b))
4070 } else {
4071 ((a, at), (b, at))
4072 };
4073 let pcurve = linear(from, to, (0.0, tau), self.tol)?;
4074 crate::build::attach_pcurve(
4075 self.model,
4076 &edges[edge],
4077 pcurve,
4078 surface,
4079 Location::identity(),
4080 (0.0, tau),
4081 )?;
4082 }
4083 let seam_curve: Curve = match curved.shape {
4087 Canonical::Torus(t) if round_tube => ogeom_geom::CircleCurve::new(
4088 ogeom_math::Circle::new(frame, t.major_radius() + t.minor_radius(), self.tol)?,
4089 )
4090 .into(),
4091 Canonical::Sphere(s) => {
4092 let normal =
4093 Direction::new(frame.x().vector().cross(frame.z().vector()), self.tol)?;
4094 ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(
4095 Frame::new(s.centre(), normal, frame.x(), self.tol)?,
4096 s.radius(),
4097 self.tol,
4098 )?)
4099 .into()
4100 }
4101 Canonical::Torus(t) => {
4102 let spine = frame.origin() + frame.x().vector() * t.major_radius();
4103 let normal =
4104 Direction::new(frame.x().vector().cross(frame.z().vector()), self.tol)?;
4105 ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(
4106 Frame::new(spine, normal, frame.x(), self.tol)?,
4107 t.minor_radius(),
4108 self.tol,
4109 )?)
4110 .into()
4111 }
4112 _ => LineCurve::segment(low_at, high_at, self.tol)?.into(),
4113 };
4114 let seam_range = match curved.shape {
4115 Canonical::Torus(_) | Canonical::Sphere(_) => (v_low, v_high),
4116 _ => (0.0, low_at.distance(high_at)),
4117 };
4118 let placed = |at: Point| {
4121 self.plan
4122 .placed
4123 .iter()
4124 .any(|p| p.distance(at) <= self.tol.confusion())
4125 };
4126 if !placed(low_at) || !placed(high_at) {
4127 ogeom_bail!(
4128 Construction,
4129 "a band's rim vertex is not where its seam starts"
4130 );
4131 }
4132 let vertex = |edge: usize| -> OgeomResult<Shape> {
4133 match self.model.children_of(&edges[edge])?.first() {
4134 Some(v) => Ok(v.clone()),
4135 None => ogeom_bail!(Construction, "a rim has no vertex"),
4136 }
4137 };
4138 let (from_vertex, to_vertex) = (vertex(low)?, vertex(high)?);
4139 let id = self.model.geometry_mut().add_curve(seam_curve);
4140 let data = EdgeData::on_curve(id, Location::identity(), seam_range);
4141 let seam = self.model.add_edge(data, &[from_vertex, to_vertex])?;
4142 let (forward, back) = if round_tube {
4144 (
4145 linear((v_low, 0.0), (v_high, 0.0), seam_range, self.tol)?,
4146 linear((v_low, tau), (v_high, tau), seam_range, self.tol)?,
4147 )
4148 } else {
4149 (
4150 linear((tau, v_low), (tau, v_high), seam_range, self.tol)?,
4151 linear((0.0, v_low), (0.0, v_high), seam_range, self.tol)?,
4152 )
4153 };
4154 crate::build::attach_seam(
4155 self.model,
4156 &seam,
4157 forward,
4158 back,
4159 surface,
4160 Location::identity(),
4161 seam_range,
4162 )?;
4163 let mut ring = if round_tube {
4169 vec![
4170 seam.clone(),
4171 oriented(&edges[high], high_with),
4172 seam.reversed(),
4173 oriented(&edges[low], !low_with),
4174 ]
4175 } else {
4176 vec![
4177 oriented(&edges[low], low_with),
4178 seam.clone(),
4179 oriented(&edges[high], !high_with),
4180 seam.reversed(),
4181 ]
4182 };
4183 if !outward {
4184 ring.reverse();
4185 ring = ring.iter().map(Shape::reversed).collect();
4186 }
4187 let wire = self.model.add_wire(&ring)?;
4188 let mut data = FaceData::new(surface, Location::identity());
4189 data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
4190 let face = self.model.add_face(data, std::slice::from_ref(&wire))?;
4191 Ok(if outward { face } else { face.reversed() })
4192 }
4193}
4194
4195impl Builder<'_> {
4196 fn cap_face(
4200 &mut self,
4201 curved: &Curved,
4202 rings: &[Vec<Half>],
4203 edges: &[Shape],
4204 ) -> OgeomResult<Shape> {
4205 use ogeom_geom::Curve3d as _;
4206 let tau = core::f64::consts::TAU;
4207 let north = core::f64::consts::FRAC_PI_2;
4208 let g = self.groups.of[rings[0][0] / 3];
4209 let Canonical::Sphere(sphere) = curved.shape else {
4210 ogeom_bail!(Construction, "a cap is a sphere's");
4211 };
4212 let Some(geometry) = self.plan.surfaces[g].clone() else {
4213 ogeom_bail!(Construction, "a cap was planned without its surface");
4214 };
4215 let surface = self.model.geometry_mut().add_surface(geometry);
4216 let outward = self.outward(curved, g);
4217 let frame = sphere.frame();
4218 let (rim, _) = self.entry(rings[0][0]);
4219 let spec = &self.plan.edges[rim];
4220 let Curve::Circle(c) = &spec.curve else {
4221 ogeom_bail!(Construction, "a cap's rim is not a circle");
4222 };
4223 let with = c.circle().frame().z().vector().dot(frame.z().vector()) > 0.0;
4224 let start = spec.curve.point_at(0.0, self.tol)?;
4225 let Some((_, v_rim)) = unwrapped(curved, start, self.tol) else {
4226 ogeom_bail!(Construction, "a cap's rim has no chart position");
4227 };
4228 let (a, b) = if with { (0.0, tau) } else { (tau, 0.0) };
4229 crate::build::attach_pcurve(
4230 self.model,
4231 &edges[rim],
4232 linear((a, v_rim), (b, v_rim), (0.0, tau), self.tol)?,
4233 surface,
4234 Location::identity(),
4235 (0.0, tau),
4236 )?;
4237 let Some(rim_vertex) = self.model.children_of(&edges[rim])?.first().cloned() else {
4238 ogeom_bail!(Construction, "a rim has no vertex");
4239 };
4240 let pole = self.model.add_vertex(VertexData::new(
4241 sphere.centre() + frame.z().vector() * sphere.radius(),
4242 ));
4243 let normal = Direction::new(frame.x().vector().cross(frame.z().vector()), self.tol)?;
4244 let meridian: Curve = ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(
4245 Frame::new(sphere.centre(), normal, frame.x(), self.tol)?,
4246 sphere.radius(),
4247 self.tol,
4248 )?)
4249 .into();
4250 let id = self.model.geometry_mut().add_curve(meridian);
4251 let seam_range = (v_rim, north);
4252 let seam = self.model.add_edge(
4253 EdgeData::on_curve(id, Location::identity(), seam_range),
4254 &[rim_vertex, pole.clone()],
4255 )?;
4256 crate::build::attach_seam(
4257 self.model,
4258 &seam,
4259 linear((tau, v_rim), (tau, north), seam_range, self.tol)?,
4260 linear((0.0, v_rim), (0.0, north), seam_range, self.tol)?,
4261 surface,
4262 Location::identity(),
4263 seam_range,
4264 )?;
4265 let mut data = EdgeData::new();
4266 data.degenerate = true;
4267 let tip = self.model.add_edge(data, &[pole.clone(), pole])?;
4268 crate::build::attach_pcurve(
4269 self.model,
4270 &tip,
4271 linear((0.0, north), (tau, north), (0.0, tau), self.tol)?,
4272 surface,
4273 Location::identity(),
4274 (0.0, tau),
4275 )?;
4276 let mut ring = vec![
4279 oriented(&edges[rim], with),
4280 seam.clone(),
4281 tip.reversed(),
4282 seam.reversed(),
4283 ];
4284 if !outward {
4285 ring.reverse();
4286 ring = ring.iter().map(Shape::reversed).collect();
4287 }
4288 let wire = self.model.add_wire(&ring)?;
4289 let mut data = FaceData::new(surface, Location::identity());
4290 data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
4291 let face = self.model.add_face(data, std::slice::from_ref(&wire))?;
4292 Ok(if outward { face } else { face.reversed() })
4293 }
4294
4295 fn holed_face(
4302 &mut self,
4303 curved: &Curved,
4304 g: usize,
4305 rings: &[Vec<Half>],
4306 edges: &[Shape],
4307 ) -> OgeomResult<Shape> {
4308 let outward = self.outward(curved, g);
4309 let whole = self.whole_face(curved, g)?;
4310 let whole = if outward { whole } else { whole.reversed() };
4311 let Some(ogeom_topo::NodeData::Face(data)) =
4312 self.model.node(&whole).map(|n| n.data().clone())
4313 else {
4314 ogeom_bail!(Construction, "a whole surface's face has no data");
4315 };
4316 let surface = data.surface;
4317 let mut wires = self.model.ordered_children_of(&whole)?;
4318 for ring in rings {
4319 let mut ring_edges = Vec::new();
4320 for (edge, forward) in self.entries(ring) {
4321 if !self.has_pcurve(&edges[edge], surface) {
4322 let Some((pcurve, deviation)) = self.plan.pcurves.get(&(edge, g)).cloned()
4323 else {
4324 ogeom_bail!(
4325 Construction,
4326 "an edge was planned without its image on a face"
4327 );
4328 };
4329 self.model.widen(
4330 &edges[edge],
4331 Tolerance::new(deviation.max(self.tol.confusion()))?,
4332 )?;
4333 crate::build::attach_pcurve(
4334 self.model,
4335 &edges[edge],
4336 pcurve,
4337 surface,
4338 Location::identity(),
4339 self.plan.edges[edge].range,
4340 )?;
4341 }
4342 ring_edges.push(oriented(&edges[edge], forward));
4343 }
4344 if !outward {
4345 ring_edges.reverse();
4346 ring_edges = ring_edges.iter().map(Shape::reversed).collect();
4347 }
4348 wires.push(self.model.add_wire(&ring_edges)?);
4349 }
4350 crate::build::chain_wire_branches(self.model, surface, &wires, self.tol)?;
4351 let mut face_data = FaceData::new(surface, Location::identity());
4352 face_data.tolerance = data.tolerance;
4353 let face = self.model.add_face(face_data, &wires)?;
4354 Ok(if outward { face } else { face.reversed() })
4355 }
4356
4357 fn whole_face(&mut self, curved: &Curved, g: usize) -> OgeomResult<Shape> {
4358 let outward = self.outward(curved, g);
4359 let built = match curved.shape {
4360 Canonical::Sphere(s) => {
4361 crate::primitive::make_sphere(self.model, s.frame(), s.radius(), self.tol)?
4362 }
4363 Canonical::Torus(t) => crate::primitive::make_torus(
4364 self.model,
4365 t.frame(),
4366 t.major_radius(),
4367 t.minor_radius(),
4368 self.tol,
4369 )?,
4370 _ => ogeom_bail!(Construction, "only a sphere or a torus is whole"),
4371 };
4372 let Some(face) =
4373 ogeom_topo::explore_unique(self.model, &built.shape, ogeom_topo::ShapeType::Face)?
4374 .into_iter()
4375 .next()
4376 else {
4377 ogeom_bail!(Construction, "a primitive came back with no face");
4378 };
4379 if let Some(node) = self.model.node_mut(&face)
4380 && let ogeom_topo::NodeData::Face(data) = node.data_mut()
4381 {
4382 data.tolerance = Tolerance::new(curved.deviation.max(self.tol.confusion()))?;
4383 }
4384 Ok(if outward { face } else { face.reversed() })
4385 }
4386}
4387
4388fn oriented(edge: &Shape, forward: bool) -> Shape {
4389 if forward {
4390 edge.clone()
4391 } else {
4392 edge.reversed()
4393 }
4394}
4395
4396fn linear(
4399 a: (f64, f64),
4400 b: (f64, f64),
4401 range: (f64, f64),
4402 tol: Tolerances,
4403) -> OgeomResult<PlanarCurve> {
4404 let knots = ogeom_math::KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
4405 Ok(ogeom_geom::BSpline2d::new(
4406 knots,
4407 vec![Point2::new(a.0, a.1), Point2::new(b.0, b.1)],
4408 tol,
4409 )?
4410 .into())
4411}
4412
4413type RimStart = ((usize, bool), Shape, Point);
4416
4417type SeamChoice = (usize, usize, (f64, f64), (f64, f64));
4420
4421fn hole_polygons(
4424 shape: &Canonical,
4425 holes: &[&[Half]],
4426 triangles: &[[u32; 3]],
4427 points: &[Point],
4428 tol: Tolerances,
4429) -> Vec<Vec<(f64, f64)>> {
4430 holes
4431 .iter()
4432 .filter_map(|ring| {
4433 let mut out: Vec<(f64, f64)> = Vec::new();
4434 for &h in *ring {
4435 let (a, _) = from_to(triangles, h);
4436 let (u, v) = chart(shape, points[a as usize], tol)?;
4437 let u = match out.last() {
4438 Some(&(last, _)) => last + ogeom_math::elementary::wrap_signed_angle(u - last),
4439 None => u,
4440 };
4441 out.push((u, v));
4442 }
4443 Some(out)
4444 })
4445 .collect()
4446}
4447
4448fn choose_seam(
4453 shape: &Canonical,
4454 from: &[Point],
4455 to: &[Point],
4456 holes: &[Vec<(f64, f64)>],
4457 tol: Tolerances,
4458) -> Option<SeamChoice> {
4459 let tau = core::f64::consts::TAU;
4460 let crosses = |a: (f64, f64), b: (f64, f64)| {
4461 holes.iter().any(|ring| {
4462 [-tau, 0.0, tau].iter().any(|shift| {
4463 (0..ring.len()).any(|i| {
4464 let (p, q) = (ring[i], ring[(i + 1) % ring.len()]);
4465 segments_cross(a, b, (p.0 + shift, p.1), (q.0 + shift, q.1))
4466 })
4467 })
4468 })
4469 };
4470 let mut best: Option<(f64, SeamChoice)> = None;
4471 for (i, pa) in from.iter().enumerate() {
4472 let Some((ua, va)) = chart(shape, *pa, tol) else {
4473 continue;
4474 };
4475 for (j, pb) in to.iter().enumerate() {
4476 let Some((ub, vb)) = chart(shape, *pb, tol) else {
4477 continue;
4478 };
4479 let turn = ogeom_math::elementary::wrap_signed_angle(ub - ua);
4480 let (a, b) = ((ua, va), (ua + turn, vb));
4481 if crosses(a, b) {
4482 continue;
4483 }
4484 let score = turn.abs() * 1e3 + pa.distance(*pb);
4485 if best.is_none_or(|held| score < held.0) {
4486 best = Some((score, (i, j, a, b)));
4487 }
4488 }
4489 }
4490 best.map(|(_, choice)| choice)
4491}
4492
4493fn free_angle(holes: &[Vec<(f64, f64)>]) -> Option<f64> {
4495 let tau = core::f64::consts::TAU;
4496 let mut angles: Vec<f64> = holes
4497 .iter()
4498 .flatten()
4499 .map(|(u, _)| u.rem_euclid(tau))
4500 .collect();
4501 if angles.is_empty() {
4502 return None;
4503 }
4504 angles.sort_by(f64::total_cmp);
4505 let mut best = (
4506 angles[0] + tau - angles[angles.len() - 1],
4507 angles[angles.len() - 1],
4508 );
4509 for pair in angles.windows(2) {
4510 if pair[1] - pair[0] > best.0 {
4511 best = (pair[1] - pair[0], pair[0]);
4512 }
4513 }
4514 Some(best.1 + best.0 / 2.0)
4515}
4516
4517fn segments_cross(a: (f64, f64), b: (f64, f64), p: (f64, f64), q: (f64, f64)) -> bool {
4519 let side = |o: (f64, f64), x: (f64, f64), y: (f64, f64)| {
4520 (x.0 - o.0).mul_add(y.1 - o.1, -((x.1 - o.1) * (y.0 - o.0)))
4521 };
4522 let (d1, d2) = (side(p, q, a), side(p, q, b));
4523 let (d3, d4) = (side(a, b, p), side(a, b, q));
4524 d1 * d2 < 0.0 && d3 * d4 < 0.0
4525}
4526
4527fn winding(
4530 shape: &Canonical,
4531 ring: &[Half],
4532 triangles: &[[u32; 3]],
4533 points: &[Point],
4534 tol: Tolerances,
4535) -> Option<i32> {
4536 let mut turned = 0.0;
4537 for &h in ring {
4538 let (a, b) = from_to(triangles, h);
4539 let (ua, _) = chart(shape, points[a as usize], tol)?;
4540 let (ub, _) = chart(shape, points[b as usize], tol)?;
4541 turned += ogeom_math::elementary::wrap_signed_angle(ub - ua);
4542 }
4543 #[allow(clippy::cast_possible_truncation, reason = "a handful of turns")]
4544 Some((turned / core::f64::consts::TAU).round() as i32)
4545}
4546
4547fn windings(
4550 shape: &Canonical,
4551 ring: &[Half],
4552 triangles: &[[u32; 3]],
4553 points: &[Point],
4554 tol: Tolerances,
4555) -> Option<(i32, i32)> {
4556 let (_, wraps_v) = periodic(shape);
4557 let mut turned = (0.0, 0.0);
4558 for &h in ring {
4559 let (a, b) = from_to(triangles, h);
4560 let (ua, va) = chart(shape, points[a as usize], tol)?;
4561 let (ub, vb) = chart(shape, points[b as usize], tol)?;
4562 turned.0 += ogeom_math::elementary::wrap_signed_angle(ub - ua);
4563 if wraps_v {
4564 turned.1 += ogeom_math::elementary::wrap_signed_angle(vb - va);
4565 }
4566 }
4567 let tau = core::f64::consts::TAU;
4568 #[allow(clippy::cast_possible_truncation, reason = "a handful of turns")]
4569 Some((
4570 (turned.0 / tau).round() as i32,
4571 (turned.1 / tau).round() as i32,
4572 ))
4573}
4574
4575fn ring_area(
4577 ring: &[Half],
4578 triangles: &[[u32; 3]],
4579 chart: impl Fn(Point) -> Option<Point2>,
4580 points: &[Point],
4581) -> f64 {
4582 let mut area = 0.0;
4583 for &h in ring {
4584 let (a, b) = from_to(triangles, h);
4585 if let (Some(a), Some(b)) = (chart(points[a as usize]), chart(points[b as usize])) {
4586 area += a.x * b.y - b.x * a.y;
4587 }
4588 }
4589 area
4590}
4591
4592fn distance_to_line(p: Point, a: Point, b: Point) -> f64 {
4593 let d = b - a;
4594 let m = d.magnitude();
4595 if m == 0.0 {
4596 return p.distance(a);
4597 }
4598 (p - a).cross(d).magnitude() / m
4599}