1use core::cmp::Ordering;
43use core::f64::consts::{FRAC_PI_2, FRAC_PI_4, FRAC_PI_6, PI};
44use std::collections::BTreeSet;
45
46use ogeom_core::{FastMap, OgeomResult, Tolerances, ogeom_bail, parallel};
47use ogeom_math::{Axis, Direction, Frame, Point, Vector};
48use ogeom_topo::Triangulation;
49
50use crate::Cutoff;
51use crate::recognize::{covariance, smallest_direction};
52
53#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct Directions {
57 pub resolution: f64,
63 pub crease: f64,
67 pub rounds: u32,
70 pub cutoff: Cutoff,
76}
77
78impl Default for Directions {
79 fn default() -> Self {
88 Self {
89 resolution: 5.0_f64.to_radians(),
90 crease: 30.0_f64.to_radians(),
91 rounds: 3,
92 cutoff: Cutoff::Sigma(3.0),
93 }
94 }
95}
96
97impl Directions {
98 pub fn check(&self) -> OgeomResult<()> {
106 self.cutoff.check()?;
107 if !(self.resolution.is_finite() && self.resolution > 0.0 && self.resolution <= FRAC_PI_6) {
108 ogeom_bail!(
109 Domain,
110 "the resolution {} must be finite, positive and at most π/6",
111 self.resolution
112 );
113 }
114 if !(self.crease.is_finite() && self.crease > 0.0 && self.crease <= PI) {
115 ogeom_bail!(
116 Domain,
117 "the crease angle {} must be finite, positive and at most π",
118 self.crease
119 );
120 }
121 Ok(())
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq)]
127pub struct FaceDirection {
128 pub direction: Direction,
134 pub area: f64,
136 pub spread: f64,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq)]
144pub struct Revolution {
145 pub axis: Axis,
148 pub share: f64,
153}
154
155#[derive(Debug, Clone, PartialEq)]
158pub struct Rim {
159 pub vertices: Vec<u32>,
162 pub vector_area: Vector,
168 pub centre: Point,
170 pub flatness: f64,
173 pub closed: bool,
177}
178
179pub fn face_directions(
190 mesh: &Triangulation,
191 options: &Directions,
192 tol: Tolerances,
193) -> OgeomResult<Vec<FaceDirection>> {
194 options.check()?;
195 let read = Read::of(mesh, options)?;
196 let kept = read.kept_directions(tol);
197 Ok(read.face_directions(&kept, tol))
198}
199
200pub fn axis_of_revolution(
225 mesh: &Triangulation,
226 options: &Directions,
227 tol: Tolerances,
228) -> OgeomResult<Option<Revolution>> {
229 options.check()?;
230 let read = Read::of(mesh, options)?;
231 let total: f64 = read.facets.area.iter().sum();
232 if total <= 0.0 {
233 return Ok(None);
234 }
235 let mut starts: Vec<Vector> = read.kept_directions(tol);
236 let spread = covariance(
237 read.smooth
238 .iter()
239 .zip(&read.facets.area)
240 .map(|(s, a)| *s * a.sqrt()),
241 );
242 let eigen = nalgebra::SymmetricEigen::new(spread);
243 for i in 0..3 {
244 let v = eigen.eigenvectors.column(i);
245 let v = Vector::new(v[0], v[1], v[2]);
246 if v.magnitude() > 0.0 {
247 starts.push(canonical(v / v.magnitude()));
248 }
249 }
250 let found: Vec<Option<(Vector, Point, f64)>> =
251 parallel::map_ordered(&starts, |_, start| read.revolution_from(*start, tol));
252 let mut best: Option<(Vector, Point, f64)> = None;
253 for (direction, location, area) in found.into_iter().flatten() {
254 let better = match best {
255 None => true,
256 Some((d, _, a)) => match area.total_cmp(&a) {
257 Ordering::Greater => true,
258 Ordering::Less => false,
259 Ordering::Equal => lexical(direction, d) == Ordering::Less,
260 },
261 };
262 if better {
263 best = Some((direction, location, area));
264 }
265 }
266 let Some((direction, location, area)) = best else {
267 return Ok(None);
268 };
269 Ok(Some(Revolution {
270 axis: Axis::new(location, Direction::new(direction, tol)?),
271 share: area / total,
272 }))
273}
274
275pub fn principal_frame(mesh: &Triangulation, tol: Tolerances) -> OgeomResult<Frame> {
296 check_indices(mesh)?;
297 let Some(&reference) = mesh.positions.first() else {
298 ogeom_bail!(Construction, "an empty mesh has no principal frame");
299 };
300 let corners = |t: &[u32; 3]| t.map(|i| mesh.positions[i as usize] - reference);
301 let mut area = 0.0;
302 let mut first = Vector::ZERO;
303 for t in &mesh.triangles {
304 let [a, b, c] = corners(t);
305 let w = (b - a).cross(c - a).magnitude() * 0.5;
306 if w.is_finite() {
307 area += w;
308 first += (a + b + c) * (w / 3.0);
309 }
310 }
311 if area.is_nan() || area <= 0.0 {
312 ogeom_bail!(Construction, "a mesh with no area has no principal frame");
313 }
314 let centre = first / area;
315 let mut second = nalgebra::Matrix3::<f64>::zeros();
316 for t in &mesh.triangles {
317 let [a, b, c] = corners(t).map(|p| p - centre);
318 let w = (b - a).cross(c - a).magnitude() * 0.5;
319 if !w.is_finite() || w <= 0.0 {
320 continue;
321 }
322 let s = a + b + c;
323 let column = |v: Vector| nalgebra::Vector3::new(v.x, v.y, v.z);
324 let sum = [a, b, c, s]
325 .iter()
326 .map(|v| column(*v) * column(*v).transpose())
327 .fold(nalgebra::Matrix3::zeros(), |m, x| m + x);
328 second += sum * (w / 12.0);
329 }
330 let second = second / area;
331 let eigen = nalgebra::SymmetricEigen::new(second);
332 let mut axes: Vec<(f64, Vector)> = (0..3)
333 .map(|i| {
334 let v = eigen.eigenvectors.column(i);
335 (eigen.eigenvalues[i].max(0.0), Vector::new(v[0], v[1], v[2]))
336 })
337 .collect();
338 axes.sort_by(|p, q| q.0.total_cmp(&p.0).then_with(|| lexical(p.1, q.1)));
339 let reach = tol.confusion();
340 let tied = |i: usize, j: usize| {
341 (axes[i].0 - axes[j].0).abs() <= 2.0 * reach * (axes[i].0.sqrt() + axes[j].0.sqrt())
342 };
343 let third = |e: Vector| -> f64 {
344 mesh.triangles
345 .iter()
346 .map(|t| {
347 let [a, b, c] = corners(t).map(|p| p - centre);
348 let w = (b - a).cross(c - a).magnitude() * 0.5;
349 if !w.is_finite() {
350 return 0.0;
351 }
352 let [f, g, h] = [a.dot(e), b.dot(e), c.dot(e)];
353 let mixed = f * f * (g + h) + g * g * (f + h) + h * h * (f + g) + f * g * h;
354 w / 10.0 * (f * f * f + g * g * g + h * h * h + mixed)
355 })
356 .sum()
357 };
358 let sensed = |e: Vector, spread: f64| -> Vector {
359 let skew = third(e);
360 if skew.abs() > 3.0 * reach * area * spread {
361 if skew > 0.0 { e } else { -e }
362 } else {
363 canonical(e)
364 }
365 };
366 let world = [Vector::X, Vector::Y, Vector::Z];
367 let (x, y) = match (tied(0, 1), tied(1, 2)) {
368 (false, false) => (sensed(axes[0].1, axes[0].0), sensed(axes[1].1, axes[1].0)),
369 (true, false) => {
370 let z = sensed(axes[2].1, axes[2].0);
371 let x = projected_world(&world, z);
372 (x, z.cross(x))
373 }
374 (false, true) => {
375 let x = sensed(axes[0].1, axes[0].0);
376 (x, projected_world(&world, x))
377 }
378 (true, true) => (Vector::X, Vector::Y),
379 };
380 let z = x.cross(y);
381 let origin = reference + centre;
382 Frame::from_axes(
383 origin,
384 Direction::new(x, tol)?,
385 Direction::new(y, tol)?,
386 Direction::new(z, tol)?,
387 tol,
388 )
389}
390
391pub fn open_rims(mesh: &Triangulation) -> OgeomResult<Vec<Rim>> {
405 check_indices(mesh)?;
406 let uses = edge_uses(&mesh.triangles);
407 let mut rim: Vec<(u32, u32, u32)> = Vec::new();
408 for (t, tri) in mesh.triangles.iter().enumerate() {
409 for k in 0..3 {
410 let (a, b) = (tri[k], tri[(k + 1) % 3]);
411 if a != b && uses.get(&key(a, b)).is_some_and(|u| u.count == 1) {
412 rim.push((a, b, as_index(t)));
413 }
414 }
415 }
416 let at: FastMap<(u32, u32), usize> = rim
417 .iter()
418 .enumerate()
419 .map(|(i, &(a, b, _))| ((a, b), i))
420 .collect();
421 let limit = mesh.triangles.len();
422 let next: Vec<Option<usize>> = rim
423 .iter()
424 .map(|&(a, b, t)| {
425 let mut tri = t as usize;
426 let mut far = third_vertex(mesh.triangles[tri], a, b)?;
427 for _ in 0..=limit {
428 let uses = uses.get(&key(b, far))?;
429 match uses.count {
430 1 => return at.get(&(b, far)).copied(),
431 2 => {
432 let other = if uses.first as usize == tri {
433 uses.second
434 } else {
435 uses.first
436 } as usize;
437 let turned = third_vertex(mesh.triangles[other], b, far)?;
438 tri = other;
439 far = turned;
440 }
441 _ => return None,
442 }
443 }
444 None
445 })
446 .collect();
447 let mut entered = vec![false; rim.len()];
448 for n in next.iter().flatten() {
449 entered[*n] = true;
450 }
451 let mut taken = vec![false; rim.len()];
452 let mut chains: Vec<(Vec<usize>, bool)> = Vec::new();
453 let walk = |start: usize, taken: &mut Vec<bool>| -> (Vec<usize>, bool) {
454 let mut chain = Vec::new();
455 let mut at = start;
456 loop {
457 taken[at] = true;
458 chain.push(at);
459 match next[at] {
460 Some(n) if n == start => return (chain, true),
461 Some(n) if !taken[n] => at = n,
462 _ => return (chain, false),
463 }
464 }
465 };
466 for start in 0..rim.len() {
467 if !entered[start] && !taken[start] {
468 chains.push(walk(start, &mut taken));
469 }
470 }
471 for start in 0..rim.len() {
472 if !taken[start] {
473 chains.push(walk(start, &mut taken));
474 }
475 }
476 let position = |v: u32| mesh.positions[v as usize];
477 let mut rims: Vec<Rim> = chains
478 .into_iter()
479 .map(|(mut chain, closed)| {
480 if closed {
481 let lowest = (0..chain.len())
482 .min_by(|&i, &j| {
483 let (a, b) = (rim[chain[i]].0, rim[chain[j]].0);
484 lexical(position(a).to_vector(), position(b).to_vector()).then(a.cmp(&b))
485 })
486 .unwrap_or(0);
487 chain.rotate_left(lowest);
488 }
489 let mut vertices: Vec<u32> = chain.iter().map(|&e| rim[e].0).collect();
490 if !closed && let Some(&last) = chain.last() {
491 vertices.push(rim[last].1);
492 }
493 rim_of(vertices, closed, &position)
494 })
495 .collect();
496 rims.sort_by(|p, q| {
497 q.vector_area
498 .magnitude()
499 .total_cmp(&p.vector_area.magnitude())
500 .then_with(|| lexical(p.centre.to_vector(), q.centre.to_vector()))
501 });
502 Ok(rims)
503}
504
505fn rim_of(vertices: Vec<u32>, closed: bool, position: &impl Fn(u32) -> Point) -> Rim {
507 let count = vertices.len();
508 let segments = |k: usize| (position(vertices[k]), position(vertices[(k + 1) % count]));
509 let edges = if closed {
510 count
511 } else {
512 count.saturating_sub(1)
513 };
514 let mut length = 0.0;
515 let mut weighted = Vector::ZERO;
516 let reference = position(vertices[0]);
517 for k in 0..edges {
518 let (p, q) = segments(k);
519 let l = p.distance(q);
520 length += l;
521 weighted += ((p - reference) + (q - reference)) * (l * 0.5);
522 }
523 let centre = if length > 0.0 {
524 reference + weighted / length
525 } else {
526 let sum = vertices
527 .iter()
528 .fold(Vector::ZERO, |s, v| s + (position(*v) - reference));
529 #[allow(
530 clippy::cast_precision_loss,
531 reason = "vertex counts are far below 2^52"
532 )]
533 let n = count as f64;
534 reference + sum / n
535 };
536 let mut traversed = Vector::ZERO;
537 for k in 0..count {
538 let (p, q) = segments(k);
539 traversed += (p - centre).cross(q - centre);
540 }
541 let vector_area = traversed * -0.5;
542 let size = vector_area.magnitude();
543 let flatness = if size > 0.0 {
544 let n = vector_area / size;
545 vertices
546 .iter()
547 .map(|v| (position(*v) - centre).dot(n).abs())
548 .fold(0.0, f64::max)
549 } else {
550 0.0
551 };
552 Rim {
553 vertices,
554 vector_area,
555 centre,
556 flatness,
557 closed,
558 }
559}
560
561fn third_vertex(tri: [u32; 3], a: u32, b: u32) -> Option<u32> {
563 tri.into_iter().find(|&v| v != a && v != b)
564}
565
566#[derive(Debug, Clone, Copy)]
568pub(crate) struct Uses {
569 pub(crate) count: u32,
570 pub(crate) first: u32,
571 pub(crate) second: u32,
572}
573
574pub(crate) fn key(a: u32, b: u32) -> (u32, u32) {
575 (a.min(b), a.max(b))
576}
577
578fn as_index(t: usize) -> u32 {
579 #[allow(
580 clippy::cast_possible_truncation,
581 reason = "triangles are indexed by u32 throughout the mesh"
582 )]
583 let t = t as u32;
584 t
585}
586
587pub(crate) fn edge_uses(triangles: &[[u32; 3]]) -> FastMap<(u32, u32), Uses> {
588 let mut uses: FastMap<(u32, u32), Uses> = FastMap::default();
589 uses.reserve(triangles.len() * 3 / 2 + 3);
590 for (t, tri) in triangles.iter().enumerate() {
591 let t = as_index(t);
592 for k in 0..3 {
593 let (a, b) = (tri[k], tri[(k + 1) % 3]);
594 if a == b {
595 continue;
596 }
597 let entry = uses.entry(key(a, b)).or_insert(Uses {
598 count: 0,
599 first: t,
600 second: t,
601 });
602 if entry.count == 1 {
603 entry.second = t;
604 }
605 entry.count += 1;
606 }
607 }
608 uses
609}
610
611pub(crate) fn check_indices(mesh: &Triangulation) -> OgeomResult<()> {
612 let count = mesh.positions.len();
613 if let Some(t) = mesh
614 .triangles
615 .iter()
616 .find(|t| t.iter().any(|&i| i as usize >= count))
617 {
618 ogeom_bail!(
619 Domain,
620 "a triangle {t:?} indexes a vertex past the mesh's {count}"
621 );
622 }
623 Ok(())
624}
625
626fn canonical(v: Vector) -> Vector {
629 let major = major_axis(v);
630 if v.to_array()[major] < 0.0 { -v } else { v }
631}
632
633fn major_axis(v: Vector) -> usize {
634 let [x, y, z] = v.to_array().map(f64::abs);
635 if x >= y && x >= z {
636 0
637 } else if y >= z {
638 1
639 } else {
640 2
641 }
642}
643
644fn lexical(a: Vector, b: Vector) -> Ordering {
646 a.x.total_cmp(&b.x)
647 .then(a.y.total_cmp(&b.y))
648 .then(a.z.total_cmp(&b.z))
649}
650
651fn projected_world(world: &[Vector; 3], normal: Vector) -> Vector {
654 let mut best = Vector::ZERO;
655 for w in world {
656 let p = *w - normal * normal.dot(*w);
657 if p.magnitude() > best.magnitude() {
658 best = p;
659 }
660 }
661 best / best.magnitude()
662}
663
664#[derive(Debug)]
667struct Facets {
668 normal: Vec<Vector>,
669 area: Vec<f64>,
670 centroid: Vec<Point>,
671}
672
673fn facets(mesh: &Triangulation) -> Facets {
674 let count = mesh.triangles.len();
675 let mut normal = Vec::with_capacity(count);
676 let mut area = Vec::with_capacity(count);
677 let mut centroid = Vec::with_capacity(count);
678 for t in &mesh.triangles {
679 let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
680 let cross = (b - a).cross(c - a);
681 let length = cross.magnitude();
682 if length.is_finite() && length > 0.0 {
683 normal.push(cross / length);
684 area.push(length * 0.5);
685 } else {
686 normal.push(Vector::ZERO);
687 area.push(0.0);
688 }
689 centroid.push(a + ((b - a) + (c - a)) / 3.0);
690 }
691 Facets {
692 normal,
693 area,
694 centroid,
695 }
696}
697
698fn smoothed(mesh: &Triangulation, facets: &Facets, options: &Directions) -> Vec<Vector> {
702 let mut smooth = facets.normal.clone();
703 if options.rounds == 0 {
704 return smooth;
705 }
706 let uses = edge_uses(&mesh.triangles);
707 let limit = options.crease.cos();
708 let neighbours: Vec<[u32; 3]> = mesh
709 .triangles
710 .iter()
711 .enumerate()
712 .map(|(t, tri)| {
713 let mut out = [u32::MAX; 3];
714 if facets.area[t] <= 0.0 {
715 return out;
716 }
717 for k in 0..3 {
718 let Some(u) = uses.get(&key(tri[k], tri[(k + 1) % 3])) else {
719 continue;
720 };
721 if u.count != 2 {
722 continue;
723 }
724 let other = if u.first as usize == t {
725 u.second
726 } else {
727 u.first
728 };
729 let o = other as usize;
730 if facets.area[o] > 0.0 && facets.normal[t].dot(facets.normal[o]) > limit {
731 out[k] = other;
732 }
733 }
734 out
735 })
736 .collect();
737 for _ in 0..options.rounds {
738 let last = smooth;
739 smooth = (0..last.len())
740 .map(|t| {
741 let mut sum = last[t] * facets.area[t];
742 for &n in &neighbours[t] {
743 if n != u32::MAX {
744 sum += last[n as usize] * facets.area[n as usize];
745 }
746 }
747 let m = sum.magnitude();
748 if m > 0.0 { sum / m } else { last[t] }
749 })
750 .collect();
751 }
752 smooth
753}
754
755#[derive(Debug, Clone, Copy)]
759struct Grid {
760 n: u64,
761 width: f64,
762}
763
764impl Grid {
765 fn new(resolution: f64) -> Self {
767 let cells = (FRAC_PI_2 / (resolution * 0.5)).ceil().max(1.0);
768 #[allow(
769 clippy::cast_possible_truncation,
770 reason = "the resolution is at least what makes cells a few thousand a side"
771 )]
772 let n = cells as u64;
773 #[allow(clippy::cast_precision_loss, reason = "n is small")]
774 let width = FRAC_PI_2 / n as f64;
775 Self { n, width }
776 }
777
778 fn reach(self) -> f64 {
783 self.width * (2.0_f64 / 3.0).sqrt()
784 }
785
786 fn index(self, angle: f64) -> u64 {
787 let at = ((angle + FRAC_PI_4) / self.width).floor().max(0.0);
788 #[allow(
789 clippy::cast_possible_truncation,
790 reason = "clamped to the cells a side"
791 )]
792 let at = at as u64;
793 at.min(self.n - 1)
794 }
795
796 fn cell(self, d: Vector) -> u64 {
797 let d = canonical(d).to_array();
798 let m = major_axis(Vector::from_array(d));
799 let (a, b) = (d[(m + 1) % 3], d[(m + 2) % 3]);
800 let (i, j) = (self.index(a.atan2(d[m])), self.index(b.atan2(d[m])));
801 m as u64 * self.n * self.n + i * self.n + j
802 }
803
804 fn angles(self, cell: u64) -> (usize, f64, f64) {
805 let m = cell / (self.n * self.n);
806 let (i, j) = ((cell / self.n) % self.n, cell % self.n);
807 #[allow(clippy::cast_precision_loss, reason = "cell indices are small")]
808 let at = |k: u64| (k as f64 + 0.5).mul_add(self.width, -FRAC_PI_4);
809 #[allow(clippy::cast_possible_truncation, reason = "a face index is 0, 1 or 2")]
810 let m = m as usize;
811 (m, at(i), at(j))
812 }
813
814 fn direction(m: usize, u: f64, v: f64) -> Vector {
815 let mut d = [0.0; 3];
816 d[m] = 1.0;
817 d[(m + 1) % 3] = u.tan();
818 d[(m + 2) % 3] = v.tan();
819 let d = Vector::from_array(d);
820 d / d.magnitude()
821 }
822
823 fn centre(self, cell: u64) -> Vector {
824 let (m, u, v) = self.angles(cell);
825 Self::direction(m, u, v)
826 }
827
828 fn neighbours(self, cell: u64) -> impl Iterator<Item = u64> {
829 let (m, u, v) = self.angles(cell);
830 let w = self.width;
831 [-1.0, 0.0, 1.0].into_iter().flat_map(move |du| {
832 [-1.0, 0.0, 1.0].into_iter().filter_map(move |dv| {
833 if du == 0.0 && dv == 0.0 {
834 return None;
835 }
836 let next = self.cell(Self::direction(m, u + du * w, v + dv * w));
837 (next != cell).then_some(next)
838 })
839 })
840 }
841}
842
843#[derive(Debug)]
846struct Read {
847 facets: Facets,
848 smooth: Vec<Vector>,
849 grid: Grid,
850 smoothed: Binned,
852 own: Binned,
854 resolution: f64,
855 cutoff: Cutoff,
856}
857
858#[derive(Debug)]
862struct Binned {
863 cells: Vec<u64>,
864 starts: Vec<usize>,
865 members: Vec<u32>,
866}
867
868impl Binned {
869 fn of(grid: Grid, normals: &[Vector], area: &[f64]) -> Self {
870 let mut binned: Vec<(u64, u32)> = normals
871 .iter()
872 .enumerate()
873 .filter(|(t, _)| area[*t] > 0.0)
874 .map(|(t, s)| (grid.cell(*s), as_index(t)))
875 .collect();
876 binned.sort_unstable();
877 let mut cells = Vec::new();
878 let mut starts = Vec::new();
879 for (k, (cell, _)) in binned.iter().enumerate() {
880 if cells.last() != Some(cell) {
881 cells.push(*cell);
882 starts.push(k);
883 }
884 }
885 starts.push(binned.len());
886 let members = binned.into_iter().map(|(_, t)| t).collect();
887 Self {
888 cells,
889 starts,
890 members,
891 }
892 }
893
894 fn in_cell(&self, cell: u64) -> &[u32] {
895 match self.cells.binary_search(&cell) {
896 Ok(k) => &self.members[self.starts[k]..self.starts[k + 1]],
897 Err(_) => &[],
898 }
899 }
900}
901
902impl Read {
903 fn of(mesh: &Triangulation, options: &Directions) -> OgeomResult<Self> {
904 check_indices(mesh)?;
905 let facets = facets(mesh);
906 let smooth = smoothed(mesh, &facets, options);
907 let grid = Grid::new(options.resolution);
908 let smoothed = Binned::of(grid, &smooth, &facets.area);
909 let own = Binned::of(grid, &facets.normal, &facets.area);
910 Ok(Self {
911 facets,
912 smooth,
913 grid,
914 smoothed,
915 own,
916 resolution: options.resolution,
917 cutoff: options.cutoff,
918 })
919 }
920
921 fn in_cell(&self, cell: u64) -> &[u32] {
922 self.smoothed.in_cell(cell)
923 }
924
925 fn cells_near(&self, d: Vector, radius: f64) -> Vec<u64> {
928 let limit = (radius + self.grid.reach()).min(FRAC_PI_2).cos();
929 let near = |cell: u64| self.grid.centre(cell).dot(d).abs() >= limit;
930 let start = self.grid.cell(d);
931 let mut seen = BTreeSet::from([start]);
932 let mut queue = vec![start];
933 while let Some(cell) = queue.pop() {
934 for next in self.grid.neighbours(cell) {
935 if near(next) && seen.insert(next) {
936 queue.push(next);
937 }
938 }
939 }
940 seen.into_iter().collect()
941 }
942
943 fn near_own(&self, d: Vector, radius: f64) -> Vec<(u32, f64)> {
947 let limit = radius.cos();
948 let mut out = Vec::new();
949 for cell in self.cells_near(d, radius) {
950 for &t in self.own.in_cell(cell) {
951 let along = self.facets.normal[t as usize].dot(d);
952 if along.abs() >= limit {
953 out.push((t, along));
954 }
955 }
956 }
957 out
958 }
959
960 fn near(&self, d: Vector, radius: f64) -> Vec<(u32, f64)> {
964 let limit = radius.cos();
965 let mut out = Vec::new();
966 for cell in self.cells_near(d, radius) {
967 for &t in self.in_cell(cell) {
968 let along = self.smooth[t as usize].dot(d);
969 if along.abs() >= limit {
970 out.push((t, along));
971 }
972 }
973 }
974 out
975 }
976
977 fn mode(&self, cell: u64, tol: Tolerances) -> Option<(Vector, f64)> {
981 let centre = self.grid.centre(cell);
982 let mean = |members: &[(u32, f64)]| -> Option<(Vector, f64)> {
983 let mut sum = Vector::ZERO;
984 let mut area = 0.0;
985 for &(t, along) in members {
986 let a = self.facets.area[t as usize];
987 let s = self.smooth[t as usize];
988 sum += if along < 0.0 { -s } else { s } * a;
989 area += a;
990 }
991 let m = sum.magnitude();
992 (m > 0.0).then(|| (canonical(sum / m), area))
993 };
994 let seed: Vec<(u32, f64)> = self
995 .in_cell(cell)
996 .iter()
997 .map(|&t| (t, self.smooth[t as usize].dot(centre)))
998 .collect();
999 let (mut d, mut area) = mean(&seed)?;
1000 for _ in 0..64 {
1004 let (next, gathered) = mean(&self.near(d, self.resolution))?;
1005 let moved = next.cross(d).magnitude().atan2(next.dot(d).abs());
1006 d = next;
1007 area = gathered;
1008 if moved <= tol.angular() {
1009 break;
1010 }
1011 }
1012 Some((d, area))
1013 }
1014
1015 fn kept_directions(&self, tol: Tolerances) -> Vec<Vector> {
1017 let area_of = |cell: u64| -> f64 {
1018 self.in_cell(cell)
1019 .iter()
1020 .map(|&t| self.facets.area[t as usize])
1021 .sum()
1022 };
1023 let areas: Vec<f64> = self.smoothed.cells.iter().map(|&c| area_of(c)).collect();
1024 let above = |p: (f64, Vector), q: (f64, Vector)| -> bool {
1025 match p.0.total_cmp(&q.0) {
1026 Ordering::Greater => true,
1027 Ordering::Less => false,
1028 Ordering::Equal => lexical(p.1, q.1) == Ordering::Less,
1029 }
1030 };
1031 let seeds: Vec<u64> = self
1032 .smoothed
1033 .cells
1034 .iter()
1035 .zip(&areas)
1036 .filter(|&(&cell, &area)| {
1037 let own = (area, self.grid.centre(cell));
1038 self.grid.neighbours(cell).all(|n| {
1039 let theirs = match self.smoothed.cells.binary_search(&n) {
1040 Ok(k) => areas[k],
1041 Err(_) => 0.0,
1042 };
1043 above(own, (theirs, self.grid.centre(n)))
1044 })
1045 })
1046 .map(|(&cell, _)| cell)
1047 .collect();
1048 let mut modes: Vec<(f64, Vector)> =
1049 parallel::map_ordered(&seeds, |_, &cell| self.mode(cell, tol))
1050 .into_iter()
1051 .flatten()
1052 .map(|(d, area)| (area, d))
1053 .collect();
1054 modes.sort_by(|p, q| q.0.total_cmp(&p.0).then_with(|| lexical(p.1, q.1)));
1055 let one = self.resolution.cos();
1056 let mut kept: Vec<Vector> = Vec::new();
1057 for (_, d) in modes {
1058 if kept.iter().any(|k| k.dot(d).abs() >= one) {
1059 continue;
1060 }
1061 let mut inner = 0.0;
1062 let mut ring = 0.0;
1063 for (t, along) in self.near(d, 3.0 * self.resolution) {
1064 let s = self.smooth[t as usize];
1065 if kept.iter().any(|k| k.dot(s).abs() >= one) {
1066 continue;
1067 }
1068 let a = self.facets.area[t as usize];
1069 if along.abs() >= one {
1070 inner += a;
1071 } else {
1072 ring += a;
1073 }
1074 }
1075 if inner > ring {
1076 kept.push(d);
1077 }
1078 }
1079 kept
1080 }
1081
1082 fn face_directions(&self, kept: &[Vector], tol: Tolerances) -> Vec<FaceDirection> {
1088 let mut owner: Vec<(usize, f64)> = vec![(usize::MAX, -1.0); self.smooth.len()];
1089 for (k, d) in kept.iter().enumerate() {
1090 let near = self.near(*d, self.resolution);
1091 for (t, along) in near.into_iter().chain(self.near_own(*d, self.resolution)) {
1092 let slot = &mut owner[t as usize];
1093 if along.abs() > slot.1 {
1094 *slot = (k, along.abs());
1095 }
1096 }
1097 }
1098 let mut owned: Vec<Vec<u32>> = vec![Vec::new(); kept.len()];
1099 for (t, &(k, _)) in owner.iter().enumerate() {
1100 if k != usize::MAX {
1101 owned[k].push(as_index(t));
1102 }
1103 }
1104 let mut out: Vec<FaceDirection> = owned
1105 .iter()
1106 .zip(kept)
1107 .filter_map(|(triangles, &d)| {
1108 let triangles = self.cut(triangles, d);
1109 self.measured(&triangles, d, tol)
1110 })
1111 .collect();
1112 out.sort_by(|p, q| {
1113 q.area.total_cmp(&p.area).then_with(|| {
1114 lexical(
1115 canonical(p.direction.vector()),
1116 canonical(q.direction.vector()),
1117 )
1118 })
1119 });
1120 out
1121 }
1122
1123 fn mean(&self, triangles: &[u32], about: Vector) -> Option<Vector> {
1126 let mut sum = Vector::ZERO;
1127 for &t in triangles {
1128 let n = self.facets.normal[t as usize];
1129 let a = self.facets.area[t as usize];
1130 sum += if n.dot(about) < 0.0 { -n } else { n } * a;
1131 }
1132 let m = sum.magnitude();
1133 (m > 0.0).then(|| sum / m)
1134 }
1135
1136 fn cut(&self, owned: &[u32], about: Vector) -> Vec<u32> {
1147 let angle_from = |d: Vector| {
1148 move |t: u32| {
1149 let n = self.facets.normal[t as usize];
1150 n.cross(d).magnitude().atan2(n.dot(d).abs())
1151 }
1152 };
1153 let within: Vec<u32> = owned
1154 .iter()
1155 .copied()
1156 .filter(|&t| angle_from(about)(t) <= self.resolution)
1157 .collect();
1158 let mut kept = if within.is_empty() {
1159 owned.to_vec()
1160 } else {
1161 within
1162 };
1163 for _ in 0..64 {
1166 let Some(d) = self.mean(&kept, about) else {
1167 break;
1168 };
1169 let angle = angle_from(d);
1170 let weighed: Vec<(f64, f64)> = kept
1171 .iter()
1172 .map(|&t| (angle(t), self.facets.area[t as usize]))
1173 .collect();
1174 let limit = self
1175 .cutoff
1176 .threshold_weighted(&weighed)
1177 .max(self.resolution);
1178 let next: Vec<u32> = owned
1179 .iter()
1180 .copied()
1181 .filter(|&t| angle(t) <= limit)
1182 .collect();
1183 if next.is_empty() || next == kept {
1184 break;
1185 }
1186 kept = next;
1187 }
1188 kept
1189 }
1190
1191 fn measured(&self, triangles: &[u32], about: Vector, tol: Tolerances) -> Option<FaceDirection> {
1193 let d = canonical(self.mean(triangles, about)?);
1194 let area: f64 = triangles
1195 .iter()
1196 .map(|&t| self.facets.area[t as usize])
1197 .sum();
1198 let mut net = 0.0;
1199 let mut square = 0.0;
1200 for &t in triangles {
1201 let n = self.facets.normal[t as usize];
1202 let a = self.facets.area[t as usize];
1203 let angle = n.cross(d).magnitude().atan2(n.dot(d).abs());
1204 net += a * n.dot(d);
1205 square += a * angle * angle;
1206 }
1207 let d = if net < -tol.confusion() * area.sqrt() {
1208 -d
1209 } else {
1210 d
1211 };
1212 Some(FaceDirection {
1213 direction: Direction::new(d, tol).ok()?,
1214 area,
1215 spread: (square / area).sqrt(),
1216 })
1217 }
1218
1219 fn revolution_from(&self, start: Vector, tol: Tolerances) -> Option<(Vector, Point, f64)> {
1221 let sine = self.resolution.sin();
1222 let walls = |a: Vector| -> Vec<u32> {
1223 (0..self.smooth.len())
1224 .filter(|&t| self.facets.area[t] > 0.0 && self.smooth[t].dot(a).abs() <= sine)
1225 .map(as_index)
1226 .collect()
1227 };
1228 let about = |a: Vector, p: Point| -> Vec<u32> {
1229 walls(a)
1230 .into_iter()
1231 .filter(|&t| {
1232 let w = self.facets.centroid[t as usize] - p;
1233 let radial = w - a * w.dot(a);
1234 let r = radial.magnitude();
1235 r > 0.0 && self.smooth[t as usize].dot(a.cross(radial) / r).abs() <= sine
1236 })
1237 .collect()
1238 };
1239 let mut a = start;
1240 let mut chosen = walls(a);
1241 let mut p = self.located(a, &chosen, tol)?;
1242 for _ in 0..64 {
1246 let next = about(a, p);
1247 if next.is_empty() {
1248 return None;
1249 }
1250 a = self.least_normal(&next)?;
1251 p = self.located(a, &next, tol)?;
1252 if next == chosen {
1253 break;
1254 }
1255 chosen = next;
1256 }
1257 let mut area = 0.0;
1258 let mut weighted = Vector::ZERO;
1259 for &t in &chosen {
1260 let w = self.facets.area[t as usize];
1261 area += w;
1262 weighted += (self.facets.centroid[t as usize] - p) * w;
1263 }
1264 let level = p + a * (weighted / area).dot(a);
1265 Some((a, level, area))
1266 }
1267
1268 fn least_normal(&self, triangles: &[u32]) -> Option<Vector> {
1271 let m = covariance(
1272 triangles
1273 .iter()
1274 .map(|&t| self.facets.normal[t as usize] * self.facets.area[t as usize].sqrt()),
1275 );
1276 let d = smallest_direction(m);
1277 let length = d.magnitude();
1278 (length > 0.0).then(|| canonical(d / length))
1279 }
1280
1281 fn located(&self, a: Vector, triangles: &[u32], tol: Tolerances) -> Option<Point> {
1285 let reference = self.facets.centroid[*triangles.first()? as usize];
1286 let e1 = Direction::new(a, tol).ok()?.any_perpendicular().vector();
1287 let e2 = a.cross(e1);
1288 let (mut m11, mut m12, mut m22, mut b1, mut b2) = (0.0, 0.0, 0.0, 0.0, 0.0);
1289 for &t in triangles {
1290 let w = self.facets.area[t as usize];
1291 let m = a.cross(self.facets.normal[t as usize]);
1292 let (x, y) = (m.dot(e1), m.dot(e2));
1293 let rhs = (self.facets.centroid[t as usize] - reference).dot(m);
1294 m11 += w * x * x;
1295 m12 += w * x * y;
1296 m22 += w * y * y;
1297 b1 += w * x * rhs;
1298 b2 += w * y * rhs;
1299 }
1300 let det = m11.mul_add(m22, -m12 * m12);
1301 let trace = m11 + m22;
1302 let angular = tol.angular();
1303 if det.is_nan() || det <= angular * angular * trace * trace {
1304 return None;
1305 }
1306 let x = (b1 * m22 - b2 * m12) / det;
1307 let y = (b2 * m11 - b1 * m12) / det;
1308 Some(reference + e1 * x + e2 * y)
1309 }
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314 #![allow(clippy::unwrap_used, reason = "test code")]
1315 use super::*;
1316
1317 const T: Tolerances = Tolerances::millimetres();
1318
1319 #[test]
1320 fn every_direction_lands_in_a_cell_its_antipode_shares_and_its_centre_is_near() {
1321 let grid = Grid::new(5.0_f64.to_radians());
1322 let mut seed = 0x9e37_79b9_7f4a_7c15_u64;
1323 let mut unit = || {
1324 seed ^= seed << 13;
1325 seed ^= seed >> 7;
1326 seed ^= seed << 17;
1327 #[allow(clippy::cast_precision_loss, reason = "test draws")]
1328 let u = (seed >> 11) as f64 / (1_u64 << 53) as f64;
1329 2.0f64.mul_add(u, -1.0)
1330 };
1331 for _ in 0..20_000 {
1332 let v = Vector::new(unit(), unit(), unit());
1333 if v.magnitude() < 1e-3 {
1334 continue;
1335 }
1336 let d = v / v.magnitude();
1337 let cell = grid.cell(d);
1338 assert_eq!(cell, grid.cell(-d));
1339 let c = grid.centre(cell);
1340 let angle = c.cross(d).magnitude().atan2(c.dot(d).abs());
1341 assert!(angle <= grid.reach(), "{angle} past {}", grid.reach());
1342 }
1343 }
1344
1345 #[test]
1346 fn a_resolution_out_of_range_is_refused() {
1347 for resolution in [0.0, -0.1, f64::NAN, 0.6] {
1348 let options = Directions {
1349 resolution,
1350 ..Directions::default()
1351 };
1352 assert!(options.check().is_err());
1353 }
1354 assert!(
1355 Directions {
1356 crease: 0.0,
1357 ..Directions::default()
1358 }
1359 .check()
1360 .is_err()
1361 );
1362 let bad = Triangulation {
1363 positions: vec![Point::ORIGIN],
1364 triangles: vec![[0, 1, 2]],
1365 ..Triangulation::new()
1366 };
1367 assert!(face_directions(&bad, &Directions::default(), T).is_err());
1368 assert!(open_rims(&bad).is_err());
1369 }
1370
1371 #[test]
1372 fn an_empty_mesh_has_no_directions_no_axis_and_no_rims() {
1373 let empty = Triangulation::new();
1374 assert!(
1375 face_directions(&empty, &Directions::default(), T)
1376 .unwrap()
1377 .is_empty()
1378 );
1379 assert!(
1380 axis_of_revolution(&empty, &Directions::default(), T)
1381 .unwrap()
1382 .is_none()
1383 );
1384 assert!(open_rims(&empty).unwrap().is_empty());
1385 assert!(principal_frame(&empty, T).is_err());
1386 }
1387}