1use ogeom_core::clock::Instant;
30use ogeom_core::{Exact, OgeomResult, Predicates, Tolerances, ogeom_bail};
31use ogeom_geom::Curve3d as _;
32use ogeom_geom::{Curve2d, Surface, SurfaceGeometry};
33use ogeom_math::{Direction, Point, Point2, Vector};
34use ogeom_topo::{EdgeRepr, Model, NodeData, Orientation, Shape, ShapeType, Triangulation};
35use spade::handles::FixedVertexHandle;
36use spade::{
37 ConstrainedDelaunayTriangulation, Point2 as SpadePoint, Triangulation as _, mitigate_underflow,
38};
39
40use crate::discretize::{Deflection, discretize};
41
42pub fn triangulate_face(
52 model: &Model,
53 face: &Shape,
54 deflection: Deflection,
55 tol: Tolerances,
56) -> OgeomResult<Triangulation> {
57 let nothing = EdgeChords::new();
61 let (mesh, verdict) = triangulate_reporting(model, face, deflection, Some(¬hing), tol)?;
62 if verdict != Verdict::Short {
63 return Ok(mesh);
64 }
65 triangulate_with(model, face, deflection, None, None, tol)
66}
67
68pub fn triangulate_face_kept(
75 model: &Model,
76 face: &Shape,
77 deflection: Deflection,
78 kept: &FaceMeshCache,
79 tol: Tolerances,
80) -> OgeomResult<Triangulation> {
81 let nothing = EdgeChords::new();
82 let (mesh, verdict) = reporting_kept(
83 model,
84 face,
85 deflection,
86 Some(¬hing),
87 None,
88 Some(kept),
89 Pass::Agreeing,
90 tol,
91 )?;
92 if verdict != Verdict::Short {
93 return Ok(mesh);
94 }
95 triangulate_with(model, face, deflection, None, Some(kept), tol)
96}
97
98#[derive(Debug, Default)]
109pub struct FaceMeshCache {
110 held: std::sync::Mutex<ogeom_core::FastMap<Vec<u8>, (Triangulation, Verdict)>>,
111 chords: std::sync::Mutex<ogeom_core::FastMap<Vec<u8>, Vec<Option<f64>>>>,
115}
116
117impl FaceMeshCache {
118 #[must_use]
120 pub fn new() -> Self {
121 Self::default()
122 }
123
124 fn get(&self, key: &[u8]) -> Option<(Triangulation, Verdict)> {
125 self.held
126 .lock()
127 .unwrap_or_else(std::sync::PoisonError::into_inner)
128 .get(key)
129 .cloned()
130 }
131
132 fn put(&self, key: Vec<u8>, found: (Triangulation, Verdict)) {
133 self.held
134 .lock()
135 .unwrap_or_else(std::sync::PoisonError::into_inner)
136 .insert(key, found);
137 }
138
139 fn chords(&self, key: &[u8]) -> Option<Vec<Option<f64>>> {
140 self.chords
141 .lock()
142 .unwrap_or_else(std::sync::PoisonError::into_inner)
143 .get(key)
144 .cloned()
145 }
146
147 fn keep_chords(&self, key: Vec<u8>, found: Vec<Option<f64>>) {
148 self.chords
149 .lock()
150 .unwrap_or_else(std::sync::PoisonError::into_inner)
151 .insert(key, found);
152 }
153}
154
155fn mesh_key(
160 model: &Model,
161 face: &Shape,
162 deflection: Deflection,
163 finer: Option<&EdgeChords>,
164 pass: Pass,
165 tol: Tolerances,
166) -> Option<Vec<u8>> {
167 let mut key = ogeom_topo::face_content(model, face)?;
168 key.extend_from_slice(format!("{deflection:?}{tol:?}{pass:?}").as_bytes());
169 match finer {
170 None => key.push(b'o'),
171 Some(finer) => {
172 key.push(b's');
173 for wire in model.ordered_children_of(face).ok()? {
174 for edge in model.ordered_children_of(&wire).ok()? {
175 match finer.get(&edge.node().index()) {
176 Some(chord) => {
177 key.push(1);
178 key.extend_from_slice(&chord.to_bits().to_le_bytes());
179 }
180 None => key.push(0),
181 }
182 }
183 }
184 }
185 }
186 Some(key)
187}
188
189#[allow(
191 clippy::too_many_arguments,
192 reason = "the pass's settings, each passed through once"
193)]
194fn reporting_kept(
195 model: &Model,
196 face: &Shape,
197 deflection: Deflection,
198 finer: Option<&EdgeChords>,
199 prepared: Option<Trimming>,
200 kept: Option<&FaceMeshCache>,
201 pass: Pass,
202 tol: Tolerances,
203) -> OgeomResult<(Triangulation, Verdict)> {
204 let Some(kept) = kept else {
205 return triangulate_reporting_from(model, face, deflection, finer, prepared, pass, tol);
206 };
207 let key = mesh_key(model, face, deflection, finer, pass, tol);
208 if let Some(key) = &key
209 && let Some(found) = kept.get(key)
210 {
211 return Ok(found);
212 }
213 let found = triangulate_reporting_from(model, face, deflection, finer, prepared, pass, tol)?;
214 if let Some(key) = key {
215 kept.put(key, found.clone());
216 }
217 Ok(found)
218}
219
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222enum Verdict {
223 Whole,
225 Short,
228}
229
230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232enum Pass {
233 Agreeing,
236 Last,
239}
240
241fn triangulate_with(
246 model: &Model,
247 face: &Shape,
248 deflection: Deflection,
249 finer: Option<&EdgeChords>,
250 kept: Option<&FaceMeshCache>,
251 tol: Tolerances,
252) -> OgeomResult<Triangulation> {
253 let (mesh, verdict) =
254 reporting_kept(model, face, deflection, finer, None, kept, Pass::Last, tol)?;
255 if verdict == Verdict::Short && mesh.triangles.is_empty() {
256 ogeom_bail!(
257 NotDone,
258 "the face's boundary enclosed no triangulable region"
259 );
260 }
261 Ok(mesh)
262}
263
264fn limit_normal(surface: &SurfaceGeometry, u: f64, v: f64, tol: Tolerances) -> Option<Vector> {
276 if let Ok(n) = surface.normal_at(u, v, tol) {
277 return Some(n.vector());
278 }
279 let ((ua, ub), (va, vb)) = surface.domain();
280 let su = if u < f64::midpoint(ua, ub) { 1.0 } else { -1.0 };
281 let sv = if v < f64::midpoint(va, vb) { 1.0 } else { -1.0 };
282 for scale in [1e-6, 1e-4, 1e-2] {
290 let du = (ub - ua).abs().max(f64::EPSILON) * scale * su;
291 let dv = (vb - va).abs().max(f64::EPSILON) * scale * sv;
292 let found = [(u, v + dv), (u + du, v), (u + du, v + dv)]
293 .into_iter()
294 .find_map(|(nu, nv)| surface.normal_at(nu, nv, tol).ok().map(|n| n.vector()));
295 if found.is_some() {
296 return found;
297 }
298 }
299 None
300}
301
302fn triangulate_reporting(
309 model: &Model,
310 face: &Shape,
311 deflection: Deflection,
312 finer: Option<&EdgeChords>,
313 tol: Tolerances,
314) -> OgeomResult<(Triangulation, Verdict)> {
315 triangulate_reporting_from(model, face, deflection, finer, None, Pass::Agreeing, tol)
316}
317
318type Walked = (Trimming, Option<f64>);
321
322fn rim_holds(
332 model: &Model,
333 face: &Shape,
334 deflection: Deflection,
335 tol: Tolerances,
336) -> OgeomResult<Rows> {
337 use ogeom_geom::{Curve2d as _, Surface as _};
338 let mut rows = Rows::Ends;
339 for edge in ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge))? {
340 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
341 continue;
342 };
343 if data.tolerance.get() > deflection.chord * RIM_SLOP {
344 return Ok(Rows::Full);
345 }
346 let mut normals = Vec::new();
350 for repr in &data.representations {
351 if let ogeom_topo::EdgeRepr::PCurve {
352 curve,
353 surface,
354 range,
355 ..
356 } = repr
357 && let (Some(c), Some(s)) = (
358 model.geometry().pcurve(*curve),
359 model.geometry().surface(*surface),
360 )
361 && let Ok(uv) = c.point_at(f64::midpoint(range.0, range.1), tol)
362 && let Ok(n) = s.normal_at(uv.x, uv.y, tol)
363 {
364 normals.push((
365 *surface,
366 n,
367 matches!(s.kind(), ogeom_geom::SurfaceKind::Plane),
368 ));
369 }
370 }
371 for (i, (sa, na, pa)) in normals.iter().enumerate() {
372 for (sb, nb, pb) in &normals[i + 1..] {
373 if sa != sb && na.dot(*nb).abs() > SMOOTH_RIM {
374 if *pa || *pb {
375 rows = Rows::Sparse;
376 } else {
377 return Ok(Rows::Full);
378 }
379 }
380 }
381 }
382 }
383 Ok(rows)
384}
385
386fn widest_tolerance(model: &Model, face: &Shape) -> OgeomResult<f64> {
389 let mut widest = 0.0_f64;
390 for kind in [ShapeType::Edge, ShapeType::Vertex] {
391 for shape in ogeom_topo::explore_unique(model, face, kind)? {
392 widest = widest.max(model.tolerance_of(&shape)?.map_or(0.0, |t| t.get()));
393 }
394 }
395 Ok(widest)
396}
397
398fn with_strips(mut planar: PlanarMesh, strips: &[crate::tails::Strip]) -> Option<PlanarMesh> {
402 let key = |p: Point2| (p.x.to_bits(), p.y.to_bits());
403 let mut index: ogeom_core::FastMap<(u64, u64), u32> = ogeom_core::FastMap::default();
404 for (i, &(u, v)) in planar.parameters.iter().enumerate() {
405 index.insert(key(Point2::new(u, v)), u32::try_from(i).ok()?);
406 }
407 let mut sides: ogeom_core::FastSet<(u32, u32)> = ogeom_core::FastSet::default();
408 for t in &planar.triangles {
409 for k in 0..3 {
410 sides.insert((t[k], t[(k + 1) % 3]));
411 }
412 }
413 for strip in strips {
414 let (from, to) = (
415 *index.get(&key(strip.mouth.0))?,
416 *index.get(&key(strip.mouth.1))?,
417 );
418 let turned = if sides.contains(&(from, to)) {
421 false
422 } else if sides.contains(&(to, from)) {
423 true
424 } else {
425 return None;
426 };
427 for triangle in &strip.triangles {
428 let mut corners = [0_u32; 3];
429 for (corner, p) in corners.iter_mut().zip(triangle) {
430 *corner = match index.get(&key(*p)) {
431 Some(&i) => i,
432 None => {
433 let i = u32::try_from(planar.parameters.len()).ok()?;
434 planar.parameters.push((p.x, p.y));
435 index.insert(key(*p), i);
436 i
437 }
438 };
439 }
440 planar.triangles.push(if turned {
441 [corners[0], corners[2], corners[1]]
442 } else {
443 corners
444 });
445 }
446 }
447 Some(planar)
448}
449
450#[derive(Clone, Copy, PartialEq, Eq, Debug)]
452enum Rows {
453 Full,
455 Sparse,
457 Ends,
459}
460
461const SMOOTH_RIM: f64 = 0.866_025_403_784_438_6;
464
465const RIM_SLOP: f64 = 1e-2;
468
469fn in_own_frame(model: &Model, face: &Shape, deflection: Deflection) -> Deflection {
473 let scale = face
474 .transform(model.datums())
475 .map_or(1.0, |t| t.scale_factor().abs());
476 if scale > 0.0 && scale.is_finite() && (scale - 1.0).abs() > f64::EPSILON {
477 Deflection {
478 chord: deflection.chord / scale,
479 ..deflection
480 }
481 } else {
482 deflection
483 }
484}
485
486fn triangulate_reporting_from(
490 model: &Model,
491 face: &Shape,
492 deflection: Deflection,
493 finer: Option<&EdgeChords>,
494 prepared: Option<Trimming>,
495 pass: Pass,
496 tol: Tolerances,
497) -> OgeomResult<(Triangulation, Verdict)> {
498 deflection.validate()?;
499 if model.kind_of(face)? != ShapeType::Face {
500 ogeom_bail!(Construction, "expected a face");
501 }
502 let deflection = in_own_frame(model, face, deflection);
503 let Some(node) = model.node(face) else {
504 ogeom_bail!(Dangling, "face is not in this model");
505 };
506 let NodeData::Face(data) = node.data() else {
507 ogeom_bail!(Construction, "face node holds no face data");
508 };
509 let Some(surface) = model.geometry().surface(data.surface) else {
510 ogeom_bail!(Dangling, "face refers to a surface not in this model");
511 };
512 let placement = face.transform(model.datums())?;
513
514 let own;
515 let finer = match finer {
516 Some(shared) => shared,
517 None => {
518 own = face_chords(
519 model,
520 face,
521 data.surface,
522 surface,
523 deflection,
524 &EdgeChords::new(),
525 tol,
526 )?;
527 &own
528 }
529 };
530 let phase = Instant::now();
531 let Trimming {
532 rings: uv,
533 anchors,
534 met,
535 walked,
536 } = match prepared {
537 Some(trim) => trim,
538 None => trimming_rings(
539 model,
540 face,
541 data.surface,
542 surface,
543 deflection,
544 finer,
545 false,
546 tol,
547 )?,
548 };
549 let rings_ms = phase.elapsed().as_secs_f64() * 1e3;
550 if uv.is_empty() {
551 let reversed = face.orientation() == Orientation::Reversed;
556 return Ok((
557 sliver_fan(&walked, surface, &placement, reversed, tol),
558 Verdict::Whole,
559 ));
560 }
561 let phase = Instant::now();
562 let rows = rim_holds(model, face, deflection, tol)?;
563 let mut planar = triangulate_region(&uv, surface, deflection, rows, tol)?;
564 let mut drawn = None;
568 if planar.crossed
569 && pass == Pass::Last
570 && let Some(peeled) = crate::tails::peel(
571 &uv,
572 surface,
573 &placement,
574 widest_tolerance(model, face)?,
575 tol,
576 )?
577 {
578 let again = triangulate_region(&peeled.rings, surface, deflection, rows, tol)?;
579 if !again.crossed
580 && let Some(joined) = with_strips(again, &peeled.strips)
581 {
582 planar = joined;
583 drawn = Some(peeled.rings);
584 }
585 }
586 let drawn = drawn.as_ref().unwrap_or(&uv);
587 let region_ms = phase.elapsed().as_secs_f64() * 1e3;
588 let phase = Instant::now();
589
590 let boundary: usize = drawn.iter().map(Vec::len).sum();
602 let narrow = narrow_chord(surface, drawn, deflection, tol)
603 .is_some_and(|want| !edges_already_at(model, face, finer, want));
604 let crossed = narrow || planar.crossed || planar.triangles.len() < planar.boundary_triangles;
605 if *MESH_DEBUG_REFINE {
606 eprintln!(
607 "SHORT {} triangles against {boundary} boundary points in {} rings: crossed {crossed} (boundary pass {})",
608 planar.triangles.len(),
609 drawn.len(),
610 planar.crossed
611 );
612 }
613
614 let mut anchored: ogeom_core::FastMap<(u64, u64), Point> = ogeom_core::FastMap::default();
617 for (ring, ring_anchor) in uv.iter().zip(&anchors) {
618 for (p, a) in ring.iter().zip(ring_anchor) {
619 if let Some(point) = a {
620 anchored.insert((p.x.to_bits(), p.y.to_bits()), *point);
621 }
622 }
623 }
624
625 let reversed = face.orientation() == Orientation::Reversed;
633 let flip = reversed != !face.location().preserves_handedness(model.datums())?;
634 let mut mesh = Triangulation::new();
635 mesh.deflection_met = met;
636 let mut hits = 0_usize;
637 for (u, v) in planar.parameters {
638 let point = match anchored.get(&(u.to_bits(), v.to_bits())) {
641 Some(anchor) => {
642 hits += 1;
643 *anchor
644 }
645 None => placement.apply(surface.point_at(u, v, tol)?),
646 };
647 let normal =
648 limit_normal(surface, u, v, tol).map_or(Vector::ZERO, |n| placement.apply_vector(n));
649 mesh.positions.push(point);
650 mesh.normals.push(if reversed { -normal } else { normal });
651 mesh.parameters.push((u, v));
652 }
653 if *MESH_DEBUG_REFINE && (rings_ms + region_ms) > 50.0 {
654 eprintln!(
655 "PHASE rings {rings_ms:.0}ms region {region_ms:.0}ms lift {:.0}ms ({} ring points, {} tris)",
656 phase.elapsed().as_secs_f64() * 1e3,
657 uv.iter().map(Vec::len).sum::<usize>(),
658 planar.triangles.len()
659 );
660 }
661 if *MESH_DEBUG {
662 eprintln!(
663 "DBG anchors map={} hits={} verts={}",
664 anchored.len(),
665 hits,
666 mesh.positions.len()
667 );
668 }
669 mesh.triangles = planar
670 .triangles
671 .into_iter()
672 .map(|t| if flip { [t[0], t[2], t[1]] } else { t })
673 .collect();
674 split_flat_triangles(&mut mesh, tol);
675 if mesh.deflection_met && !matches!(surface.kind(), ogeom_geom::SurfaceKind::Plane) {
684 let chord = deflection.chord * placement.scale_factor().abs();
685 for t in &mesh.triangles {
686 let [a, b, c] = t.map(|i| i as usize);
687 let (pa, pb, pc) = (mesh.positions[a], mesh.positions[b], mesh.positions[c]);
688 let Ok(normal) = (pb - pa).cross(pc - pa).normalized(tol) else {
689 continue;
690 };
691 let (ua, ub, uc) = (mesh.parameters[a], mesh.parameters[b], mesh.parameters[c]);
692 let middle = ((ua.0 + ub.0 + uc.0) / 3.0, (ua.1 + ub.1 + uc.1) / 3.0);
693 let Ok(on) = surface.point_at(middle.0, middle.1, tol) else {
694 continue;
695 };
696 let on = placement.apply(on);
697 let mut off = (on - pa).dot(normal).abs();
698 if let Ok(across) = surface.normal_at(middle.0, middle.1, tol)
699 && let Ok(across) = placement.apply_vector(across.vector()).normalized(tol)
700 {
701 let centre =
702 Point::from_vector((pa.to_vector() + pb.to_vector() + pc.to_vector()) / 3.0);
703 off = off.max((on - centre).dot(across).abs());
704 }
705 if off > chord + tol.confusion() {
706 mesh.deflection_met = false;
707 break;
708 }
709 }
710 }
711 Ok((
712 mesh,
713 if crossed {
714 Verdict::Short
715 } else {
716 Verdict::Whole
717 },
718 ))
719}
720
721pub fn triangulate(
727 model: &Model,
728 shape: &Shape,
729 deflection: Deflection,
730 tol: Tolerances,
731) -> OgeomResult<Triangulation> {
732 Ok(triangulate_with_chords(model, shape, deflection, tol)?.0)
733}
734
735pub fn triangulate_with_chords(
743 model: &Model,
744 shape: &Shape,
745 deflection: Deflection,
746 tol: Tolerances,
747) -> OgeomResult<(Triangulation, EdgeChords)> {
748 let (mesh, chords, _) = triangulate_with_face_meshes(model, shape, deflection, None, tol)?;
749 Ok((mesh, chords))
750}
751
752pub fn triangulate_with_face_meshes(
762 model: &Model,
763 shape: &Shape,
764 deflection: Deflection,
765 kept: Option<&FaceMeshCache>,
766 tol: Tolerances,
767) -> OgeomResult<(Triangulation, EdgeChords, Vec<FaceMesh>)> {
768 let (faces, chords) = face_meshes_with_chords(model, shape, deflection, kept, tol)?;
774
775 let mut mesh = Triangulation::new();
776 let mut pieces: Vec<(usize, usize)> = Vec::with_capacity(faces.len());
777 for (_, piece) in &faces {
778 let piece = piece.as_ref().map_err(Clone::clone)?;
779 let t0 = mesh.triangles.len();
780 mesh.append(piece);
781 pieces.push((t0, mesh.triangles.len()));
782 }
783 orient_pieces(&mut mesh, &pieces);
784 let mesh = mesh.welded(tol);
785
786 let mut reach = 0.0_f64;
791 for kind in [ShapeType::Edge, ShapeType::Vertex] {
792 for shape in ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(kind))? {
793 let recorded = model.node(&shape).map_or(0.0, |n| match n.data() {
794 NodeData::Edge(d) => d.tolerance.get(),
795 NodeData::Vertex(d) => d.tolerance.get(),
796 _ => 0.0,
797 });
798 reach = reach.max(recorded);
799 }
800 }
801 let reach = reach.max(deflection.chord * 0.1);
805 let mesh = if reach > tol.confusion() {
806 let reach = reach + tol.confusion();
807 mesh.border_welded(reach).border_stitched(reach)
808 } else {
809 mesh
810 };
811 let faces = faces
815 .into_iter()
816 .map(|(face, piece)| {
817 let piece =
818 piece.and_then(|piece| own_answer(model, &face, piece, deflection, &chords, tol));
819 (face, piece)
820 })
821 .collect();
822 Ok((mesh.sealed(deflection.chord), chords, faces))
823}
824
825pub type FaceMesh = (Shape, OgeomResult<Triangulation>);
827
828pub fn face_meshes_for(
839 model: &Model,
840 shape: &Shape,
841 deflection: Deflection,
842 kept: Option<&FaceMeshCache>,
843 tol: Tolerances,
844) -> OgeomResult<(Vec<FaceMesh>, EdgeChords)> {
845 deflection.validate()?;
846 let (faces, chords) = face_meshes_with_chords(model, shape, deflection, kept, tol)?;
847 let faces = faces
848 .into_iter()
849 .map(|(face, piece)| {
850 let piece =
851 piece.and_then(|piece| own_answer(model, &face, piece, deflection, &chords, tol));
852 (face, piece)
853 })
854 .collect();
855 Ok((faces, chords))
856}
857
858fn face_meshes_with_chords(
860 model: &Model,
861 shape: &Shape,
862 deflection: Deflection,
863 kept: Option<&FaceMeshCache>,
864 tol: Tolerances,
865) -> OgeomResult<(Vec<FaceMesh>, EdgeChords)> {
866 let faces: Vec<Shape> =
867 ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(ShapeType::Face))?;
868 let (computed, chords) = face_meshes(model, &faces, deflection, kept, tol)?;
869 Ok((faces.into_iter().zip(computed).collect(), chords))
870}
871
872fn own_answer(
876 model: &Model,
877 face: &Shape,
878 piece: Triangulation,
879 deflection: Deflection,
880 chords: &EdgeChords,
881 tol: Tolerances,
882) -> OgeomResult<Triangulation> {
883 if piece.triangles.is_empty() {
884 triangulate_face_with(model, face, deflection, chords, tol)
885 } else {
886 Ok(piece)
887 }
888}
889
890pub(crate) fn face_meshes(
901 read_model: &Model,
902 faces: &[Shape],
903 deflection: Deflection,
904 kept: Option<&FaceMeshCache>,
905 tol: Tolerances,
906) -> OgeomResult<(Vec<OgeomResult<Triangulation>>, EdgeChords)> {
907 let FirstPass {
908 mut finer,
909 mut computed,
910 mut changed,
911 short,
912 mut drawn,
913 } = first_pass(read_model, faces, deflection, kept, tol)?;
914 let mut still = vec![false; faces.len()];
915 for index in short {
916 still[index] = true;
917 }
918 for _ in 0..SETTLING_ROUNDS {
925 if changed.is_empty() {
926 break;
927 }
928 let again: Vec<usize> = (0..faces.len())
929 .filter(|&i| {
930 ogeom_topo::explore(
931 read_model,
932 &faces[i],
933 ogeom_topo::Filter::OfType(ShapeType::Edge),
934 )
935 .is_ok_and(|es| es.iter().any(|e| changed.contains(&e.node().index())))
936 })
937 .collect();
938 let redone: Vec<OgeomResult<(Triangulation, Verdict)>> =
939 ogeom_core::parallel::map_ordered(&again, |_, &index| {
940 ogeom_core::progress::checkpoint()?;
941 reporting_kept(
942 read_model,
943 &faces[index],
944 deflection,
945 Some(&finer),
946 None,
947 kept,
948 Pass::Agreeing,
949 tol,
950 )
951 });
952 let mut short: Vec<usize> = Vec::new();
953 for (index, one) in again.into_iter().zip(redone) {
954 still[index] = matches!(one, Ok((_, Verdict::Short)));
955 computed[index] = match one {
956 Ok((mesh, Verdict::Short)) if mesh.triangles.is_empty() => {
957 Err(ogeom_core::ogeom_err!(
958 NotDone,
959 "the face's boundary enclosed no triangulable region"
960 ))
961 }
962 Ok((mesh, verdict)) => {
963 if verdict == Verdict::Short {
964 short.push(index);
965 }
966 Ok(mesh)
967 }
968 Err(e) => Err(e),
969 };
970 }
971 drawn = finer.clone();
972 changed = refine_short(read_model, faces, &short, &mut finer, deflection, kept, tol)?;
973 }
974 let last: Vec<usize> = (0..faces.len()).filter(|&i| still[i]).collect();
978 let redone: Vec<OgeomResult<(Triangulation, Verdict)>> =
979 ogeom_core::parallel::map_ordered(&last, |_, &index| {
980 ogeom_core::progress::checkpoint()?;
981 reporting_kept(
982 read_model,
983 &faces[index],
984 deflection,
985 Some(&drawn),
986 None,
987 kept,
988 Pass::Last,
989 tol,
990 )
991 });
992 for (index, one) in last.into_iter().zip(redone) {
993 if let Ok((mesh, Verdict::Whole)) = one {
994 computed[index] = Ok(mesh);
995 }
996 }
997 Ok((computed, finer))
998}
999
1000const SETTLING_ROUNDS: usize = 4;
1002
1003fn refine_short(
1007 read_model: &Model,
1008 faces: &[Shape],
1009 short: &[usize],
1010 finer: &mut EdgeChords,
1011 deflection: Deflection,
1012 kept: Option<&FaceMeshCache>,
1013 tol: Tolerances,
1014) -> OgeomResult<ogeom_core::FastSet<u32>> {
1015 let mut changed: ogeom_core::FastSet<u32> = ogeom_core::FastSet::default();
1016 let held = finer.clone();
1021 let asked: Vec<OgeomResult<Option<EdgeChords>>> =
1022 ogeom_core::parallel::map_ordered(short, |_, &index| {
1023 ogeom_core::progress::checkpoint()?;
1024 let face = &faces[index];
1025 let Some(node) = read_model.node(face) else {
1026 return Ok(None);
1027 };
1028 let NodeData::Face(data) = node.data() else {
1029 return Ok(None);
1030 };
1031 let Some(surface) = read_model.geometry().surface(data.surface) else {
1032 return Ok(None);
1033 };
1034 face_chords_kept(
1035 read_model,
1036 face,
1037 data.surface,
1038 surface,
1039 deflection,
1040 &held,
1041 kept,
1042 tol,
1043 )
1044 .map(Some)
1045 });
1046 for answer in asked {
1047 let Some(chords) = answer? else {
1048 continue;
1049 };
1050 for (edge, chord) in chords {
1051 let held = finer.entry(edge).or_insert(f64::INFINITY);
1052 if chord < *held {
1053 *held = chord;
1054 changed.insert(edge);
1055 }
1056 }
1057 }
1058 Ok(changed)
1059}
1060
1061struct FirstPass {
1066 finer: EdgeChords,
1067 computed: Vec<OgeomResult<Triangulation>>,
1068 changed: ogeom_core::FastSet<u32>,
1069 short: Vec<usize>,
1071 drawn: EdgeChords,
1073}
1074
1075fn first_pass(
1084 read_model: &Model,
1085 faces: &[Shape],
1086 deflection: Deflection,
1087 cache: Option<&FaceMeshCache>,
1088 tol: Tolerances,
1089) -> OgeomResult<FirstPass> {
1090 let (mut finer, kept) = walk_for_chords(read_model, faces, deflection, tol);
1103 let touched = |face: &Shape| -> bool {
1104 ogeom_topo::explore(
1105 read_model,
1106 face,
1107 ogeom_topo::Filter::OfType(ShapeType::Edge),
1108 )
1109 .is_ok_and(|es| es.iter().any(|e| finer.contains_key(&e.node().index())))
1110 };
1111 let jobs: Vec<(&Shape, std::sync::Mutex<Option<Trimming>>)> = faces
1115 .iter()
1116 .zip(kept)
1117 .map(|(face, prep)| {
1118 let trim = prep.filter(|_| !touched(face));
1119 (face, std::sync::Mutex::new(trim))
1120 })
1121 .collect();
1122 let first: Vec<OgeomResult<(Triangulation, Verdict)>> =
1123 ogeom_core::parallel::map_ordered(&jobs, |_, (face, slot)| {
1124 ogeom_core::progress::checkpoint()?;
1125 let trim = slot.lock().ok().and_then(|mut held| held.take());
1126 reporting_kept(
1127 read_model,
1128 face,
1129 deflection,
1130 Some(&finer),
1131 trim,
1132 cache,
1133 Pass::Agreeing,
1134 tol,
1135 )
1136 });
1137 let mut computed: Vec<OgeomResult<Triangulation>> = Vec::with_capacity(faces.len());
1138 let mut crossed: Vec<usize> = Vec::new();
1139 for (index, one) in first.into_iter().enumerate() {
1140 match one {
1141 Ok((mesh, Verdict::Short)) => {
1142 crossed.push(index);
1143 computed.push(Ok(mesh));
1144 }
1145 Ok((mesh, Verdict::Whole)) => computed.push(Ok(mesh)),
1146 Err(e) => computed.push(Err(e)),
1147 }
1148 }
1149 if *MESH_DEBUG_REFINE && !crossed.is_empty() {
1150 eprintln!(
1151 "REFINE {} of {} faces came up short",
1152 crossed.len(),
1153 faces.len()
1154 );
1155 }
1156 let drawn = finer.clone();
1161 let changed = refine_short(
1162 read_model, faces, &crossed, &mut finer, deflection, cache, tol,
1163 )?;
1164 Ok(FirstPass {
1165 finer,
1166 computed,
1167 changed,
1168 short: crossed,
1169 drawn,
1170 })
1171}
1172
1173fn walk_for_chords(
1180 read_model: &Model,
1181 faces: &[Shape],
1182 deflection: Deflection,
1183 tol: Tolerances,
1184) -> (EdgeChords, Vec<Option<Trimming>>) {
1185 let nothing = EdgeChords::new();
1186 let prepared: Vec<OgeomResult<Option<Walked>>> =
1187 ogeom_core::parallel::map_ordered(faces, |_, face| {
1188 ogeom_core::progress::checkpoint()?;
1189 let Some(node) = read_model.node(face) else {
1190 return Ok(None);
1191 };
1192 let NodeData::Face(data) = node.data() else {
1193 return Ok(None);
1194 };
1195 let Some(surface) = read_model.geometry().surface(data.surface) else {
1196 return Ok(None);
1197 };
1198 let deflection = in_own_frame(read_model, face, deflection);
1199 let trim = trimming_rings(
1200 read_model,
1201 face,
1202 data.surface,
1203 surface,
1204 deflection,
1205 ¬hing,
1206 false,
1207 tol,
1208 )?;
1209 let narrow = narrow_chord(surface, &trim.rings, deflection, tol);
1210 Ok(Some((trim, narrow)))
1211 });
1212 let mut finer = EdgeChords::new();
1213 let mut kept: Vec<Option<Trimming>> = Vec::with_capacity(faces.len());
1214 for (face, one) in faces.iter().zip(prepared) {
1215 match one {
1216 Ok(Some((prep, narrow))) => {
1217 if let Some(chord) = narrow
1218 && let Ok(edges) = ogeom_topo::explore(
1219 read_model,
1220 face,
1221 ogeom_topo::Filter::OfType(ShapeType::Edge),
1222 )
1223 {
1224 for edge in edges {
1225 let held = finer.entry(edge.node().index()).or_insert(chord);
1226 *held = held.min(chord);
1227 }
1228 }
1229 kept.push(Some(prep));
1230 }
1231 Ok(None) | Err(_) => kept.push(None),
1232 }
1233 }
1234 (finer, kept)
1235}
1236
1237pub fn edge_chords_for(
1258 model: &Model,
1259 shape: &Shape,
1260 deflection: Deflection,
1261 tol: Tolerances,
1262) -> OgeomResult<EdgeChords> {
1263 deflection.validate()?;
1264 let faces: Vec<Shape> =
1265 ogeom_topo::explore(model, shape, ogeom_topo::Filter::OfType(ShapeType::Face))?;
1266 Ok(first_pass(model, &faces, deflection, None, tol)?.finer)
1267}
1268
1269pub fn triangulate_face_with(
1283 model: &Model,
1284 face: &Shape,
1285 deflection: Deflection,
1286 chords: &EdgeChords,
1287 tol: Tolerances,
1288) -> OgeomResult<Triangulation> {
1289 triangulate_with(model, face, deflection, Some(chords), None, tol)
1290}
1291
1292fn orient_pieces(mesh: &mut Triangulation, pieces: &[(usize, usize)]) {
1310 use ogeom_core::FastMap;
1311 type Key = (u64, u64, u64);
1312 let key = |p: &Point| -> Key { (p.x.to_bits(), p.y.to_bits(), p.z.to_bits()) };
1313
1314 let mut owners: FastMap<(Key, Key), Vec<(usize, bool)>> = FastMap::default();
1317 for (i, &(t0, t1)) in pieces.iter().enumerate() {
1318 let mut inside: FastMap<(u32, u32), u32> = FastMap::default();
1319 for t in &mesh.triangles[t0..t1] {
1320 for k in 0..3 {
1321 let (a, b) = (t[k], t[(k + 1) % 3]);
1322 *inside.entry((a.min(b), a.max(b))).or_default() += 1;
1323 }
1324 }
1325 for t in &mesh.triangles[t0..t1] {
1326 for k in 0..3 {
1327 let (a, b) = (t[k], t[(k + 1) % 3]);
1328 if inside.get(&(a.min(b), a.max(b))).copied() != Some(1) {
1329 continue;
1330 }
1331 let (ka, kb) = (
1332 key(&mesh.positions[a as usize]),
1333 key(&mesh.positions[b as usize]),
1334 );
1335 if ka == kb {
1340 continue;
1341 }
1342 let (lo, hi, forward) = if ka <= kb {
1345 (ka, kb, true)
1346 } else {
1347 (kb, ka, false)
1348 };
1349 owners.entry((lo, hi)).or_default().push((i, forward));
1350 }
1351 }
1352 }
1353
1354 let mut votes: std::collections::BTreeMap<(usize, usize), (u32, u32)> =
1361 std::collections::BTreeMap::new();
1362 for list in owners.values() {
1363 if let [(a, fa), (b, fb)] = list[..]
1364 && a != b
1365 {
1366 let pair = (a.min(b), a.max(b));
1367 let entry = votes.entry(pair).or_insert((0, 0));
1368 if fa == fb {
1369 entry.0 += 1;
1370 } else {
1371 entry.1 += 1;
1372 }
1373 }
1374 }
1375 let mut constraints: Vec<(u32, usize, usize, bool)> = votes
1383 .iter()
1384 .map(|(&(a, b), &(same, opposite))| (same.abs_diff(opposite), a, b, same > opposite))
1385 .collect();
1386 constraints.sort_by(|x, y| y.0.cmp(&x.0).then(x.1.cmp(&y.1)).then(x.2.cmp(&y.2)));
1387
1388 let mut parent: Vec<usize> = (0..pieces.len()).collect();
1389 let mut parity: Vec<bool> = vec![false; pieces.len()];
1391 fn find(parent: &mut [usize], parity: &mut [bool], i: usize) -> (usize, bool) {
1392 if parent[i] == i {
1393 return (i, false);
1394 }
1395 let (root, above) = find(parent, parity, parent[i]);
1396 parent[i] = root;
1397 parity[i] ^= above;
1398 (root, parity[i])
1399 }
1400 for &(_, a, b, same_direction) in &constraints {
1401 let (ra, pa) = find(&mut parent, &mut parity, a);
1402 let (rb, pb) = find(&mut parent, &mut parity, b);
1403 let need = same_direction;
1405 if ra == rb {
1406 continue;
1409 }
1410 parent[rb] = ra;
1411 parity[rb] = (pa != pb) != need;
1412 }
1413
1414 let mut flip: Vec<bool> = vec![false; pieces.len()];
1415 let mut components: std::collections::BTreeMap<usize, Vec<usize>> =
1416 std::collections::BTreeMap::new();
1417 for (i, f) in flip.iter_mut().enumerate() {
1418 let (root, p) = find(&mut parent, &mut parity, i);
1419 *f = p;
1420 components.entry(root).or_default().push(i);
1421 }
1422 for members in components.values() {
1423 let flipped = members.iter().filter(|&&i| flip[i]).count();
1424 if flipped * 2 > members.len() {
1425 for &i in members {
1426 flip[i] = !flip[i];
1427 }
1428 }
1429 }
1430
1431 for (i, &(t0, t1)) in pieces.iter().enumerate() {
1432 if !flip[i] {
1433 continue;
1434 }
1435 let mut flipped_vertices: Vec<u32> = Vec::new();
1436 for t in &mut mesh.triangles[t0..t1] {
1437 t.swap(1, 2);
1438 flipped_vertices.extend_from_slice(&t[..]);
1439 }
1440 flipped_vertices.sort_unstable();
1441 flipped_vertices.dedup();
1442 for v in flipped_vertices {
1443 mesh.normals[v as usize] = -mesh.normals[v as usize];
1444 }
1445 }
1446}
1447
1448pub fn face_boundary(
1463 model: &Model,
1464 face: &Shape,
1465 deflection: Deflection,
1466 tol: Tolerances,
1467) -> OgeomResult<Vec<Vec<Point2>>> {
1468 drawn_boundary(model, face, deflection, false, tol)
1469}
1470
1471pub fn face_boundary_in_chart(
1486 model: &Model,
1487 face: &Shape,
1488 deflection: Deflection,
1489 tol: Tolerances,
1490) -> OgeomResult<Vec<Vec<Point2>>> {
1491 drawn_boundary(model, face, deflection, true, tol)
1492}
1493
1494pub fn face_boundary_in_chart_met(
1503 model: &Model,
1504 face: &Shape,
1505 deflection: Deflection,
1506 tol: Tolerances,
1507) -> OgeomResult<Option<Vec<Vec<Point2>>>> {
1508 let drawn = drawn_trimming(model, face, deflection, true, tol)?;
1509 Ok(drawn.met.then_some(drawn.rings))
1510}
1511
1512fn drawn_boundary(
1514 model: &Model,
1515 face: &Shape,
1516 deflection: Deflection,
1517 in_chart: bool,
1518 tol: Tolerances,
1519) -> OgeomResult<Vec<Vec<Point2>>> {
1520 Ok(drawn_trimming(model, face, deflection, in_chart, tol)?.rings)
1521}
1522
1523fn drawn_trimming(
1524 model: &Model,
1525 face: &Shape,
1526 deflection: Deflection,
1527 in_chart: bool,
1528 tol: Tolerances,
1529) -> OgeomResult<Trimming> {
1530 deflection.validate()?;
1531 if model.kind_of(face)? != ShapeType::Face {
1532 ogeom_bail!(Construction, "expected a face");
1533 }
1534 let Some(node) = model.node(face) else {
1535 ogeom_bail!(Dangling, "face is not in this model");
1536 };
1537 let NodeData::Face(data) = node.data() else {
1538 ogeom_bail!(Construction, "face node holds no face data");
1539 };
1540 let Some(surface) = model.geometry().surface(data.surface) else {
1541 ogeom_bail!(Dangling, "face refers to a surface not in this model");
1542 };
1543
1544 trimming_rings(
1545 model,
1546 face,
1547 data.surface,
1548 surface,
1549 deflection,
1550 &EdgeChords::new(),
1551 in_chart,
1552 tol,
1553 )
1554}
1555
1556#[allow(
1563 clippy::disallowed_types,
1564 reason = "public type; a std map keeps the API"
1565)]
1566pub type EdgeChords = std::collections::HashMap<u32, f64>;
1567
1568const REFINEMENTS: usize = 6;
1576
1577#[allow(
1582 clippy::too_many_arguments,
1583 reason = "face_chords' own arguments and the cache"
1584)]
1585fn face_chords_kept(
1586 model: &Model,
1587 face: &Shape,
1588 id: ogeom_topo::SurfaceId,
1589 surface: &SurfaceGeometry,
1590 deflection: Deflection,
1591 held: &EdgeChords,
1592 kept: Option<&FaceMeshCache>,
1593 tol: Tolerances,
1594) -> OgeomResult<EdgeChords> {
1595 let walked = || -> Option<Vec<u32>> {
1596 let mut edges = Vec::new();
1597 for wire in model.ordered_children_of(face).ok()? {
1598 for edge in model.ordered_children_of(&wire).ok()? {
1599 edges.push(edge.node().index());
1600 }
1601 }
1602 Some(edges)
1603 };
1604 let (Some(kept), Some(key), Some(walk)) = (
1605 kept,
1606 mesh_key(model, face, deflection, Some(held), Pass::Agreeing, tol),
1607 walked(),
1608 ) else {
1609 return face_chords(model, face, id, surface, deflection, held, tol);
1610 };
1611 if let Some(found) = kept.chords(&key)
1612 && found.len() == walk.len()
1613 {
1614 return Ok(walk
1615 .iter()
1616 .zip(found)
1617 .filter_map(|(&edge, chord)| chord.map(|c| (edge, c)))
1618 .collect());
1619 }
1620 let found = face_chords(model, face, id, surface, deflection, held, tol)?;
1621 if found.keys().all(|e| walk.contains(e)) {
1623 kept.keep_chords(key, walk.iter().map(|e| found.get(e).copied()).collect());
1624 }
1625 Ok(found)
1626}
1627
1628fn face_chords(
1644 model: &Model,
1645 face: &Shape,
1646 id: ogeom_topo::SurfaceId,
1647 surface: &SurfaceGeometry,
1648 deflection: Deflection,
1649 held: &EdgeChords,
1650 tol: Tolerances,
1651) -> OgeomResult<EdgeChords> {
1652 let deflection = in_own_frame(model, face, deflection);
1653 let edges: Vec<u32> =
1654 ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge))?
1655 .iter()
1656 .map(|e| e.node().index())
1657 .collect();
1658 let mut finer: EdgeChords = edges
1664 .iter()
1665 .filter_map(|e| held.get(e).map(|c| (*e, *c)))
1666 .collect();
1667 let lower = |finer: &mut EdgeChords, chord: f64| {
1668 for edge in &edges {
1669 let at = finer.entry(*edge).or_insert(chord);
1670 *at = at.min(chord);
1671 }
1672 };
1673 let mut chord = deflection.chord;
1674 let first = trimming_rings(model, face, id, surface, deflection, &finer, false, tol)?.rings;
1677 if let Some(want) = narrow_chord(surface, &first, deflection, tol) {
1678 chord = want;
1679 lower(&mut finer, chord);
1680 }
1681 for _ in 0..=REFINEMENTS {
1682 let rings = trimming_rings(model, face, id, surface, deflection, &finer, false, tol)?.rings;
1683 let planar = region_mesh(
1686 &rings,
1687 surface,
1688 deflection,
1689 rim_holds(model, face, deflection, tol)?,
1690 Upto::Crossing,
1691 tol,
1692 )?;
1693 if !planar.crossed && planar.triangles.len() >= planar.boundary_triangles {
1694 break;
1695 }
1696 chord *= 0.5;
1697 lower(&mut finer, chord);
1698 }
1699 Ok(finer)
1700}
1701
1702const FLAT_PASSES: usize = 16;
1705
1706fn split_flat_triangles(mesh: &mut Triangulation, tol: Tolerances) {
1716 let mut across: ogeom_core::FastMap<(u32, u32), usize> = ogeom_core::FastMap::default();
1717 for (i, t) in mesh.triangles.iter().enumerate() {
1718 for k in 0..3 {
1719 across.insert((t[k], t[(k + 1) % 3]), i);
1720 }
1721 }
1722 let at = |mesh: &Triangulation, i: u32| mesh.positions[i as usize];
1723 let between = |p: Point, q: Point, m: Point| {
1725 let side = q - p;
1726 let length = side.magnitude();
1727 if length <= tol.confusion() {
1728 return false;
1729 }
1730 let along = (m - p).dot(side) / (length * length);
1731 let off = (m - p).cross(side).magnitude() / length;
1732 off <= tol.confusion()
1733 && along * length > tol.confusion()
1734 && (1.0 - along) * length > tol.confusion()
1735 };
1736 let flat = |mesh: &Triangulation, t: [u32; 3]| {
1737 let [a, b, c] = t.map(|i| at(mesh, i));
1738 (b - a).cross(c - a).magnitude()
1739 <= tol.confusion() * a.distance(b).max(b.distance(c)).max(c.distance(a))
1740 };
1741 let mut gone = vec![false; mesh.triangles.len()];
1742 for _ in 0..FLAT_PASSES {
1747 let mut split = false;
1748 for i in 0..mesh.triangles.len() {
1749 if gone[i] {
1750 continue;
1751 }
1752 let t = mesh.triangles[i];
1753 let Some([p, q, m]) = (0..3)
1755 .map(|k| [t[k], t[(k + 1) % 3], t[(k + 2) % 3]])
1756 .find(|[p, q, m]| between(at(mesh, *p), at(mesh, *q), at(mesh, *m)))
1757 else {
1758 continue;
1759 };
1760 let Some(&n) = across.get(&(q, p)) else {
1761 continue;
1762 };
1763 if n == i || gone[n] {
1764 continue;
1765 }
1766 let other = mesh.triangles[n];
1767 let Some(&d) = other.iter().find(|&&v| v != p && v != q) else {
1768 continue;
1769 };
1770 let halves = [[q, m, d], [m, p, d]];
1771 if halves.iter().any(|h| flat(mesh, *h)) {
1772 continue;
1773 }
1774 gone[i] = true;
1775 mesh.triangles[n] = halves[0];
1776 for k in 0..3 {
1777 across.insert((halves[0][k], halves[0][(k + 1) % 3]), n);
1778 }
1779 across.remove(&(q, p));
1780 across.remove(&(p, q));
1781 let added = mesh.triangles.len();
1782 mesh.triangles.push(halves[1]);
1783 gone.push(false);
1784 for k in 0..3 {
1785 across.insert((halves[1][k], halves[1][(k + 1) % 3]), added);
1786 }
1787 split = true;
1788 }
1789 if !split {
1790 break;
1791 }
1792 }
1793 let mut kept = gone.iter();
1794 mesh.triangles
1795 .retain(|_| !kept.next().copied().unwrap_or(false));
1796}
1797
1798const NARROW: f64 = 4.0;
1807
1808fn narrow_chord(
1816 surface: &SurfaceGeometry,
1817 rings: &[Vec<Point2>],
1818 deflection: Deflection,
1819 tol: Tolerances,
1820) -> Option<f64> {
1821 let (lo, hi) = chart_extent(rings);
1822 if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) {
1823 return None;
1824 }
1825 let (mut du, mut dv, mut n) = (0.0_f64, 0.0_f64, 0usize);
1826 for i in 0..=2 {
1827 for j in 0..=2 {
1828 let u = lo.x + (hi.x - lo.x) * (0.25 + 0.25 * f64::from(i));
1829 let v = lo.y + (hi.y - lo.y) * (0.25 + 0.25 * f64::from(j));
1830 if let Ok((a, b)) = surface.d1_at(u, v, tol) {
1831 du += a.magnitude();
1832 dv += b.magnitude();
1833 n += 1;
1834 }
1835 }
1836 }
1837 if n == 0 {
1838 return None;
1839 }
1840 #[allow(clippy::cast_precision_loss)]
1841 let width = ((hi.x - lo.x) * du / n as f64).min((hi.y - lo.y) * dv / n as f64);
1842 if !width.is_finite() || width <= tol.confusion() || width >= deflection.chord * NARROW {
1843 return None;
1844 }
1845 Some(width / NARROW)
1846}
1847
1848fn edges_already_at(model: &Model, face: &Shape, finer: &EdgeChords, want: f64) -> bool {
1850 ogeom_topo::explore(model, face, ogeom_topo::Filter::OfType(ShapeType::Edge)).is_ok_and(
1851 |edges| {
1852 edges
1853 .iter()
1854 .all(|e| finer.get(&e.node().index()).is_some_and(|&c| c <= want))
1855 },
1856 )
1857}
1858
1859struct Trimming {
1861 rings: Vec<Vec<Point2>>,
1864 anchors: Vec<Vec<Option<Point>>>,
1867 met: bool,
1869 walked: Vec<Vec<(Point2, Point)>>,
1872}
1873
1874type WalkedRing = (
1877 Vec<Point2>,
1878 Vec<Option<Point>>,
1879 bool,
1880 Vec<(usize, f64)>,
1881 Vec<usize>,
1882);
1883
1884fn turns_into_domain(extent: (f64, f64), domain: (f64, f64)) -> f64 {
1893 let ((least, most), (lo, hi)) = (extent, domain);
1894 let span = hi - lo;
1895 let slack = span * 1e-6;
1896 if least < lo - slack {
1897 ((lo - least - slack) / span).ceil()
1898 } else if most > hi + slack {
1899 -((most - hi - slack) / span).ceil()
1900 } else {
1901 0.0
1902 }
1903}
1904
1905#[allow(
1908 clippy::too_many_arguments,
1909 reason = "the walk's settings, each read once"
1910)]
1911fn trimming_rings(
1912 model: &Model,
1913 face: &Shape,
1914 id: ogeom_topo::SurfaceId,
1915 surface: &SurfaceGeometry,
1916 deflection: Deflection,
1917 finer: &EdgeChords,
1918 in_chart: bool,
1919 tol: Tolerances,
1920) -> OgeomResult<Trimming> {
1921 let mut rings = Vec::new();
1922 let mut ring_anchors = Vec::new();
1923 let mut ring_folds: Vec<Vec<(usize, f64)>> = Vec::new();
1924 let mut ring_ties: Vec<Vec<usize>> = Vec::new();
1925 let mut ring_wires: Vec<Shape> = Vec::new();
1926 let mut met = true;
1927 let wires = model.ordered_children_of(face)?;
1928 let bounded = !wires.is_empty();
1929 let mut walked = Vec::new();
1930 for wire in wires {
1931 let (ring, anchors, ring_met, folds, ties) =
1932 boundary_ring(model, &wire, id, deflection, finer, in_chart, tol)?;
1933 met &= ring_met;
1934 if ring.len() >= 3 {
1935 let mut here = Vec::with_capacity(ring.len());
1936 for (uv, anchor) in ring.iter().zip(&anchors) {
1937 let at = match anchor {
1938 Some(p) => *p,
1939 None => surface.point_at(uv.x, uv.y, tol)?,
1940 };
1941 here.push((*uv, at));
1942 }
1943 walked.push(here);
1944 rings.push(ring);
1945 ring_anchors.push(anchors);
1946 ring_folds.push(folds);
1947 ring_ties.push(ties);
1948 ring_wires.push(wire);
1949 }
1950 }
1951
1952 {
1963 use ogeom_geom::Surface as _;
1964 let ((ua, ub), (va, vb)) = surface.domain();
1965 let axes = [
1966 (surface.is_periodic_u(), ub - ua),
1967 (surface.is_periodic_v(), vb - va),
1968 ];
1969 for (axis, (periodic, period)) in axes.into_iter().enumerate() {
1970 if !periodic || period <= 0.0 {
1971 continue;
1972 }
1973 let along = |p: Point2| if axis == 0 { p.x } else { p.y };
1974 let across = |p: Point2| if axis == 0 { p.y } else { p.x };
1975 let make = |a: f64, c: f64| {
1976 if axis == 0 {
1977 Point2::new(a, c)
1978 } else {
1979 Point2::new(c, a)
1980 }
1981 };
1982 let d_of = |ring: &[Point2]| -> f64 {
1983 ring.last().map_or(0.0, |l| along(*l) - along(ring[0]))
1984 };
1985 let open_wound: Vec<usize> = rings
1986 .iter()
1987 .enumerate()
1988 .filter(|(_, ring)| {
1989 ring.len() >= 3
1990 && (d_of(ring).abs() - period).abs() <= period * 1e-3
1991 && ring
1992 .last()
1993 .is_some_and(|l| ring[0].distance(*l) > period * 0.5)
1994 })
1995 .map(|(i, _)| i)
1996 .collect();
1997 let [i, j] = open_wound[..] else {
1998 continue;
1999 };
2000 if d_of(&rings[i]).signum() == d_of(&rings[j]).signum() {
2001 continue;
2002 }
2003 let sign = d_of(&rings[i]).signum();
2004 let column = {
2014 let mut spans: Vec<(f64, f64)> = Vec::new();
2015 for (k, ring) in rings.iter().enumerate() {
2016 if k == i || k == j {
2017 continue;
2018 }
2019 let (lo, hi) = ring
2020 .iter()
2021 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
2022 (lo.min(along(*p)), hi.max(along(*p)))
2023 });
2024 if lo.is_finite() && hi - lo < period {
2025 spans.push((lo.rem_euclid(period), hi - lo));
2026 }
2027 }
2028 let fallback = rings[i].last().map_or(0.0, |l| along(*l));
2029 if spans.is_empty() {
2030 fallback
2031 } else {
2032 let mut edges: Vec<(f64, f64)> =
2034 spans.iter().map(|(lo, len)| (*lo, lo + len)).collect();
2035 edges.sort_by(|a, b| a.0.total_cmp(&b.0));
2036 let mut best = (f64::NEG_INFINITY, fallback);
2037 for k in 0..edges.len() {
2038 let end = edges[k].1;
2039 let next = if k + 1 < edges.len() {
2040 edges[k + 1].0
2041 } else {
2042 edges[0].0 + period
2043 };
2044 if next - end > best.0 {
2045 best = (next - end, f64::midpoint(end, next));
2046 }
2047 }
2048 best.1
2049 }
2050 };
2051 let recut = |chain: &mut Vec<Point2>, anchors: &mut Vec<Option<Point>>| {
2052 let n = chain.len();
2057 if n < 3 {
2058 return;
2059 }
2060 let forward = d_of(chain) > 0.0;
2061 let start = along(chain[0]);
2062 let target = if forward {
2063 start + (column - start).rem_euclid(period)
2064 } else {
2065 start - (start - column).rem_euclid(period)
2066 };
2067 let reached = |k: usize| {
2068 if forward {
2069 along(chain[k]) >= target
2070 } else {
2071 along(chain[k]) <= target
2072 }
2073 };
2074 let Some(k) = (1..n - 1).find(|&k| reached(k)) else {
2075 return;
2076 };
2077 if k <= 1 {
2078 return;
2079 }
2080 let carry = if forward { period } else { -period };
2081 let mut turned: Vec<Point2> = chain[k..].to_vec();
2082 let mut turned_anchors: Vec<Option<Point>> = anchors[k..].to_vec();
2083 for (p, a) in chain[1..=k].iter().zip(&anchors[1..=k]) {
2084 turned.push(make(along(*p) + carry, across(*p)));
2085 turned_anchors.push(*a);
2086 }
2087 *chain = turned;
2088 *anchors = turned_anchors;
2089 };
2090 recut(&mut rings[i], &mut ring_anchors[i]);
2091 recut(&mut rings[j], &mut ring_anchors[j]);
2092 let b = rings.remove(j);
2093 let b_anchors = ring_anchors.remove(j);
2094 ring_folds.remove(j);
2095 ring_ties.remove(j);
2096 ring_wires.remove(j);
2097 let a = &mut rings[i];
2098 let a_anchors = &mut ring_anchors[i];
2099 let a_last = a.last().copied().unwrap_or(a[0]);
2100 let shift = ((along(a_last) - along(b[0])) / period).round() * period;
2103 let b_first = make(along(b[0]) + shift, across(b[0]));
2104 let fractions = joining_run(surface, a_last, b_first, deflection, tol);
2107 let inner = &fractions[1..fractions.len() - 1];
2108 for &f in inner {
2109 a.push(make(
2110 along(a_last) + (along(b_first) - along(a_last)) * f,
2111 across(a_last) + (across(b_first) - across(a_last)) * f,
2112 ));
2113 a_anchors.push(None);
2114 }
2115 for (p, anchor) in b.iter().zip(&b_anchors) {
2116 a.push(make(along(*p) + shift, across(*p)));
2117 a_anchors.push(*anchor);
2118 }
2119 for &f in inner.iter().rev() {
2122 a.push(make(
2123 along(a_last) + (along(b_first) - along(a_last)) * f - sign * period,
2124 across(a_last) + (across(b_first) - across(a_last)) * f,
2125 ));
2126 a_anchors.push(None);
2127 }
2128 let (band_lo, band_hi) = a
2134 .iter()
2135 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
2136 (lo.min(along(*p)), hi.max(along(*p)))
2137 });
2138 for (k, ring) in rings.iter_mut().enumerate() {
2139 if k == i || ring.is_empty() {
2140 continue;
2141 }
2142 #[allow(clippy::cast_precision_loss)]
2143 let mean = ring.iter().map(|p| along(*p)).sum::<f64>() / ring.len() as f64;
2144 if mean >= band_lo && mean <= band_hi {
2145 continue;
2146 }
2147 let turns = ((mean - band_lo) / period).floor();
2148 if turns == 0.0 {
2149 continue;
2150 }
2151 for p in ring.iter_mut() {
2152 *p = make(along(*p) - turns * period, across(*p));
2153 }
2154 }
2155 }
2156 }
2157
2158 if geometry_winds(surface) {
2165 use ogeom_geom::Surface as _;
2166 let ((ua, ub), _) = surface.domain();
2167 let period = ub - ua;
2168 for ((ring, folds), ties) in rings.iter_mut().zip(&ring_folds).zip(&ring_ties) {
2169 let Some(last) = ring.last().copied() else {
2170 continue;
2171 };
2172 let du = last.x - ring[0].x;
2173 let k = (du / period).round();
2174 if k == 0.0
2175 || (du - k * period).abs() > period * 1e-3
2176 || ring[0].distance(last) <= period * 1e-3
2177 {
2178 continue;
2179 }
2180 let fold = folds
2186 .iter()
2187 .rev()
2188 .find(|(_, shift)| (shift - k * period).abs() <= period * 1e-6)
2189 .map(|&(fold_start, _)| fold_start)
2190 .filter(|&fold_start| fold_start < ring.len());
2191 if fold.is_none()
2192 && let Some(&tie) = ties.last()
2193 && tie < ring.len()
2194 {
2195 for p in &mut ring[tie..] {
2196 p.x -= k * period;
2197 }
2198 continue;
2199 }
2200 if let Some(fold_start) = fold {
2201 let start = ties
2205 .iter()
2206 .rev()
2207 .find(|&&t| t < fold_start)
2208 .copied()
2209 .unwrap_or(fold_start);
2210 for p in &mut ring[start..] {
2211 p.x -= k * period;
2212 }
2213 }
2214 }
2215 }
2216
2217 for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
2220 close_wound_ring(ring, anchors, surface, tol);
2221 snap_chart_junctions(ring, anchors, surface, tol);
2222 }
2223
2224 for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
2228 collapse_close_points(ring, anchors);
2229 }
2230 {
2238 use ogeom_geom::Surface as _;
2239 let ((ua, ub), (va, vb)) = surface.domain();
2240 let axes = [
2241 (surface.is_periodic_u(), ub - ua),
2242 (surface.is_periodic_v(), vb - va),
2243 ];
2244 let area = |ring: &[Point2]| -> f64 {
2245 let Some(&o) = ring.first() else {
2246 return 0.0;
2247 };
2248 ring.iter()
2249 .zip(ring.iter().cycle().skip(1))
2250 .map(|(a, b)| (*a - o).cross(*b - o))
2251 .sum::<f64>()
2252 .abs()
2253 };
2254 let bound = (0..rings.len()).max_by(|&a, &b| area(&rings[a]).total_cmp(&area(&rings[b])));
2255 for (axis, (periodic, period)) in axes.into_iter().enumerate() {
2256 let Some(bound) = bound else {
2257 break;
2258 };
2259 if !periodic || period <= 0.0 {
2260 continue;
2261 }
2262 let along = |p: &Point2| if axis == 0 { p.x } else { p.y };
2263 let (lo, hi) = rings[bound]
2264 .iter()
2265 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
2266 (lo.min(along(p)), hi.max(along(p)))
2267 });
2268 if !(lo.is_finite() && hi.is_finite()) {
2269 continue;
2270 }
2271 for (k, ring) in rings.iter_mut().enumerate() {
2272 if k == bound || ring.is_empty() {
2273 continue;
2274 }
2275 #[allow(clippy::cast_precision_loss)]
2276 let mean = ring.iter().map(along).sum::<f64>() / ring.len() as f64;
2277 if mean >= lo && mean <= hi {
2278 continue;
2279 }
2280 let turns = ((mean - f64::midpoint(lo, hi)) / period).round();
2281 let moved = mean - turns * period;
2282 if turns == 0.0 || moved < lo || moved > hi {
2283 continue;
2284 }
2285 for p in ring.iter_mut() {
2286 if axis == 0 {
2287 p.x -= turns * period;
2288 } else {
2289 p.y -= turns * period;
2290 }
2291 }
2292 }
2293 }
2294 }
2295 {
2304 use ogeom_geom::Surface as _;
2305 let ((ua, ub), (va, vb)) = surface.domain();
2306 let slides = [
2307 (!surface.is_periodic_u() && surface.is_closed_u(tol), ua, ub),
2308 (!surface.is_periodic_v() && surface.is_closed_v(tol), va, vb),
2309 ];
2310 for (across, (closed, lo, hi)) in [true, false].into_iter().zip(slides) {
2311 if !closed || hi <= lo {
2312 continue;
2313 }
2314 let span = hi - lo;
2315 for ring in &mut rings {
2316 let read = |p: &Point2| if across { p.x } else { p.y };
2317 let (least, most) = ring
2318 .iter()
2319 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), p| {
2320 (a.min(read(p)), b.max(read(p)))
2321 });
2322 if !(least.is_finite() && most.is_finite()) || most - least > span * (1.0 + 1e-9) {
2323 continue;
2324 }
2325 let turns = turns_into_domain((least, most), (lo, hi));
2326 if turns == 0.0 {
2327 continue;
2328 }
2329 for p in ring.iter_mut() {
2330 if across {
2331 p.x += turns * span;
2332 } else {
2333 p.y += turns * span;
2334 }
2335 }
2336 }
2337 }
2338 }
2339 if *MESH_DEBUG {
2340 for (i, (ring, anchors)) in rings.iter().zip(&ring_anchors).enumerate() {
2341 eprintln!("DBG ring {i}: {} points", ring.len());
2342 for (p, a) in ring.iter().zip(anchors) {
2343 eprintln!(
2344 "DBG uv({:.5},{:.5}) anchor {}",
2345 p.x,
2346 p.y,
2347 a.map_or("-".to_string(), |q| format!(
2348 "({:.4},{:.4},{:.4})",
2349 q.x, q.y, q.z
2350 ))
2351 );
2352 }
2353 }
2354 }
2355 for (ring, anchors) in rings.iter_mut().zip(ring_anchors.iter_mut()) {
2368 let reach = chart_reach(ring);
2369 merge_near_duplicates(ring, anchors, reach);
2370 remove_spikes(ring, anchors, reach);
2371 }
2372 let long_enough: Vec<bool> = rings.iter().map(|r| r.len() >= 3).collect();
2373 let mut it = long_enough.iter();
2374 ring_wires.retain(|_| *it.next().unwrap_or(&true));
2375 rings.retain(|r| r.len() >= 3);
2376 ring_anchors.retain(|a| a.len() >= 3);
2377
2378 if rings.len() > 1 {
2386 let chart_area = |ring: &[Point2]| -> f64 {
2389 let Some(&anchor) = ring.first() else {
2390 return 0.0;
2391 };
2392 let mut a = 0.0;
2393 for i in 0..ring.len() {
2394 a += (ring[i] - anchor).cross(ring[(i + 1) % ring.len()] - anchor);
2395 }
2396 a.abs()
2397 };
2398 let outer = (0..rings.len())
2399 .max_by(|&i, &j| chart_area(&rings[i]).total_cmp(&chart_area(&rings[j])))
2400 .unwrap_or(0);
2401 let mut keep = Vec::with_capacity(rings.len());
2402 for i in 0..rings.len() {
2403 keep.push(i == outer || !is_slit(model, &ring_wires[i], &ring_anchors[i], tol)?);
2404 }
2405 let mut it = keep.iter();
2406 rings.retain(|_| *it.next().unwrap_or(&true));
2407 let mut it = keep.iter();
2408 ring_anchors.retain(|_| *it.next().unwrap_or(&true));
2409 }
2410
2411 if rings.is_empty() && !bounded {
2417 let ring = domain_ring(surface, deflection, tol);
2418 ring_anchors.push(vec![None; ring.len()]);
2419 rings.push(ring);
2420 }
2421 Ok(Trimming {
2422 rings,
2423 anchors: ring_anchors,
2424 met,
2425 walked,
2426 })
2427}
2428
2429fn collapse_close_points(ring: &mut Vec<Point2>, anchors: &mut Vec<Option<Point>>) {
2433 if ring.len() < 3 {
2434 return;
2435 }
2436 let mut extent = 0.0_f64;
2437 for pair in ring.windows(2) {
2438 extent = extent.max(pair[0].distance(pair[1]));
2439 }
2440 let eps = extent.mul_add(1e-9, 1e-12);
2441 let mut kept_ring = Vec::with_capacity(ring.len());
2442 let mut kept_anchors = Vec::with_capacity(anchors.len());
2443 for (p, a) in ring.iter().zip(anchors.iter()) {
2444 if kept_ring
2445 .last()
2446 .is_some_and(|held: &Point2| held.distance(*p) <= eps)
2447 {
2448 continue;
2449 }
2450 kept_ring.push(*p);
2451 kept_anchors.push(*a);
2452 }
2453 if kept_ring.len() > 2
2454 && let (Some(first), Some(last)) = (kept_ring.first(), kept_ring.last())
2455 && first.distance(*last) <= eps
2456 {
2457 kept_ring.pop();
2458 kept_anchors.pop();
2459 }
2460 *ring = kept_ring;
2461 *anchors = kept_anchors;
2462}
2463
2464fn is_slit(
2487 model: &Model,
2488 wire: &Shape,
2489 anchors: &[Option<Point>],
2490 tol: Tolerances,
2491) -> OgeomResult<bool> {
2492 let measure = || -> Option<(f64, f64)> {
2493 let pts: Option<Vec<Point>> = anchors.iter().copied().collect();
2494 let pts = pts?;
2495 let anchor = *pts.first()?;
2498 let mut normal = Vector::ZERO;
2499 let mut perimeter = 0.0;
2500 for i in 0..pts.len() {
2501 let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
2502 normal += (a - anchor).cross(b - anchor);
2503 perimeter += a.distance(b);
2504 }
2505 (perimeter > 0.0).then(|| (normal.magnitude() / perimeter, perimeter))
2506 };
2507 let Some((width, perimeter)) = measure() else {
2508 return Ok(false);
2509 };
2510 if width >= tol.confusion() * 1e4 || !walks_back(model, wire)? {
2511 return Ok(false);
2512 }
2513 Ok(model.ordered_children_of(wire)?.len() > 2 || width < perimeter * 1e-3)
2514}
2515
2516fn walks_back(model: &Model, wire: &Shape) -> OgeomResult<bool> {
2521 let mut ends: Vec<(Shape, Shape)> = Vec::new();
2522 for edge in model.ordered_children_of(wire)? {
2523 let bounds = model.children_of(&edge)?;
2524 let (Some(first), Some(last)) = (bounds.first(), bounds.last()) else {
2525 return Ok(false);
2526 };
2527 if first.is_same(last) {
2528 return Ok(false);
2529 }
2530 ends.push(if edge.orientation() == Orientation::Reversed {
2531 (last.clone(), first.clone())
2532 } else {
2533 (first.clone(), last.clone())
2534 });
2535 }
2536 if ends.len() < 2 || !ends.len().is_multiple_of(2) {
2537 return Ok(false);
2538 }
2539 let mut answered = vec![false; ends.len()];
2540 for i in 0..ends.len() {
2541 if answered[i] {
2542 continue;
2543 }
2544 let (from, to) = &ends[i];
2545 let Some(j) = (i + 1..ends.len())
2546 .find(|&j| !answered[j] && ends[j].0.is_same(to) && ends[j].1.is_same(from))
2547 else {
2548 return Ok(false);
2549 };
2550 answered[i] = true;
2551 answered[j] = true;
2552 }
2553 Ok(true)
2554}
2555
2556pub fn open_chart_ring(
2573 model: &Model,
2574 face: &Shape,
2575 wire: &Shape,
2576 largest: bool,
2577 deflection: Deflection,
2578 tol: Tolerances,
2579) -> OgeomResult<Option<Vec<Point2>>> {
2580 use ogeom_geom::Surface as _;
2581 deflection.validate()?;
2582 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
2583 ogeom_bail!(Construction, "expected a face");
2584 };
2585 let Some(surface) = model.geometry().surface(data.surface) else {
2586 ogeom_bail!(Dangling, "face refers to a surface not in this model");
2587 };
2588 if surface.is_periodic_u()
2589 || surface.is_periodic_v()
2590 || surface.is_closed_u(tol)
2591 || surface.is_closed_v(tol)
2592 {
2593 ogeom_bail!(
2594 Construction,
2595 "a chart that repeats or closes draws its rings together"
2596 );
2597 }
2598 let (mut ring, mut anchors, ..) = boundary_ring(
2599 model,
2600 wire,
2601 data.surface,
2602 deflection,
2603 &EdgeChords::new(),
2604 true,
2605 tol,
2606 )?;
2607 if ring.len() < 3 {
2608 return Ok(None);
2609 }
2610 close_wound_ring(&mut ring, &mut anchors, surface, tol);
2611 collapse_close_points(&mut ring, &mut anchors);
2612 let reach = chart_reach(&ring);
2613 merge_near_duplicates(&mut ring, &mut anchors, reach);
2614 remove_spikes(&mut ring, &mut anchors, reach);
2615 if ring.len() < 3 || anchors.len() < 3 || (!largest && is_slit(model, wire, &anchors, tol)?) {
2616 return Ok(None);
2617 }
2618 Ok(Some(ring))
2619}
2620
2621fn sliver_fan(
2626 walked: &[Vec<(Point2, Point)>],
2627 surface: &SurfaceGeometry,
2628 placement: &ogeom_math::Transform,
2629 reversed: bool,
2630 tol: Tolerances,
2631) -> Triangulation {
2632 let mut mesh = Triangulation::new();
2633 for ring in walked {
2634 let base = u32::try_from(mesh.positions.len()).unwrap_or(u32::MAX);
2635 for (uv, at) in ring {
2636 mesh.positions.push(*at);
2637 mesh.parameters.push((uv.x, uv.y));
2638 let normal = surface
2639 .normal_at(uv.x, uv.y, tol)
2640 .map_or(ogeom_math::Vector::Z, |n| {
2641 placement.apply_vector(n.vector())
2642 });
2643 mesh.normals.push(if reversed { -normal } else { normal });
2644 }
2645 let count = u32::try_from(ring.len()).unwrap_or(0);
2646 for k in 1..count.saturating_sub(1) {
2647 mesh.triangles.push([base, base + k, base + k + 1]);
2648 }
2649 }
2650 mesh
2651}
2652
2653#[derive(Debug, Clone, Copy)]
2662struct ChartReach {
2663 u: f64,
2664 v: f64,
2665}
2666
2667impl ChartReach {
2668 fn joins(self, p: Point2, q: Point2) -> bool {
2670 (p.x - q.x).abs() <= self.u && (p.y - q.y).abs() <= self.v
2671 }
2672}
2673
2674fn chart_reach(ring: &[Point2]) -> Option<ChartReach> {
2676 let (mut lo, mut hi) = (
2677 Point2::new(f64::INFINITY, f64::INFINITY),
2678 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
2679 );
2680 for p in ring {
2681 lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
2682 hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
2683 }
2684 let (du, dv) = (hi.x - lo.x, hi.y - lo.y);
2685 (du.is_finite() && dv.is_finite() && du.max(dv) > 0.0).then_some(ChartReach {
2686 u: du * 1e-6,
2687 v: dv * 1e-6,
2688 })
2689}
2690
2691fn merge_near_duplicates(
2694 ring: &mut Vec<Point2>,
2695 anchors: &mut Vec<Option<Point>>,
2696 reach: Option<ChartReach>,
2697) {
2698 let Some(reach) = reach else {
2699 return;
2700 };
2701 if ring.len() < 2 || anchors.len() != ring.len() {
2702 return;
2703 }
2704 let mut i = 0;
2705 while i < ring.len() && ring.len() >= 2 {
2706 let next = (i + 1) % ring.len();
2707 if reach.joins(ring[i], ring[next]) {
2708 if next == 0 {
2712 ring.remove(i);
2713 anchors.remove(i);
2714 break;
2715 }
2716 ring.remove(next);
2717 anchors.remove(next);
2718 } else {
2719 i += 1;
2720 }
2721 }
2722}
2723
2724fn remove_spikes(
2729 ring: &mut Vec<Point2>,
2730 anchors: &mut Vec<Option<Point>>,
2731 reach: Option<ChartReach>,
2732) {
2733 let Some(reach) = reach else {
2734 return;
2735 };
2736 loop {
2737 let n = ring.len();
2738 if n < 3 || anchors.len() != n {
2739 return;
2740 }
2741 let Some(tip) = (0..n).find(|&i| reach.joins(ring[(i + n - 1) % n], ring[(i + 1) % n]))
2742 else {
2743 return;
2744 };
2745 let neighbour = (tip + 1) % n;
2747 let (first, second) = if tip < neighbour {
2748 (neighbour, tip)
2749 } else {
2750 (tip, neighbour)
2751 };
2752 ring.remove(first);
2753 anchors.remove(first);
2754 ring.remove(second);
2755 anchors.remove(second);
2756 }
2757}
2758
2759fn snap_chart_junctions(
2763 ring: &mut Vec<Point2>,
2764 anchors: &mut Vec<Option<Point>>,
2765 surface: &SurfaceGeometry,
2766 tol: Tolerances,
2767) {
2768 let original = ring.to_vec();
2769 if original.len() < 3 {
2770 return;
2771 }
2772 let indexed: Vec<_> = original.iter().map(|p| Point2::new(p.y, p.x)).collect();
2775 let x_bands = RingBands::over(std::slice::from_ref(&indexed));
2776 let y_bands = RingBands::over(std::slice::from_ref(&original));
2777 if x_bands.bands.is_empty() || y_bands.bands.is_empty() {
2778 return;
2779 }
2780 let mut junctions = Vec::new();
2781 for (i, p) in original.iter().copied().enumerate() {
2782 let nearest = junction_candidates(&x_bands, &y_bands, p, tol.parametric())
2783 .iter()
2784 .flatten()
2785 .map(|&k| k as usize)
2786 .filter(|&k| k != i && (k + 1) % original.len() != i)
2787 .filter_map(|k| {
2788 let (a, b) = (original[k], original[(k + 1) % original.len()]);
2789 let (dx, dy) = (b.x - a.x, b.y - a.y);
2790 let l2 = dx * dx + dy * dy;
2791 if l2 == 0.0 {
2792 return None;
2793 }
2794 let along = ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2;
2795 if !(0.0 < along && along < 1.0) {
2796 return None;
2797 }
2798 let q = Point2::new(a.x + dx * along, a.y + dy * along);
2799 let distance = p.distance(q);
2800 (distance <= tol.parametric()).then_some((k, along, q, distance))
2801 })
2802 .min_by(|a, b| a.3.total_cmp(&b.3));
2803 if let Some((side, along, q, _)) = nearest
2804 && let (Ok(at), Ok(on)) = (
2805 surface.point_at(p.x, p.y, tol),
2806 surface.point_at(q.x, q.y, tol),
2807 )
2808 && at.distance(on) <= tol.confusion()
2809 {
2810 junctions.push((side, along, i));
2811 }
2812 }
2813 if junctions.is_empty() {
2814 return;
2815 }
2816 junctions.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1)));
2819 let held = std::mem::take(anchors);
2820 ring.clear();
2821 let mut junctions = junctions.into_iter().peekable();
2822 for (side, (&p, &anchor)) in original.iter().zip(&held).enumerate() {
2823 ring.push(p);
2824 anchors.push(anchor);
2825 while let Some(&(at, _, vertex)) = junctions.peek() {
2826 if at != side {
2827 break;
2828 }
2829 ring.push(original[vertex]);
2830 anchors.push(held[vertex]);
2831 junctions.next();
2832 }
2833 }
2834}
2835
2836fn junction_candidates<'a>(
2840 x: &'a RingBands,
2841 y: &'a RingBands,
2842 p: Point2,
2843 reach: f64,
2844) -> &'a [Vec<u32>] {
2845 let range = |bands: &RingBands, height: f64| {
2846 let first = RingBands::band_of(height - reach, bands.low, bands.step, bands.bands.len());
2847 let last = RingBands::band_of(height + reach, bands.low, bands.step, bands.bands.len());
2848 first..=last
2849 };
2850 let (xs, ys) = (&x.bands[range(x, p.x)], &y.bands[range(y, p.y)]);
2851 if xs.iter().map(Vec::len).sum::<usize>() <= ys.iter().map(Vec::len).sum::<usize>() {
2852 xs
2853 } else {
2854 ys
2855 }
2856}
2857
2858fn geometry_winds(surface: &SurfaceGeometry) -> bool {
2861 use ogeom_geom::Surface as _;
2862 surface.is_periodic_u() && !surface.is_periodic_v()
2863}
2864
2865#[must_use]
2878pub fn inside_boundary(rings: &[Vec<Point2>], p: Point2) -> bool {
2879 inside_boundary_with::<Exact>(rings, p)
2880}
2881
2882#[must_use]
2895pub fn inside_boundary_with<P: Predicates>(rings: &[Vec<Point2>], p: Point2) -> bool {
2896 let mut inside = false;
2897 for ring in rings {
2898 if crosses_odd_times::<P>(ring, p) {
2899 inside = !inside;
2900 }
2901 }
2902 inside
2903}
2904
2905fn boundary_ring(
2914 model: &Model,
2915 wire: &Shape,
2916 surface: ogeom_topo::SurfaceId,
2917 deflection: Deflection,
2918 finer: &EdgeChords,
2919 in_chart: bool,
2920 tol: Tolerances,
2921) -> OgeomResult<WalkedRing> {
2922 let mut ring: Vec<Point2> = Vec::new();
2923 let mut anchors: Vec<Option<Point>> = Vec::new();
2924 let mut folds: Vec<(usize, f64)> = Vec::new();
2929 let mut ties: Vec<usize> = Vec::new();
2931 let mut met = true;
2932 let mut stepped_ring = [false; 2];
2935 let (wraps_u, wraps_v) = model
2940 .geometry()
2941 .surface(surface)
2942 .map_or((false, false), |g| {
2943 use ogeom_geom::Surface as _;
2944 (
2945 g.is_periodic_u() || g.is_closed_u(tol),
2946 g.is_periodic_v() || g.is_closed_v(tol),
2947 )
2948 });
2949
2950 let mut children = model.ordered_children_of(wire)?;
2955 let is_seam = |model: &Model, e: &Shape| -> bool {
2956 model
2957 .node(e)
2958 .and_then(|n| n.data().as_edge())
2959 .and_then(|d| d.pcurve_for(surface, e.location()))
2960 .is_some_and(|r| matches!(r, EdgeRepr::Seam { .. }))
2961 };
2962 if let Some(start) = children.iter().position(|e| !is_seam(model, e)) {
2963 children.rotate_left(start);
2964 }
2965
2966 let mut seam_walked: ogeom_core::FastMap<ogeom_topo::TShapeId, Point2> =
2971 ogeom_core::FastMap::default();
2972
2973 for edge in children {
2974 let Some(node) = model.node(&edge) else {
2975 ogeom_bail!(Dangling, "edge is not in this model");
2976 };
2977 let NodeData::Edge(data) = node.data() else {
2978 ogeom_bail!(Construction, "edge node holds no edge data");
2979 };
2980 let mut seam: Option<bool> = None;
2983 let (pcurve_id, pcurve_range) = match data.pcurve_for(surface, edge.location()) {
2984 Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
2985 Some(EdgeRepr::Seam {
2993 forward,
2994 reversed,
2995 range,
2996 ..
2997 }) => {
2998 let (f, r) = (*forward, *reversed);
2999 let side_start = |id: ogeom_topo::PCurveId| -> Option<Point2> {
3000 model.geometry().pcurve(id)?.point_at(range.0, tol).ok()
3001 };
3002 seam = Some(match (side_start(f), side_start(r)) {
3003 (Some(a), Some(b)) => (a.x - b.x).abs() >= (a.y - b.y).abs(),
3004 _ => true,
3005 });
3006 let picked = if let Some(last) = ring.last().copied() {
3007 let start_of = |id: ogeom_topo::PCurveId| -> Option<Point2> {
3008 let pc = model.geometry().pcurve(id)?;
3009 let t = if edge.orientation() == Orientation::Reversed {
3010 range.1
3011 } else {
3012 range.0
3013 };
3014 pc.point_at(t, tol).ok()
3015 };
3016 match (start_of(f), start_of(r)) {
3017 (Some(a), Some(b)) => {
3018 if last.distance(a) <= last.distance(b) {
3019 f
3020 } else {
3021 r
3022 }
3023 }
3024 _ => f,
3025 }
3026 } else if edge.orientation() == Orientation::Reversed {
3027 r
3028 } else {
3029 f
3030 };
3031 (picked, *range)
3032 }
3033 _ => ogeom_bail!(
3034 Construction,
3035 "edge has no pcurve on this face, so the face cannot be \
3036 triangulated in its own parameter space"
3037 ),
3038 };
3039 let Some(pcurve) = model.geometry().pcurve(pcurve_id) else {
3040 ogeom_bail!(Dangling, "pcurve is not in this model");
3041 };
3042
3043 let mut edge_anchors: Vec<Option<Point>> = Vec::new();
3051 let along = match finer.get(&edge.node().index()) {
3055 Some(chord) => Deflection {
3056 chord: *chord,
3057 ..deflection
3058 },
3059 None => deflection,
3060 };
3061 let mut snapped_ends: Vec<usize> = Vec::new();
3063 let mut stepped_ends = [false; 2];
3066 let samples = match sample_parameters(model, data, along, tol)? {
3067 Some((parameters, edge_met)) => {
3068 met &= edge_met;
3069 let mut parameters = match (in_chart, model.geometry().surface(surface)) {
3070 (true, Some(geometry)) => {
3071 let (refined, chart_met) = refine_in_chart(
3072 parameters,
3073 |t| to_pcurve_parameter(t, data, pcurve_range),
3074 pcurve,
3075 geometry,
3076 along,
3077 tol,
3078 );
3079 met &= chart_met;
3080 refined
3081 }
3082 _ => parameters,
3083 };
3084 if let Some(EdgeRepr::Curve3d { curve, .. }) = data.curve3d()
3085 && let Some(geometry) = model.geometry().curve(*curve)
3086 && let Ok(edge_placement) = edge.transform(model.datums())
3087 {
3088 for t in ¶meters {
3089 edge_anchors.push(
3090 geometry
3091 .point_at(*t, tol)
3092 .ok()
3093 .map(|p| edge_placement.apply(p)),
3094 );
3095 }
3096 let chord = along.chord * edge_placement.scale_factor().abs();
3120 let corners: Vec<(Point, f64)> = model
3121 .ordered_children_of(&edge)?
3122 .iter()
3123 .filter_map(|v| {
3124 let data = model.node(v)?.data().as_vertex()?;
3125 let at = v.transform(model.datums()).ok()?.apply(data.point);
3126 Some((at, data.tolerance.get()))
3127 })
3128 .collect();
3129 let reach = data.tolerance.get();
3130 let mut snapped = [false; 2];
3132 for i in [edge_anchors.len().saturating_sub(1), 0] {
3133 let Some(Some(end)) = edge_anchors.get(i).copied() else {
3134 continue;
3135 };
3136 let Some(&(at, within)) = corners
3137 .iter()
3138 .min_by(|a, b| a.0.distance(end).total_cmp(&b.0.distance(end)))
3139 else {
3140 continue;
3141 };
3142 let off = at.distance(end);
3143 if off > within + reach {
3144 continue;
3145 }
3146 let next = if i == 0 { 1 } else { i - 1 };
3147 let mut stepped = false;
3148 if off > chord
3149 && let (Some(&from), Some(&to)) =
3150 (parameters.get(i), parameters.get(next))
3151 && let Some(Some(inner)) = edge_anchors.get(next).copied()
3152 && inner.distance(end) > 2.0 * off
3153 {
3154 let t = from + (to - from) * off / inner.distance(end);
3155 let step = if i == 0 { 1 } else { i };
3156 parameters.insert(step, t);
3157 edge_anchors.insert(
3158 step,
3159 geometry
3160 .point_at(t, tol)
3161 .ok()
3162 .map(|p| edge_placement.apply(p)),
3163 );
3164 stepped = true;
3165 }
3166 let i = if i == 0 { 0 } else { edge_anchors.len() - 1 };
3167 edge_anchors[i] = Some(at);
3168 snapped[usize::from(i != 0)] = !stepped;
3169 stepped_ends[usize::from(i != 0)] = stepped;
3170 }
3171 if snapped[0] {
3172 snapped_ends.push(0);
3173 }
3174 if snapped[1] {
3175 snapped_ends.push(edge_anchors.len() - 1);
3176 }
3177 }
3178 map_to_pcurve(¶meters, data, pcurve_range)
3179 }
3180 None => {
3184 let Some(geometry) = model.geometry().surface(surface) else {
3185 ogeom_bail!(Dangling, "face refers to a surface not in this model");
3186 };
3187 let (_, parameters) = crate::discretize::discretize_on_surface(
3191 pcurve,
3192 pcurve_range,
3193 geometry,
3194 along,
3195 tol,
3196 )?;
3197 if in_chart {
3198 let (refined, chart_met) =
3199 refine_in_chart(parameters, |t| t, pcurve, geometry, along, tol);
3200 met &= chart_met;
3201 refined
3202 } else {
3203 parameters
3204 }
3205 }
3206 };
3207
3208 let mut points: Vec<Point2> = samples
3209 .iter()
3210 .map(|u| pcurve.point_at(*u, tol))
3211 .collect::<OgeomResult<_>>()?;
3212 if !snapped_ends.is_empty()
3217 && let Some(geometry) = model.geometry().surface(surface)
3218 && let Ok(placement) = wire.transform(model.datums())
3219 && let Ok(local) = placement.inverse()
3220 {
3221 use ogeom_geom::Surface as _;
3222 for &i in &snapped_ends {
3223 let (Some(Some(target)), Some(start)) = (edge_anchors.get(i), points.get(i)) else {
3224 continue;
3225 };
3226 let target = local.apply(*target);
3227 let mut uv = *start;
3228 let mut solved = false;
3229 for _ in 0..8 {
3230 let Ok(jet) = geometry.jet_at(uv.x, uv.y, tol) else {
3231 break;
3232 };
3233 let r = jet.point - target;
3234 if r.magnitude() <= tol.confusion() {
3235 solved = true;
3236 break;
3237 }
3238 let (a, b, c) = (jet.du.dot(jet.du), jet.du.dot(jet.dv), jet.dv.dot(jet.dv));
3239 let det = a.mul_add(c, -(b * b));
3240 if det.abs() <= f64::MIN_POSITIVE {
3241 break;
3242 }
3243 let (gu, gv) = (jet.du.dot(r), jet.dv.dot(r));
3244 uv = Point2::new(
3245 uv.x - (c * gu - b * gv) / det,
3246 uv.y - (a * gv - b * gu) / det,
3247 );
3248 }
3249 if !solved
3251 && let Ok(p) = geometry.point_at(uv.x, uv.y, tol)
3252 && let Ok(q) = geometry.point_at(start.x, start.y, tol)
3253 {
3254 solved = p.distance(target) < q.distance(target);
3255 }
3256 if solved && let Some(slot) = points.get_mut(i) {
3257 *slot = uv;
3258 }
3259 }
3260 }
3261 if edge.orientation() == Orientation::Reversed {
3262 points.reverse();
3263 edge_anchors.reverse();
3264 stepped_ends.reverse();
3265 }
3266 if edge_anchors.len() != points.len() {
3267 edge_anchors = vec![None; points.len()];
3268 }
3269 if !points.is_empty()
3278 && let Ok(vs) = model.children_of(&edge)
3279 && vs.len() >= 2
3280 && let Ok(edge_placement) = edge.transform(model.datums())
3281 {
3282 let point_of = |v: &Shape| -> Option<(Point, f64)> {
3283 let data = model.node(v)?.data().as_vertex()?;
3284 Some((edge_placement.apply(data.point), data.tolerance.get()))
3285 };
3286 let (from, to) = if edge.orientation() == Orientation::Reversed {
3287 (&vs[vs.len() - 1], &vs[0])
3288 } else {
3289 (&vs[0], &vs[vs.len() - 1])
3290 };
3291 if let Some((p, within)) = point_of(from)
3292 && let Some(a) = edge_anchors.first_mut()
3293 && a.is_none_or(|end| end.distance(p) <= within + tol.confusion())
3294 {
3295 *a = Some(p);
3296 }
3297 if let Some((p, within)) = point_of(to)
3298 && let Some(a) = edge_anchors.last_mut()
3299 && a.is_none_or(|end| end.distance(p) <= within + tol.confusion())
3300 {
3301 *a = Some(p);
3302 }
3303 }
3304 if let Some(last) = ring.last().copied()
3311 && let Some(first) = points.first().copied()
3312 && let Some(geometry) = model.geometry().surface(surface)
3313 {
3314 use ogeom_geom::Surface as _;
3315 let ((ua, ub), (va, vb)) = geometry.domain();
3316 let whole_periods = |gap: f64, span: f64| -> f64 {
3323 let r = gap / span;
3324 if (r.fract().abs() - 0.5).abs() <= 1e-9 {
3325 r.trunc() * span
3326 } else {
3327 r.round() * span
3328 }
3329 };
3330 let mut shift = Point2::new(0.0, 0.0);
3331 let prior = seam.and_then(|_| seam_walked.get(&edge.node())).copied();
3340 if wraps_u && (ub - ua) > 0.0 {
3341 let span = ub - ua;
3342 let gap = last.x - first.x;
3343 shift.x = whole_periods(gap, span);
3344 let mut bracketed = false;
3345 if seam == Some(true)
3346 && let Some(prior) = prior
3347 && (first.x + shift.x - prior.x).abs() < span * 0.5
3348 {
3349 let (lo, hi) = ring
3350 .iter()
3351 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
3352 (lo.min(p.x), hi.max(p.x))
3353 });
3354 if hi - lo >= span * 0.5 {
3364 let side = if f64::midpoint(lo, hi) >= prior.x {
3365 1.0
3366 } else {
3367 -1.0
3368 };
3369 shift.x = prior.x + side * span - first.x;
3370 bracketed = true;
3371 }
3372 }
3373 if !bracketed {
3374 if shift.x != 0.0 && (gap - shift.x).abs() <= span * 1e-6 {
3375 folds.push((ring.len(), shift.x));
3379 } else if ((gap / span).fract().abs() - 0.5).abs() <= 1e-9 {
3380 ties.push(ring.len());
3384 }
3385 }
3386 }
3387 if wraps_v && (vb - va) > 0.0 {
3388 let span = vb - va;
3389 shift.y = whole_periods(last.y - first.y, span);
3390 if seam == Some(false)
3391 && let Some(prior) = prior
3392 && (first.y + shift.y - prior.y).abs() < span * 0.5
3393 {
3394 let (lo, hi) = ring
3395 .iter()
3396 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
3397 (lo.min(p.y), hi.max(p.y))
3398 });
3399 let side = if f64::midpoint(lo, hi) >= prior.y {
3400 1.0
3401 } else {
3402 -1.0
3403 };
3404 shift.y = prior.y + side * span - first.y;
3405 }
3406 }
3407 if shift.x != 0.0 || shift.y != 0.0 {
3408 for p in &mut points {
3409 p.x += shift.x;
3410 p.y += shift.y;
3411 }
3412 }
3413 }
3414 if seam.is_some()
3415 && let Some(first) = points.first().copied()
3416 {
3417 seam_walked.entry(edge.node()).or_insert(first);
3418 }
3419 let keep_gap = if let (Some(last), Some(first)) = (ring.last(), points.first()) {
3440 model.geometry().surface(surface).is_some_and(|geometry| {
3441 use ogeom_geom::Surface as _;
3442 let ((ua, ub), (va, vb)) = geometry.domain();
3443 let wide = (wraps_u && (last.x - first.x).abs() > (ub - ua) * 1e-3)
3444 || (wraps_v && (last.y - first.y).abs() > (vb - va) * 1e-3);
3445 if !wide {
3446 return false;
3447 }
3448 let mid = Point2::new(
3449 f64::midpoint(last.x, first.x),
3450 f64::midpoint(last.y, first.y),
3451 );
3452 let reach = tol.confusion() * 1e4;
3453 match (
3454 geometry.point_at(last.x, last.y, tol),
3455 geometry.point_at(mid.x, mid.y, tol),
3456 geometry.point_at(first.x, first.y, tol),
3457 ) {
3458 (Ok(a), Ok(m), Ok(b)) => a.distance(m) <= reach && m.distance(b) <= reach,
3459 _ => false,
3460 }
3461 })
3462 } else {
3463 false
3464 };
3465 if !ring.is_empty() && !points.is_empty() && !keep_gap {
3466 points.remove(0);
3467 edge_anchors.remove(0);
3468 }
3469 if ring.is_empty() {
3470 stepped_ring[0] = stepped_ends[0];
3471 }
3472 stepped_ring[1] = stepped_ends[1];
3473 ring.extend(points);
3474 anchors.extend(edge_anchors);
3475 }
3476
3477 if ring.len() > 2
3480 && let (Some(first), Some(last)) = (ring.first().copied(), ring.last().copied())
3481 {
3482 let extent = ring.iter().fold(
3495 (
3496 Point2::new(f64::INFINITY, f64::INFINITY),
3497 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
3498 ),
3499 |(lo, hi), p| {
3500 (
3501 Point2::new(lo.x.min(p.x), lo.y.min(p.y)),
3502 Point2::new(hi.x.max(p.x), hi.y.max(p.y)),
3503 )
3504 },
3505 );
3506 let extent = (extent.1.x - extent.0.x).max(extent.1.y - extent.0.y);
3507 let near_in_chart = (first.x - last.x).hypot(first.y - last.y) <= extent * 1e-3;
3508 let same_vertex = near_in_chart
3509 && match (anchors.first(), anchors.last()) {
3510 (Some(Some(a)), Some(Some(b))) => a.distance(*b) <= tol.confusion() * 1e5,
3511 _ => false,
3512 };
3513 let stepped = stepped_ring == [true, true]
3518 && match (anchors.first(), anchors.last()) {
3519 (Some(Some(a)), Some(Some(b))) => a.distance(*b) <= tol.confusion(),
3520 _ => false,
3521 }
3522 && model.geometry().surface(surface).is_some_and(|geometry| {
3523 use ogeom_geom::Surface as _;
3524 match (
3525 geometry.point_at(first.x, first.y, tol),
3526 geometry.point_at(last.x, last.y, tol),
3527 ) {
3528 (Ok(a), Ok(b)) => a.distance(b) > tol.confusion(),
3529 _ => false,
3530 }
3531 });
3532 if first.is_equal(last, tol) || same_vertex || stepped {
3533 ring.pop();
3534 anchors.pop();
3535 }
3536 }
3537 Ok((ring, anchors, met, folds, ties))
3538}
3539
3540fn close_wound_ring(
3558 ring: &mut Vec<Point2>,
3559 anchors: &mut Vec<Option<Point>>,
3560 geometry: &SurfaceGeometry,
3561 tol: Tolerances,
3562) {
3563 let (Some(first), Some(last)) = (ring.first().copied(), ring.last().copied()) else {
3564 return;
3565 };
3566 use ogeom_geom::Surface as _;
3567 let ((ua, ub), (va, vb)) = geometry.domain();
3568 let du = last.x - first.x;
3569 let dv = last.y - first.y;
3570 let winds_u = geometry.is_periodic_u()
3571 && (du.abs() - (ub - ua)).abs() <= (ub - ua) * 1e-3
3572 && dv.abs() <= (vb - va).max(1.0) * 1e-3;
3573 let winds_v = geometry.is_periodic_u()
3574 && geometry.is_periodic_v()
3575 && (dv.abs() - (vb - va)).abs() <= (vb - va) * 1e-3
3576 && du.abs() <= (ub - ua).max(1.0) * 1e-3;
3577 let degenerate_row = |v: f64| -> bool {
3583 let (Ok(p), Ok(q), Ok(r)) = (
3584 geometry.point_at(ua, v, tol),
3585 geometry.point_at(f64::midpoint(ua, ub), v, tol),
3586 geometry.point_at(ub, v, tol),
3587 ) else {
3588 return false;
3589 };
3590 p.distance(q) <= tol.confusion() * 10.0 && p.distance(r) <= tol.confusion() * 10.0
3591 };
3592 let target_v = if winds_u && !geometry.is_periodic_v() {
3593 let mid_v = f64::midpoint(first.y, last.y);
3595 if degenerate_row(va) && (mid_v - va).abs() <= (mid_v - vb).abs() {
3596 Some(va)
3597 } else if degenerate_row(vb) {
3598 Some(vb)
3599 } else if degenerate_row(va) {
3600 Some(va)
3601 } else {
3602 None
3603 }
3604 } else {
3605 None
3606 };
3607 let column_steps = 8;
3608 if let Some(v_apex) = target_v {
3609 let row_steps = ring.len().max(8);
3613 for k in 1..=column_steps {
3614 let f = f64::from(k) / f64::from(column_steps);
3615 ring.push(Point2::new(last.x, last.y + (v_apex - last.y) * f));
3616 anchors.push(None);
3617 }
3618 for k in 1..row_steps {
3619 #[allow(clippy::cast_precision_loss)]
3620 let f = k as f64 / row_steps as f64;
3621 ring.push(Point2::new(last.x + (first.x - last.x) * f, v_apex));
3622 anchors.push(None);
3623 }
3624 for k in 0..column_steps {
3625 let f = f64::from(column_steps - k) / f64::from(column_steps);
3626 ring.push(Point2::new(first.x, first.y + (v_apex - first.y) * f));
3627 anchors.push(None);
3628 }
3629 } else if (winds_u && geometry.is_periodic_v()) || winds_v {
3630 let shift = if winds_u {
3631 Point2::new(0.0, -(vb - va))
3632 } else {
3633 Point2::new(-(ub - ua), 0.0)
3634 };
3635 let chain: Vec<Point2> = ring.clone();
3636 let chain_anchors = anchors.clone();
3637 for k in 1..=column_steps {
3639 let f = f64::from(k) / f64::from(column_steps);
3640 ring.push(Point2::new(last.x + shift.x * f, last.y + shift.y * f));
3641 anchors.push(None);
3642 }
3643 for (p, a) in chain.iter().rev().zip(chain_anchors.iter().rev()).skip(1) {
3645 ring.push(Point2::new(p.x + shift.x, p.y + shift.y));
3646 anchors.push(*a);
3647 }
3648 for k in 1..column_steps {
3651 let f = f64::from(column_steps - k) / f64::from(column_steps);
3652 ring.push(Point2::new(first.x + shift.x * f, first.y + shift.y * f));
3653 anchors.push(None);
3654 }
3655 }
3656}
3657
3658fn sample_parameters(
3660 model: &Model,
3661 data: &ogeom_topo::EdgeData,
3662 deflection: Deflection,
3663 tol: Tolerances,
3664) -> OgeomResult<Option<(Vec<f64>, bool)>> {
3665 Ok(edge_polyline(model, data, deflection, tol)?
3666 .map(|line| (line.parameters, line.deflection_met)))
3667}
3668
3669pub(crate) fn edge_polyline(
3676 model: &Model,
3677 data: &ogeom_topo::EdgeData,
3678 deflection: Deflection,
3679 tol: Tolerances,
3680) -> OgeomResult<Option<crate::discretize::Polyline>> {
3681 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
3682 return Ok(None);
3683 };
3684 let Some(geometry) = model.geometry().curve(*curve) else {
3685 ogeom_bail!(Dangling, "curve is not in this model");
3686 };
3687 let line = discretize(geometry, *range, deflection, tol)?;
3688 let drawn = line.parameters.len();
3689 let (parameters, met) =
3690 refine_in_every_chart(model, data, line.parameters.clone(), deflection, tol);
3691 if parameters.len() == drawn {
3692 return Ok(Some(crate::discretize::Polyline {
3693 deflection_met: line.deflection_met && met,
3694 ..line
3695 }));
3696 }
3697 let points = parameters
3698 .iter()
3699 .map(|&t| geometry.point_at(t, tol))
3700 .collect::<OgeomResult<Vec<Point>>>()?;
3701 Ok(Some(crate::discretize::Polyline {
3702 points,
3703 parameters,
3704 deflection_met: line.deflection_met && met,
3705 }))
3706}
3707
3708fn map_to_pcurve(
3713 parameters: &[f64],
3714 data: &ogeom_topo::EdgeData,
3715 pcurve_range: (f64, f64),
3716) -> Vec<f64> {
3717 let Some(EdgeRepr::Curve3d { range, .. }) = data.curve3d() else {
3718 return parameters.to_vec();
3719 };
3720 let (ca, cb) = *range;
3721 let (pa, pb) = pcurve_range;
3722 if (cb - ca).abs() <= f64::MIN_POSITIVE {
3723 return parameters.to_vec();
3724 }
3725 parameters
3726 .iter()
3727 .map(|u| pa + (pb - pa) * (u - ca) / (cb - ca))
3728 .collect()
3729}
3730
3731fn to_pcurve_parameter(u: f64, data: &ogeom_topo::EdgeData, pcurve_range: (f64, f64)) -> f64 {
3733 map_to_pcurve(&[u], data, pcurve_range)[0]
3734}
3735
3736fn refine_in_chart(
3750 parameters: Vec<f64>,
3751 to_pcurve: impl Fn(f64) -> f64,
3752 pcurve: &ogeom_geom::PlanarCurve,
3753 surface: &SurfaceGeometry,
3754 deflection: Deflection,
3755 tol: Tolerances,
3756) -> (Vec<f64>, bool) {
3757 let at = |t: f64| pcurve.point_at(to_pcurve(t), tol).ok();
3758 let stands_off = |a: f64, b: f64| -> bool {
3759 let (Some(pa), Some(pb)) = (at(a), at(b)) else {
3760 return false;
3761 };
3762 [0.25, 0.5, 0.75].into_iter().any(|share| {
3763 let t = (b - a).mul_add(share, a);
3764 let Some(q) = at(t) else {
3765 return false;
3766 };
3767 let Ok((du, dv)) = surface.d1_at(q.x, q.y, tol) else {
3768 return false;
3769 };
3770 let (su, sv) = (du.magnitude(), dv.magnitude());
3771 let scaled = |p: Point2| Point2::new((p.x - q.x) * su, (p.y - q.y) * sv);
3772 let (a, b) = (scaled(pa), scaled(pb));
3773 let d = b - a;
3774 let squared = d.dot(d);
3775 let w = if squared <= f64::MIN_POSITIVE {
3776 0.0
3777 } else {
3778 (-(a.x * d.x + a.y * d.y) / squared).clamp(0.0, 1.0)
3779 };
3780 let foot = a + d * w;
3781 foot.x.hypot(foot.y) > deflection.chord
3782 })
3783 };
3784 let mut parameters = parameters;
3785 let ceiling = deflection.max_segments.max(parameters.len());
3786 let mut i = 0;
3787 while i + 1 < parameters.len() {
3788 let (a, b) = (parameters[i], parameters[i + 1]);
3789 let mid = f64::midpoint(a, b);
3790 if mid > a && mid < b && stands_off(a, b) {
3791 if parameters.len() > ceiling {
3792 return (parameters, false);
3793 }
3794 parameters.insert(i + 1, mid);
3795 } else {
3796 i += 1;
3797 }
3798 }
3799 (parameters, true)
3800}
3801
3802fn refine_in_every_chart(
3817 model: &Model,
3818 data: &ogeom_topo::EdgeData,
3819 parameters: Vec<f64>,
3820 deflection: Deflection,
3821 tol: Tolerances,
3822) -> (Vec<f64>, bool) {
3823 use ogeom_geom::Surface as _;
3824 let charts: Vec<(&ogeom_geom::PlanarCurve, &SurfaceGeometry, (f64, f64))> = data
3825 .representations
3826 .iter()
3827 .filter_map(|repr| {
3828 let (curve, surface, range) = match repr {
3829 EdgeRepr::PCurve {
3830 curve,
3831 surface,
3832 range,
3833 ..
3834 } => (*curve, *surface, *range),
3835 EdgeRepr::Seam {
3836 forward,
3837 surface,
3838 range,
3839 ..
3840 } => (*forward, *surface, *range),
3841 _ => return None,
3842 };
3843 let surface = model.geometry().surface(surface)?;
3844 if matches!(surface, SurfaceGeometry::Plane(_)) {
3845 return None;
3846 }
3847 Some((model.geometry().pcurve(curve)?, surface, range))
3848 })
3849 .collect();
3850 let mut parameters = parameters;
3851 let ceiling = deflection.max_segments.max(parameters.len());
3852 loop {
3853 let before = parameters.len();
3854 for &(pcurve, surface, range) in &charts {
3855 let at = |t: f64| {
3856 pcurve
3857 .point_at(to_pcurve_parameter(t, data, range), tol)
3858 .ok()
3859 };
3860 let lift = |p: Point2| surface.point_at(p.x, p.y, tol).ok();
3861 let bows = |a: f64, b: f64| -> bool {
3862 let (Some(pa), Some(pb)) = (at(a), at(b)) else {
3863 return false;
3864 };
3865 let (Some(start), Some(end)) = (lift(pa), lift(pb)) else {
3866 return false;
3867 };
3868 lift(pa + (pb - pa) * 0.5)
3869 .is_some_and(|m| chord_sag(start, end, m, tol) > deflection.chord * REPAIR_SAG)
3870 };
3871 let mut i = 0;
3872 while i + 1 < parameters.len() {
3873 let (a, b) = (parameters[i], parameters[i + 1]);
3874 let mid = f64::midpoint(a, b);
3875 if mid > a && mid < b && bows(a, b) {
3876 if parameters.len() > ceiling {
3877 return (parameters, false);
3878 }
3879 parameters.insert(i + 1, mid);
3880 } else {
3881 i += 1;
3882 }
3883 }
3884 }
3885 if parameters.len() == before {
3886 return (parameters, true);
3887 }
3888 }
3889}
3890
3891fn domain_ring(surface: &SurfaceGeometry, deflection: Deflection, tol: Tolerances) -> Vec<Point2> {
3899 let ((ua, ub), (va, vb)) = surface.domain();
3900 if ![ua, ub, va, vb].iter().all(|x| x.is_finite()) {
3901 return Vec::new();
3902 }
3903
3904 let along_u = |v: f64| refine_line(surface, (ua, ub), |u| (u, v), deflection, tol).0;
3905 let along_v = |u: f64| refine_line(surface, (va, vb), |v| (u, v), deflection, tol).0;
3906
3907 let (bottom, top) = (along_u(va), along_u(vb));
3911 let (left, right) = (along_v(ua), along_v(ub));
3912 let mut ring = Vec::new();
3913 ring.extend(
3914 bottom[..bottom.len() - 1]
3915 .iter()
3916 .map(|u| Point2::new(*u, va)),
3917 );
3918 ring.extend(right[..right.len() - 1].iter().map(|v| Point2::new(ub, *v)));
3919 ring.extend(top[1..].iter().rev().map(|u| Point2::new(*u, vb)));
3920 ring.extend(left[1..].iter().rev().map(|v| Point2::new(ua, *v)));
3921 ring
3922}
3923
3924struct PlanarMesh {
3927 parameters: Vec<(f64, f64)>,
3929 triangles: Vec<[u32; 3]>,
3931 boundary_triangles: usize,
3933 crossed: bool,
3936}
3937
3938fn triangulate_region(
3942 rings: &[Vec<Point2>],
3943 surface: &SurfaceGeometry,
3944 deflection: Deflection,
3945 rows_kept: Rows,
3946 tol: Tolerances,
3947) -> OgeomResult<PlanarMesh> {
3948 region_mesh(rings, surface, deflection, rows_kept, Upto::Whole, tol)
3949}
3950
3951#[derive(Clone, Copy, PartialEq, Eq)]
3953enum Upto {
3954 Whole,
3956 Crossing,
3959}
3960
3961fn region_mesh(
3963 rings: &[Vec<Point2>],
3964 surface: &SurfaceGeometry,
3965 deflection: Deflection,
3966 rows_kept: Rows,
3967 upto: Upto,
3968 tol: Tolerances,
3969) -> OgeomResult<PlanarMesh> {
3970 for ring in rings {
3977 let mut extent = 0.0_f64;
3978 for p in ring {
3979 if !p.x.is_finite() || !p.y.is_finite() {
3980 ogeom_bail!(
3981 NotDone,
3982 "a boundary ring carries a non-finite chart coordinate; \
3983 the face's trim does not describe a region"
3984 );
3985 }
3986 extent = extent.max(p.x.abs()).max(p.y.abs());
3987 }
3988 if extent > 1e12 {
3989 ogeom_bail!(
3990 NotDone,
3991 "a boundary ring reaches {extent:.1e} in the chart; a trim \
3992 that far out describes no face"
3993 );
3994 }
3995 }
3996 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3997 triangulate_region_inner(rings, surface, deflection, rows_kept, upto, tol)
3998 })) {
3999 Ok(result) => result,
4000 Err(_) => ogeom_bail!(
4001 NotDone,
4002 "the face's boundary broke the triangulation; the chart is too \
4003 degenerate to mesh"
4004 ),
4005 }
4006}
4007
4008#[derive(Debug, Clone, Copy)]
4023struct ChartScale {
4024 su: f64,
4025 sv: f64,
4026}
4027
4028impl ChartScale {
4029 fn of(surface: &SurfaceGeometry, rings: &[Vec<Point2>], tol: Tolerances) -> Self {
4032 let (lo, hi) = chart_extent(rings);
4033 if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) {
4034 return Self { su: 1.0, sv: 1.0 };
4035 }
4036 let (mut du, mut dv, mut n) = (0.0_f64, 0.0_f64, 0usize);
4037 for i in 0..=2 {
4038 for j in 0..=2 {
4039 let u = lo.x + (hi.x - lo.x) * (0.25 + 0.25 * f64::from(i));
4040 let v = lo.y + (hi.y - lo.y) * (0.25 + 0.25 * f64::from(j));
4041 if let Ok((a, b)) = surface.d1_at(u, v, tol) {
4042 du += a.magnitude();
4043 dv += b.magnitude();
4044 n += 1;
4045 }
4046 }
4047 }
4048 if n == 0 || !(du > 0.0 && dv > 0.0) || !du.is_finite() || !dv.is_finite() {
4049 return Self { su: 1.0, sv: 1.0 };
4050 }
4051 let longer = du.max(dv);
4052 Self {
4053 su: du / longer,
4054 sv: dv / longer,
4055 }
4056 }
4057
4058 fn to(self, u: f64, v: f64) -> SpadePoint<f64> {
4060 SpadePoint::new(u * self.su, v * self.sv)
4061 }
4062
4063 fn from(self, x: f64, y: f64) -> (f64, f64) {
4065 (x / self.su, y / self.sv)
4066 }
4067}
4068
4069fn chart_extent(rings: &[Vec<Point2>]) -> (Point2, Point2) {
4071 rings.iter().flatten().fold(
4072 (
4073 Point2::new(f64::INFINITY, f64::INFINITY),
4074 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
4075 ),
4076 |(lo, hi), p| {
4077 (
4078 Point2::new(lo.x.min(p.x), lo.y.min(p.y)),
4079 Point2::new(hi.x.max(p.x), hi.y.max(p.y)),
4080 )
4081 },
4082 )
4083}
4084
4085fn triangulate_region_inner(
4086 rings: &[Vec<Point2>],
4087 surface: &SurfaceGeometry,
4088 deflection: Deflection,
4089 rows_kept: Rows,
4090 upto: Upto,
4091 tol: Tolerances,
4092) -> OgeomResult<PlanarMesh> {
4093 let mut cdt: ConstrainedDelaunayTriangulation<SpadePoint<f64>> =
4094 ConstrainedDelaunayTriangulation::new();
4095 let sub = Instant::now();
4096 let mut refused_total = 0usize;
4097 let mut junctions = 0usize;
4098
4099 let scale = ChartScale::of(surface, rings, tol);
4103 let scaled: Vec<Vec<Point2>> = rings
4104 .iter()
4105 .map(|ring| {
4106 ring.iter()
4107 .map(|p| {
4108 let q = scale.to(p.x, p.y);
4109 Point2::new(q.x, q.y)
4110 })
4111 .collect()
4112 })
4113 .collect();
4114 let mut exact: ogeom_core::FastMap<(u64, u64), (f64, f64)> = ogeom_core::FastMap::default();
4115
4116 for (ring, chart) in scaled.iter().zip(rings) {
4119 let mut ring_handles = Vec::with_capacity(ring.len());
4120 for (p, uv) in ring.iter().zip(chart) {
4121 let held = mitigate_underflow(SpadePoint::new(p.x, p.y));
4127 exact.insert((held.x.to_bits(), held.y.to_bits()), (uv.x, uv.y));
4128 let handle = cdt
4129 .insert(held)
4130 .map_err(|e| ogeom_core::ogeom_err!(NotDone, "boundary insertion failed: {e}"))?;
4131 ring_handles.push(handle);
4132 }
4133 let distinct: ogeom_core::FastSet<_> = ring_handles.iter().copied().collect();
4134 junctions += ring_handles.len() - distinct.len();
4135 let mut refused = 0usize;
4136 let mut same = 0usize;
4137 for i in 0..ring_handles.len() {
4138 let (a, b) = (ring_handles[i], ring_handles[(i + 1) % ring_handles.len()]);
4139 if a == b {
4140 same += 1;
4141 } else if cdt.can_add_constraint(a, b) {
4142 cdt.add_constraint(a, b);
4143 } else {
4144 refused += 1;
4145 if *MESH_DEBUG_REFINE {
4146 let n = ring.len();
4147 let partners: Vec<usize> = (0..n)
4148 .filter(|&j| j != i && j != (i + 1) % n && j != (i + n - 1) % n)
4149 .filter(|&j| {
4150 segments_cross(ring[i], ring[(i + 1) % n], ring[j], ring[(j + 1) % n])
4151 })
4152 .collect();
4153 let len = ring[i].distance(ring[(i + 1) % n]);
4154 eprintln!(
4155 "REFUSED segment {i} of {n} (chart length {len:.3e}) crosses {partners:?}"
4156 );
4157 for j in [(i + n - 1) % n, i, (i + 1) % n, (i + 2) % n, (i + 3) % n] {
4158 eprintln!(" ring[{j}] = ({:.12}, {:.12})", ring[j].x, ring[j].y);
4159 }
4160 }
4161 }
4162 }
4163 if *MESH_DEBUG_REFINE && (refused > 0 || same > 0) {
4164 eprintln!(
4165 "CONSTRAINTS ring of {}: {refused} refused, {same} zero-length",
4166 ring.len()
4167 );
4168 }
4169 refused_total += refused;
4170 }
4171
4172 let bands = RingBands::over(&scaled);
4177 let boundary_triangles = (cdt.num_vertices() + 2 * rings.len()).saturating_sub(4 + junctions);
4179 let crossed = refused_total > 0
4180 || inside_by_parity(&cdt).is_none_or(|inside| inside != boundary_triangles);
4181 if *MESH_DEBUG_REFINE && crossed {
4182 let points: usize = rings.iter().map(Vec::len).sum();
4183 eprintln!(
4184 "PARITY inside {:?} vertices {} points {points} rings {} inner faces {}",
4185 inside_by_parity(&cdt),
4186 cdt.num_vertices(),
4187 rings.len(),
4188 cdt.num_inner_faces()
4189 );
4190 }
4191 if crossed && upto == Upto::Crossing {
4192 return Ok(PlanarMesh {
4193 parameters: Vec::new(),
4194 triangles: Vec::new(),
4195 boundary_triangles,
4196 crossed,
4197 });
4198 }
4199
4200 let boundary_ms = sub.elapsed().as_secs_f64() * 1e3;
4204 let sub = Instant::now();
4205 add_interior_points(&mut cdt, rings, surface, deflection, scale, rows_kept, tol)?;
4206 let interior_ms = sub.elapsed().as_secs_f64() * 1e3;
4207 let interior_points = cdt.num_vertices();
4208 let sub = Instant::now();
4209 let mut rounds_run = 0usize;
4210
4211 let (lo, hi) = chart_extent(&scaled);
4218 let extent = (hi.x - lo.x).max(hi.y - lo.y);
4219 let degenerate_area = extent.max(1.0).powi(2) * 1e-12;
4220
4221 if !matches!(surface.kind(), ogeom_geom::SurfaceKind::Plane) {
4237 let mut sags: ogeom_core::FastMap<([u64; 2], [u64; 2]), bool> =
4241 ogeom_core::FastMap::default();
4242 let mut lifted: ogeom_core::FastMap<[u64; 2], Option<Point>> =
4245 ogeom_core::FastMap::default();
4246 for _ in 0..REFINEMENT_ROUNDS {
4247 rounds_run += 1;
4248 let before = cdt.num_vertices();
4249 let mut worst: Vec<SpadePoint<f64>> = Vec::new();
4250 for triangle in cdt.inner_faces() {
4251 let vertices = triangle.vertices();
4252 let centre = triangle.center();
4253 let at = Point2::new(centre.x, centre.y);
4254 if !bands.holds(at) {
4255 continue;
4256 }
4257 let corners: [(f64, f64); 3] = [
4258 (vertices[0].position().x, vertices[0].position().y),
4259 (vertices[1].position().x, vertices[1].position().y),
4260 (vertices[2].position().x, vertices[2].position().y),
4261 ];
4262 let area = ((corners[1].0 - corners[0].0) * (corners[2].1 - corners[0].1)
4266 - (corners[1].1 - corners[0].1) * (corners[2].0 - corners[0].0))
4267 .abs()
4268 / 2.0;
4269 if area < degenerate_area {
4270 continue;
4271 }
4272 let sagged = (0..3).any(|i| {
4281 let (a, b) = (corners[i], corners[(i + 1) % 3]);
4282 let bits = |p: (f64, f64)| [p.0.to_bits(), p.1.to_bits()];
4283 let key = if bits(a) <= bits(b) {
4284 (bits(a), bits(b))
4285 } else {
4286 (bits(b), bits(a))
4287 };
4288 *sags.entry(key).or_insert_with(|| {
4289 let (from, to) = (scale.from(a.0, a.1), scale.from(b.0, b.1));
4290 let mut lift = |p: (f64, f64), uv: (f64, f64)| {
4291 *lifted
4292 .entry(bits(p))
4293 .or_insert_with(|| GridSample::point_at(surface, uv, tol))
4294 };
4295 let (start, end) = (lift(a, from), lift(b, to));
4296 let mid = (f64::midpoint(from.0, to.0), f64::midpoint(from.1, to.1));
4297 let sag = match (start, end, GridSample::point_at(surface, mid, tol)) {
4298 (Some(start), Some(end), Some(middle)) => {
4299 chord_sag(start, end, middle, tol)
4300 }
4301 _ => 0.0,
4303 };
4304 sag > deflection.chord * REPAIR_SAG
4305 })
4306 });
4307 let middle_off = || {
4316 let lift =
4317 |p: (f64, f64)| GridSample::point_at(surface, scale.from(p.0, p.1), tol);
4318 let [a, b, c] = corners.map(lift);
4319 let (Some(a), Some(b), Some(c)) = (a, b, c) else {
4320 return false;
4321 };
4322 let Ok(n) = (b - a).cross(c - a).normalized(tol) else {
4323 return false;
4324 };
4325 let half = |i: usize, j: usize| {
4326 (
4327 f64::midpoint(corners[i].0, corners[j].0),
4328 f64::midpoint(corners[i].1, corners[j].1),
4329 )
4330 };
4331 [(centre.x, centre.y), half(0, 1), half(1, 2), half(2, 0)]
4332 .into_iter()
4333 .filter_map(lift)
4334 .any(|m| (m - a).dot(n).abs() > deflection.chord)
4335 };
4336 if sagged || middle_off() {
4337 worst.push(SpadePoint::new(centre.x, centre.y));
4338 }
4339 }
4340 if worst.is_empty() {
4341 break;
4342 }
4343 let mut inserted = 0usize;
4344 for point in worst {
4345 if lands_on_the_mesh(&cdt, point, extent * 1e-9, (1.0, 1.0)) {
4354 continue;
4355 }
4356 cdt.insert(mitigate_underflow(point)).map_err(|e| {
4357 ogeom_core::ogeom_err!(NotDone, "refinement insertion failed: {e}")
4358 })?;
4359 inserted += 1;
4360 }
4361 if inserted == 0 {
4362 break;
4365 }
4366 if *MESH_DEBUG_REFINE {
4367 eprintln!(
4368 "ROUND {rounds_run}: +{} vertices",
4369 cdt.num_vertices() - before
4370 );
4371 }
4372 }
4373 }
4374
4375 let refine_ms = sub.elapsed().as_secs_f64() * 1e3;
4376 if *MESH_DEBUG_REFINE && boundary_ms + interior_ms + refine_ms > 50.0 {
4377 eprintln!(
4378 "SUB boundary {boundary_ms:.0}ms interior {interior_ms:.0}ms ({interior_points} verts) refine {refine_ms:.0}ms ({rounds_run} rounds, {} verts)",
4379 cdt.num_vertices()
4380 );
4381 }
4382 let mut parameters = Vec::new();
4383 let mut index_of = ogeom_core::FastMap::default();
4384 for (i, vertex) in cdt.vertices().enumerate() {
4385 let p = vertex.position();
4386 index_of.insert(vertex.fix(), i);
4387 parameters.push(
4388 exact
4389 .get(&(p.x.to_bits(), p.y.to_bits()))
4390 .copied()
4391 .unwrap_or_else(|| scale.from(p.x, p.y)),
4392 );
4393 }
4394
4395 let mut triangles = Vec::new();
4396 let (mut dbg_total, mut dbg_outside, mut dbg_degenerate) = (0usize, 0usize, 0usize);
4397 for triangle in cdt.inner_faces() {
4398 dbg_total += 1;
4399 let vertices = triangle.vertices();
4400 let centre = triangle.center();
4401 if !bands.holds(Point2::new(centre.x, centre.y)) {
4405 dbg_outside += 1;
4406 continue;
4407 }
4408 let area = {
4415 let [a, b, c] = [
4416 vertices[0].position(),
4417 vertices[1].position(),
4418 vertices[2].position(),
4419 ];
4420 ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() / 2.0
4421 };
4422 if area < degenerate_area {
4423 dbg_degenerate += 1;
4424 continue;
4425 }
4426 #[allow(clippy::cast_possible_truncation)]
4427 let indices = [
4428 index_of[&vertices[0].fix()] as u32,
4429 index_of[&vertices[1].fix()] as u32,
4430 index_of[&vertices[2].fix()] as u32,
4431 ];
4432 triangles.push(indices);
4433 }
4434
4435 if *MESH_DEBUG_REFINE {
4436 eprintln!(
4437 "FILTER {dbg_total} triangles: {dbg_outside} outside, {dbg_degenerate} degenerate (area < {degenerate_area:.3e}), {} kept",
4438 triangles.len()
4439 );
4440 }
4441 Ok(PlanarMesh {
4448 parameters,
4449 triangles,
4450 boundary_triangles,
4451 crossed,
4452 })
4453}
4454
4455fn add_interior_points(
4472 cdt: &mut ConstrainedDelaunayTriangulation<SpadePoint<f64>>,
4473 rings: &[Vec<Point2>],
4474 surface: &SurfaceGeometry,
4475 deflection: Deflection,
4476 scale: ChartScale,
4477 rows_kept: Rows,
4478 tol: Tolerances,
4479) -> OgeomResult<()> {
4480 if matches!(surface.kind(), ogeom_geom::SurfaceKind::Plane) {
4482 return Ok(());
4483 }
4484 let region = RingBands::over(rings);
4487
4488 let bound = rings
4489 .iter()
4490 .flatten()
4491 .fold(ogeom_math::Aabb::EMPTY, |acc, p| {
4492 acc.with_point(Point::new(p.x, p.y, 0.0))
4493 });
4494 let (Some(low), Some(high)) = (bound.low(), bound.high()) else {
4495 return Ok(());
4496 };
4497 let reach = ((high.x - low.x) * scale.su)
4500 .max((high.y - low.y) * scale.sv)
4501 .max(1.0)
4502 * 1e-9;
4503
4504 #[allow(clippy::cast_precision_loss)]
4508 let probes: Vec<f64> = (0..=U_PROBES)
4509 .map(|i| low.x + (high.x - low.x) * i as f64 / U_PROBES as f64)
4510 .collect();
4511 let (rows, row_samples) = refine_lines(
4512 surface,
4513 (low.y, high.y),
4514 &probes,
4515 |u, v| (u, v),
4516 deflection,
4517 tol,
4518 );
4519
4520 let span = |p: (f64, f64), q: (f64, f64)| -> f64 {
4534 use ogeom_geom::Surface as _;
4535 match (
4536 surface.point_at(p.0, p.1, tol),
4537 surface.point_at(q.0, q.1, tol),
4538 ) {
4539 (Ok(a), Ok(b)) => a.distance(b),
4540 _ => 0.0,
4541 }
4542 };
4543 let columns_at = |v: f64| refine_line(surface, (low.x, high.x), |u| (u, v), deflection, tol);
4545 let aspect = if matches!(surface.kind(), ogeom_geom::SurfaceKind::Cone) {
4550 CONE_CELL_ASPECT
4551 } else {
4552 CELL_ASPECT
4553 };
4554 let ruled = matches!(
4558 surface.kind(),
4559 ogeom_geom::SurfaceKind::Cylinder
4560 | ogeom_geom::SurfaceKind::Cone
4561 | ogeom_geom::SurfaceKind::Extrusion
4562 );
4563 let apex = || {
4570 let um = f64::midpoint(low.x, high.x);
4571 let across = |v: f64| span((low.x, v), (um, v)) + span((um, v), (high.x, v));
4572 let (bottom, top) = (across(low.y), across(high.y));
4573 bottom.min(top) <= bottom.max(top) * 1e-3
4574 };
4575 let round = matches!(
4576 surface.kind(),
4577 ogeom_geom::SurfaceKind::Cylinder | ogeom_geom::SurfaceKind::Cone
4578 );
4579 let rows = if round && rows_kept == Rows::Ends && rows.len() > 2 && !apex() {
4580 vec![rows[0], rows[rows.len() - 1]]
4581 } else if ruled {
4582 rows
4583 } else {
4584 spread_to_aspect(
4585 rows,
4586 aspect,
4587 low.x,
4588 high.x,
4589 |v| columns_at(v).0.len(),
4590 |i| {
4591 row_samples[i]
4592 .iter()
4593 .zip(&row_samples[i + 1])
4594 .map(|(a, b)| a.distance(b))
4595 .fold(0.0_f64, f64::max)
4596 },
4597 |a, b, v| span((a, v), (b, v)),
4598 )
4599 };
4600
4601 if *MESH_DEBUG_REFINE {
4602 let v = f64::midpoint(low.y, high.y);
4603 let (columns, _) = columns_at(v);
4604 eprintln!(
4605 "GRID u [{:.3},{:.3}] v [{:.3},{:.3}]: {} rows after aspect, {} columns at the middle row, chord {}",
4606 low.x,
4607 high.x,
4608 low.y,
4609 high.y,
4610 rows.len(),
4611 columns.len(),
4612 deflection.chord
4613 );
4614 }
4615 let lifted = |u: f64, v: f64| {
4627 use ogeom_geom::Surface as _;
4628 surface.point_at(u, v, tol).ok()
4629 };
4630 let cells: Vec<(f64, f64, f64, f64)> = rows
4631 .windows(2)
4632 .flat_map(|pair| {
4633 let (v0, v1) = (pair[0], pair[1]);
4634 let (columns, _) = columns_at(f64::midpoint(v0, v1));
4635 columns
4636 .windows(2)
4637 .map(|cell| (cell[0], cell[1], v0, v1))
4638 .collect::<Vec<_>>()
4639 })
4640 .collect();
4641 let sags = !ruled
4644 && cells.len() >= MIN_CENTRED_CELLS
4645 && cells.iter().any(|&(u0, u1, v0, v1)| {
4646 let (um, vm) = (f64::midpoint(u0, u1), f64::midpoint(v0, v1));
4647 match (
4648 lifted(u0, v0),
4649 lifted(u1, v1),
4650 lifted(u1, v0),
4651 lifted(u0, v1),
4652 lifted(um, vm),
4653 ) {
4654 (Some(a), Some(b), Some(c), Some(d), Some(m)) => {
4655 chord_sag(a, b, m, tol).max(chord_sag(c, d, m, tol)) > deflection.chord
4656 }
4657 _ => false,
4658 }
4659 });
4660 let round_ruled = matches!(
4661 surface.kind(),
4662 ogeom_geom::SurfaceKind::Cylinder | ogeom_geom::SurfaceKind::Cone
4663 );
4664 let mut placed: Vec<Vec<(usize, f64, FixedVertexHandle)>> = Vec::new();
4665 let mut row_of: Vec<usize> = Vec::new();
4666 for (row, &v) in rows
4667 .iter()
4668 .enumerate()
4669 .take(rows.len().saturating_sub(1))
4670 .skip(1)
4671 {
4672 let mut here: Vec<(usize, f64, FixedVertexHandle)> = Vec::new();
4673 let dv = (v - rows[row - 1]).abs().min((rows[row + 1] - v).abs());
4676 let (columns, column_samples) = columns_at(v);
4678 let columns = spread_to_aspect(
4682 columns,
4683 CELL_ASPECT,
4684 low.y,
4685 high.y,
4686 |_| rows.len(),
4687 |i| column_samples[i].distance(&column_samples[i + 1]),
4688 |a, b, u| span((u, a), (u, b)),
4689 );
4690 for (column, &u) in columns
4691 .iter()
4692 .enumerate()
4693 .take(columns.len().saturating_sub(1))
4694 .skip(1)
4695 {
4696 if round_ruled && rows_kept == Rows::Sparse && column % 4 != 0 {
4697 continue;
4698 }
4699 if !region.holds(Point2::new(u, v)) {
4702 continue;
4703 }
4704 let point = scale.to(u, v);
4711 if lands_on_the_mesh(cdt, point, reach, (1.0, 1.0)) {
4712 continue;
4713 }
4714 let du = (u - columns[column - 1])
4725 .abs()
4726 .min((columns[column + 1] - u).abs());
4727 if du > 0.0
4728 && dv > 0.0
4729 && lands_on_the_mesh(
4730 cdt,
4731 point,
4732 KEEP_OUT,
4733 (1.0 / (du * scale.su), 1.0 / (dv * scale.sv)),
4734 )
4735 {
4736 continue;
4737 }
4738 let handle = cdt
4739 .insert(mitigate_underflow(point))
4740 .map_err(|e| ogeom_core::ogeom_err!(NotDone, "interior insertion failed: {e}"))?;
4741 if sags {
4742 here.push((column, u, handle));
4743 }
4744 }
4745 if sags {
4746 for pair in here.windows(2) {
4748 if pair[1].0 == pair[0].0 + 1 && cdt.can_add_constraint(pair[0].2, pair[1].2) {
4749 cdt.add_constraint(pair[0].2, pair[1].2);
4750 }
4751 }
4752 if let (Some(before), Some(&last)) = (placed.last(), row_of.last())
4754 && last + 1 == row
4755 {
4756 for &(_, u, handle) in &here {
4757 if let Some(&(_, _, above)) =
4758 before.iter().find(|p| p.1.to_bits() == u.to_bits())
4759 && cdt.can_add_constraint(above, handle)
4760 {
4761 cdt.add_constraint(above, handle);
4762 }
4763 }
4764 }
4765 let edge = |cdt: &ConstrainedDelaunayTriangulation<SpadePoint<f64>>, u: f64, v: f64| {
4769 cdt.locate_vertex(mitigate_underflow(scale.to(u, v)))
4770 .map(|h| h.fix())
4771 };
4772 let mut ties: Vec<(FixedVertexHandle, FixedVertexHandle)> = Vec::new();
4773 if let (Some(&(first, _, start)), Some(&(last, _, end))) = (here.first(), here.last()) {
4774 if first == 1
4775 && let Some(b) = edge(cdt, columns[0], v)
4776 {
4777 ties.push((b, start));
4778 }
4779 if last + 2 == columns.len()
4780 && let Some(b) = edge(cdt, columns[columns.len() - 1], v)
4781 {
4782 ties.push((end, b));
4783 }
4784 }
4785 for &(_, u, handle) in &here {
4786 if row == 1
4787 && let Some(b) = edge(cdt, u, rows[0])
4788 {
4789 ties.push((b, handle));
4790 }
4791 if row + 2 == rows.len()
4792 && let Some(b) = edge(cdt, u, rows[rows.len() - 1])
4793 {
4794 ties.push((handle, b));
4795 }
4796 }
4797 for (a, b) in ties {
4798 if a != b && cdt.can_add_constraint(a, b) {
4799 cdt.add_constraint(a, b);
4800 }
4801 }
4802 placed.push(here);
4803 row_of.push(row);
4804 }
4805 }
4806 if sags {
4807 for (u0, u1, v0, v1) in cells {
4808 let (um, vm) = (f64::midpoint(u0, u1), f64::midpoint(v0, v1));
4809 if !region.holds(Point2::new(um, vm)) {
4810 continue;
4811 }
4812 let collapsed = |v: f64| match (lifted(u0, v), lifted(u1, v)) {
4816 (Some(a), Some(b)) => a.distance(b) <= tol.confusion(),
4817 _ => true,
4818 };
4819 if collapsed(v0) || collapsed(v1) {
4820 continue;
4821 }
4822 let point = scale.to(um, vm);
4823 let (du, dv) = ((u1 - u0) * 0.5, (v1 - v0) * 0.5);
4824 if lands_on_the_mesh(cdt, point, reach, (1.0, 1.0))
4825 || (du > 0.0
4826 && dv > 0.0
4827 && lands_on_the_mesh(
4828 cdt,
4829 point,
4830 KEEP_OUT,
4831 (1.0 / (du * scale.su), 1.0 / (dv * scale.sv)),
4832 ))
4833 {
4834 continue;
4835 }
4836 cdt.insert(mitigate_underflow(point))
4837 .map_err(|e| ogeom_core::ogeom_err!(NotDone, "interior insertion failed: {e}"))?;
4838 }
4839 }
4840 if ruled {
4841 let (bottom, _) = columns_at(low.y);
4845 let (top, _) = columns_at(high.y);
4846 let columns = if bottom.len() >= top.len() {
4847 bottom
4848 } else {
4849 top
4850 };
4851 let mut across: Vec<(f64, f64, FixedVertexHandle)> = cdt
4856 .vertices()
4857 .map(|v| (v.position().x, v.position().y, v.fix()))
4858 .collect();
4859 across.sort_by(|a, b| a.0.total_cmp(&b.0));
4860 for (k, &u) in columns
4861 .iter()
4862 .enumerate()
4863 .take(columns.len().saturating_sub(1))
4864 .skip(1)
4865 {
4866 let du = (u - columns[k - 1]).abs().min((columns[k + 1] - u).abs());
4867 let margin = KEEP_OUT * du * scale.su / scale.sv;
4868 let snap = du * 1e-6;
4872 let mut crossings: Vec<(f64, Option<Point2>)> = Vec::new();
4873 for ring in rings {
4874 for (i, &p) in ring.iter().enumerate() {
4875 let q = ring[(i + 1) % ring.len()];
4876 if (p.x < u) != (q.x < u) {
4877 let y = p.y + (u - p.x) / (q.x - p.x) * (q.y - p.y);
4878 let on = [p, q].into_iter().find(|w| (w.x - u).abs() <= snap);
4879 crossings.push((on.map_or(y, |w| w.y), on));
4880 }
4881 }
4882 }
4883 crossings.sort_by(|a, b| a.0.total_cmp(&b.0));
4884 for pair in crossings.windows(2) {
4885 let (low_end, high_end) = (pair[0], pair[1]);
4886 if !region.holds(Point2::new(u, f64::midpoint(low_end.0, high_end.0))) {
4887 continue;
4888 }
4889 if (high_end.0 - low_end.0) * scale.sv <= CELL_ASPECT * du * scale.su {
4892 continue;
4893 }
4894 let pair = [low_end.0, high_end.0];
4895 let mut ends = Vec::with_capacity(2);
4896 for ((at, on), inward) in [(low_end, margin), (high_end, -margin)] {
4899 let vertex = on.unwrap_or(Point2::new(u, at));
4900 if let Some(h) =
4901 cdt.locate_vertex(mitigate_underflow(scale.to(vertex.x, vertex.y)))
4902 {
4903 ends.push(h.fix());
4904 continue;
4905 }
4906 let v = at + inward;
4907 if (v - f64::midpoint(pair[0], pair[1])).abs()
4908 >= (pair[1] - pair[0]).abs() * 0.5
4909 {
4910 break;
4911 }
4912 if surface.point_at(u, v, tol).is_err() {
4915 break;
4916 }
4917 let point = mitigate_underflow(scale.to(u, v));
4918 let handle = match cdt.locate_vertex(point) {
4919 Some(h) => h.fix(),
4920 None if !lands_on_the_mesh(cdt, point, reach, (1.0, 1.0)) => {
4921 cdt.insert(point).map_err(|e| {
4922 ogeom_core::ogeom_err!(NotDone, "column insertion failed: {e}")
4923 })?
4924 }
4925 None => break,
4926 };
4927 ends.push(handle);
4928 }
4929 let [a, b] = ends[..] else {
4930 continue;
4931 };
4932 if a == b {
4933 continue;
4934 }
4935 let (ya, yb) = (cdt.vertex(a).position().y, cdt.vertex(b).position().y);
4936 let (x, near) = (u * scale.su, 1e-3 * du * scale.su);
4937 let first = across.partition_point(|p| p.0 < x - near);
4938 let mut through: Vec<(f64, FixedVertexHandle)> = across[first..]
4939 .iter()
4940 .take_while(|p| p.0 <= x + near)
4941 .filter(|p| p.1 > ya.min(yb) && p.1 < ya.max(yb))
4942 .map(|p| (p.1, p.2))
4943 .collect();
4944 through.sort_by(|p, q| p.0.total_cmp(&q.0));
4945 if ya > yb {
4946 through.reverse();
4947 }
4948 let chain: Vec<FixedVertexHandle> = core::iter::once(a)
4949 .chain(through.into_iter().map(|p| p.1))
4950 .chain(core::iter::once(b))
4951 .collect();
4952 for link in chain.windows(2) {
4953 if link[0] != link[1] && cdt.can_add_constraint(link[0], link[1]) {
4954 cdt.add_constraint(link[0], link[1]);
4955 }
4956 }
4957 }
4958 }
4959 }
4960 Ok(())
4961}
4962
4963pub(crate) fn segments_cross(a: Point2, b: Point2, c: Point2, d: Point2) -> bool {
4966 let orient =
4967 |p: Point2, q: Point2, r: Point2| (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
4968 let (o1, o2) = (orient(a, b, c), orient(a, b, d));
4969 let (o3, o4) = (orient(c, d, a), orient(c, d, b));
4970 o1 != 0.0
4971 && o2 != 0.0
4972 && o3 != 0.0
4973 && o4 != 0.0
4974 && (o1 > 0.0) != (o2 > 0.0)
4975 && (o3 > 0.0) != (o4 > 0.0)
4976}
4977
4978fn inside_by_parity(cdt: &ConstrainedDelaunayTriangulation<SpadePoint<f64>>) -> Option<usize> {
4990 use ogeom_core::FastMap;
4991 let mut parity: FastMap<spade::handles::FixedFaceHandle<spade::handles::InnerTag>, bool> =
4992 FastMap::with_capacity_and_hasher(cdt.num_inner_faces(), Default::default());
4993 let mut queue = Vec::new();
4994 for hull in cdt.convex_hull() {
4995 let Some(face) = hull.rev().face().as_inner() else {
4999 continue;
5000 };
5001 let inside = hull.is_constraint_edge();
5002 match parity.get(&face.fix()) {
5003 Some(&known) if known != inside => return None,
5004 Some(_) => {}
5005 None => {
5006 parity.insert(face.fix(), inside);
5007 queue.push((face.fix(), inside));
5008 }
5009 }
5010 }
5011 while let Some((face, inside)) = queue.pop() {
5012 for edge in cdt.face(face).adjacent_edges() {
5013 let Some(next) = edge.rev().face().as_inner() else {
5014 continue;
5015 };
5016 let next_inside = inside != edge.is_constraint_edge();
5017 match parity.get(&next.fix()) {
5018 Some(&known) if known != next_inside => return None,
5019 Some(_) => {}
5020 None => {
5021 parity.insert(next.fix(), next_inside);
5022 queue.push((next.fix(), next_inside));
5023 }
5024 }
5025 }
5026 }
5027 Some(parity.values().filter(|&&inside| inside).count())
5028}
5029
5030fn lands_on_the_mesh(
5039 cdt: &ConstrainedDelaunayTriangulation<SpadePoint<f64>>,
5040 point: SpadePoint<f64>,
5041 reach: f64,
5042 scale: (f64, f64),
5043) -> bool {
5044 use spade::PositionInTriangulation as At;
5045 let scaled = |p: SpadePoint<f64>| ((p.x - point.x) * scale.0, (p.y - point.y) * scale.1);
5049 let near = |v: SpadePoint<f64>| {
5050 let (x, y) = scaled(v);
5051 x.hypot(y) <= reach
5052 };
5053 let along = |a: SpadePoint<f64>, b: SpadePoint<f64>| {
5054 let (ax, ay) = scaled(a);
5056 let (bx, by) = scaled(b);
5057 let (dx, dy) = (bx - ax, by - ay);
5058 let len2 = dx * dx + dy * dy;
5059 let t = if len2 > 0.0 {
5060 ((-ax * dx - ay * dy) / len2).clamp(0.0, 1.0)
5061 } else {
5062 0.0
5063 };
5064 (ax + t * dx).hypot(ay + t * dy) <= reach
5065 };
5066 match cdt.locate(point) {
5067 At::OnVertex(_) => true,
5068 At::OnEdge(edge) => {
5069 let edge = cdt.directed_edge(edge);
5070 edge.is_constraint_edge() || edge.vertices().iter().any(|v| near(v.position()))
5071 }
5072 At::OnFace(face) => {
5073 let face = cdt.face(face);
5074 face.vertices().iter().any(|v| near(v.position()))
5075 || face.adjacent_edges().iter().any(|e| {
5076 e.is_constraint_edge() && along(e.from().position(), e.to().position())
5077 })
5078 }
5079 At::OutsideOfConvexHull(_) | At::NoTriangulation => false,
5080 }
5081}
5082
5083const KEEP_OUT: f64 = 0.35;
5090
5091const CELL_ASPECT: f64 = 6.0;
5098
5099const CONE_CELL_ASPECT: f64 = 1.5;
5101
5102fn spread_to_aspect(
5114 lines: Vec<f64>,
5115 aspect: f64,
5116 lo: f64,
5117 hi: f64,
5118 crossings_at: impl Fn(f64) -> usize,
5119 length: impl Fn(usize) -> f64,
5120 width: impl Fn(f64, f64, f64) -> f64,
5121) -> Vec<f64> {
5122 if lines.len() < 2 {
5123 return lines;
5124 }
5125 let mid = f64::midpoint(lines[0], lines[lines.len() - 1]);
5126 let crossings = crossings_at(mid);
5127 if crossings < 4 {
5128 return lines;
5131 }
5132 #[allow(clippy::cast_precision_loss)]
5142 let step = (hi - lo) / (crossings - 1) as f64;
5143 let (first, last) = (lines[0], lines[lines.len() - 1]);
5144 let quarter = (last - first) * 0.25;
5145 let widths = [first + quarter, mid, last - quarter].map(|t| width(lo, lo + step, t));
5146 let cell = widths[1].max(widths.iter().sum::<f64>() / 3.0);
5147 if cell.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) {
5148 return lines;
5149 }
5150 let mut out = Vec::with_capacity(lines.len());
5151 for (i, pair) in lines.windows(2).enumerate() {
5152 let (a, b) = (pair[0], pair[1]);
5153 out.push(a);
5154 let tall = length(i);
5155 let pieces = (tall / (aspect * cell)).ceil();
5156 if pieces.is_finite() && pieces > 1.0 {
5157 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
5158 let n = (pieces as usize).min(MAX_DIRECTION_STEPS);
5159 #[allow(clippy::cast_precision_loss)]
5160 for i in 1..n {
5161 out.push(a + (b - a) * i as f64 / n as f64);
5162 }
5163 }
5164 }
5165 out.push(lines[lines.len() - 1]);
5166 out
5167}
5168
5169const U_PROBES: usize = 8;
5171
5172const REFINEMENT_ROUNDS: usize = 12;
5178
5179const REPAIR_SAG: f64 = 3.0;
5182
5183const MIN_CENTRED_CELLS: usize = 128;
5191
5192const MAX_DIRECTION_STEPS: usize = 512;
5199
5200fn direction_steps(deflection: Deflection) -> usize {
5202 deflection.max_segments.max(MAX_DIRECTION_STEPS)
5203}
5204
5205fn refine_direction<S, M>(
5221 (lo, hi): (f64, f64),
5222 chord: f64,
5223 cap: usize,
5224 sample: impl Fn(f64) -> S,
5225 middle: impl Fn(f64) -> M,
5226 error: impl Fn(&S, &S, &M) -> f64,
5227 promote: impl Fn(f64, M) -> S,
5228) -> (Vec<f64>, Vec<S>) {
5229 let centre = f64::midpoint(lo, hi);
5230 let mut values = vec![lo, centre, hi];
5231 let mut samples: Vec<S> = vec![sample(lo), sample(centre), sample(hi)];
5232 let mut settled = vec![false; values.len() - 1];
5235 loop {
5236 let mut erring: Vec<(usize, f64, f64, M)> = Vec::new();
5238 for i in 0..settled.len() {
5239 if settled[i] {
5240 continue;
5241 }
5242 let mid = f64::midpoint(values[i], values[i + 1]);
5243 let at_mid = middle(mid);
5244 let err = error(&samples[i], &samples[i + 1], &at_mid);
5245 if err <= chord || mid <= values[i] || mid >= values[i + 1] {
5248 settled[i] = true;
5249 continue;
5250 }
5251 erring.push((i, err, mid, at_mid));
5252 }
5253 if erring.is_empty() {
5254 break;
5255 }
5256 let room = cap.saturating_sub(values.len());
5257 if room == 0 {
5258 break;
5259 }
5260 if erring.len() > room {
5261 erring.sort_by(|x, y| y.1.total_cmp(&x.1).then(x.0.cmp(&y.0)));
5262 erring.truncate(room);
5263 erring.sort_by_key(|e| e.0);
5264 }
5265 let mut grown_values = Vec::with_capacity(values.len() + erring.len());
5266 let mut grown_samples = Vec::with_capacity(values.len() + erring.len());
5267 let mut grown_settled = Vec::with_capacity(settled.len() + erring.len());
5268 let mut splits = erring.into_iter().peekable();
5269 for (i, (value, sampled)) in values.into_iter().zip(samples).enumerate() {
5270 grown_values.push(value);
5271 grown_samples.push(sampled);
5272 if i == settled.len() {
5273 continue;
5274 }
5275 match splits.next_if(|e| e.0 == i) {
5276 Some((_, _, mid, at_mid)) => {
5277 grown_settled.extend([false, false]);
5278 grown_values.push(mid);
5279 grown_samples.push(promote(mid, at_mid));
5280 }
5281 None => grown_settled.push(settled[i]),
5282 }
5283 }
5284 values = grown_values;
5285 samples = grown_samples;
5286 settled = grown_settled;
5287 }
5288 (values, samples)
5289}
5290
5291#[derive(Clone, Copy)]
5294struct GridSample {
5295 point: Option<Point>,
5296 normal: Option<Direction>,
5297}
5298
5299impl GridSample {
5300 fn at(surface: &SurfaceGeometry, uv: (f64, f64), tol: Tolerances) -> Self {
5301 Self {
5302 point: Self::point_at(surface, uv, tol),
5303 normal: Self::normal_at(surface, uv, tol),
5304 }
5305 }
5306
5307 fn point_at(surface: &SurfaceGeometry, (u, v): (f64, f64), tol: Tolerances) -> Option<Point> {
5308 surface.point_at(u, v, tol).ok()
5309 }
5310
5311 fn normal_at(
5312 surface: &SurfaceGeometry,
5313 (u, v): (f64, f64),
5314 tol: Tolerances,
5315 ) -> Option<Direction> {
5316 surface.normal_at(u, v, tol).ok()
5317 }
5318
5319 fn distance(&self, other: &Self) -> f64 {
5322 match (self.point, other.point) {
5323 (Some(a), Some(b)) => a.distance(b),
5324 _ => 0.0,
5325 }
5326 }
5327}
5328
5329fn refine_line(
5334 surface: &SurfaceGeometry,
5335 range: (f64, f64),
5336 at: impl Fn(f64) -> (f64, f64),
5337 deflection: Deflection,
5338 tol: Tolerances,
5339) -> (Vec<f64>, Vec<GridSample>) {
5340 refine_direction(
5341 range,
5342 deflection.chord,
5343 direction_steps(deflection),
5344 |t| GridSample::at(surface, at(t), tol),
5345 |t| GridSample::point_at(surface, at(t), tol),
5346 |a, b, m| cell_error(a, b, *m, deflection, tol),
5347 |t, point| GridSample {
5348 point,
5349 normal: GridSample::normal_at(surface, at(t), tol),
5350 },
5351 )
5352}
5353
5354fn joining_run(
5359 surface: &SurfaceGeometry,
5360 from: Point2,
5361 to: Point2,
5362 deflection: Deflection,
5363 tol: Tolerances,
5364) -> Vec<f64> {
5365 const EVEN_STEPS: u32 = 8;
5366 let at = |f: f64| (from.x + (to.x - from.x) * f, from.y + (to.y - from.y) * f);
5367 let mut fractions: Vec<f64> = (0..=EVEN_STEPS)
5368 .map(|k| f64::from(k) / f64::from(EVEN_STEPS))
5369 .collect();
5370 let mut samples: Vec<GridSample> = fractions
5371 .iter()
5372 .map(|&f| GridSample::at(surface, at(f), tol))
5373 .collect();
5374 let cap = direction_steps(deflection);
5375 loop {
5376 let mut grown = Vec::with_capacity(fractions.len() * 2);
5377 let mut grown_samples = Vec::with_capacity(fractions.len() * 2);
5378 let mut split = false;
5379 for i in 0..fractions.len() {
5380 grown.push(fractions[i]);
5381 grown_samples.push(samples[i]);
5382 let Some(&next) = fractions.get(i + 1) else {
5383 continue;
5384 };
5385 let mid = f64::midpoint(fractions[i], next);
5386 if mid <= fractions[i] || mid >= next {
5387 continue;
5388 }
5389 let middle = GridSample::at(surface, at(mid), tol);
5390 if cell_error(&samples[i], &samples[i + 1], middle.point, deflection, tol)
5391 > deflection.chord
5392 {
5393 grown.push(mid);
5394 grown_samples.push(middle);
5395 split = true;
5396 }
5397 }
5398 if !split || grown.len() > cap {
5399 break;
5400 }
5401 fractions = grown;
5402 samples = grown_samples;
5403 }
5404 fractions
5405}
5406
5407fn refine_lines(
5412 surface: &SurfaceGeometry,
5413 range: (f64, f64),
5414 lines: &[f64],
5415 at: impl Fn(f64, f64) -> (f64, f64),
5416 deflection: Deflection,
5417 tol: Tolerances,
5418) -> (Vec<f64>, Vec<Vec<GridSample>>) {
5419 refine_direction(
5420 range,
5421 deflection.chord,
5422 direction_steps(deflection),
5423 |t| {
5424 lines
5425 .iter()
5426 .map(|&line| GridSample::at(surface, at(line, t), tol))
5427 .collect::<Vec<_>>()
5428 },
5429 |t| {
5430 lines
5431 .iter()
5432 .map(|&line| GridSample::point_at(surface, at(line, t), tol))
5433 .collect::<Vec<_>>()
5434 },
5435 |a, b, m| {
5436 a.iter()
5437 .zip(b)
5438 .zip(m)
5439 .map(|((a, b), m)| cell_error(a, b, *m, deflection, tol))
5440 .fold(0.0_f64, f64::max)
5441 },
5442 |t, points| {
5443 lines
5444 .iter()
5445 .zip(points)
5446 .map(|(&line, point)| GridSample {
5447 point,
5448 normal: GridSample::normal_at(surface, at(line, t), tol),
5449 })
5450 .collect()
5451 },
5452 )
5453}
5454
5455fn cell_error(
5468 from: &GridSample,
5469 to: &GridSample,
5470 middle: Option<Point>,
5471 deflection: Deflection,
5472 tol: Tolerances,
5473) -> f64 {
5474 let sag = match (from.point, to.point, middle) {
5475 (Some(a), Some(b), Some(m)) => chord_sag(a, b, m, tol),
5476 _ => 0.0,
5478 };
5479 let turn = match (from.normal, to.normal) {
5480 (Some(a), Some(b)) => a.angle(b),
5481 _ => 0.0,
5483 };
5484 sag.max(turn / deflection.angular * deflection.chord)
5485}
5486
5487fn chord_sag(a: Point, b: Point, m: Point, tol: Tolerances) -> f64 {
5493 ogeom_math::Axis::through(a, b, tol).map_or_else(|_| a.distance(m), |axis| axis.distance_to(m))
5494}
5495
5496#[cfg(test)]
5498fn inside_region(rings: &[Vec<Point2>], p: Point2) -> bool {
5499 inside_boundary_with::<Exact>(rings, p)
5500}
5501
5502fn crosses_odd_times<P: Predicates>(ring: &[Point2], p: Point2) -> bool {
5516 let mut inside = false;
5517 let n = ring.len();
5518 let at = |q: Point2| [q.x, q.y];
5519 for i in 0..n {
5520 let (a, b) = (ring[i], ring[(i + 1) % n]);
5521 if (a.y > p.y) == (b.y > p.y) {
5522 continue;
5523 }
5524 let side = P::orient2d(at(a), at(b), [p.x, p.y]);
5528 let rightwards = if b.y > a.y {
5529 side == ogeom_core::Sign::Positive
5530 } else {
5531 side == ogeom_core::Sign::Negative
5532 };
5533 if rightwards {
5534 inside = !inside;
5535 }
5536 }
5537 inside
5538}
5539
5540static MESH_DEBUG_REFINE: std::sync::LazyLock<bool> =
5545 std::sync::LazyLock::new(|| std::env::var("OGEOM_MESH_DEBUG_REFINE").is_ok());
5546
5547static MESH_DEBUG: std::sync::LazyLock<bool> =
5549 std::sync::LazyLock::new(|| std::env::var("OGEOM_MESH_DEBUG").is_ok());
5550
5551struct RingBands {
5572 edges: Vec<(Point2, Point2)>,
5574 bands: Vec<Vec<u32>>,
5576 low: f64,
5577 high: f64,
5578 step: f64,
5581}
5582
5583impl RingBands {
5584 fn over(rings: &[Vec<Point2>]) -> Self {
5587 let mut edges = Vec::new();
5588 for ring in rings {
5589 for i in 0..ring.len() {
5590 edges.push((ring[i], ring[(i + 1) % ring.len()]));
5591 }
5592 }
5593 let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
5594 for (a, b) in &edges {
5595 low = low.min(a.y).min(b.y);
5596 high = high.max(a.y).max(b.y);
5597 }
5598 if edges.is_empty() || !low.is_finite() || !high.is_finite() {
5599 return Self {
5600 edges,
5601 bands: Vec::new(),
5602 low: 0.0,
5603 high: 0.0,
5604 step: 0.0,
5605 };
5606 }
5607 let count = edges.len().div_ceil(4).clamp(1, 4096);
5610 #[allow(
5611 clippy::cast_precision_loss,
5612 reason = "a band count, far below the integers f64 represents exactly"
5613 )]
5614 let step = (high - low) / count as f64;
5615 let mut bands: Vec<Vec<u32>> = vec![Vec::new(); count];
5616 for (i, (a, b)) in edges.iter().enumerate() {
5617 let (lo, hi) = (a.y.min(b.y), a.y.max(b.y));
5618 let first = Self::band_of(lo, low, step, count);
5619 let last = Self::band_of(hi, low, step, count);
5620 for band in &mut bands[first..=last] {
5621 #[allow(
5622 clippy::cast_possible_truncation,
5623 reason = "an edge index, bounded by the ring lengths"
5624 )]
5625 band.push(i as u32);
5626 }
5627 }
5628 Self {
5629 edges,
5630 bands,
5631 low,
5632 high,
5633 step,
5634 }
5635 }
5636
5637 fn band_of(y: f64, low: f64, step: f64, count: usize) -> usize {
5639 if step <= 0.0 {
5640 return 0;
5641 }
5642 let raw = (y - low) / step;
5643 if raw <= 0.0 {
5644 return 0;
5645 }
5646 #[allow(
5647 clippy::cast_possible_truncation,
5648 clippy::cast_sign_loss,
5649 reason = "clamped to the band count on the next line"
5650 )]
5651 let index = raw as usize;
5652 index.min(count - 1)
5653 }
5654
5655 fn holds(&self, p: Point2) -> bool {
5657 if self.bands.is_empty() || p.y < self.low || p.y > self.high {
5658 return false;
5659 }
5660 let band = Self::band_of(p.y, self.low, self.step, self.bands.len());
5661 let at = |q: Point2| [q.x, q.y];
5662 let mut inside = false;
5663 for &i in &self.bands[band] {
5664 let (a, b) = self.edges[i as usize];
5665 if (a.y > p.y) == (b.y > p.y) {
5666 continue;
5667 }
5668 let side = Exact::orient2d(at(a), at(b), [p.x, p.y]);
5669 let rightwards = if b.y > a.y {
5670 side == ogeom_core::Sign::Positive
5671 } else {
5672 side == ogeom_core::Sign::Negative
5673 };
5674 if rightwards {
5675 inside = !inside;
5676 }
5677 }
5678 inside
5679 }
5680}
5681
5682#[must_use]
5684pub fn triangle_normal(a: Point, b: Point, c: Point, tol: Tolerances) -> Option<Direction> {
5685 Direction::from_cross(b - a, c - a, tol).ok()
5686}
5687
5688pub fn polyline_of_edge(
5694 model: &Model,
5695 edge: &Shape,
5696 deflection: Deflection,
5697 tol: Tolerances,
5698) -> OgeomResult<Vec<Point>> {
5699 if model.kind_of(edge)? != ShapeType::Edge {
5700 ogeom_bail!(Construction, "expected an edge");
5701 }
5702 let Some(node) = model.node(edge) else {
5703 ogeom_bail!(Dangling, "edge is not in this model");
5704 };
5705 let NodeData::Edge(data) = node.data() else {
5706 ogeom_bail!(Construction, "edge node holds no edge data");
5707 };
5708 let Some(line) = edge_polyline(model, data, deflection, tol)? else {
5709 return Ok(Vec::new());
5710 };
5711 let placement = edge.transform(model.datums())?;
5712 let mut points: Vec<Point> = line.points.iter().map(|p| placement.apply(*p)).collect();
5713 if edge.orientation() == Orientation::Reversed {
5714 points.reverse();
5715 }
5716 Ok(points)
5717}
5718
5719#[cfg(test)]
5720#[allow(clippy::unwrap_used)]
5721mod tests {
5722 use super::*;
5723
5724 #[test]
5729 fn a_side_from_a_point_to_itself_turns_no_piece() {
5730 let (far, farther) = (Point::new(5.0, 5.0, 5.0), Point::new(6.0, 5.0, 5.0));
5731 let mut mesh = Triangulation::new();
5732 mesh.positions = vec![
5733 Point::new(0.0, 0.0, 0.0),
5734 Point::new(1.0, 0.0, 0.0),
5735 Point::new(0.0, 1.0, 0.0),
5736 Point::new(1.0, 1.0, 0.0),
5737 far,
5738 far,
5739 farther,
5740 farther,
5741 far,
5742 far,
5743 farther,
5744 farther,
5745 ];
5746 mesh.normals = vec![Vector::new(0.0, 0.0, 1.0); mesh.positions.len()];
5747 let first = [[0, 1, 2], [0, 4, 5], [0, 6, 7]];
5748 let second = [[1, 3, 2], [3, 8, 9], [3, 10, 11]];
5749 mesh.triangles = first.iter().chain(&second).copied().collect();
5750 let before = mesh.triangles.clone();
5751 orient_pieces(&mut mesh, &[(0, 3), (3, 6)]);
5752 assert_eq!(mesh.triangles, before);
5753 }
5754
5755 #[test]
5762 #[allow(
5763 clippy::excessive_precision,
5764 clippy::approx_constant,
5765 reason = "the values as a file states them, a hair short of pi"
5766 )]
5767 fn a_ring_a_whole_turn_out_slides_back_one_turn() {
5768 let domain = (-3.141_592_653_589_79, 3.141_592_653_589_79);
5769 let span = domain.1 - domain.0;
5770 let low = (-9.424_777_960_769_370_47, -3.141_592_653_589_744_27);
5771 assert!((domain.0 - low.0) / span > 1.0);
5772 assert_eq!(turns_into_domain(low, domain), 1.0);
5773 let high = (low.0 + 2.0 * span, low.1 + 2.0 * span);
5774 assert_eq!(turns_into_domain(high, domain), -1.0);
5775 assert_eq!(turns_into_domain((domain.0 - 1e-9, domain.1), domain), 0.0);
5776 assert_eq!(turns_into_domain((-1.0, 2.0), domain), 0.0);
5777 let two = (low.0 - span, low.1 - span);
5779 assert_eq!(turns_into_domain(two, domain), 2.0);
5780 }
5781
5782 #[test]
5789 fn a_strip_with_one_rim_held_fine_asks_for_its_other_rim_finer() {
5790 use ogeom_geom::{CircleCurve, Curve, PlaneSurface};
5791 use ogeom_math::{Circle, Frame, Plane};
5792 let mut model = Model::new();
5793 let (inner, outer, angle) = (15.0, 15.03, core::f64::consts::FRAC_PI_3);
5794 let at = |r: f64, a: f64| Point::new(r * a.cos(), r * a.sin(), 0.0);
5795 let corners = [
5796 at(outer, 0.0),
5797 at(outer, angle),
5798 at(inner, angle),
5799 at(inner, 0.0),
5800 ];
5801 let vertices: Vec<Shape> = corners
5802 .iter()
5803 .map(|p| ogeom_algo::make_vertex(&mut model, *p).shape)
5804 .collect();
5805 let arc =
5806 |r: f64| -> Curve { CircleCurve::new(Circle::new(Frame::WORLD, r, T).unwrap()).into() };
5807 let line = |a: Point, b: Point| -> Curve {
5808 ogeom_geom::LineCurve::segment(a, b, T).unwrap().into()
5809 };
5810 let mut edge = |curve: Curve, range: (f64, f64), from: usize, to: usize| {
5811 ogeom_algo::make_edge_between(
5812 &mut model,
5813 curve,
5814 range,
5815 &vertices[from],
5816 &vertices[to],
5817 T,
5818 )
5819 .unwrap()
5820 .shape
5821 };
5822 let outer_edge = edge(arc(outer), (0.0, angle), 0, 1);
5823 let top = edge(
5824 line(corners[1], corners[2]),
5825 (0.0, corners[1].distance(corners[2])),
5826 1,
5827 2,
5828 );
5829 let inner_edge = edge(arc(inner), (0.0, angle), 3, 2);
5830 let bottom = edge(
5831 line(corners[3], corners[0]),
5832 (0.0, corners[3].distance(corners[0])),
5833 3,
5834 0,
5835 );
5836 let plane: SurfaceGeometry = PlaneSurface::new(Plane::new(Frame::WORLD)).into();
5837 let ring = vec![outer_edge.clone(), top, inner_edge.reversed(), bottom];
5838 let face = ogeom_algo::make_face_with_pcurves(&mut model, plane, &[ring], T)
5839 .unwrap()
5840 .shape;
5841 let data = model.node(&face).unwrap().data().as_face().unwrap().clone();
5842 let surface = model.geometry().surface(data.surface).unwrap().clone();
5843 let coarse = Deflection::default();
5844 let alone = face_chords(
5845 &model,
5846 &face,
5847 data.surface,
5848 &surface,
5849 coarse,
5850 &EdgeChords::new(),
5851 T,
5852 )
5853 .unwrap();
5854 assert!(!alone.contains_key(&outer_edge.node().index()), "{alone:?}");
5855 let held: EdgeChords = [(inner_edge.node().index(), coarse.chord / 128.0)].into();
5856 let asked = face_chords(&model, &face, data.surface, &surface, coarse, &held, T).unwrap();
5857 let outer_chord = asked.get(&outer_edge.node().index()).copied();
5858 assert!(outer_chord.is_some_and(|c| c < coarse.chord), "{asked:?}");
5859 assert!(asked[&inner_edge.node().index()] <= coarse.chord / 128.0);
5860 }
5861
5862 #[test]
5863 fn a_wild_chart_refuses_instead_of_crashing() {
5864 let plane: SurfaceGeometry = ogeom_geom::PlaneSurface::over(
5865 ogeom_math::Plane::through(
5866 ogeom_math::Point::new(0.0, 0.0, 0.0),
5867 ogeom_math::Direction::Z,
5868 ),
5869 (-10.0, 10.0),
5870 (-10.0, 10.0),
5871 )
5872 .unwrap()
5873 .into();
5874 let tol = Tolerances::millimetres();
5875 let wild = vec![vec![
5877 Point2::new(0.0, 0.0),
5878 Point2::new(1e15, 0.0),
5879 Point2::new(0.0, 1e15),
5880 ]];
5881 let Err(err) = triangulate_region(&wild, &plane, Deflection::default(), Rows::Ends, tol)
5882 else {
5883 panic!("a wild chart must refuse");
5884 };
5885 assert!(err.to_string().contains("describes no face"), "{err}");
5886 let nan = vec![vec![
5888 Point2::new(0.0, 0.0),
5889 Point2::new(f64::NAN, 1.0),
5890 Point2::new(1.0, 1.0),
5891 ]];
5892 let Err(err) = triangulate_region(&nan, &plane, Deflection::default(), Rows::Ends, tol)
5893 else {
5894 panic!("a NaN chart must refuse");
5895 };
5896 assert!(err.to_string().contains("non-finite"), "{err}");
5897 }
5898
5899 use approx::assert_relative_eq;
5900
5901 #[test]
5902 fn chart_tangency_retains_both_lobes_and_all_boundary_sides() {
5903 let tol = Tolerances::millimetres();
5904 let frame = ogeom_math::Frame::new(
5905 Point::ORIGIN,
5906 ogeom_math::Direction::Z,
5907 ogeom_math::Direction::X,
5908 tol,
5909 )
5910 .unwrap();
5911 let plane: SurfaceGeometry =
5912 ogeom_geom::PlaneSurface::new(ogeom_math::Plane::new(frame)).into();
5913 for angle in [0.0_f64, 0.1, 0.7] {
5914 let (sin, cos) = angle.sin_cos();
5915 let rotate = |p: Point2| Point2::new(cos * p.x - sin * p.y, sin * p.x + cos * p.y);
5916 for offset in [-1e-12, 0.0, 1e-12] {
5917 let mut ring: Vec<_> = [
5918 Point2::new(0.0, 0.0),
5919 Point2::new(0.0, 10.0),
5920 Point2::new(1.0, 10.0),
5921 Point2::new(offset, 3.0),
5922 Point2::new(1.0, 0.0),
5923 ]
5924 .into_iter()
5925 .map(rotate)
5926 .collect();
5927 let original = ring.clone();
5928 let mut anchors = vec![None; ring.len()];
5929 snap_chart_junctions(&mut ring, &mut anchors, &plane, tol);
5930 assert_eq!(
5931 ring[1], original[3],
5932 "the target side shares the touching vertex"
5933 );
5934 let boundary = ring.clone();
5935 let mesh = triangulate_region(
5936 &[ring],
5937 &plane,
5938 Deflection::with_chord(0.1).unwrap(),
5939 Rows::Ends,
5940 tol,
5941 )
5942 .unwrap();
5943 assert!(!mesh.crossed, "angle {angle}, offset {offset}");
5944 assert_eq!(mesh.triangles.len(), 2);
5945 for (&a, &b) in boundary.iter().zip(boundary.iter().cycle().skip(1)) {
5946 assert!(
5947 mesh.triangles.iter().any(|t| (0..3).any(|i| {
5948 let x = mesh.parameters[t[i] as usize];
5949 let y = mesh.parameters[t[(i + 1) % 3] as usize];
5950 (x == (a.x, a.y) && y == (b.x, b.y))
5951 || (x == (b.x, b.y) && y == (a.x, a.y))
5952 })),
5953 "missing boundary side {a:?} -- {b:?}"
5954 );
5955 }
5956 let area: f64 = mesh
5959 .triangles
5960 .iter()
5961 .map(|t| {
5962 let [a, b, c] = t.map(|i| {
5963 let (u, v) = mesh.parameters[i as usize];
5964 Point2::new(u, v)
5965 });
5966 (b - a).cross(c - a).abs() * 0.5
5967 })
5968 .sum();
5969 assert!((area - 5.0).abs() < 1e-9, "{area}");
5970 let mut model = Model::new();
5971 let points: Vec<_> = original.iter().map(|p| Point::new(p.x, p.y, 0.0)).collect();
5972 let wire = ogeom_algo::make_polygon(&mut model, &points, true, tol)
5973 .unwrap()
5974 .shape;
5975 let face = ogeom_algo::make_face(&mut model, plane.clone(), &[wire], tol)
5976 .unwrap()
5977 .shape;
5978 ogeom_algo::build::trimmed_where_bare(&mut model, &face, tol).unwrap();
5979 let (mesh, verdict) = triangulate_reporting(
5980 &model,
5981 &face,
5982 Deflection::with_chord(0.1).unwrap(),
5983 Some(&EdgeChords::new()),
5984 tol,
5985 )
5986 .unwrap();
5987 assert!(verdict != Verdict::Short, "angle {angle}, offset {offset}");
5988 let area: f64 = mesh
5989 .triangles
5990 .iter()
5991 .map(|t| {
5992 let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
5993 (b - a).cross(c - a).magnitude() * 0.5
5994 })
5995 .sum();
5996 assert!((area - 5.0).abs() < 1e-9, "face area {area}");
5997 }
5998 }
5999 let mut separate = vec![
6000 Point2::new(0.0, 0.0),
6001 Point2::new(0.0, 10.0),
6002 Point2::new(1.0, 10.0),
6003 Point2::new(2.0 * tol.parametric(), 5.0),
6004 Point2::new(1.0, 0.0),
6005 ];
6006 let before = separate.clone();
6007 snap_chart_junctions(&mut separate, &mut vec![None; before.len()], &plane, tol);
6008 assert_eq!(separate, before, "a resolvable gap must stay separate");
6009 }
6010 #[test]
6011 fn chart_junction_candidates_stay_bounded_on_dense_seams() {
6012 let samples = 1024;
6013 for transpose in [false, true] {
6014 let mut ring = Vec::new();
6015 for side in 0..4 {
6016 for i in 0..samples {
6017 let t = i as f64 / samples as f64;
6018 let p = match side {
6019 0 => Point2::new(10.0 * t, 0.0),
6020 1 => Point2::new(10.0, t),
6021 2 => Point2::new(10.0 * (1.0 - t), 1.0),
6022 _ => Point2::new(0.0, 1.0 - t),
6023 };
6024 ring.push(if transpose { Point2::new(p.y, p.x) } else { p });
6025 }
6026 }
6027 let swapped: Vec<_> = ring.iter().map(|p| Point2::new(p.y, p.x)).collect();
6028 let x = RingBands::over(&[swapped]);
6029 let y = RingBands::over(std::slice::from_ref(&ring));
6030 let mut visited = 0;
6031 for (i, &p) in ring.iter().enumerate() {
6032 let candidates: Vec<_> = junction_candidates(&x, &y, p, 1e-9)
6033 .iter()
6034 .flatten()
6035 .copied()
6036 .collect();
6037 let current = u32::try_from(i).unwrap();
6039 let previous = u32::try_from((i + ring.len() - 1) % ring.len()).unwrap();
6040 assert!(candidates.contains(¤t));
6041 assert!(candidates.contains(&previous));
6042 visited += candidates.len();
6043 }
6044 assert!(visited < 32 * ring.len(), "visited {visited} sides");
6047 }
6048 }
6049
6050 use ogeom_algo::make_box;
6051 use ogeom_math::Frame;
6052 use ogeom_topo::explore_unique;
6053
6054 const T: Tolerances = Tolerances::millimetres();
6055
6056 const UNCAPPED: usize = 1 << 16;
6058
6059 fn refine_by_rescan<F: Fn(f64, f64) -> f64>(lo: f64, hi: f64, chord: f64, sag: F) -> Vec<f64> {
6062 let mut values = vec![lo, f64::midpoint(lo, hi), hi];
6063 while values.len() < UNCAPPED {
6064 let Some(i) = (0..values.len() - 1).find(|&i| sag(values[i], values[i + 1]) > chord)
6065 else {
6066 break;
6067 };
6068 let mid = f64::midpoint(values[i], values[i + 1]);
6069 if mid <= values[i] || mid >= values[i + 1] {
6070 break;
6071 }
6072 values.insert(i + 1, mid);
6073 }
6074 values
6075 }
6076
6077 #[test]
6078 fn rounds_split_where_rescanning_did() {
6079 type Profile = (&'static str, Box<dyn Fn(f64, f64) -> f64>);
6086 let cases: Vec<Profile> = vec![
6087 ("flat", Box::new(|_a: f64, _b: f64| 0.0)),
6088 ("width", Box::new(|a: f64, b: f64| (b - a).abs())),
6089 (
6090 "steep at the low end",
6091 Box::new(|a: f64, b: f64| (b - a).abs() / a.abs().max(1e-3)),
6092 ),
6093 (
6094 "periodic",
6095 Box::new(|a: f64, b: f64| (b - a).abs() * (a * 12.0).sin().abs()),
6096 ),
6097 (
6098 "beyond the cap",
6099 Box::new(|a: f64, b: f64| (b - a).abs() * 1e6),
6100 ),
6101 ];
6102 for (name, sag) in cases {
6103 for chord in [1.0, 0.1, 0.01, 1e-3] {
6104 let capped = name == "beyond the cap";
6105 let cap = if capped {
6106 MAX_DIRECTION_STEPS
6107 } else {
6108 UNCAPPED
6109 };
6110 let (walked, samples) = refine_direction(
6111 (0.0, 1.0),
6112 chord,
6113 cap,
6114 |t| t,
6115 |t| t,
6116 |a, b, _| sag(*a, *b),
6117 |t, _| t,
6118 );
6119 assert_eq!(walked, samples, "{name}: each value keeps its own sample");
6120 if capped {
6121 assert!(walked.len() <= cap, "{name}: {} values", walked.len());
6123 let gaps: Vec<f64> = walked.windows(2).map(|w| w[1] - w[0]).collect();
6124 let (least, most) = gaps
6125 .iter()
6126 .fold((f64::INFINITY, 0.0_f64), |(l, m), g| (l.min(*g), m.max(*g)));
6127 assert!(most <= least * 2.0, "{name}: gaps from {least} to {most}");
6128 continue;
6129 }
6130 let rescanned = refine_by_rescan(0.0, 1.0, chord, &sag);
6131 assert_eq!(
6132 walked, rescanned,
6133 "{name} at chord {chord}: the rounds split somewhere the rescan did not"
6134 );
6135 }
6136 }
6137 }
6138
6139 #[test]
6140 fn banded_containment_answers_what_the_full_scan_answers() {
6141 let outer: Vec<Point2> = vec![
6145 Point2::new(0.0, 0.0),
6146 Point2::new(4.0, 0.0),
6147 Point2::new(4.0, 3.0),
6148 Point2::new(2.0, 1.5),
6149 Point2::new(0.0, 3.0),
6150 ];
6151 let hole: Vec<Point2> = vec![
6152 Point2::new(1.0, 0.5),
6153 Point2::new(1.0, 1.0),
6154 Point2::new(1.5, 1.0),
6155 Point2::new(1.5, 0.5),
6156 ];
6157 let rings = vec![outer, hole];
6158 let bands = RingBands::over(&rings);
6159 for i in 0..=80 {
6160 for j in 0..=60 {
6161 #[allow(clippy::cast_precision_loss)]
6162 let p = Point2::new(f64::from(i) * 0.05 - 0.1, f64::from(j) * 0.05 - 0.1);
6163 assert_eq!(
6164 bands.holds(p),
6165 inside_region(&rings, p),
6166 "the index disagreed with the full scan at {p:?}"
6167 );
6168 }
6169 }
6170 }
6171
6172 fn fine() -> Deflection {
6173 Deflection {
6174 chord: 1e-3,
6175 angular: 0.05,
6176 ..Deflection::default()
6177 }
6178 }
6179
6180 #[test]
6183 fn apex_and_pole_vertices_carry_the_limit_normal() {
6184 let mut model = Model::new();
6185 let cone = ogeom_algo::make_cone(&mut model, Frame::WORLD, 2.0, 0.0, 3.0, T).unwrap();
6186 let sphere = ogeom_algo::make_sphere(&mut model, Frame::WORLD, 1.5, T).unwrap();
6187 for (shape, what) in [(&cone.shape, "cone"), (&sphere.shape, "sphere")] {
6188 for face in explore_unique(&model, shape, ShapeType::Face).unwrap() {
6189 let mesh = triangulate_face(&model, &face, fine(), T).unwrap();
6190 for (i, n) in mesh.normals.iter().enumerate() {
6191 assert!(
6192 (n.magnitude() - 1.0).abs() < 1e-9,
6193 "{what} vertex {i} at {:?} has normal {n:?}",
6194 mesh.positions[i]
6195 );
6196 }
6197 }
6198 }
6199 let faces = explore_unique(&model, &cone.shape, ShapeType::Face).unwrap();
6203 let lean = (2.0_f64 / 3.0).atan();
6204 for face in &faces {
6205 let mesh = triangulate_face(&model, face, fine(), T).unwrap();
6206 for (i, p) in mesh.positions.iter().enumerate() {
6207 if p.distance(Point::new(0.0, 0.0, 3.0)) < 1e-9 {
6208 let n = mesh.normals[i];
6209 let from_axis = n.z.abs().acos();
6210 assert!(
6211 ((std::f64::consts::FRAC_PI_2 - from_axis) - lean).abs() < 1e-6,
6212 "apex normal {n:?} leans {from_axis} from the axis"
6213 );
6214 }
6215 }
6216 }
6217 }
6218
6219 #[test]
6220 fn a_box_face_triangulates_into_two_triangles() {
6221 let mut model = Model::new();
6225 let built = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
6226 let faces = explore_unique(&model, &built.shape, ShapeType::Face).unwrap();
6227
6228 for face in &faces {
6229 let mesh = triangulate_face(&model, face, fine(), T).unwrap();
6230 assert_eq!(mesh.triangle_count(), 2, "a rectangle is two triangles");
6231 assert_eq!(mesh.vertex_count(), 4);
6232 assert!(mesh.deflection_met);
6233 }
6234 }
6235
6236 #[test]
6237 fn a_boxs_mesh_is_closed_and_reports_the_right_volume() {
6238 let mut model = Model::new();
6243 let size = (2.0, 3.0, 4.0);
6244 let built = make_box(&mut model, Frame::WORLD, size, T).unwrap();
6245 let mesh = triangulate(&model, &built.shape, fine(), T).unwrap();
6246
6247 assert_eq!(mesh.triangle_count(), 12, "six faces, two triangles each");
6248 assert_eq!(mesh.vertex_count(), 8, "welding merged the shared corners");
6249 assert!(
6250 mesh.is_closed(),
6251 "every triangle edge should be shared by two"
6252 );
6253 assert_relative_eq!(mesh.volume(), size.0 * size.1 * size.2, epsilon = 1e-9);
6254 assert_relative_eq!(
6255 mesh.area(),
6256 2.0 * (size.0 * size.1 + size.1 * size.2 + size.2 * size.0),
6257 epsilon = 1e-9
6258 );
6259 }
6260
6261 fn circle_segments(r: f64, c: f64) -> usize {
6266 let wanted = std::f64::consts::PI / (1.0 - c / r).acos();
6267 let mut n = Deflection::default().min_segments;
6268 #[allow(clippy::cast_precision_loss, reason = "a segment count")]
6269 while (n as f64) < wanted {
6270 n *= 2;
6271 }
6272 n
6273 }
6274
6275 #[test]
6280 fn a_box_is_two_triangles_a_face_however_long_its_faces() {
6281 let deflection = Deflection::curvature_only(0.01, 0.5).unwrap();
6282 for size in [(20.0, 20.0, 20.0), (1000.0, 0.1, 0.1)] {
6283 let mut model = Model::new();
6284 let built = make_box(&mut model, Frame::WORLD, size, T).unwrap();
6285 let mesh = triangulate(&model, &built.shape, deflection, T).unwrap();
6286 assert_eq!(mesh.triangle_count(), 12, "{size:?}");
6287 assert!(mesh.is_closed(), "{size:?}");
6288 assert_relative_eq!(
6290 mesh.volume(),
6291 size.0 * size.1 * size.2,
6292 max_relative = 1e-12
6293 );
6294 for face in explore_unique(&model, &built.shape, ShapeType::Face).unwrap() {
6295 assert_faces_out(&triangulate_face(&model, &face, deflection, T).unwrap());
6296 }
6297 }
6298 }
6299
6300 fn assert_faces_out(mesh: &Triangulation) {
6303 for t in &mesh.triangles {
6304 let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
6305 let along = t
6306 .iter()
6307 .map(|&i| mesh.normals[i as usize])
6308 .fold(Vector::ZERO, |sum, n| sum + n);
6309 let normal = (b - a).cross(c - a);
6310 assert!(normal.magnitude() > 0.0, "a triangle with no area");
6311 assert!(normal.dot(along) > 0.0, "a triangle turned over");
6312 }
6313 }
6314
6315 #[test]
6320 fn a_cylinder_takes_the_segments_its_chord_asks_at_any_height() {
6321 let (r, c) = (10.0, 0.01);
6322 let n = circle_segments(r, c);
6323 assert_eq!(n, 128);
6326 let deflection = Deflection::curvature_only(c, 0.5).unwrap();
6327 for height in [20.0, 2000.0] {
6328 let mut model = Model::new();
6329 let built = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, r, height, T).unwrap();
6330 let mesh = triangulate(&model, &built.shape, deflection, T).unwrap();
6331 assert!(mesh.is_closed(), "height {height}");
6332 assert_eq!(
6333 mesh.triangle_count(),
6334 2 * n + 2 * (n - 2),
6335 "height {height}"
6336 );
6337 let mut counts: Vec<usize> = explore_unique(&model, &built.shape, ShapeType::Face)
6338 .unwrap()
6339 .iter()
6340 .map(|face| {
6341 let drawn = triangulate_face(&model, face, deflection, T).unwrap();
6342 assert_faces_out(&drawn);
6343 drawn.triangle_count()
6344 })
6345 .collect();
6346 counts.sort_unstable();
6347 assert_eq!(counts, [n - 2, n - 2, 2 * n], "height {height}");
6348 }
6349 }
6350
6351 #[test]
6357 fn a_thin_annulus_is_its_outline_s_triangulation() {
6358 use ogeom_geom::Curve3d as _;
6359 let (outer, inner, height, c) = (10.05, 10.0, 5.0, 0.01);
6360 let n = circle_segments(inner, c);
6361 assert_eq!(n, circle_segments(outer, c));
6362 let mut model = Model::new();
6363 let ring = |model: &mut Model, frame: Frame, radius: f64| {
6364 let circle = ogeom_math::Circle::new(frame, radius, T).unwrap();
6365 let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
6366 let domain = curve.domain();
6367 ogeom_algo::make_edge(model, curve, domain, T)
6368 .unwrap()
6369 .shape
6370 };
6371 let down =
6372 Frame::from_axes(Point::ORIGIN, Direction::X, -Direction::Y, -Direction::Z, T).unwrap();
6373 let rim = ring(&mut model, Frame::WORLD, outer);
6374 let hole = ring(&mut model, down, inner);
6375 let plane = ogeom_math::Plane::new(Frame::WORLD);
6376 let surface: SurfaceGeometry =
6377 ogeom_geom::PlaneSurface::over(plane, (-20.0, 20.0), (-20.0, 20.0))
6378 .unwrap()
6379 .into();
6380 let cap =
6381 ogeom_algo::make_face_with_pcurves(&mut model, surface, &[vec![rim], vec![hole]], T)
6382 .unwrap()
6383 .shape;
6384 let tube =
6385 ogeom_algo::make_prism(&mut model, &cap, Vector::new(0.0, 0.0, height), T).unwrap();
6386
6387 let deflection = Deflection::curvature_only(c, 0.5).unwrap();
6388 let mesh = triangulate(&model, &tube.shape, deflection, T).unwrap();
6389 assert!(mesh.is_closed());
6390 #[allow(clippy::cast_precision_loss, reason = "a segment count")]
6391 let polygons = n as f64 / 2.0 * (std::f64::consts::TAU / n as f64).sin();
6392 assert_relative_eq!(
6394 mesh.volume(),
6395 polygons * (outer * outer - inner * inner) * height,
6396 max_relative = 1e-9
6397 );
6398 for face in explore_unique(&model, &tube.shape, ShapeType::Face).unwrap() {
6399 let drawn = triangulate_face(&model, &face, deflection, T).unwrap();
6400 assert_faces_out(&drawn);
6401 assert_eq!(drawn.triangle_count(), 2 * n);
6402 }
6403 }
6404
6405 #[test]
6406 fn welding_is_what_closes_the_mesh() {
6407 let mut model = Model::new();
6410 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6411
6412 let mut loose = Triangulation::new();
6413 for face in ogeom_topo::explore(
6414 &model,
6415 &built.shape,
6416 ogeom_topo::Filter::OfType(ShapeType::Face),
6417 )
6418 .unwrap()
6419 {
6420 loose.append(&triangulate_face(&model, &face, fine(), T).unwrap());
6421 }
6422 assert_eq!(loose.vertex_count(), 24, "four corners per face, unmerged");
6423 assert!(!loose.is_closed());
6424
6425 let welded = loose.welded(T);
6426 assert_eq!(welded.vertex_count(), 8);
6427 assert!(welded.is_closed());
6428 }
6429
6430 #[test]
6431 fn a_reversed_face_presents_the_other_side() {
6432 let mut model = Model::new();
6435 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6436 let face = explore_unique(&model, &built.shape, ShapeType::Face).unwrap()[0].clone();
6437
6438 let forward = triangulate_face(&model, &face, fine(), T).unwrap();
6439 let backward = triangulate_face(&model, &face.reversed(), fine(), T).unwrap();
6440
6441 assert_eq!(forward.triangle_count(), backward.triangle_count());
6442 for (a, b) in forward.normals.iter().zip(&backward.normals) {
6443 assert!(a.is_equal(-*b, T), "normals did not flip: {a:?} vs {b:?}");
6444 }
6445 let winding = |m: &Triangulation, i: usize| {
6447 let [a, b, c] = m.triangles[i].map(|k| m.positions[k as usize]);
6448 (b - a).cross(c - a)
6449 };
6450 assert!(winding(&forward, 0).dot(winding(&backward, 0)) < 0.0);
6451 }
6452
6453 fn against(mesh: &Triangulation) -> usize {
6456 mesh.triangles
6457 .iter()
6458 .filter(|t| {
6459 let [a, b, c] = t.map(|k| mesh.positions[k as usize]);
6460 let facing = (b - a).cross(c - a);
6461 let normal = t
6462 .iter()
6463 .fold(Vector::ZERO, |sum, &k| sum + mesh.normals[k as usize]);
6464 facing.dot(normal) < 0.0
6465 })
6466 .collect::<Vec<_>>()
6467 .len()
6468 }
6469
6470 #[test]
6471 fn a_mirrored_face_is_shaded_the_way_its_triangles_face() {
6472 use ogeom_geom::{CircleCurve, Curve};
6476 use ogeom_math::{Circle, Transform};
6477 let mut model = Model::new();
6478 let frame = Frame::new(Point::ORIGIN, Direction::Z, Direction::X, T).unwrap();
6479 let circle = Circle::new(frame, 10.0, T).unwrap();
6480 let arc = ogeom_algo::make_edge(
6481 &mut model,
6482 Curve::Circle(CircleCurve::new(circle)),
6483 (0.0, std::f64::consts::PI),
6484 T,
6485 )
6486 .unwrap()
6487 .shape;
6488 let wall = ogeom_algo::make_prism(&mut model, &arc, Vector::new(0.0, 0.0, 5.0), T)
6489 .unwrap()
6490 .shape;
6491 let face = explore_unique(&model, &wall, ShapeType::Face)
6492 .unwrap()
6493 .remove(0);
6494 let copy = ogeom_algo::copied(&mut model, &face).unwrap().shape;
6495 let mirror = Transform::plane_mirror(Point::ORIGIN, Direction::Y);
6496 let image = ogeom_algo::transformed(&mut model, ©, mirror)
6497 .unwrap()
6498 .shape;
6499 for f in [&face, &image, &image.reversed()] {
6500 let mesh = triangulate_face(&model, f, Deflection::default(), T).unwrap();
6501 assert!(mesh.triangle_count() > 0);
6502 assert_eq!(against(&mesh), 0, "{} triangles", mesh.triangle_count());
6503 }
6504 let original = triangulate_face(&model, &face, Deflection::default(), T).unwrap();
6507 let mirrored = triangulate_face(&model, &image, Deflection::default(), T).unwrap();
6508 let at = |mesh: &Triangulation| {
6509 let i = mesh
6510 .positions
6511 .iter()
6512 .position(|p| p.distance(Point::new(10.0, 0.0, 0.0)) < 1e-9)
6513 .unwrap();
6514 mesh.normals[i]
6515 };
6516 assert!(at(&mirrored).is_equal(at(&original), T));
6517 let whole = triangulate(&model, &image, Deflection::default(), T).unwrap();
6519 assert_eq!(against(&whole), 0);
6520 }
6521
6522 #[test]
6523 fn every_triangle_vertex_lies_on_the_surface_it_came_from() {
6524 let mut model = Model::new();
6525 let built = make_box(&mut model, Frame::WORLD, (2.0, 1.0, 3.0), T).unwrap();
6526
6527 for face in explore_unique(&model, &built.shape, ShapeType::Face).unwrap() {
6528 let mesh = triangulate_face(&model, &face, fine(), T).unwrap();
6529 let data = model.node(&face).unwrap().data().as_face().unwrap().clone();
6530 let surface = model.geometry().surface(data.surface).unwrap();
6531 for (position, (u, v)) in mesh.positions.iter().zip(&mesh.parameters) {
6532 let exact = surface.point_at(*u, *v, T).unwrap();
6533 assert!(
6534 position.is_equal(exact, T),
6535 "{position:?} is not on its surface"
6536 );
6537 }
6538 }
6539 }
6540
6541 #[test]
6542 fn a_finer_deflection_never_gives_fewer_triangles() {
6543 let mut model = Model::new();
6544 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6545 let coarse = triangulate(&model, &built.shape, Deflection::default(), T).unwrap();
6546 let detailed = triangulate(&model, &built.shape, fine(), T).unwrap();
6547 assert!(detailed.triangle_count() >= coarse.triangle_count());
6548 assert_relative_eq!(coarse.volume(), 1.0, epsilon = 1e-9);
6550 assert_relative_eq!(detailed.volume(), 1.0, epsilon = 1e-9);
6551 }
6552
6553 #[test]
6554 fn a_sphere_gets_interior_points_and_converges_on_its_true_area() {
6555 use ogeom_algo::make_natural_face;
6560 use ogeom_geom::SphereSurface;
6561 use ogeom_math::Sphere;
6562
6563 let radius = 10.0;
6564 let exact = 4.0 * std::f64::consts::PI * radius * radius;
6565 let mut previous = 0.0;
6566
6567 for chord in [1.0_f64, 0.25, 0.05] {
6568 let mut model = Model::new();
6569 let surface = SphereSurface::new(Sphere::new(Frame::WORLD, radius, T).unwrap());
6570 let face = make_natural_face(&mut model, surface.into()).unwrap().shape;
6571 let deflection = Deflection {
6572 chord,
6573 ..Deflection::default()
6574 };
6575 let mesh = triangulate_face(&model, &face, deflection, T).unwrap();
6576
6577 assert!(
6578 mesh.triangle_count() > 2,
6579 "a curved face needs interior points, got {} triangles",
6580 mesh.triangle_count()
6581 );
6582 let area = mesh.area();
6585 assert!(area < exact, "a chord-cut area cannot exceed the surface's");
6586 assert!(
6587 area > previous,
6588 "tightening the chord from the previous step lost area: \
6589 {area} after {previous}"
6590 );
6591 previous = area;
6592 }
6593 assert!(
6594 previous > exact * 0.99,
6595 "at a chord of 0.05 on a radius of 10 the area should be within a \
6596 percent, got {previous} against {exact}"
6597 );
6598 }
6599
6600 #[test]
6601 fn every_sphere_vertex_sits_at_the_right_radius() {
6602 use ogeom_algo::make_natural_face;
6606 use ogeom_geom::SphereSurface;
6607 use ogeom_math::Sphere;
6608
6609 let mut model = Model::new();
6610 let surface = SphereSurface::new(Sphere::new(Frame::WORLD, 3.0, T).unwrap());
6611 let face = make_natural_face(&mut model, surface.into()).unwrap().shape;
6612 let mesh = triangulate_face(&model, &face, Deflection::default(), T).unwrap();
6613
6614 for p in &mesh.positions {
6615 assert_relative_eq!(p.to_vector().magnitude(), 3.0, epsilon = 1e-9);
6616 }
6617 }
6618
6619 #[test]
6620 fn a_curved_domains_boundary_is_refined_not_just_its_corners() {
6621 use ogeom_geom::{PlaneSurface, SphereSurface};
6626 use ogeom_math::{Plane, Sphere};
6627
6628 let sphere: SurfaceGeometry =
6629 SphereSurface::new(Sphere::new(Frame::WORLD, 10.0, T).unwrap()).into();
6630 let coarse = domain_ring(&sphere, Deflection::default(), T);
6631 let fine_ring = domain_ring(&sphere, fine(), T);
6632 assert!(coarse.len() > 4, "a sphere's domain edge is curved");
6633 assert!(
6634 fine_ring.len() > coarse.len(),
6635 "a tighter chord should place more boundary points"
6636 );
6637
6638 for w in fine_ring.windows(2) {
6640 assert!(!w[0].is_equal(w[1], T), "the ring repeats a point");
6641 }
6642
6643 let plane: SurfaceGeometry = PlaneSurface::new(Plane::new(Frame::WORLD)).into();
6645 assert!(domain_ring(&plane, fine(), T).len() >= 4);
6646 }
6647
6648 #[test]
6649 fn winding_detects_points_inside_and_outside_a_ring() {
6650 let square = vec![
6651 Point2::new(0.0, 0.0),
6652 Point2::new(1.0, 0.0),
6653 Point2::new(1.0, 1.0),
6654 Point2::new(0.0, 1.0),
6655 ];
6656 assert!(inside_region(
6657 std::slice::from_ref(&square),
6658 Point2::new(0.5, 0.5)
6659 ));
6660 assert!(!inside_region(
6661 std::slice::from_ref(&square),
6662 Point2::new(1.5, 0.5)
6663 ));
6664 assert!(!inside_region(
6665 std::slice::from_ref(&square),
6666 Point2::new(0.5, -0.5)
6667 ));
6668
6669 let hole = vec![
6671 Point2::new(0.4, 0.4),
6672 Point2::new(0.6, 0.4),
6673 Point2::new(0.6, 0.6),
6674 Point2::new(0.4, 0.6),
6675 ];
6676 let with_hole = vec![square, hole];
6677 assert!(!inside_region(&with_hole, Point2::new(0.5, 0.5)));
6678 assert!(inside_region(&with_hole, Point2::new(0.2, 0.2)));
6679 }
6680
6681 #[test]
6682 fn a_polyline_of_an_edge_runs_in_the_edges_direction() {
6683 let mut model = Model::new();
6684 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6685 let edge = explore_unique(&model, &built.shape, ShapeType::Edge).unwrap()[0].clone();
6686
6687 let forward = polyline_of_edge(&model, &edge, fine(), T).unwrap();
6688 let backward = polyline_of_edge(&model, &edge.reversed(), fine(), T).unwrap();
6689 assert!(forward.len() >= 2);
6690 assert!(forward[0].is_equal(backward[backward.len() - 1], T));
6691 assert!(forward[forward.len() - 1].is_equal(backward[0], T));
6692 }
6693
6694 #[test]
6695 fn triangulating_something_that_is_not_a_face_is_refused() {
6696 let mut model = Model::new();
6697 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
6698 assert!(triangulate_face(&model, &built.shape, fine(), T).is_err());
6699
6700 let vertex = explore_unique(&model, &built.shape, ShapeType::Vertex).unwrap()[0].clone();
6701 assert!(triangulate_face(&model, &vertex, fine(), T).is_err());
6702 assert!(polyline_of_edge(&model, &vertex, fine(), T).is_err());
6703 }
6704
6705 #[test]
6706 fn a_triangles_normal_is_none_when_it_is_degenerate() {
6707 let a = Point::ORIGIN;
6708 let b = Point::new(1.0, 0.0, 0.0);
6709 assert!(triangle_normal(a, b, Point::new(0.0, 1.0, 0.0), T).is_some());
6710 assert!(triangle_normal(a, b, Point::new(2.0, 0.0, 0.0), T).is_none());
6711 assert!(triangle_normal(a, a, a, T).is_none());
6712 }
6713
6714 fn plate_with(at: Vector, hole: impl Fn(&mut Model, Vector) -> Shape) -> (i64, f64) {
6718 use ogeom_algo::{make_face, make_polygon};
6719 let mut model = Model::new();
6720 let p = |x: f64, y: f64| Point::new(x, y, 0.0) + at;
6721 let outer = make_polygon(
6722 &mut model,
6723 &[p(0.0, 0.0), p(10.0, 0.0), p(10.0, 10.0), p(0.0, 10.0)],
6724 true,
6725 T,
6726 )
6727 .unwrap()
6728 .shape;
6729 let inner = hole(&mut model, at);
6730 let plane: SurfaceGeometry = ogeom_geom::PlaneSurface::new(ogeom_math::Plane::through(
6731 Point::ORIGIN + at,
6732 Direction::Z,
6733 ))
6734 .into();
6735 let face = make_face(&mut model, plane, &[outer, inner], T)
6736 .unwrap()
6737 .shape;
6738 ogeom_algo::build::trimmed_where_bare(&mut model, &face, T).unwrap();
6739 let mesh = triangulate_face(&model, &face, Deflection::default(), T).unwrap();
6740 let mut edges = ogeom_core::FastSet::default();
6741 let mut area = 0.0;
6742 for t in &mesh.triangles {
6743 for i in 0..3 {
6744 let (a, b) = (t[i], t[(i + 1) % 3]);
6745 edges.insert((a.min(b), a.max(b)));
6746 }
6747 let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
6748 area += 0.5 * (b - a).cross(c - a).magnitude();
6749 }
6750 #[allow(clippy::cast_possible_wrap)]
6751 let euler = mesh.positions.len() as i64 - edges.len() as i64 + mesh.triangles.len() as i64;
6752 (euler, area)
6753 }
6754
6755 #[test]
6756 fn a_hole_thinner_than_a_micron_stays_a_hole() {
6757 let (euler, area) = plate_with(Vector::ZERO, |model, _| {
6762 let p = |x: f64, y: f64| Point::new(x, y, 0.0);
6763 ogeom_algo::make_polygon(
6764 model,
6765 &[p(4.0, 5.0), p(4.0, 5.0006), p(6.0, 5.0006), p(6.0, 5.0)],
6766 true,
6767 T,
6768 )
6769 .unwrap()
6770 .shape
6771 });
6772 assert_eq!(euler, 0, "one hole: V - E + F is 1 - 1");
6773 assert_relative_eq!(area, 100.0 - 2.0 * 0.0006, epsilon = 1e-7);
6774 }
6775
6776 #[test]
6777 fn a_ring_walked_out_and_back_thinner_than_a_micron_is_a_slit() {
6778 for offset in [0.0, 1.0e6, 1.0e7, 2.0e7] {
6784 let (euler, area) = slit_plate(Vector::new(offset, -0.7 * offset, 0.3 * offset));
6785 assert_eq!(euler, 1, "no hole at {offset:e}: V - E + F is 1");
6786 assert_relative_eq!(area, 100.0, epsilon = 1e-6);
6787 }
6788 }
6789
6790 #[test]
6791 fn a_small_round_hole_in_two_halves_stays_a_hole_far_from_the_origin() {
6792 for radius in [0.005, 0.0004] {
6800 for offset in [0.0, 1.0e6, 1.0e7, 2.0e7] {
6801 let at = Vector::new(offset, -0.7 * offset, 0.3 * offset);
6802 let (euler, _) = plate_with(at, |model, by| {
6803 use ogeom_algo::{make_edge_between, make_wire};
6804 let frame = ogeom_math::Frame::new(
6805 Point::new(5.0, 5.0, 0.0) + by,
6806 Direction::Z,
6807 Direction::X,
6808 T,
6809 )
6810 .unwrap();
6811 let circle = ogeom_math::Circle::new(frame, radius, T).unwrap();
6812 let ends = [
6813 frame.origin() + frame.x().vector() * radius,
6814 frame.origin() - frame.x().vector() * radius,
6815 ];
6816 let v: Vec<Shape> = ends
6817 .iter()
6818 .map(|q| model.add_vertex(ogeom_topo::VertexData::new(*q)))
6819 .collect();
6820 let half = |model: &mut Model, range: (f64, f64), from: &Shape, to: &Shape| {
6821 let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
6822 make_edge_between(model, curve, range, from, to, T)
6823 .unwrap()
6824 .shape
6825 };
6826 let pi = core::f64::consts::PI;
6827 let edges = [
6828 half(model, (0.0, pi), &v[0], &v[1]),
6829 half(model, (pi, 2.0 * pi), &v[1], &v[0]),
6830 ];
6831 make_wire(model, &edges, T).unwrap().shape.reversed()
6832 });
6833 assert_eq!(
6834 euler, 0,
6835 "radius {radius}, one hole at {offset:e}: V - E + F is 1 - 1"
6836 );
6837 }
6838 }
6839 }
6840
6841 #[test]
6842 fn two_edges_lying_along_each_other_thinner_than_a_micron_are_a_slit() {
6843 let (euler, area) = plate_with(Vector::ZERO, |model, _| {
6849 use ogeom_algo::{Spacing, interpolate, make_edge_between, make_wire};
6850 use ogeom_geom::Curve3d as _;
6851 let at = [Point::new(4.0, 5.0, 0.0), Point::new(6.0, 5.0, 0.0)];
6852 let v: Vec<Shape> = at
6853 .iter()
6854 .map(|q| model.add_vertex(ogeom_topo::VertexData::new(*q)))
6855 .collect();
6856 let line: ogeom_geom::Curve = ogeom_geom::LineCurve::segment(at[0], at[1], T)
6857 .unwrap()
6858 .into();
6859 let out = make_edge_between(model, line, (0.0, 2.0), &v[0], &v[1], T)
6860 .unwrap()
6861 .shape;
6862 let mid = at[0].midpoint(at[1]) + Vector::new(0.0, 0.0006, 0.0);
6863 let arc: ogeom_geom::Curve =
6864 interpolate(&[at[1], mid, at[0]], 2, Spacing::Centripetal, T)
6865 .unwrap()
6866 .into();
6867 let range = arc.domain();
6868 let back = make_edge_between(model, arc, range, &v[1], &v[0], T)
6869 .unwrap()
6870 .shape;
6871 make_wire(model, &[out, back], T).unwrap().shape
6872 });
6873 assert_eq!(euler, 1, "no hole: V - E + F is 1");
6874 assert_relative_eq!(area, 100.0, epsilon = 1e-6);
6877 }
6878
6879 fn slit_plate(at: Vector) -> (i64, f64) {
6881 plate_with(at, |model, by| {
6882 use ogeom_algo::{Spacing, interpolate, make_edge_between, make_wire};
6883 use ogeom_geom::Curve3d as _;
6884 let p = |x: f64, y: f64| Point::new(x, y, 0.0) + by;
6885 let at = [p(3.0, 5.0), p(5.0, 5.0), p(7.0, 5.0)];
6886 let v: Vec<Shape> = at
6887 .iter()
6888 .map(|q| model.add_vertex(ogeom_topo::VertexData::new(*q)))
6889 .collect();
6890 let mut edges = Vec::new();
6891 for i in 0..2 {
6892 let line: ogeom_geom::Curve = ogeom_geom::LineCurve::segment(at[i], at[i + 1], T)
6893 .unwrap()
6894 .into();
6895 let range = (0.0, at[i].distance(at[i + 1]));
6896 edges.push(
6897 make_edge_between(model, line, range, &v[i], &v[i + 1], T)
6898 .unwrap()
6899 .shape,
6900 );
6901 }
6902 for i in (0..2).rev() {
6903 let mid = at[i].midpoint(at[i + 1]) + Vector::new(0.0, 0.0006, 0.0);
6904 let arc: ogeom_geom::Curve =
6905 interpolate(&[at[i + 1], mid, at[i]], 2, Spacing::Centripetal, T)
6906 .unwrap()
6907 .into();
6908 let range = arc.domain();
6909 edges.push(
6910 make_edge_between(model, arc, range, &v[i + 1], &v[i], T)
6911 .unwrap()
6912 .shape,
6913 );
6914 }
6915 make_wire(model, &edges, T).unwrap().shape
6916 })
6917 }
6918}
6919
6920#[cfg(test)]
6921#[allow(clippy::unwrap_used)]
6922mod predicate_tests {
6923 use super::*;
6924 use ogeom_core::Fast;
6925
6926 fn sliver() -> Vec<Vec<Point2>> {
6933 vec![vec![
6934 Point2::new(0.5, 0.5),
6935 Point2::new(1000.0, 1000.0),
6936 Point2::new(1000.0, 0.5),
6937 ]]
6938 }
6939
6940 #[test]
6941 fn the_two_implementations_are_a_real_choice_and_not_a_decoration() {
6942 let rings = sliver();
6950 let step = 2.0_f64.powi(-48);
6951 let mut disagreements = 0;
6952 for i in 0..256i32 {
6953 for j in 0..256i32 {
6954 let p = Point2::new(
6955 250.0 + f64::from(i - 128) * step,
6956 250.0 + f64::from(j - 128) * step,
6957 );
6958 if inside_boundary_with::<Exact>(&rings, p)
6959 != inside_boundary_with::<Fast>(&rings, p)
6960 {
6961 disagreements += 1;
6962 }
6963 }
6964 }
6965 assert!(
6966 disagreements > 0,
6967 "the exact and fast predicates never disagreed, so the seam is not \
6968 carrying anything"
6969 );
6970 }
6971
6972 #[test]
6973 fn the_exact_predicate_is_the_one_that_is_right_near_the_edge() {
6974 let rings = sliver();
6983 let base = 250.0_f64;
6984 let mut off = base;
6985 for k in 1..64 {
6986 off = off.next_up();
6987 assert_ne!(off, base, "step {k} did not move the point at all");
6988 assert!(
6990 inside_boundary_with::<Exact>(&rings, Point2::new(off, base)),
6991 "a point {k} ulps below the diagonal was reported outside"
6992 );
6993 assert!(
6994 !inside_boundary_with::<Exact>(&rings, Point2::new(base, off)),
6995 "a point {k} ulps above the diagonal was reported inside"
6996 );
6997 }
6998 }
6999
7000 #[test]
7001 fn the_exact_answer_is_the_one_the_geometry_supports() {
7002 let square = vec![vec![
7005 Point2::new(0.0, 0.0),
7006 Point2::new(1.0, 0.0),
7007 Point2::new(1.0, 1.0),
7008 Point2::new(0.0, 1.0),
7009 ]];
7010 assert!(inside_boundary_with::<Exact>(
7011 &square,
7012 Point2::new(0.5, 0.5)
7013 ));
7014 assert!(!inside_boundary_with::<Exact>(
7015 &square,
7016 Point2::new(1.5, 0.5)
7017 ));
7018 assert!(!inside_boundary_with::<Exact>(
7019 &square,
7020 Point2::new(-0.5, 0.5)
7021 ));
7022 assert!(!inside_boundary_with::<Exact>(
7023 &square,
7024 Point2::new(0.5, 1.5)
7025 ));
7026
7027 let diamond = vec![vec![
7030 Point2::new(0.0, 0.0),
7031 Point2::new(1.0, 1.0),
7032 Point2::new(2.0, 0.0),
7033 Point2::new(1.0, -1.0),
7034 ]];
7035 assert!(inside_boundary_with::<Exact>(
7036 &diamond,
7037 Point2::new(1.0, 0.0)
7038 ));
7039 assert!(!inside_boundary_with::<Exact>(
7040 &diamond,
7041 Point2::new(3.0, 0.0)
7042 ));
7043 assert!(!inside_boundary_with::<Exact>(
7044 &diamond,
7045 Point2::new(-1.0, 0.0)
7046 ));
7047 }
7048
7049 #[test]
7052 fn rows_are_spread_until_no_cell_is_taller_than_its_aspect() {
7053 let sagged = vec![0.0, 50.0, 100.0];
7056 let spread = spread_to_aspect(
7057 sagged.clone(),
7058 CELL_ASPECT,
7059 0.0,
7060 16.0,
7061 |_| 17,
7062 |i| (sagged[i + 1] - sagged[i]).abs(),
7063 |a, b, _| (b - a).abs(),
7064 );
7065 assert_eq!(
7066 spread.len(),
7067 2 * 9 + 1,
7068 "each 50 mm half in nine 6 mm pieces: {spread:?}"
7069 );
7070 assert_eq!(spread[0], 0.0);
7071 assert_eq!(spread[9], 50.0, "the rows sag chose stay where they were");
7072 assert_eq!(spread[18], 100.0);
7073 assert!(spread.windows(2).all(|w| w[1] > w[0]));
7074
7075 let flat = spread_to_aspect(
7077 sagged.clone(),
7078 CELL_ASPECT,
7079 0.0,
7080 16.0,
7081 |_| 3,
7082 |i| (sagged[i + 1] - sagged[i]).abs(),
7083 |a, b, _| (b - a).abs(),
7084 );
7085 assert_eq!(flat, sagged);
7086
7087 let fine = vec![0.0, 4.0, 8.0];
7089 let same = spread_to_aspect(
7090 fine.clone(),
7091 CELL_ASPECT,
7092 0.0,
7093 16.0,
7094 |_| 17,
7095 |i| (fine[i + 1] - fine[i]).abs(),
7096 |a, b, _| (b - a).abs(),
7097 );
7098 assert_eq!(same, fine);
7099 }
7100
7101 #[test]
7104 fn a_ring_is_cleaned_of_duplicates_and_spikes() {
7105 let p = |x: f64, y: f64| Point2::new(x, y);
7106 let mut ring = vec![
7109 p(0.0, 0.0),
7110 p(10.0, 0.0),
7111 p(10.0, 5.0),
7112 p(10.2, 5.0),
7113 p(10.0, 5.0 + 1e-9),
7114 p(10.0, 10.0),
7115 p(0.0, 10.0),
7116 p(1e-9, 1e-9),
7117 ];
7118 let mut anchors = vec![None; ring.len()];
7119 let reach = chart_reach(&ring);
7120 merge_near_duplicates(&mut ring, &mut anchors, reach);
7121 assert_eq!(ring.len(), 7, "the closing duplicate is gone: {ring:?}");
7122 remove_spikes(&mut ring, &mut anchors, reach);
7123 assert_eq!(
7124 ring,
7125 vec![
7126 p(0.0, 0.0),
7127 p(10.0, 0.0),
7128 p(10.0, 5.0),
7129 p(10.0, 10.0),
7130 p(0.0, 10.0)
7131 ],
7132 "the spike is gone and the ring is the square with a point on one side"
7133 );
7134 assert_eq!(anchors.len(), ring.len());
7135 }
7136
7137 #[test]
7144 fn a_ring_narrow_in_one_chart_direction_keeps_its_width() {
7145 let p = |x: f64, y: f64| Point2::new(x, y);
7146 let w = 1.77e-4;
7147 let strip = vec![
7148 p(0.0, 0.0),
7149 p(w / 2.0, 0.0),
7150 p(w, 0.0),
7151 p(w, 365.0),
7152 p(w / 2.0, 365.0),
7153 p(0.0, 365.0),
7154 ];
7155 let mut ring = strip.clone();
7156 ring.push(p(w * 1e-9, 1e-9));
7157 let mut anchors = vec![None; ring.len()];
7158 let reach = chart_reach(&ring);
7159 merge_near_duplicates(&mut ring, &mut anchors, reach);
7160 remove_spikes(&mut ring, &mut anchors, reach);
7161 assert_eq!(ring, strip, "the strip keeps every corner");
7162 assert_eq!(anchors.len(), ring.len());
7163 }
7164
7165 #[test]
7166 fn a_hole_is_outside_however_either_ring_is_wound() {
7167 let outer = vec![
7170 Point2::new(0.0, 0.0),
7171 Point2::new(4.0, 0.0),
7172 Point2::new(4.0, 4.0),
7173 Point2::new(0.0, 4.0),
7174 ];
7175 let hole: Vec<Point2> = vec![
7176 Point2::new(1.0, 1.0),
7177 Point2::new(3.0, 1.0),
7178 Point2::new(3.0, 3.0),
7179 Point2::new(1.0, 3.0),
7180 ];
7181 let backwards: Vec<Point2> = hole.iter().rev().copied().collect();
7182 for inner in [hole, backwards] {
7183 let rings = vec![outer.clone(), inner];
7184 assert!(!inside_boundary_with::<Exact>(
7185 &rings,
7186 Point2::new(2.0, 2.0)
7187 ));
7188 assert!(inside_boundary_with::<Exact>(&rings, Point2::new(0.5, 0.5)));
7189 }
7190 }
7191}