1use core::fmt;
5
6use ogeom_core::{OgeomResult, Tolerances};
7use ogeom_math::{Axis, Cylinder, Frame, Plane, Point, Sphere, Torus, Vector};
8use ogeom_topo::{Model, Triangulation};
9
10use super::frames::{align_one, hole_frame, slit_band};
11use super::planner::sags_as_the_surface;
12use super::regions::{region_patch, sphere_axis, swept_claim};
13use super::rounds::{plane_normals, tangent_blends, tangent_rounds};
14use super::segment::{axis_frame, unit_normal};
15use super::snap::plane_through;
16use super::weld::from_to;
17use super::{Carrier, Curved, Found, MeshSolid, MeshSolidOptions, MeshSolidReport};
18use crate::recognize::{self, Canonical, recognize_curved, worst_deviation};
19use crate::recognize_patch::Refused;
20
21#[derive(Clone)]
56pub struct MeshRegions {
57 found: Found,
58 options: MeshSolidOptions,
59 tol: Tolerances,
60 touched: Vec<usize>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub struct RegionId(pub(super) usize);
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub struct RegionFallback {
74 pub region: RegionId,
76 pub reason: FallbackReason,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82#[non_exhaustive]
83pub enum FallbackReason {
84 BoundaryNotPlaced,
87 ReachesPast,
90 TurnedIn,
92 VolumeOff,
95 FoldedSeam,
98 Overlaps,
100 MeshesOpen,
103 BuildFailed,
105 BoundaryOpen,
108 Crosses,
111 SpansShort,
114}
115
116impl fmt::Display for FallbackReason {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.write_str(match self {
119 Self::BoundaryNotPlaced => "its boundary could not be placed on its surface",
120 Self::ReachesPast => "its face reached past its triangles",
121 Self::TurnedIn => "its face faced into the material",
122 Self::VolumeOff => "its body did not hold the mesh's volume",
123 Self::FoldedSeam => "a seam of it folded in its chart",
124 Self::Overlaps => "its face overlapped a neighbour",
125 Self::MeshesOpen => "its face did not mesh closed with its neighbours",
126 Self::BuildFailed => "building or meshing its face failed",
127 Self::BoundaryOpen => "its face's boundary did not close",
128 Self::Crosses => "its face passed through another face",
129 Self::SpansShort => {
130 "a pcurve of its face stood off an edge farther than the edge is long"
131 }
132 })
133 }
134}
135
136impl RegionId {
137 #[must_use]
140 pub const fn index(self) -> usize {
141 self.0
142 }
143}
144
145#[derive(Debug, Clone, PartialEq)]
148pub struct MeshRegion {
149 pub id: RegionId,
151 pub triangles: Vec<usize>,
153 pub surface: Option<Canonical>,
158 pub deviation: Option<f64>,
161 pub neighbours: Vec<RegionId>,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub enum SurfaceKind {
168 Plane,
170 Cylinder,
172 Cone,
174 Sphere,
176 Torus,
178 Patch,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Default)]
187pub struct FitConstraints {
188 pub axis: Option<Axis>,
191 pub radius: Option<f64>,
194}
195
196#[derive(Debug, Clone, PartialEq)]
198pub enum RegionRefusal {
199 NoSuchRegion(RegionId),
202 SameRegion(RegionId),
204 NotAdjacent(RegionId, RegionId),
206 PathTooShort,
208 NotAnEdgeOfTheRegion(u32, u32),
211 DoesNotCut {
213 pieces: usize,
215 },
216 ConstraintDoesNotApply {
218 kind: SurfaceKind,
220 constraint: &'static str,
222 },
223 InvalidRadius(f64),
225 TooFewVertices {
227 kind: SurfaceKind,
229 needed: usize,
231 found: usize,
233 },
234 NoFit(SurfaceKind),
236 DoesNotVerify {
239 kind: SurfaceKind,
241 deviation: f64,
243 distance: f64,
245 },
246 TriangleOffTheSurface {
249 kind: SurfaceKind,
251 triangle: usize,
253 },
254 NotADisk,
257 TooNarrowForAPatch,
260 PatchDoesNotVerify,
264}
265
266impl fmt::Display for RegionRefusal {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 match self {
269 Self::NoSuchRegion(id) => write!(f, "no region {} with triangles", id.0),
270 Self::SameRegion(id) => write!(f, "region {} cannot be merged with itself", id.0),
271 Self::NotAdjacent(a, b) => {
272 write!(f, "regions {} and {} share no mesh edge", a.0, b.0)
273 }
274 Self::PathTooShort => write!(f, "a path needs at least two vertices"),
275 Self::NotAnEdgeOfTheRegion(a, b) => {
276 write!(
277 f,
278 "vertices {a} and {b} are not joined by an edge of the region"
279 )
280 }
281 Self::DoesNotCut { pieces } => write!(
282 f,
283 "the path leaves the region in {pieces} piece(s), not two"
284 ),
285 Self::ConstraintDoesNotApply { kind, constraint } => {
286 write!(f, "a {kind:?} has no {constraint} to fix")
287 }
288 Self::InvalidRadius(r) => write!(f, "the radius {r} is not a positive distance"),
289 Self::TooFewVertices {
290 kind,
291 needed,
292 found,
293 } => write!(
294 f,
295 "a {kind:?} needs {needed} vertices and the region has {found}"
296 ),
297 Self::NoFit(kind) => write!(f, "no {kind:?} fits the region's vertices"),
298 Self::DoesNotVerify {
299 kind,
300 deviation,
301 distance,
302 } => write!(
303 f,
304 "the fitted {kind:?} misses a vertex by {deviation:e}, past the distance {distance:e}"
305 ),
306 Self::TriangleOffTheSurface { kind, triangle } => write!(
307 f,
308 "triangle {triangle} spans off the fitted {kind:?} by more than its sag"
309 ),
310 Self::NotADisk => write!(f, "the region is not one disk, as a patch needs"),
311 Self::TooNarrowForAPatch => {
312 write!(f, "too few vertices lie inside the region to hold a patch")
313 }
314 Self::PatchDoesNotVerify => write!(f, "no patch fitted to the region verifies"),
315 }
316 }
317}
318
319impl std::error::Error for RegionRefusal {}
320
321impl fmt::Debug for MeshRegions {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 f.debug_struct("MeshRegions")
324 .field("triangles", &self.found.triangles.len())
325 .field("regions", &self.ids().len())
326 .field("distance", &self.found.flat)
327 .finish_non_exhaustive()
328 }
329}
330
331impl MeshRegions {
332 pub fn find(
339 mesh: &Triangulation,
340 options: &MeshSolidOptions,
341 tol: Tolerances,
342 ) -> OgeomResult<Self> {
343 Ok(Self {
344 found: super::find(mesh, options, tol)?,
345 options: *options,
346 tol,
347 touched: Vec::new(),
348 })
349 }
350
351 #[cfg(feature = "test-hooks")]
361 #[doc(hidden)]
362 pub fn find_as_listed(
363 mesh: &Triangulation,
364 options: &MeshSolidOptions,
365 tol: Tolerances,
366 ) -> OgeomResult<Self> {
367 Ok(Self {
368 found: super::find_in(mesh, options, false, tol)?,
369 options: *options,
370 tol,
371 touched: Vec::new(),
372 })
373 }
374
375 pub fn build(&self, model: &mut Model) -> OgeomResult<MeshSolid> {
388 let protected: Vec<bool> = if self.touched.is_empty() {
389 Vec::new()
390 } else {
391 let mut by_region = vec![false; self.found.groups.carriers.len()];
392 for &g in &self.touched {
393 by_region[g] = true;
394 }
395 self.found
396 .groups
397 .of
398 .iter()
399 .map(|&g| by_region.get(g).copied().unwrap_or(false))
400 .collect()
401 };
402 super::build(model, &self.found, &self.options, &protected, self.tol)
403 }
404
405 #[must_use]
408 pub fn points(&self) -> &[Point] {
409 &self.found.points
410 }
411
412 #[must_use]
416 pub fn triangles(&self) -> &[[u32; 3]] {
417 &self.found.triangles
418 }
419
420 #[must_use]
423 pub fn vertex_of(&self, input: usize) -> Option<u32> {
424 self.found.remap.get(input).copied()
425 }
426
427 #[must_use]
430 pub fn distance(&self) -> f64 {
431 self.found.flat
432 }
433
434 #[must_use]
437 pub fn report(&self) -> &MeshSolidReport {
438 &self.found.report
439 }
440
441 #[must_use]
443 pub fn region_of(&self, triangle: usize) -> Option<RegionId> {
444 self.found
445 .groups
446 .of
447 .get(triangle)
448 .filter(|&&g| g < self.found.groups.carriers.len())
449 .map(|&g| RegionId(g))
450 }
451
452 #[must_use]
454 pub fn ids(&self) -> Vec<RegionId> {
455 let mut held = vec![false; self.found.groups.carriers.len()];
456 for &g in &self.found.groups.of {
457 if let Some(h) = held.get_mut(g) {
458 *h = true;
459 }
460 }
461 held.iter()
462 .enumerate()
463 .filter(|(_, h)| **h)
464 .map(|(g, _)| RegionId(g))
465 .collect()
466 }
467
468 #[must_use]
470 pub fn regions(&self) -> Vec<MeshRegion> {
471 let members = self.members_all();
472 members
473 .into_iter()
474 .enumerate()
475 .filter(|(_, m)| !m.is_empty())
476 .map(|(g, m)| self.describe(g, m))
477 .collect()
478 }
479
480 #[must_use]
482 pub fn region(&self, id: RegionId) -> Option<MeshRegion> {
483 let members = self.members(id.0);
484 (!members.is_empty()).then(|| self.describe(id.0, members))
485 }
486
487 pub fn merge(&mut self, a: RegionId, b: RegionId) -> Result<RegionId, RegionRefusal> {
505 let first = self.held(a)?;
506 let second = self.held(b)?;
507 if a == b {
508 return Err(RegionRefusal::SameRegion(a));
509 }
510 let groups = &self.found.groups;
511 let adjacent = first.iter().any(|&t| {
512 (3 * t..3 * t + 3)
513 .any(|h| self.found.adjacency.twin[h].is_some_and(|o| groups.of[o / 3] == b.0))
514 });
515 if !adjacent {
516 return Err(RegionRefusal::NotAdjacent(a, b));
517 }
518 let mut union: Vec<usize> = first.iter().chain(&second).copied().collect();
519 union.sort_unstable();
520 let carrier = self.refitted(&union, || {
521 let (large, small) = if first.len() >= second.len() {
522 (a.0, &second)
523 } else {
524 (b.0, &first)
525 };
526 match &self.found.groups.carriers[large] {
527 Carrier::Curved(c) if c.patch.is_some() => Carrier::Gone,
528 carrier => self.kept(carrier, small, &union),
529 }
530 });
531 self.found.groups.narrower.clear();
532 for &t in &union {
533 self.found.groups.of[t] = a.0;
534 }
535 self.found.groups.carriers[b.0] = Carrier::Gone;
536 self.found.groups.carriers[a.0] = carrier;
537 self.touched.retain(|&g| g != b.0);
538 self.lay_out(a.0, false, true);
539 Ok(a)
540 }
541
542 pub fn split(
560 &mut self,
561 id: RegionId,
562 path: &[u32],
563 ) -> Result<(RegionId, RegionId), RegionRefusal> {
564 let members = self.held(id)?;
565 if path.len() < 2 {
566 return Err(RegionRefusal::PathTooShort);
567 }
568 let triangles = &self.found.triangles;
569 let mut cut: ogeom_core::FastSet<(u32, u32)> = ogeom_core::FastSet::default();
570 for pair in path.windows(2) {
571 let (u, v) = (pair[0], pair[1]);
572 let on = members.iter().any(|&t| {
573 (3 * t..3 * t + 3).any(|h| {
574 let (a, b) = from_to(triangles, h);
575 (a, b) == (u, v) || (a, b) == (v, u)
576 })
577 });
578 if !on {
579 return Err(RegionRefusal::NotAnEdgeOfTheRegion(u, v));
580 }
581 cut.insert((u.min(v), u.max(v)));
582 }
583 let mut piece = vec![usize::MAX; triangles.len()];
584 let mut pieces = 0_usize;
585 for &start in &members {
586 if piece[start] != usize::MAX {
587 continue;
588 }
589 piece[start] = pieces;
590 let mut stack = vec![start];
591 while let Some(t) = stack.pop() {
592 for h in 3 * t..3 * t + 3 {
593 let (a, b) = from_to(triangles, h);
594 if cut.contains(&(a.min(b), a.max(b))) {
595 continue;
596 }
597 if let Some(twin) = self.found.adjacency.twin[h] {
598 let o = twin / 3;
599 if self.found.groups.of[o] == id.0 && piece[o] == usize::MAX {
600 piece[o] = pieces;
601 stack.push(o);
602 }
603 }
604 }
605 }
606 pieces += 1;
607 }
608 if pieces != 2 {
609 return Err(RegionRefusal::DoesNotCut { pieces });
610 }
611 let (kept, other): (Vec<usize>, Vec<usize>) = members.iter().partition(|&&t| piece[t] == 0);
612 let parent = self.found.groups.carriers[id.0].clone();
613 let carriers =
614 [&kept, &other].map(|part| self.refitted(part, || self.kept(&parent, part, part)));
615 let [first, second] = carriers;
616 self.found.groups.narrower.clear();
617 let new = self.found.groups.carriers.len();
618 self.found.groups.carriers.push(second);
619 self.found.groups.carriers[id.0] = first;
620 for &t in &other {
621 self.found.groups.of[t] = new;
622 }
623 self.lay_out(id.0, false, true);
624 self.lay_out(new, false, true);
625 Ok((id, RegionId(new)))
626 }
627
628 pub fn fit(
661 &mut self,
662 id: RegionId,
663 kind: SurfaceKind,
664 constraints: &FitConstraints,
665 ) -> Result<f64, RegionRefusal> {
666 let members = self.held(id)?;
667 applies(kind, constraints, self.tol)?;
668 if kind == SurfaceKind::Patch {
669 let claim = self.patch(&members).map_err(|refused| match refused {
670 Refused::NotDisk => RegionRefusal::NotADisk,
671 Refused::Narrow => RegionRefusal::TooNarrowForAPatch,
672 Refused::Unverified => RegionRefusal::PatchDoesNotVerify,
673 })?;
674 let deviation = claim.deviation;
675 self.found.groups.narrower.clear();
676 self.found.groups.carriers[id.0] = Carrier::Curved(claim);
677 self.lay_out(id.0, false, false);
678 return Ok(deviation);
679 }
680 let vertices = self.vertices(&members);
681 let (pts, nrm) = self.samples(&vertices, &members);
682 let shape = fitted(kind, constraints, &pts, &nrm, self.found.flat, self.tol)?;
683 let flat = self.found.flat;
684 let deviation = worst_deviation(&shape, &pts);
685 if deviation.is_nan() || deviation > flat {
686 return Err(RegionRefusal::DoesNotVerify {
687 kind,
688 deviation,
689 distance: flat,
690 });
691 }
692 if kind != SurfaceKind::Plane
693 && let Some(&t) = members.iter().find(|&&t| !self.sags(&shape, t))
694 {
695 return Err(RegionRefusal::TriangleOffTheSurface { kind, triangle: t });
696 }
697 let carrier = match shape {
698 Canonical::Plane(plane) => Carrier::Plane(self.outward(plane, &members)),
699 shape => Carrier::Curved(curved(shape, deviation, vertices)),
700 };
701 self.found.groups.narrower.clear();
702 self.found.groups.carriers[id.0] = carrier;
703 let free = constraints.axis.is_none() && constraints.radius.is_none();
704 self.lay_out(id.0, constraints.axis.is_some(), free);
705 Ok(match &self.found.groups.carriers[id.0] {
707 Carrier::Curved(c) => c.deviation,
708 _ => deviation,
709 })
710 }
711
712 #[cfg(feature = "test-hooks")]
724 #[doc(hidden)]
725 pub fn put_surface_unverified(
726 &mut self,
727 id: RegionId,
728 surface: Canonical,
729 ) -> Result<(), RegionRefusal> {
730 self.held(id)?;
731 self.found.groups.narrower.clear();
732 let carrier = &mut self.found.groups.carriers[id.0];
733 match (carrier, surface) {
734 (Carrier::Plane(plane), Canonical::Plane(given)) => *plane = given,
735 (Carrier::Curved(curved), given)
736 if core::mem::discriminant(&curved.shape) == core::mem::discriminant(&given) =>
737 {
738 curved.shape = given;
739 }
740 (_, given) => {
741 return Err(RegionRefusal::NoFit(match given {
742 Canonical::Plane(_) => SurfaceKind::Plane,
743 Canonical::Cylinder(_) => SurfaceKind::Cylinder,
744 Canonical::Cone(_) => SurfaceKind::Cone,
745 Canonical::Sphere(_) => SurfaceKind::Sphere,
746 Canonical::Torus(_) => SurfaceKind::Torus,
747 Canonical::Swept(_) => SurfaceKind::Patch,
748 }));
749 }
750 }
751 Ok(())
752 }
753
754 fn members_all(&self) -> Vec<Vec<usize>> {
756 let mut members = vec![Vec::new(); self.found.groups.carriers.len()];
757 for (t, &g) in self.found.groups.of.iter().enumerate() {
758 if let Some(m) = members.get_mut(g) {
759 m.push(t);
760 }
761 }
762 members
763 }
764
765 fn members(&self, g: usize) -> Vec<usize> {
766 (0..self.found.triangles.len())
767 .filter(|&t| self.found.groups.of[t] == g)
768 .collect()
769 }
770
771 fn held(&self, id: RegionId) -> Result<Vec<usize>, RegionRefusal> {
773 let members = self.members(id.0);
774 if members.is_empty() {
775 return Err(RegionRefusal::NoSuchRegion(id));
776 }
777 Ok(members)
778 }
779
780 fn vertices(&self, members: &[usize]) -> Vec<u32> {
781 let mut vertices: Vec<u32> = members
782 .iter()
783 .flat_map(|&t| self.found.triangles[t])
784 .collect();
785 vertices.sort_unstable();
786 vertices.dedup();
787 vertices
788 }
789
790 fn points_of(&self, vertices: &[u32]) -> Vec<Point> {
791 vertices
792 .iter()
793 .map(|&v| self.found.points[v as usize])
794 .collect()
795 }
796
797 fn samples(&self, vertices: &[u32], members: &[usize]) -> (Vec<Point>, Vec<Vector>) {
799 let mut sum: ogeom_core::FastMap<u32, Vector> = ogeom_core::FastMap::default();
800 for &t in members {
801 let n = unit_normal(&self.found.points, self.found.triangles[t]);
802 for &v in &self.found.triangles[t] {
803 *sum.entry(v).or_insert(Vector::ZERO) += n;
804 }
805 }
806 let nrm = vertices
807 .iter()
808 .map(|v| {
809 let s = sum.get(v).copied().unwrap_or(Vector::Z);
810 let m = s.magnitude();
811 if m > 0.0 { s / m } else { Vector::Z }
812 })
813 .collect();
814 (self.points_of(vertices), nrm)
815 }
816
817 fn sags(&self, shape: &Canonical, t: usize) -> bool {
819 let corners = self.found.triangles[t].map(|v| self.found.points[v as usize]);
820 sags_as_the_surface(shape, corners, self.found.flat)
821 }
822
823 fn outward(&self, plane: Plane, members: &[usize]) -> Plane {
825 let facing = members.iter().fold(Vector::ZERO, |s, &t| {
826 s + unit_normal(&self.found.points, self.found.triangles[t])
827 });
828 let frame = plane.frame();
829 if facing.dot(frame.z().vector()) >= 0.0 {
830 plane
831 } else {
832 Plane::new(Frame::about(frame.origin(), -frame.z()))
833 }
834 }
835
836 fn recognized(&self, members: &[usize]) -> Option<Carrier> {
839 let flat = self.found.flat;
840 let vertices = self.vertices(members);
841 let (pts, nrm) = self.samples(&vertices, members);
842 if pts.len() >= 3 && recognize::is_flat(&pts, flat, self.tol) {
843 let (centre, normal) = plane_through(&pts, self.tol)?;
844 let plane = Plane::new(Frame::about(centre, normal));
845 return Some(Carrier::Plane(self.outward(plane, members)));
846 }
847 if !(self.options.recognize && self.options.merge_coplanar) {
848 return None;
849 }
850 let stride = members.len().div_ceil(100).max(1);
851 let chords: Vec<(Point, Point)> = members
852 .iter()
853 .step_by(stride)
854 .flat_map(|&t| {
855 let [a, b, c] = self.found.triangles[t].map(|v| self.found.points[v as usize]);
856 [(a, b), (b, c), (c, a)]
857 })
858 .collect();
859 if let Some(found) = recognize_curved(&pts, &nrm, &chords, flat, self.tol)
860 && members.iter().all(|&t| self.sags(&found.surface, t))
861 {
862 return Some(Carrier::Curved(curved(
863 found.surface,
864 found.deviation,
865 vertices,
866 )));
867 }
868 if self.options.sweeps {
869 let normals: Vec<Vector> = self
870 .found
871 .triangles
872 .iter()
873 .map(|t| unit_normal(&self.found.points, *t))
874 .collect();
875 return swept_claim(
876 &self.found.points,
877 &self.found.triangles,
878 &normals,
879 members,
880 flat,
881 self.tol,
882 )
883 .map(Carrier::Curved);
884 }
885 None
886 }
887
888 fn refitted(&self, members: &[usize], kept: impl FnOnce() -> Carrier) -> Carrier {
892 if let Some(carrier) = self.recognized(members) {
893 return carrier;
894 }
895 let carrier = kept();
896 if !matches!(carrier, Carrier::Gone) {
897 return carrier;
898 }
899 if self.options.recognize && self.options.merge_coplanar && self.options.patches {
900 return self.patch(members).map_or(Carrier::Gone, Carrier::Curved);
901 }
902 Carrier::Gone
903 }
904
905 fn patch(&self, members: &[usize]) -> Result<Curved, Refused> {
909 let f = &self.found;
910 region_patch(
911 &f.points,
912 &f.triangles,
913 &f.adjacency,
914 &f.groups,
915 members,
916 f.flat,
917 self.tol,
918 )
919 }
920
921 fn kept(&self, carrier: &Carrier, added: &[usize], all: &[usize]) -> Carrier {
925 let flat = self.found.flat;
926 let pts = self.points_of(&self.vertices(added));
927 match carrier {
928 Carrier::Plane(plane) => {
929 if pts
930 .iter()
931 .all(|p| plane.signed_distance_to(*p).abs() <= flat)
932 {
933 Carrier::Plane(*plane)
934 } else {
935 Carrier::Gone
936 }
937 }
938 Carrier::Curved(c) => {
939 if worst_deviation(&c.shape, &pts) > flat {
940 return Carrier::Gone;
941 }
942 let vertices = self.vertices(all);
943 let deviation = worst_deviation(&c.shape, &self.points_of(&vertices));
944 Carrier::Curved(Curved {
945 deviation,
946 vertices,
947 ..c.clone()
948 })
949 }
950 Carrier::Gone => Carrier::Gone,
951 }
952 }
953
954 fn lay_out(&mut self, g: usize, axis_fixed: bool, tangent: bool) {
964 if !self.touched.contains(&g) {
965 self.touched.push(g);
966 }
967 let cos_crease = self.options.crease.cos();
968 let f = &mut self.found;
969 let tol = self.tol;
970 sphere_axis(
971 &f.points,
972 &f.triangles,
973 &f.adjacency,
974 &mut f.groups,
975 g,
976 f.flat,
977 tol,
978 );
979 let tangent =
980 tangent && matches!(&f.groups.carriers[g], Carrier::Curved(c) if c.patch.is_none());
981 if tangent {
982 let planes = f.groups.clone();
983 tangent_rounds(
984 &f.points,
985 &f.triangles,
986 &f.adjacency,
987 &mut f.groups,
988 &planes,
989 cos_crease,
990 f.flat,
991 Some(g),
992 tol,
993 );
994 }
995 let Carrier::Curved(mut curved) = f.groups.carriers[g].clone() else {
996 return;
997 };
998 let (mut leaders, normals) = if axis_fixed {
999 (Vec::new(), Vec::new())
1000 } else {
1001 let mut others: Vec<(usize, usize, Frame)> = f
1002 .groups
1003 .carriers
1004 .iter()
1005 .enumerate()
1006 .filter(|&(o, _)| o != g)
1007 .filter_map(|(_, c)| match c {
1008 Carrier::Curved(c) => {
1009 let rank = match c.shape {
1010 Canonical::Cylinder(_) => 0,
1011 Canonical::Cone(_) => 1,
1012 Canonical::Torus(_) => 2,
1013 _ => return None,
1014 };
1015 Some((rank, usize::MAX - c.vertices.len(), axis_frame(&c.shape)?))
1016 }
1017 _ => None,
1018 })
1019 .collect();
1020 others.sort_by_key(|&(rank, size, _)| (rank, size));
1021 (
1022 others.into_iter().map(|(_, _, frame)| frame).collect(),
1023 plane_normals(&f.groups),
1024 )
1025 };
1026 align_one(&f.points, &mut curved, &mut leaders, &normals, f.flat, tol);
1027 f.groups.carriers[g] = Carrier::Curved(curved);
1028 if tangent {
1029 let planes = f.groups.clone();
1030 tangent_blends(
1031 &f.points,
1032 &f.triangles,
1033 &f.adjacency,
1034 &mut f.groups,
1035 &planes,
1036 cos_crease,
1037 f.flat,
1038 Some(g),
1039 tol,
1040 );
1041 }
1042 hole_frame(&f.points, &f.triangles, &f.adjacency, &mut f.groups, g, tol);
1043 slit_band(&f.points, &f.triangles, &f.adjacency, &mut f.groups, g, tol);
1044 }
1045
1046 fn describe(&self, g: usize, triangles: Vec<usize>) -> MeshRegion {
1047 let groups = &self.found.groups;
1048 let mut neighbours: Vec<RegionId> = triangles
1049 .iter()
1050 .flat_map(|&t| {
1051 (3 * t..3 * t + 3).filter_map(|h| self.found.adjacency.twin[h].map(|o| o / 3))
1052 })
1053 .map(|o| groups.of[o])
1054 .filter(|&o| o != g && o < groups.carriers.len())
1055 .map(RegionId)
1056 .collect();
1057 neighbours.sort_unstable();
1058 neighbours.dedup();
1059 let (surface, deviation) = match &groups.carriers[g] {
1060 Carrier::Plane(plane) => {
1061 let pts = self.points_of(&self.vertices(&triangles));
1062 let deviation = pts
1063 .iter()
1064 .map(|p| plane.signed_distance_to(*p).abs())
1065 .fold(0.0_f64, f64::max);
1066 (Some(Canonical::Plane(*plane)), Some(deviation))
1067 }
1068 Carrier::Curved(c) => (Some(c.shape.clone()), Some(c.deviation)),
1069 Carrier::Gone => (None, None),
1070 };
1071 MeshRegion {
1072 id: RegionId(g),
1073 triangles,
1074 surface,
1075 deviation,
1076 neighbours,
1077 }
1078 }
1079}
1080
1081fn curved(shape: Canonical, deviation: f64, vertices: Vec<u32>) -> Curved {
1083 Curved {
1084 shape,
1085 deviation,
1086 fitted: deviation,
1087 centre: (0.0, 0.0),
1088 wraps: false,
1089 wraps_v: false,
1090 fixed: false,
1091 vertices,
1092 patch: None,
1093 }
1094}
1095
1096const fn floor(kind: SurfaceKind) -> usize {
1100 match kind {
1101 SurfaceKind::Plane => 3,
1102 SurfaceKind::Sphere => 6,
1103 SurfaceKind::Cylinder => 8,
1104 SurfaceKind::Cone => 9,
1105 SurfaceKind::Torus => 11,
1106 SurfaceKind::Patch => 0,
1107 }
1108}
1109
1110fn applies(
1113 kind: SurfaceKind,
1114 constraints: &FitConstraints,
1115 tol: Tolerances,
1116) -> Result<(), RegionRefusal> {
1117 let refuse = |constraint| Err(RegionRefusal::ConstraintDoesNotApply { kind, constraint });
1118 match kind {
1119 SurfaceKind::Plane | SurfaceKind::Sphere | SurfaceKind::Patch
1120 if constraints.axis.is_some() =>
1121 {
1122 return refuse("axis");
1123 }
1124 SurfaceKind::Plane | SurfaceKind::Cone | SurfaceKind::Patch
1125 if constraints.radius.is_some() =>
1126 {
1127 return refuse("radius");
1128 }
1129 _ => {}
1130 }
1131 if let Some(r) = constraints.radius
1132 && !(r.is_finite() && r > tol.confusion())
1133 {
1134 return Err(RegionRefusal::InvalidRadius(r));
1135 }
1136 Ok(())
1137}
1138
1139fn fitted(
1142 kind: SurfaceKind,
1143 constraints: &FitConstraints,
1144 pts: &[Point],
1145 nrm: &[Vector],
1146 flat: f64,
1147 tol: Tolerances,
1148) -> Result<Canonical, RegionRefusal> {
1149 let needed = floor(kind);
1150 if pts.len() < needed {
1151 return Err(RegionRefusal::TooFewVertices {
1152 kind,
1153 needed,
1154 found: pts.len(),
1155 });
1156 }
1157 let none = RegionRefusal::NoFit(kind);
1158 let free = |seed: Option<Canonical>| -> Result<Canonical, RegionRefusal> {
1159 let seed = seed.ok_or_else(|| none.clone())?;
1160 Ok(recognize::refine(seed.clone(), pts, flat, tol).unwrap_or(seed))
1161 };
1162 let (axis, radius) = (constraints.axis, constraints.radius);
1163 let centroid = Point::from_vector(
1164 pts.iter().fold(Vector::ZERO, |s, p| s + p.to_vector()) / count(pts.len()),
1165 );
1166 match kind {
1167 SurfaceKind::Patch => Err(none),
1169 SurfaceKind::Plane => {
1170 let (centre, normal) = plane_through(pts, tol).ok_or(none)?;
1171 Ok(Canonical::Plane(Plane::new(Frame::about(centre, normal))))
1172 }
1173 SurfaceKind::Sphere => {
1174 let seed = free(recognize::fit_sphere(pts, tol))?;
1175 let Some(r) = radius else {
1176 return Ok(seed);
1177 };
1178 let Canonical::Sphere(s) = seed else {
1179 return Err(none);
1180 };
1181 let c = s.centre();
1182 let y = solve(vec![c.x, c.y, c.z], pts, &|y, p| {
1183 p.distance(Point::new(y[0], y[1], y[2])) - r
1184 });
1185 Sphere::centred(Point::new(y[0], y[1], y[2]), r, tol)
1186 .map(Canonical::Sphere)
1187 .map_err(|_| none)
1188 }
1189 SurfaceKind::Cylinder => match (axis, radius) {
1190 (Some(axis), r) => {
1191 let origin = on_line(axis, centroid);
1192 let r = r.unwrap_or_else(|| {
1193 pts.iter()
1194 .map(|p| distance_from_line(axis, *p))
1195 .sum::<f64>()
1196 / count(pts.len())
1197 });
1198 Cylinder::new(Frame::about(origin, axis.direction), r, tol)
1199 .map(Canonical::Cylinder)
1200 .map_err(|_| none)
1201 }
1202 (None, Some(r)) => {
1203 let seed = free(recognize::fit_cylinder(pts, nrm, tol))?;
1204 let Canonical::Cylinder(c) = seed else {
1205 return Err(none);
1206 };
1207 let (o, d) = (c.frame().origin(), c.frame().z().vector());
1208 let like = Canonical::Cylinder(c);
1209 let y = solve(vec![o.x, o.y, o.z, d.x, d.y, d.z], pts, &|y, p| {
1210 recognize::residual(&like, &[y[0], y[1], y[2], y[3], y[4], y[5], r], p)
1211 });
1212 recognize::rebuild(&like, &[y[0], y[1], y[2], y[3], y[4], y[5], r], tol).ok_or(none)
1213 }
1214 (None, None) => free(recognize::fit_cylinder(pts, nrm, tol)),
1215 },
1216 SurfaceKind::Cone => {
1217 let Some(axis) = axis else {
1218 return free(recognize::fit_cone(pts, nrm, tol));
1219 };
1220 let Some(Canonical::Cone(seed)) =
1221 recognize::ruled_about(pts, axis.location, axis.direction, 0.0, tol)
1222 else {
1223 return Err(none);
1224 };
1225 let (o, d) = (seed.frame().origin(), seed.frame().z().vector());
1226 let like = Canonical::Cone(seed);
1227 let x = |y: &[f64]| [o.x, o.y, o.z, d.x, d.y, d.z, y[0], y[1]];
1228 let y = solve(
1229 vec![seed.reference_radius(), seed.half_angle()],
1230 pts,
1231 &|y, p| recognize::residual(&like, &x(y), p),
1232 );
1233 recognize::rebuild(&like, &x(&y), tol).ok_or(none)
1234 }
1235 SurfaceKind::Torus => match axis {
1236 Some(axis) => {
1237 let d = axis.direction.vector();
1238 let base = on_line(axis, centroid);
1239 let (major, height, minor) =
1240 recognize::profile_circle(pts, base, axis).ok_or(none.clone())?;
1241 let like = Canonical::Torus(
1242 Torus::new(
1243 Frame::about(base + d * height, axis.direction),
1244 major,
1245 radius.unwrap_or(minor),
1246 tol,
1247 )
1248 .map_err(|_| none.clone())?,
1249 );
1250 let x = |y: &[f64]| {
1251 let o = base + d * y[0];
1252 [
1253 o.x,
1254 o.y,
1255 o.z,
1256 d.x,
1257 d.y,
1258 d.z,
1259 y[1],
1260 radius.unwrap_or_else(|| y[2]),
1261 ]
1262 };
1263 let y0 = match radius {
1264 Some(_) => vec![height, major],
1265 None => vec![height, major, minor],
1266 };
1267 let y = solve(y0, pts, &|y, p| recognize::residual(&like, &x(y), p));
1268 recognize::rebuild(&like, &x(&y), tol).ok_or(none)
1269 }
1270 None => {
1271 let seed = free(recognize::fit_torus(pts, nrm, tol))?;
1272 let Some(r) = radius else {
1273 return Ok(seed);
1274 };
1275 let Canonical::Torus(t) = seed else {
1276 return Err(none);
1277 };
1278 let (o, d) = (t.frame().origin(), t.frame().z().vector());
1279 let like = Canonical::Torus(t);
1280 let x = |y: &[f64]| [y[0], y[1], y[2], y[3], y[4], y[5], y[6], r];
1281 let y = solve(
1282 vec![o.x, o.y, o.z, d.x, d.y, d.z, t.major_radius()],
1283 pts,
1284 &|y, p| recognize::residual(&like, &x(y), p),
1285 );
1286 recognize::rebuild(&like, &x(&y), tol).ok_or(none)
1287 }
1288 },
1289 }
1290}
1291
1292#[allow(
1293 clippy::cast_precision_loss,
1294 reason = "vertex counts are far below 2^52"
1295)]
1296fn count(n: usize) -> f64 {
1297 n.max(1) as f64
1298}
1299
1300fn on_line(axis: Axis, p: Point) -> Point {
1302 let d = axis.direction.vector();
1303 axis.location + d * (p - axis.location).dot(d)
1304}
1305
1306fn distance_from_line(axis: Axis, p: Point) -> f64 {
1307 let d = axis.direction.vector();
1308 let w = p - axis.location;
1309 (w - d * w.dot(d)).magnitude()
1310}
1311
1312fn solve(mut y: Vec<f64>, points: &[Point], residual: &dyn Fn(&[f64], Point) -> f64) -> Vec<f64> {
1318 const FIT_SAMPLES: usize = 1500;
1319 let stride = points.len().div_ceil(FIT_SAMPLES).max(1);
1320 let points: Vec<Point> = points.iter().step_by(stride).copied().collect();
1321 let n = y.len();
1322 let cost = |y: &[f64]| -> f64 { points.iter().map(|p| residual(y, *p).powi(2)).sum() };
1323 let scale = points
1324 .iter()
1325 .map(|p| p.distance(points[0]))
1326 .fold(f64::MIN_POSITIVE, f64::max);
1327 let mut current = cost(&y);
1328 for _ in 0..60 {
1329 let steps: Vec<f64> = y.iter().map(|v| 1e-7 * v.abs().max(scale)).collect();
1330 let mut jacobian = nalgebra::DMatrix::<f64>::zeros(points.len(), n);
1331 let mut r = nalgebra::DVector::<f64>::zeros(points.len());
1332 for (i, p) in points.iter().enumerate() {
1333 r[i] = residual(&y, *p);
1334 for k in 0..n {
1335 let mut up = y.clone();
1336 let mut down = y.clone();
1337 up[k] += steps[k];
1338 down[k] -= steps[k];
1339 jacobian[(i, k)] = (residual(&up, *p) - residual(&down, *p)) / (2.0 * steps[k]);
1340 }
1341 }
1342 let svd = jacobian.svd(true, true);
1343 let largest = svd.singular_values.max();
1344 if largest.is_nan() || largest <= 0.0 {
1345 break;
1346 }
1347 let Ok(step) = svd.solve(&(-r), largest * 1e-8) else {
1348 break;
1349 };
1350 let mut scale_step = 1.0;
1351 let mut gained = 0.0;
1352 for _ in 0..20 {
1353 let trial: Vec<f64> = y
1354 .iter()
1355 .zip(step.iter())
1356 .map(|(a, b)| b.mul_add(scale_step, *a))
1357 .collect();
1358 let c = cost(&trial);
1359 if c.is_finite() && c < current {
1360 gained = current - c;
1361 y = trial;
1362 current = c;
1363 break;
1364 }
1365 scale_step *= 0.5;
1366 }
1367 if gained <= current * 1e-12 {
1368 break;
1369 }
1370 }
1371 y
1372}