1mod arrange;
41mod aside;
42mod ball_chart;
43mod bins;
44mod box_tree;
45mod defeature;
46mod half_space;
47mod mesh_region;
48mod seam_join;
49mod seated;
50mod section_face;
51mod self_union;
52mod sub_shapes;
53#[cfg(feature = "test-hooks")]
54#[doc(hidden)]
55pub mod work;
56
57pub use ball_chart::TOUCHING as POLE_TOUCHING;
58pub use ball_chart::{POLE_MARGIN, Recharted, rechart_clear_of_poles};
59pub use defeature::remove_faces;
60pub use mesh_region::cut_by_mesh_region;
61pub use section_face::section_face;
62pub use self_union::self_union;
63
64use ogeom_algo::{
65 Built, Containment, History, is_shell_closed, make_edge_between, make_face_on, make_vertex,
66 make_wire, sew_around, sew_around_within, shape_bounds,
67};
68use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
69use ogeom_geom::Curve2d as _;
70use ogeom_geom::Curve3d as _;
71use ogeom_geom::Surface as _;
72use ogeom_geom::{Curve, PlanarCurve, SurfaceGeometry, Transformable};
73use ogeom_math::{Point, Point2};
74use ogeom_topo::{
75 EdgeRepr, Filter, Location, Model, NodeData, Shape, ShapeType, explore, explore_unique,
76};
77
78use arrange::{Strand, Traversal, assemble as arrange_pieces, inside_many, inside_rings};
79
80const SCAFFOLD_CHORD: f64 = 1e-3;
82
83const STRAND_SAG: f64 = 1e-4;
92
93fn sag_count(
98 at: &dyn Fn(f64) -> OgeomResult<Point2>,
99 lo: f64,
100 hi: f64,
101 mut count: usize,
102) -> OgeomResult<(usize, f64)> {
103 const MOST: usize = 512;
104 let mut measured = f64::INFINITY;
105 for _ in 0..3 {
106 if count >= MOST {
107 break;
108 }
109 let mut sag = 0.0_f64;
110 #[allow(clippy::cast_precision_loss)]
111 let step = (hi - lo) / count as f64;
112 let mut previous = at(lo)?;
113 for i in 1..=count {
114 #[allow(clippy::cast_precision_loss)]
115 let t = (i as f64).mul_add(step, lo);
116 let here = at(t)?;
117 let middle = at(t - step / 2.0)?;
118 sag = sag.max(middle.distance(Point2::midpoint(previous, here)));
119 previous = here;
120 }
121 if sag <= STRAND_SAG {
122 measured = sag;
123 break;
124 }
125 #[allow(
127 clippy::cast_possible_truncation,
128 clippy::cast_sign_loss,
129 clippy::cast_precision_loss
130 )]
131 let raised = ((count as f64) * (sag / STRAND_SAG).sqrt()).ceil() as usize;
132 count = raised.clamp(count + 1, MOST);
133 }
134 Ok((count, measured))
135}
136
137fn circle_sag_count(pcurve: &PlanarCurve, lo: f64, hi: f64, count: usize) -> Option<usize> {
140 const MOST: usize = 512;
141 let PlanarCurve::Circle(circle) = pcurve else {
142 return None;
143 };
144 let radius = circle.circle().radius();
145 if radius <= STRAND_SAG {
146 return Some(count);
147 }
148 let step = 2.0 * (1.0 - STRAND_SAG / radius).acos();
149 #[allow(
150 clippy::cast_possible_truncation,
151 clippy::cast_sign_loss,
152 clippy::cast_precision_loss
153 )]
154 let needed = ((hi - lo).abs() / step).ceil().min(MOST as f64) as usize;
155 Some(count.max(needed))
156}
157
158fn circle_sag(pcurve: &PlanarCurve, lo: f64, hi: f64, count: usize) -> Option<f64> {
162 let PlanarCurve::Circle(circle) = pcurve else {
163 return None;
164 };
165 #[allow(clippy::cast_precision_loss)]
166 let step = (hi - lo).abs() / count.max(1) as f64;
167 Some(circle.circle().radius() * (1.0 - (step / 2.0).cos()))
168}
169
170const PARAM_SNAP: f64 = 1e-6;
172
173const SEAM_HAIR: f64 = 1e-3;
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
187struct EdgeKey {
188 node: ogeom_topo::TShapeId,
189 placement: u64,
190}
191
192impl EdgeKey {
193 fn of(edge: &Shape) -> Self {
194 use core::hash::{Hash as _, Hasher as _};
195 if edge.location().is_identity() {
197 return Self {
198 node: edge.node(),
199 placement: 0,
200 };
201 }
202 let mut hasher = std::collections::hash_map::DefaultHasher::new();
203 edge.location().hash(&mut hasher);
204 Self {
205 node: edge.node(),
206 placement: hasher.finish(),
207 }
208 }
209
210 fn index(self) -> u32 {
212 self.node.index()
213 }
214}
215
216struct Occurrence {
219 key: EdgeKey,
220 curve: std::sync::Arc<Curve>,
222 ends: [(Point, f64); 2],
227 vertices: [Point; 2],
231 ends_tolerance: f64,
233 sampled: (ogeom_math::Aabb, ogeom_math::Aabb, f64),
237}
238
239impl Occurrence {
240 fn of(
241 model: &Model,
242 edge: &Shape,
243 curve: Curve,
244 range: (f64, f64),
245 tol: Tolerances,
246 ) -> OgeomResult<Self> {
247 let vertices = model.children_of(edge)?;
248 let ends_tolerance = vertices
249 .iter()
250 .filter_map(|v| model.node(v).and_then(|n| n.data().as_vertex()))
251 .fold(0.0_f64, |acc, d| acc.max(d.tolerance.get()));
252 let (a, b) = (curve.point_at(range.0, tol)?, curve.point_at(range.1, tol)?);
253 let mut ends = [(a, tol.confusion()), (b, tol.confusion())];
254 let mut at_vertex = [a, b];
255 for v in &vertices {
256 if let Some(d) = model.node(v).and_then(|n| n.data().as_vertex()) {
257 let at = v.transform(model.datums())?.apply(d.point);
258 let k = usize::from(at.distance(b) < at.distance(a));
259 ends[k].1 = ends[k].1.max(d.tolerance.get());
260 at_vertex[k] = at;
261 }
262 }
263 let mut previous: Option<Point> = None;
264 let (mut points, mut mids) = (ogeom_math::Aabb::EMPTY, ogeom_math::Aabb::EMPTY);
265 let mut sag = 0.0_f64;
266 for i in 0..=16 {
267 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
268 let t = range.0 + (range.1 - range.0) * f64::from(i) / 16.0;
269 let p = curve.point_at(t, tol)?;
270 if let Some(q) = previous {
271 let step = (range.1 - range.0) / 32.0;
272 let mid = curve.point_at(t - step, tol)?;
273 sag = sag.max(mid.distance(Point::midpoint(q, p)) * 2.0);
274 mids = mids.with_point(mid);
275 }
276 points = points.with_point(p);
277 previous = Some(p);
278 }
279 Ok(Self {
280 key: EdgeKey::of(edge),
281 curve: std::sync::Arc::new(curve),
282 ends,
283 vertices: at_vertex,
284 ends_tolerance,
285 sampled: (points, mids, sag),
286 })
287 }
288}
289
290struct BoundaryEdge {
293 node: EdgeKey,
295 curve: std::sync::Arc<Curve>,
297 crange: (f64, f64),
299 pcurve: PlanarCurve,
301 prange: (f64, f64),
303 other_side: Option<(PlanarCurve, (f64, f64))>,
305 seam: Option<(PlanarCurve, PlanarCurve)>,
310 tolerance: f64,
314 standoff: f64,
324 residual: f64,
328 walked: Option<bool>,
332 image: Option<std::sync::Arc<Curve>>,
340 ends_tolerance: f64,
344 ends: [(Point, f64); 2],
348 vertices: [Point; 2],
351 bound: ogeom_math::Aabb,
354}
355
356const DRAWN_APART: f64 = 1e2;
360
361const LEAN_SPREAD: f64 = 10.0;
366
367impl BoundaryEdge {
368 fn drawn(&self) -> (&Curve, (f64, f64), f64) {
372 match &self.image {
373 Some(image) => (image, ordered(self.prange), 0.0),
374 None => (&self.curve, self.crange, self.standoff),
375 }
376 }
377
378 fn onto_curve(&self, s: f64) -> f64 {
382 if self.image.is_some() {
383 rescale(s, self.prange, self.crange)
384 } else {
385 s
386 }
387 }
388
389 fn onto_drawn(&self, t: f64) -> f64 {
391 if self.image.is_some() {
392 rescale(t, self.crange, self.prange)
393 } else {
394 t
395 }
396 }
397}
398
399fn ordered(range: (f64, f64)) -> (f64, f64) {
401 if range.0 <= range.1 {
402 range
403 } else {
404 (range.1, range.0)
405 }
406}
407
408struct PoleEdge {
415 point: Point,
417 pcurve: PlanarCurve,
419 prange: (f64, f64),
420}
421
422struct GFace {
424 face: Shape,
425 surface: SurfaceGeometry,
427 bound: ogeom_math::Aabb,
434 filter: ogeom_math::Aabb,
439 chord_scale: f64,
443 tolerance: f64,
446 edges: Vec<BoundaryEdge>,
447 poles: Vec<PoleEdge>,
448 slits: ogeom_core::FastSet<EdgeKey>,
451 outline: std::sync::OnceLock<Option<Vec<Vec<Point2>>>>,
457 outline_trim: std::sync::OnceLock<Option<arrange::Trim>>,
460 holes_aside: std::sync::Arc<[aside::Hole]>,
463 own_surface: Option<ogeom_topo::SurfaceId>,
467 carrier: (ogeom_topo::SurfaceId, Location),
470 trim_lines: std::sync::OnceLock<Vec<Vec<Point2>>>,
473}
474
475impl GFace {
476 fn trim_lines(&self, tol: Tolerances) -> &[Vec<Point2>] {
477 self.trim_lines.get_or_init(|| face_trim_lines(self, tol))
478 }
479
480 fn outline_trim(&self, tol: Tolerances) -> Option<&arrange::Trim> {
481 self.outline_trim
482 .get_or_init(|| {
483 self.outline(tol)
484 .map(|lines| arrange::Trim::new(lines.to_vec()))
485 })
486 .as_ref()
487 }
488
489 fn outline(&self, tol: Tolerances) -> Option<&[Vec<Point2>]> {
490 self.outline
491 .get_or_init(|| {
492 let mut lines: Vec<Vec<Point2>> = Vec::new();
493 for e in &self.edges {
494 lines.push(
495 pcurve_polyline_within_sag(
496 &e.pcurve,
497 e.prange,
498 e.crange,
499 e.crange,
500 &self.surface,
501 tol,
502 )
503 .ok()?,
504 );
505 if let Some((other, orange)) = &e.other_side {
509 lines.push(
510 pcurve_polyline_within_sag(
511 other,
512 *orange,
513 e.crange,
514 e.crange,
515 &self.surface,
516 tol,
517 )
518 .ok()?,
519 );
520 }
521 }
522 weld_outline_ends(&mut lines, self, tol);
523 bridge_outline_gaps(&mut lines);
524 Some(lines)
525 })
526 .as_deref()
527 }
528}
529
530fn is_solid_or_lumps(model: &Model, shape: &Shape) -> OgeomResult<bool> {
534 Ok(match model.kind_of(shape)? {
535 ShapeType::Solid => true,
536 ShapeType::Compound => {
537 let parts = model.children_of(shape)?;
538 !parts.is_empty()
539 && parts
540 .iter()
541 .map(|part| model.kind_of(part))
542 .collect::<OgeomResult<Vec<_>>>()?
543 .iter()
544 .all(|kind| *kind == ShapeType::Solid)
545 }
546 _ => false,
547 })
548}
549
550fn is_sheet(model: &Model, shape: &Shape) -> OgeomResult<bool> {
553 Ok(match model.kind_of(shape)? {
554 ShapeType::Face | ShapeType::Shell => true,
555 ShapeType::Compound => {
556 let parts = model.children_of(shape)?;
557 !parts.is_empty()
558 && parts
559 .iter()
560 .map(|part| model.kind_of(part))
561 .collect::<OgeomResult<Vec<_>>>()?
562 .iter()
563 .all(|kind| matches!(kind, ShapeType::Face | ShapeType::Shell))
564 }
565 _ => false,
566 })
567}
568
569fn trim_sheet_edges(model: &mut Model, sheet: &Shape, tol: Tolerances) -> OgeomResult<()> {
573 for face in explore_unique(model, sheet, ShapeType::Face)? {
574 ogeom_algo::build::trimmed_where_bare(model, &face, tol)?;
575 }
576 Ok(())
577}
578
579struct GSolid {
581 solid: Shape,
582 faces: Vec<GFace>,
583 aside: Vec<(Shape, usize)>,
586 shared: ogeom_core::FastMap<EdgeKey, Shape>,
590 shared_vertices: Vec<(Point, Shape)>,
593 order: ogeom_core::FastMap<ogeom_topo::SameKey, usize>,
595 beside: std::sync::Arc<aside::Beside>,
597 closed: bool,
599}
600
601#[derive(Debug, Clone, Copy, PartialEq, Eq)]
603enum Operands {
604 Solids,
607 SheetBySolid,
610 SheetBySheet,
613 Itself,
617}
618
619fn gather(
620 model: &Model,
621 solid: &Shape,
622 sheet: bool,
623 aside: Option<&aside::Aside>,
624 tol: Tolerances,
625) -> OgeomResult<GSolid> {
626 let mut read_closed = false;
627 if sheet {
628 if !is_sheet(model, solid)? {
629 ogeom_bail!(
630 Construction,
631 "a sheet argument is a face, a shell or a compound of those"
632 );
633 }
634 } else {
635 if !is_solid_or_lumps(model, solid)? {
636 ogeom_bail!(Construction, "boolean arguments are solids");
637 }
638 let shells = explore_unique(model, solid, ShapeType::Shell)?;
639 read_closed =
642 shells.len() == 1 && aside.is_some_and(|aside| !aside.is_empty() && aside.closed);
643 for shell in shells {
644 if !read_closed && !is_shell_closed(model, &shell)? {
645 ogeom_bail!(Construction, "an open shell bounds no volume to operate on");
646 }
647 }
648 }
649
650 let mut faces = Vec::new();
651 let mut shared = ogeom_core::FastMap::default();
652 let mut occurrences: ogeom_core::FastMap<(ogeom_topo::TShapeId, Location), Occurrence> =
655 ogeom_core::FastMap::default();
656 let aside = aside.filter(|aside| !aside.is_empty());
657 for face in explore(model, solid, Filter::OfType(ShapeType::Face))? {
658 if aside.is_some_and(|aside| {
659 !aside
660 .gathered
661 .contains_key(&ogeom_topo::SameKey(face.clone()))
662 }) {
663 continue;
664 }
665 let Some(node) = model.node(&face) else {
666 ogeom_bail!(Dangling, "face is not in this model");
667 };
668 let NodeData::Face(data) = node.data() else {
669 ogeom_bail!(Construction, "face node holds no face data");
670 };
671 let Some(stored) = model.geometry().surface(data.surface) else {
672 ogeom_bail!(Dangling, "face refers to a surface not in this model");
673 };
674 let placement = face.transform(model.datums())?;
675 if (placement.scale_factor().abs() - 1.0).abs() > 1e-9 {
676 ogeom_bail!(
677 NotDone,
678 "a scaled placement changes a surface's parameterization out \
679 from under its pcurves; bake the scale before a boolean"
680 );
681 }
682 let surface = stored.transformed(&placement, tol)?;
683 let surface_id = data.surface;
684 let tolerance = data.tolerance.get();
685
686 let holes_aside = aside
689 .and_then(|aside| aside.holes.get(&ogeom_topo::SameKey(face.clone())))
690 .cloned()
691 .unwrap_or_else(|| std::sync::Arc::from(Vec::new()));
692 let slits = slits(model, &face, &surface, surface_id, tol)?;
693 let mut walks: ogeom_core::FastMap<EdgeKey, (bool, bool)> = ogeom_core::FastMap::default();
696 for edge in explore(model, &face, Filter::OfType(ShapeType::Edge))? {
697 let forward = edge.orientation() == ogeom_topo::Orientation::Forward;
698 let walk = walks.entry(EdgeKey::of(&edge)).or_default();
699 if forward {
700 walk.0 = true;
701 } else {
702 walk.1 = true;
703 }
704 }
705 let mut face_edges = if holes_aside.is_empty() {
706 explore_unique(model, &face, ShapeType::Edge)?
707 } else {
708 let wires = model.children_of(&face)?;
709 let mut left = vec![false; wires.len()];
710 for hole in holes_aside.iter() {
711 left[hole.at] = true;
712 }
713 let mut seen = ogeom_core::FastSet::default();
714 let mut edges = Vec::new();
715 for (wire, _) in wires.iter().zip(&left).filter(|(_, left)| !**left) {
716 for edge in explore_unique(model, wire, ShapeType::Edge)? {
717 if seen.insert(ogeom_topo::SameKey(edge.clone())) {
718 edges.push(edge);
719 }
720 }
721 }
722 edges
723 };
724 face_edges.retain(|edge| !slits.contains(&EdgeKey::of(edge)));
725 let mut shares = false;
726
727 let mut edges = Vec::new();
728 let mut samples: Vec<(ogeom_math::Aabb, ogeom_math::Aabb, f64)> = Vec::new();
729 let mut poles = Vec::new();
730 let mut pole_keys = Vec::new();
731 for edge in face_edges {
732 if let Some(aside) = aside
733 && aside.edges.contains(&EdgeKey::of(&edge))
734 {
735 shared.insert(
736 EdgeKey::of(&edge),
737 edge.oriented(ogeom_topo::Orientation::Forward),
738 );
739 shares = true;
740 }
741 let Some(edge_node) = model.node(&edge) else {
742 ogeom_bail!(Dangling, "edge is not in this model");
743 };
744 let NodeData::Edge(edge_data) = edge_node.data() else {
745 ogeom_bail!(Construction, "edge node holds no edge data");
746 };
747 let Some(EdgeRepr::Curve3d { curve, range, .. }) = edge_data.curve3d() else {
748 if let Some(EdgeRepr::PCurve {
752 curve: pc, range, ..
753 }) = edge_data.pcurve_for(surface_id, edge.location())
754 {
755 let Some(planar) = model.geometry().pcurve(*pc) else {
756 ogeom_bail!(Dangling, "pcurve is not in this model");
757 };
758 let at = explore_unique(model, &edge, ShapeType::Vertex)?;
759 let Some(point) = at
760 .first()
761 .and_then(|v| model.node(v))
762 .and_then(|n| n.data().as_vertex().map(|d| d.point))
763 else {
764 ogeom_bail!(Construction, "a pole with no vertex is nowhere");
765 };
766 let placed = edge.transform(model.datums())?.apply(point);
767 poles.push(PoleEdge {
768 point: placed,
769 pcurve: planar.clone(),
770 prange: *range,
771 });
772 pole_keys.push(EdgeKey::of(&edge));
773 }
774 continue;
775 };
776 let occurrence = match occurrences.entry((edge.node(), edge.location().clone())) {
777 ogeom_core::collections::hash_map::Entry::Occupied(known) => known.into_mut(),
778 ogeom_core::collections::hash_map::Entry::Vacant(slot) => {
779 let Some(geometry) = model.geometry().curve(*curve) else {
780 ogeom_bail!(Dangling, "curve is not in this model");
781 };
782 let placed = geometry.transformed(&edge.transform(model.datums())?, tol)?;
783 slot.insert(Occurrence::of(model, &edge, placed, *range, tol)?)
784 }
785 };
786 let world = occurrence.curve.clone();
787 samples.push(occurrence.sampled);
788 let (pcurve, prange, other_side, seam) =
789 match edge_data.pcurve_for(surface_id, edge.location()) {
790 Some(EdgeRepr::PCurve {
791 curve: pc, range, ..
792 }) => {
793 let Some(planar) = model.geometry().pcurve(*pc) else {
794 ogeom_bail!(Dangling, "pcurve is not in this model");
795 };
796 (planar.clone(), *range, None, None)
797 }
798 Some(EdgeRepr::Seam {
799 forward,
800 reversed,
801 range,
802 ..
803 }) => {
804 let (Some(f), Some(r)) = (
805 model.geometry().pcurve(*forward),
806 model.geometry().pcurve(*reversed),
807 ) else {
808 ogeom_bail!(Dangling, "seam pcurve is not in this model");
809 };
810 (
811 f.clone(),
812 *range,
813 Some((r.clone(), *range)),
814 Some((f.clone(), r.clone())),
815 )
816 }
817 _ => ogeom_bail!(
818 Construction,
819 "an edge with no pcurve on its face cannot be split in \
820 that face's parameter space"
821 ),
822 };
823 let edge_tolerance = edge_data.tolerance.get();
824 let mut standoff = edge_tolerance;
825 let mut residual = edge_tolerance;
826 if edge_tolerance > tol.confusion() * 10.0 {
827 let gap = |pc: &PlanarCurve, pr: (f64, f64)| {
828 ogeom_algo::pcurve_fit::lifted_gap(
829 (&world, *range),
830 (pc, pr),
831 &surface,
832 false,
833 tol,
834 )
835 .unwrap_or(edge_tolerance)
836 };
837 let mut measured = gap(&pcurve, prange);
838 if let Some((other, orange)) = &other_side {
839 measured = measured.max(gap(other, *orange));
840 }
841 standoff = measured.min(edge_tolerance);
842 residual = measured.max(edge_tolerance);
843 }
844
845 edges.push(BoundaryEdge {
846 node: occurrence.key,
847 curve: world,
848 crange: *range,
849 pcurve,
850 prange,
851 other_side,
852 seam,
853 tolerance: edge_tolerance,
854 standoff,
855 residual,
856 walked: match walks.get(&occurrence.key) {
857 Some((true, false)) => Some(true),
858 Some((false, true)) => Some(false),
859 _ => None,
860 },
861 image: None,
862 ends_tolerance: occurrence.ends_tolerance,
863 ends: occurrence.ends,
864 vertices: occurrence.vertices,
865 bound: ogeom_math::Aabb::EMPTY,
866 });
867 }
868 if edges.is_empty() {
869 ogeom_bail!(Construction, "a face with no boundary bounds nothing");
870 }
871
872 if aside.is_none() || !(shares || !holes_aside.is_empty()) {
873 fold_seamed_rings(
874 model,
875 &face,
876 &surface,
877 &mut edges,
878 (&poles, &pole_keys),
879 &slits,
880 tol,
881 )?;
882 }
883 let seam_indices: Vec<usize> = edges
892 .iter()
893 .enumerate()
894 .filter(|(_, e)| e.other_side.is_some())
895 .map(|(i, _)| i)
896 .collect();
897 for i in seam_indices {
898 let mut pool: Vec<(Point2, f64)> = Vec::new();
901 for (j, e) in edges.iter().enumerate() {
902 if j == i {
903 continue;
904 }
905 let reach = e.tolerance.max(e.ends_tolerance);
906 for t in [e.prange.0, e.prange.1] {
907 pool.push((e.pcurve.point_at(t, tol)?, reach));
908 }
909 if let Some((other, orange)) = &e.other_side {
910 for t in [orange.0, orange.1] {
911 pool.push((other.point_at(t, tol)?, reach));
912 }
913 }
914 }
915 for p in &poles {
916 for t in [p.prange.0, p.prange.1] {
917 pool.push((p.pcurve.point_at(t, tol)?, 0.0));
918 }
919 }
920 let own = edges[i].tolerance.max(edges[i].ends_tolerance);
921 let meets = |q: Point2, at: Point2, reach: f64| -> bool {
927 let apart = q.distance(at);
928 if apart <= PARAM_SNAP {
929 return true;
930 }
931 if apart > SEAM_HAIR {
934 return false;
935 }
936 let middle = Point2::new(f64::midpoint(q.x, at.x), f64::midpoint(q.y, at.y));
937 let Ok((du, dv)) = surface.d1_at(middle.x, middle.y, tol) else {
938 return false;
939 };
940 let across = (du.magnitude() * (q.x - at.x)).hypot(dv.magnitude() * (q.y - at.y));
941 across <= 2.0 * (reach + own) + tol.confusion()
942 };
943 let connected = |pc: &PlanarCurve, range: (f64, f64)| -> OgeomResult<bool> {
944 for t in [range.0, range.1] {
945 let at = pc.point_at(t, tol)?;
946 if !pool.iter().any(|&(q, reach)| meets(q, at, reach)) {
947 return Ok(false);
948 }
949 }
950 Ok(true)
951 };
952 let e = &edges[i];
953 let primary_connects = connected(&e.pcurve, e.prange)?;
954 let (other_pc, orange) = e
955 .other_side
956 .clone()
957 .unwrap_or_else(|| (e.pcurve.clone(), e.prange));
958 let other_connects = connected(&other_pc, orange)?;
959 match (primary_connects, other_connects) {
960 (true, true) => {}
963 (true, false) => edges[i].other_side = None,
965 (false, true) => {
966 let e = &mut edges[i];
967 e.pcurve = other_pc;
968 e.prange = orange;
969 e.other_side = None;
970 }
971 (false, false) => {}
974 }
975 }
976 let stated: Vec<Option<Point2>> = edges
985 .iter()
986 .map(|e| {
987 aside
988 .is_some_and(|aside| aside.edges.contains(&e.node))
989 .then(|| {
990 e.pcurve
991 .point_at(f64::midpoint(e.prange.0, e.prange.1), tol)
992 })
993 .transpose()
994 })
995 .collect::<OgeomResult<_>>()?;
996 fold_rings(
997 model,
998 &face,
999 &surface,
1000 &mut edges,
1001 (&poles, &pole_keys),
1002 &slits,
1003 tol,
1004 )?;
1005 let repeats = [surface.is_periodic_u(), surface.is_periodic_v()];
1006 for (e, at) in edges.iter().zip(&stated) {
1007 let Some(at) = at else { continue };
1008 let now = e
1009 .pcurve
1010 .point_at(f64::midpoint(e.prange.0, e.prange.1), tol)?;
1011 if ((now.x - at.x).abs() > PARAM_SNAP && !repeats[0])
1012 || ((now.y - at.y).abs() > PARAM_SNAP && !repeats[1])
1013 {
1014 ogeom_bail!(
1015 NotDone,
1016 "a face sharing edges with faces set aside has its ring stated \
1017 across its chart's join on one of them"
1018 );
1019 }
1020 }
1021 let moved = bring_trim_home(&surface, &mut edges, &mut poles, tol)?;
1022 close_pole_joints(
1023 model,
1024 &face,
1025 &surface,
1026 (&edges, &slits),
1027 &mut poles,
1028 &pole_keys,
1029 tol,
1030 )?;
1031 let own_surface = if aside.is_some() && (shares || !holes_aside.is_empty()) {
1035 if moved {
1036 ogeom_bail!(
1037 NotDone,
1038 "a face sharing edges with faces set aside has its trim stated \
1039 a period off its chart"
1040 );
1041 }
1042 Some(surface_id)
1043 } else {
1044 None
1045 };
1046 let mut bound = ogeom_math::Aabb::EMPTY;
1047 let ruled = matches!(
1054 &surface,
1055 SurfaceGeometry::Plane(_) | SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_)
1056 );
1057 let mut slack = 0.0_f64;
1058 for (e, &(points, mids, sag)) in edges.iter_mut().zip(&samples) {
1059 if ruled {
1060 slack = slack.max(sag);
1061 bound = bound.union(&mids);
1062 }
1063 bound = bound.union(&points);
1064 e.bound = points
1065 .union(&mids)
1066 .expanded(sag + e.tolerance + tol.confusion() * 1e2);
1067 }
1068 let bulge = match &surface {
1077 SurfaceGeometry::Plane(_) | SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_) => {
1078 slack
1079 }
1080 _ => bound.diagonal() * 0.75,
1081 };
1082 let margin = tol.confusion() * 1e2;
1088 let chord_scale = match &surface {
1089 SurfaceGeometry::Plane(_) => bound.expanded(margin).diagonal(),
1090 _ => bound.expanded(bound.diagonal() * 0.75 + margin).diagonal(),
1091 };
1092 let mut bound = bound.expanded(bulge);
1099 for pole in &poles {
1100 bound = bound.with_point(pole.point);
1101 }
1102 let bound = bound.expanded(tol.confusion() * 1e2);
1103 let filter = kept_filter(model, &face, &surface, bound, tol);
1104 let carrier = (surface_id, face.location().clone());
1105 faces.push(GFace {
1106 poles,
1107 slits,
1108 face,
1109 surface,
1110 bound,
1111 filter,
1112 chord_scale,
1113 tolerance,
1114 edges,
1115 outline: std::sync::OnceLock::new(),
1116 outline_trim: std::sync::OnceLock::new(),
1117 trim_lines: std::sync::OnceLock::new(),
1118 holes_aside,
1119 own_surface,
1120 carrier,
1121 });
1122 }
1123 if faces.is_empty() {
1124 if sheet {
1125 ogeom_bail!(Construction, "a sheet with no faces has nothing to split");
1126 }
1127 ogeom_bail!(Construction, "a solid with no faces bounds nothing");
1128 }
1129 let mut shared_vertices = Vec::new();
1130 let mut seen = ogeom_core::FastSet::default();
1131 for edge in shared.values() {
1132 for vertex in model.children_of(edge)? {
1133 if !seen.insert(ogeom_topo::SameKey(vertex.clone())) {
1134 continue;
1135 }
1136 let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) else {
1137 ogeom_bail!(Dangling, "vertex is not in this model");
1138 };
1139 let at = vertex.transform(model.datums())?.apply(data.point);
1140 shared_vertices.push((at, vertex.oriented(ogeom_topo::Orientation::Forward)));
1141 }
1142 }
1143 let mut owed: ogeom_core::FastMap<EdgeKey, f64> = ogeom_core::FastMap::default();
1149 for e in faces.iter().flat_map(|f: &GFace| f.edges.iter()) {
1150 let most = owed.entry(e.node).or_insert(0.0);
1151 *most = most.max(e.standoff);
1152 }
1153 for face in &mut faces {
1154 for e in &mut face.edges {
1155 if owed
1156 .get(&e.node)
1157 .is_some_and(|most| most * 2.0 < e.tolerance)
1158 {
1159 e.standoff = e.tolerance;
1160 continue;
1161 }
1162 if e.standoff <= tol.confusion() * DRAWN_APART
1173 || e.other_side.is_some()
1174 || e.pcurve.is_periodic()
1175 || e.curve.is_periodic()
1176 {
1177 continue;
1178 }
1179 let paced = ogeom_algo::pcurve_fit::lifted_gap(
1180 (&e.curve, e.crange),
1181 (&e.pcurve, e.prange),
1182 &face.surface,
1183 true,
1184 tol,
1185 );
1186 if !paced.is_ok_and(|gap| gap <= e.tolerance * 2.0) {
1187 continue;
1188 }
1189 let (lo, hi) = ordered(e.prange);
1190 e.image = ogeom_geom::TrimmedCurve::new(
1191 Curve::OnSurface(Box::new(ogeom_geom::CurveOnSurface::new(
1192 e.pcurve.clone(),
1193 face.surface.clone(),
1194 ))),
1195 lo,
1196 hi,
1197 tol,
1198 )
1199 .ok()
1200 .map(|trimmed| std::sync::Arc::new(Curve::Trimmed(Box::new(trimmed))));
1201 }
1202 }
1203 Ok(GSolid {
1204 solid: solid.clone(),
1205 faces,
1206 aside: aside.map(|aside| aside.faces.clone()).unwrap_or_default(),
1207 shared,
1208 shared_vertices,
1209 order: aside
1210 .map(|aside| aside.gathered.clone())
1211 .unwrap_or_default(),
1212 beside: aside.map(|aside| aside.beside.clone()).unwrap_or_default(),
1213 closed: read_closed,
1214 })
1215}
1216
1217fn kept_filter(
1223 model: &Model,
1224 face: &Shape,
1225 surface: &SurfaceGeometry,
1226 bound: ogeom_math::Aabb,
1227 tol: Tolerances,
1228) -> ogeom_math::Aabb {
1229 let Ok(kept) = ogeom_algo::face_bounds(model, face) else {
1230 return bound;
1231 };
1232 let exact = matches!(
1233 surface,
1234 SurfaceGeometry::Plane(_)
1235 | SurfaceGeometry::Cylinder(_)
1236 | SurfaceGeometry::Cone(_)
1237 | SurfaceGeometry::Sphere(_)
1238 | SurfaceGeometry::Torus(_)
1239 );
1240 let mut margin = tol.confusion() * 1e2;
1241 if !exact {
1242 margin += ogeom_mesh::Deflection::default().chord;
1243 }
1244 let filter = bound.intersection(&kept.expanded(margin));
1245 if filter.is_empty() { bound } else { filter }
1246}
1247
1248fn slits(
1268 model: &Model,
1269 face: &Shape,
1270 surface: &SurfaceGeometry,
1271 surface_id: ogeom_topo::SurfaceId,
1272 tol: Tolerances,
1273) -> OgeomResult<ogeom_core::FastSet<EdgeKey>> {
1274 let mut slits = ogeom_core::FastSet::default();
1275 let ((u0, u1), (v0, v1)) = surface.domain();
1276 let ordinary = |edge: &Shape| -> OgeomResult<bool> {
1279 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
1280 return Ok(false);
1281 };
1282 if !matches!(data.curve3d(), Some(EdgeRepr::Curve3d { .. })) {
1283 return Ok(false);
1284 }
1285 let Some((start, end)) = ogeom_algo::edge_vertices(model, edge)? else {
1286 return Ok(false);
1287 };
1288 if start.is_same(&end) {
1289 return Ok(false);
1290 }
1291 let Some(tip) = model.node(&end).and_then(|n| n.data().as_vertex()) else {
1292 return Ok(false);
1293 };
1294 let reach = tip.tolerance.get().max(tol.confusion());
1295 let (pcurve, range) = match data.pcurve_for(surface_id, edge.location()) {
1296 Some(EdgeRepr::PCurve { curve, range, .. }) => (curve, range),
1297 Some(EdgeRepr::Seam { forward, range, .. }) => (forward, range),
1298 _ => return Ok(false),
1299 };
1300 let Some(pcurve) = model.geometry().pcurve(*pcurve) else {
1301 return Ok(false);
1302 };
1303 let back = edge.orientation() == ogeom_topo::Orientation::Reversed;
1304 let at = pcurve.point_at(if back { range.0 } else { range.1 }, tol)?;
1305 for along_u in [true, false] {
1306 let (lo, hi) = if along_u { (u0, u1) } else { (v0, v1) };
1307 let width = hi - lo;
1308 let mut collapses = width.is_finite();
1309 for s in [0.0, 0.5, 1.0] {
1310 if !collapses {
1311 break;
1312 }
1313 let q = if along_u {
1314 Point2::new(lo + width * s, at.y)
1315 } else {
1316 Point2::new(at.x, lo + width * s)
1317 };
1318 let Ok((du, dv)) = surface.d1_at(q.x, q.y, tol) else {
1319 return Ok(false);
1320 };
1321 let rate = if along_u {
1322 du.magnitude()
1323 } else {
1324 dv.magnitude()
1325 };
1326 collapses = rate * width <= reach;
1327 }
1328 if collapses {
1329 return Ok(false);
1330 }
1331 }
1332 Ok(true)
1333 };
1334 let stored = face.oriented(ogeom_topo::Orientation::Forward);
1335 for wire in model.children_of(&stored)? {
1336 let mut ring = model.ordered_children_of(&wire)?;
1337 'peel: loop {
1338 let n = ring.len();
1339 for k in 0..n {
1340 let (out, back) = (&ring[k], &ring[(k + 1) % n]);
1341 if n < 2
1342 || EdgeKey::of(out) != EdgeKey::of(back)
1343 || out.orientation() == back.orientation()
1344 || !ordinary(out)?
1345 {
1346 continue;
1347 }
1348 slits.insert(EdgeKey::of(out));
1349 let (first, second) = (k.min((k + 1) % n), k.max((k + 1) % n));
1350 ring.remove(second);
1351 ring.remove(first);
1352 continue 'peel;
1353 }
1354 break;
1355 }
1356 }
1357 Ok(slits)
1358}
1359
1360fn rings_without(
1362 model: &Model,
1363 face: &Shape,
1364 slits: &ogeom_core::FastSet<EdgeKey>,
1365) -> OgeomResult<Vec<Vec<Shape>>> {
1366 model
1367 .children_of(face)?
1368 .iter()
1369 .map(|wire| {
1370 let mut ring = model.ordered_children_of(wire)?;
1371 ring.retain(|edge| !slits.contains(&EdgeKey::of(edge)));
1372 Ok(ring)
1373 })
1374 .collect()
1375}
1376
1377fn fold_rings(
1417 model: &Model,
1418 face: &Shape,
1419 surface: &SurfaceGeometry,
1420 edges: &mut [BoundaryEdge],
1421 (poles, pole_keys): (&[PoleEdge], &[EdgeKey]),
1422 slits: &ogeom_core::FastSet<EdgeKey>,
1423 tol: Tolerances,
1424) -> OgeomResult<()> {
1425 #[derive(Clone, Copy)]
1430 enum Step {
1431 Edge(usize, bool),
1432 Pole(usize, bool),
1433 Seam(usize, bool),
1434 }
1435 let ((ua, ub), (va, vb)) = surface.domain();
1436 let periods = [
1437 ((surface.is_periodic_u() || surface.is_closed_u(tol)) && ub > ua).then_some(ub - ua),
1438 ((surface.is_periodic_v() || surface.is_closed_v(tol)) && vb > va).then_some(vb - va),
1439 ];
1440 if periods.iter().all(Option::is_none) {
1441 return Ok(());
1442 }
1443 let periodic = [surface.is_periodic_u(), surface.is_periodic_v()];
1444 let inside = |pc: &PlanarCurve, range: (f64, f64)| -> OgeomResult<bool> {
1448 for k in 0..=2 {
1449 let p = pc.point_at(range.0 + (range.1 - range.0) * f64::from(k) / 2.0, tol)?;
1450 if (!periodic[0] && (p.x < ua - PARAM_SNAP || p.x > ub + PARAM_SNAP))
1451 || (!periodic[1] && (p.y < va - PARAM_SNAP || p.y > vb + PARAM_SNAP))
1452 {
1453 return Ok(false);
1454 }
1455 }
1456 Ok(true)
1457 };
1458 let whole = |to: Point2, from: Point2| -> ogeom_math::Vector2 {
1461 let mut shift = ogeom_math::Vector2::new(0.0, 0.0);
1462 if let Some(period) = periods[0] {
1463 shift.x = ((to.x - from.x) / period).round() * period;
1464 }
1465 if let Some(period) = periods[1] {
1466 shift.y = ((to.y - from.y) / period).round() * period;
1467 }
1468 shift
1469 };
1470 let mut index: ogeom_core::FastMap<EdgeKey, (usize, usize)> = ogeom_core::FastMap::default();
1471 for (i, e) in edges.iter().enumerate() {
1472 index.insert(e.node, (i, 0));
1473 }
1474 let wires = rings_without(model, face, slits)?;
1475 for edge in wires.iter().flatten() {
1476 if let Some(slot) = index.get_mut(&EdgeKey::of(edge)) {
1477 slot.1 += 1;
1478 }
1479 }
1480 let apart = |a: Point2, b: Point2| -> Option<f64> {
1483 let mid = Point2::new(f64::midpoint(a.x, b.x), f64::midpoint(a.y, b.y));
1484 let (p, m, q) = (
1485 surface.point_at(a.x, a.y, tol).ok()?,
1486 surface.point_at(mid.x, mid.y, tol).ok()?,
1487 surface.point_at(b.x, b.y, tol).ok()?,
1488 );
1489 Some(p.distance(m).max(m.distance(q)))
1490 };
1491 let mut walked: Vec<Option<Vec<usize>>> = Vec::with_capacity(wires.len());
1494 for ring in &wires {
1495 let mut walk = Vec::with_capacity(ring.len());
1496 let mut kept = false;
1497 let mut sides: Vec<(usize, PlanarCurve, (f64, f64))> = Vec::new();
1500 for edge in ring {
1501 let back = edge.orientation() == ogeom_topo::Orientation::Reversed;
1502 let key = EdgeKey::of(edge);
1503 if let Some(&(i, uses)) = index.get(&key) {
1504 let e = &edges[i];
1505 if uses == 1 && e.seam.is_some() {
1506 match walked_seam_side(e, back, tol)? {
1507 Some((pcurve, range)) => {
1508 walk.push(Step::Seam(sides.len(), back));
1509 sides.push((i, pcurve, range));
1510 }
1511 None => kept = true,
1512 }
1513 continue;
1514 }
1515 kept |= uses > 1 || e.seam.is_some() || e.other_side.is_some();
1516 walk.push(Step::Edge(i, back));
1517 } else if let Some(j) = pole_keys.iter().position(|k| *k == key) {
1518 walk.push(Step::Pole(j, back));
1519 }
1520 }
1521 let moving: Vec<usize> = walk
1522 .iter()
1523 .filter_map(|s| match s {
1524 Step::Edge(i, _) => Some(*i),
1525 Step::Pole(..) | Step::Seam(..) => None,
1526 })
1527 .collect();
1528 if kept || moving.is_empty() {
1529 walked.push(None);
1530 continue;
1531 }
1532 if walk.len() < 2 {
1535 walked.push(Some(moving));
1536 continue;
1537 }
1538 let ends_of = |step: Step| -> OgeomResult<(Point2, Point2)> {
1539 let (pc, range, back) = match step {
1540 Step::Edge(i, back) => (&edges[i].pcurve, edges[i].prange, back),
1541 Step::Pole(j, back) => (&poles[j].pcurve, poles[j].prange, back),
1542 Step::Seam(k, back) => (&sides[k].1, sides[k].2, back),
1543 };
1544 let (a, b) = (pc.point_at(range.0, tol)?, pc.point_at(range.1, tol)?);
1545 Ok(if back { (b, a) } else { (a, b) })
1546 };
1547 let radius = |step: Step| match step {
1548 Step::Edge(i, _) => edges[i].tolerance.max(edges[i].ends_tolerance),
1549 Step::Seam(k, _) => edges[sides[k].0]
1550 .tolerance
1551 .max(edges[sides[k].0].ends_tolerance),
1552 Step::Pole(..) => 0.0,
1553 };
1554 let is_pole = |step: Step| matches!(step, Step::Pole(..));
1555 let meets = |a: Point2, b: Point2, step: Step, prev: Step, within: f64| -> bool {
1563 if is_pole(step) || is_pole(prev) {
1564 a.distance(b) <= within.max(PARAM_SNAP)
1565 } else {
1566 apart(a, b).is_some_and(|d| d <= within)
1567 }
1568 };
1569 let across_a_degenerate_row = |a: Point2, b: Point2| -> bool {
1577 let gap = b - a;
1578 let length = gap.magnitude();
1579 if length <= PARAM_SNAP {
1580 return false;
1581 }
1582 let middle = Point2::new(f64::midpoint(a.x, b.x), f64::midpoint(a.y, b.y));
1583 [a, middle, b].iter().all(|p| {
1584 surface
1585 .d1_at(p.x, p.y, tol)
1586 .is_ok_and(|(du, dv)| (du * gap.x + dv * gap.y).magnitude() <= tol.confusion())
1587 })
1588 };
1589 let anchor = |step: &Step| matches!(step, Step::Pole(..) | Step::Seam(..));
1590 let start = if let Some(k) = walk.iter().position(|s| is_pole(*s)) {
1591 k
1592 } else if let Some(k) = walk.iter().position(anchor) {
1593 k
1594 } else {
1595 let mut start = 0;
1596 for k in 0..walk.len() {
1597 let (step, prev) = (walk[k], walk[(k + walk.len() - 1) % walk.len()]);
1598 let (end, from) = (ends_of(prev)?.1, ends_of(step)?.0);
1599 if across_a_degenerate_row(end, from) {
1600 start = k;
1601 break;
1602 }
1603 }
1604 start
1605 };
1606 walk.rotate_left(start);
1607 let (begin, mut last) = ends_of(walk[0])?;
1608 let mut moves: Vec<(usize, PlanarCurve)> = Vec::new();
1609 let mut broken = false;
1610 for k in 1..walk.len() {
1611 let (step, prev) = (walk[k], walk[k - 1]);
1612 let (from, to) = ends_of(step)?;
1613 let within = 2.0 * (radius(step) + radius(prev)) + tol.confusion();
1614 let Step::Edge(i, _) = step else {
1615 broken |= !meets(last, from, step, prev, within);
1619 last = to;
1620 continue;
1621 };
1622 let shift = whole(last, from);
1623 let moved = Point2::new(from.x + shift.x, from.y + shift.y);
1624 let jumps = if is_pole(step) || is_pole(prev) {
1628 !meets(last, from, step, prev, within)
1629 } else {
1630 !across_a_degenerate_row(last, from)
1631 };
1632 let fold = (shift.x != 0.0 || shift.y != 0.0)
1633 && jumps
1634 && meets(last, moved, step, prev, within);
1635 last = to;
1636 if fold {
1637 let by = ogeom_math::Transform2::translation(shift);
1638 let pcurve = edges[i].pcurve.transformed(&by, tol)?;
1639 if inside(&pcurve, edges[i].prange)? {
1640 moves.push((i, pcurve));
1641 last = Point2::new(to.x + shift.x, to.y + shift.y);
1642 }
1643 }
1644 }
1645 let (first, closing) = (walk[0], walk[walk.len() - 1]);
1648 let within = 2.0 * (radius(first) + radius(closing)) + tol.confusion();
1649 if !broken && !moves.is_empty() && meets(last, begin, first, closing, within) {
1650 for (i, pcurve) in moves {
1651 edges[i].pcurve = pcurve;
1652 }
1653 for (i, pcurve, range) in sides {
1656 edges[i].pcurve = pcurve;
1657 edges[i].prange = range;
1658 edges[i].other_side = None;
1659 }
1660 }
1661 walked.push((!walk.iter().any(anchor)).then_some(moving));
1662 }
1663 let extent = |edges: &[BoundaryEdge], ring: &[usize]| -> OgeomResult<(Point2, Point2)> {
1665 let mut low = Point2::new(f64::INFINITY, f64::INFINITY);
1666 let mut high = Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
1667 for &i in ring {
1668 let e = &edges[i];
1669 for k in 0..=4 {
1670 let t = e.prange.0 + (e.prange.1 - e.prange.0) * f64::from(k) / 4.0;
1671 let p = e.pcurve.point_at(t, tol)?;
1672 low = Point2::new(low.x.min(p.x), low.y.min(p.y));
1673 high = Point2::new(high.x.max(p.x), high.y.max(p.y));
1674 }
1675 }
1676 Ok((low, high))
1677 };
1678 let Some(outer) = wires.first() else {
1679 return Ok(());
1680 };
1681 let outer: Vec<usize> = outer
1682 .iter()
1683 .filter_map(|e| index.get(&EdgeKey::of(e)).map(|&(i, _)| i))
1684 .collect();
1685 if outer.is_empty() {
1686 return Ok(());
1687 }
1688 let (olow, ohigh) = extent(edges, &outer)?;
1689 for hole in walked.iter().skip(1).flatten() {
1690 let (low, high) = extent(edges, hole)?;
1691 let centre = Point2::new(f64::midpoint(low.x, high.x), f64::midpoint(low.y, high.y));
1692 let ocentre = Point2::new(
1693 f64::midpoint(olow.x, ohigh.x),
1694 f64::midpoint(olow.y, ohigh.y),
1695 );
1696 let shift = whole(ocentre, centre);
1697 let beside =
1698 |lo: f64, hi: f64, olo: f64, ohi: f64| hi < olo - PARAM_SNAP || lo > ohi + PARAM_SNAP;
1699 let within =
1700 |lo: f64, hi: f64, olo: f64, ohi: f64| lo >= olo - PARAM_SNAP && hi <= ohi + PARAM_SNAP;
1701 let along = |s: f64, lo: f64, hi: f64, olo: f64, ohi: f64| {
1702 s == 0.0 || (beside(lo, hi, olo, ohi) && within(lo + s, hi + s, olo, ohi))
1703 };
1704 if (shift.x == 0.0 && shift.y == 0.0)
1705 || !along(shift.x, low.x, high.x, olow.x, ohigh.x)
1706 || !along(shift.y, low.y, high.y, olow.y, ohigh.y)
1707 {
1708 continue;
1709 }
1710 let by = ogeom_math::Transform2::translation(shift);
1711 let mut moved = Vec::with_capacity(hole.len());
1712 for &i in hole {
1713 let pcurve = edges[i].pcurve.transformed(&by, tol)?;
1714 if !inside(&pcurve, edges[i].prange)? {
1715 moved.clear();
1716 break;
1717 }
1718 moved.push((i, pcurve));
1719 }
1720 for (i, pcurve) in moved {
1721 edges[i].pcurve = pcurve;
1722 }
1723 }
1724 Ok(())
1725}
1726
1727fn walked_seam_side(
1734 e: &BoundaryEdge,
1735 back: bool,
1736 tol: Tolerances,
1737) -> OgeomResult<Option<(PlanarCurve, (f64, f64))>> {
1738 let Some((other, orange)) = &e.other_side else {
1739 return Ok(None);
1740 };
1741 let (a, b) = (
1742 e.pcurve.point_at(e.prange.0, tol)?,
1743 e.pcurve.point_at(e.prange.1, tol)?,
1744 );
1745 let run = if back { a - b } else { b - a };
1746 let mid = |pc: &PlanarCurve, r: (f64, f64)| pc.point_at(f64::midpoint(r.0, r.1), tol);
1747 let towards = mid(other, *orange)? - mid(&e.pcurve, e.prange)?;
1748 let left = run.x * towards.y - run.y * towards.x;
1749 Ok(if left > 0.0 {
1750 Some((e.pcurve.clone(), e.prange))
1751 } else if left < 0.0 {
1752 Some((other.clone(), *orange))
1753 } else {
1754 None
1755 })
1756}
1757
1758fn fold_seamed_rings(
1771 model: &Model,
1772 face: &Shape,
1773 surface: &SurfaceGeometry,
1774 edges: &mut [BoundaryEdge],
1775 (poles, pole_keys): (&[PoleEdge], &[EdgeKey]),
1776 slits: &ogeom_core::FastSet<EdgeKey>,
1777 tol: Tolerances,
1778) -> OgeomResult<()> {
1779 #[derive(Clone)]
1780 enum Step {
1781 Seam(usize, Point2, Point2),
1782 Edge(usize, Point2, Point2),
1783 Pole(Point2, Point2),
1784 }
1785 let ((ua, ub), (va, vb)) = surface.domain();
1786 let periods = [
1787 ((surface.is_periodic_u() || surface.is_closed_u(tol)) && ub > ua).then_some(ub - ua),
1788 ((surface.is_periodic_v() || surface.is_closed_v(tol)) && vb > va).then_some(vb - va),
1789 ];
1790 if periods.iter().all(Option::is_none) {
1791 return Ok(());
1792 }
1793 let periodic = [surface.is_periodic_u(), surface.is_periodic_v()];
1794 let whole = |to: Point2, from: Point2| -> ogeom_math::Vector2 {
1795 let mut shift = ogeom_math::Vector2::new(0.0, 0.0);
1796 if let Some(period) = periods[0] {
1797 shift.x = ((to.x - from.x) / period).round() * period;
1798 }
1799 if let Some(period) = periods[1] {
1800 shift.y = ((to.y - from.y) / period).round() * period;
1801 }
1802 shift
1803 };
1804 let inside = |pc: &PlanarCurve, range: (f64, f64)| -> OgeomResult<bool> {
1805 for k in 0..=2 {
1806 let p = pc.point_at(range.0 + (range.1 - range.0) * f64::from(k) / 2.0, tol)?;
1807 if (!periodic[0] && (p.x < ua - PARAM_SNAP || p.x > ub + PARAM_SNAP))
1808 || (!periodic[1] && (p.y < va - PARAM_SNAP || p.y > vb + PARAM_SNAP))
1809 {
1810 return Ok(false);
1811 }
1812 }
1813 Ok(true)
1814 };
1815 let apart = |a: Point2, b: Point2| -> Option<f64> {
1816 let mid = Point2::new(f64::midpoint(a.x, b.x), f64::midpoint(a.y, b.y));
1817 let (p, m, q) = (
1818 surface.point_at(a.x, a.y, tol).ok()?,
1819 surface.point_at(mid.x, mid.y, tol).ok()?,
1820 surface.point_at(b.x, b.y, tol).ok()?,
1821 );
1822 Some(p.distance(m).max(m.distance(q)))
1823 };
1824 for e in edges.iter_mut() {
1830 let Some((other, orange)) = &e.other_side else {
1831 continue;
1832 };
1833 let mid = |pc: &PlanarCurve, r: (f64, f64)| pc.point_at(f64::midpoint(r.0, r.1), tol);
1834 let (near, far) = (mid(&e.pcurve, e.prange)?, mid(other, *orange)?);
1835 let mut shift = ogeom_math::Vector2::new(0.0, 0.0);
1836 for (axis, period) in periods.iter().enumerate() {
1837 let Some(period) = *period else { continue };
1838 if periodic[axis] {
1839 continue;
1840 }
1841 let (from, to, lo, hi) = if axis == 0 {
1842 (near.x, far.x, ua, ub)
1843 } else {
1844 (near.y, far.y, va, vb)
1845 };
1846 let turns = ((to - from) / period).round();
1847 if turns.abs() < 2.0 || ((to - from) - turns * period).abs() > PARAM_SNAP {
1848 continue;
1849 }
1850 let inside = |x: f64| x >= lo - PARAM_SNAP && x <= hi + PARAM_SNAP;
1851 let copies: Vec<f64> = [-1.0, 1.0]
1852 .into_iter()
1853 .filter(|s| inside(from + s * period))
1854 .collect();
1855 if let [s] = copies.as_slice() {
1856 let by = (from + s * period) - (from + turns * period);
1857 if axis == 0 {
1858 shift.x = by;
1859 } else {
1860 shift.y = by;
1861 }
1862 }
1863 }
1864 if shift.x != 0.0 || shift.y != 0.0 {
1865 let by = ogeom_math::Transform2::translation(shift);
1866 let moved = other.transformed(&by, tol)?;
1867 let range = *orange;
1868 e.other_side = Some((moved, range));
1869 }
1870 }
1871 let mut uses: ogeom_core::FastMap<EdgeKey, (usize, usize)> = ogeom_core::FastMap::default();
1872 for (i, e) in edges.iter().enumerate() {
1873 uses.insert(e.node, (i, 0));
1874 }
1875 let wires = rings_without(
1876 model,
1877 &face.oriented(ogeom_topo::Orientation::Forward),
1878 slits,
1879 )?;
1880 for edge in wires.iter().flatten() {
1881 if let Some(slot) = uses.get_mut(&EdgeKey::of(edge)) {
1882 slot.1 += 1;
1883 }
1884 }
1885 'rings: for ring in &wires {
1886 let mut walk: Vec<Step> = Vec::with_capacity(ring.len());
1887 for edge in ring {
1888 let back = edge.orientation() == ogeom_topo::Orientation::Reversed;
1889 let key = EdgeKey::of(edge);
1890 let ends = |pc: &PlanarCurve, r: (f64, f64)| -> OgeomResult<(Point2, Point2)> {
1891 let (a, b) = (pc.point_at(r.0, tol)?, pc.point_at(r.1, tol)?);
1892 Ok(if back { (b, a) } else { (a, b) })
1893 };
1894 if let Some(&(i, n)) = uses.get(&key) {
1895 let e = &edges[i];
1896 if e.other_side.is_some() && n == 2 {
1897 let Some((side, range)) = walked_seam_side(e, back, tol)? else {
1898 continue 'rings;
1899 };
1900 let (a, b) = ends(&side, range)?;
1901 walk.push(Step::Seam(i, a, b));
1902 } else if n == 1 && e.other_side.is_none() && e.seam.is_none() {
1903 let (a, b) = ends(&e.pcurve, e.prange)?;
1904 walk.push(Step::Edge(i, a, b));
1905 } else {
1906 continue 'rings;
1907 }
1908 } else if let Some(j) = pole_keys.iter().position(|k| *k == key) {
1909 let (a, b) = ends(&poles[j].pcurve, poles[j].prange)?;
1910 walk.push(Step::Pole(a, b));
1911 } else {
1912 continue 'rings;
1913 }
1914 }
1915 let Some(start) = walk.iter().position(|s| matches!(s, Step::Seam(..))) else {
1916 continue;
1917 };
1918 walk.rotate_left(start);
1919 let radius = |s: &Step| match s {
1920 Step::Seam(i, ..) | Step::Edge(i, ..) => {
1921 edges[*i].tolerance.max(edges[*i].ends_tolerance)
1922 }
1923 Step::Pole(..) => 0.0,
1924 };
1925 let meets = |a: Point2, b: Point2, step: &Step, prev: &Step| -> bool {
1926 if matches!(step, Step::Pole(..)) || matches!(prev, Step::Pole(..)) {
1927 a.distance(b) <= SEAM_HAIR
1928 } else {
1929 let within = 2.0 * (radius(step) + radius(prev)) + tol.confusion();
1930 a.distance(b) <= PARAM_SNAP || apart(a, b).is_some_and(|d| d <= within)
1931 }
1932 };
1933 let Step::Seam(_, begin, mut last) = walk[0] else {
1934 continue;
1935 };
1936 let mut moves: Vec<(usize, PlanarCurve)> = Vec::new();
1937 for k in 1..walk.len() {
1938 let (step, prev) = (&walk[k], &walk[k - 1]);
1939 match step {
1940 Step::Edge(i, from, to) => {
1941 let shift = whole(last, *from);
1942 let moved = Point2::new(from.x + shift.x, from.y + shift.y);
1943 if !meets(last, moved, step, prev) {
1944 continue 'rings;
1945 }
1946 if shift.x != 0.0 || shift.y != 0.0 {
1947 let by = ogeom_math::Transform2::translation(shift);
1948 let pcurve = edges[*i].pcurve.transformed(&by, tol)?;
1949 if !inside(&pcurve, edges[*i].prange)? {
1950 continue 'rings;
1951 }
1952 moves.push((*i, pcurve));
1953 }
1954 last = Point2::new(to.x + shift.x, to.y + shift.y);
1955 }
1956 Step::Seam(i, from, to) => {
1957 let across = matches!(prev, Step::Seam(j, ..) if j == i) && {
1958 let shift = whole(last, *from);
1959 (shift.x != 0.0 || shift.y != 0.0)
1960 && meets(
1961 last,
1962 Point2::new(from.x + shift.x, from.y + shift.y),
1963 step,
1964 prev,
1965 )
1966 };
1967 if !across && !meets(last, *from, step, prev) {
1968 continue 'rings;
1969 }
1970 last = *to;
1971 }
1972 Step::Pole(from, to) => {
1973 if !meets(last, *from, step, prev) {
1974 continue 'rings;
1975 }
1976 last = *to;
1977 }
1978 }
1979 }
1980 let closing = &walk[walk.len() - 1];
1981 let across = matches!((&walk[0], closing), (Step::Seam(i, ..), Step::Seam(j, ..)) if i == j)
1982 && {
1983 let shift = whole(last, begin);
1984 (shift.x != 0.0 || shift.y != 0.0)
1985 && meets(
1986 last,
1987 Point2::new(begin.x + shift.x, begin.y + shift.y),
1988 &walk[0],
1989 closing,
1990 )
1991 };
1992 if !across && !meets(last, begin, &walk[0], closing) {
1993 continue;
1994 }
1995 for (i, pcurve) in moves {
1996 edges[i].pcurve = pcurve;
1997 }
1998 }
1999 Ok(())
2000}
2001
2002fn close_pole_joints(
2023 model: &Model,
2024 face: &Shape,
2025 surface: &SurfaceGeometry,
2026 (edges, slits): (&[BoundaryEdge], &ogeom_core::FastSet<EdgeKey>),
2027 poles: &mut Vec<PoleEdge>,
2028 pole_keys: &[EdgeKey],
2029 tol: Tolerances,
2030) -> OgeomResult<()> {
2031 let index: ogeom_core::FastMap<EdgeKey, usize> =
2032 edges.iter().enumerate().map(|(i, e)| (e.node, i)).collect();
2033 let (mut low, mut high) = (
2035 Point2::new(f64::INFINITY, f64::INFINITY),
2036 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
2037 );
2038 for e in edges {
2039 for k in 0..=4 {
2040 let p = e.pcurve.point_at(
2041 e.prange.0 + (e.prange.1 - e.prange.0) * f64::from(k) / 4.0,
2042 tol,
2043 )?;
2044 low = Point2::new(low.x.min(p.x), low.y.min(p.y));
2045 high = Point2::new(high.x.max(p.x), high.y.max(p.y));
2046 }
2047 }
2048 let centre = Point2::new(f64::midpoint(low.x, high.x), f64::midpoint(low.y, high.y));
2049 let ((u0, u1), (v0, v1)) = surface.domain();
2050 let closes = [
2051 surface.is_periodic_u() || surface.is_closed_u(tol),
2052 surface.is_periodic_v() || surface.is_closed_v(tol),
2053 ];
2054 for ring in rings_without(
2055 model,
2056 &face.oriented(ogeom_topo::Orientation::Forward),
2057 slits,
2058 )? {
2059 type Walked = (usize, bool, Option<(PlanarCurve, (f64, f64))>);
2064 let mut walk: Vec<Option<Walked>> = Vec::with_capacity(ring.len());
2065 for edge in &ring {
2066 let key = EdgeKey::of(edge);
2067 if pole_keys.contains(&key) {
2068 walk.push(None);
2069 continue;
2070 }
2071 let Some(&i) = index.get(&key) else {
2072 walk.clear();
2073 break;
2074 };
2075 let back = edge.orientation() == ogeom_topo::Orientation::Reversed;
2076 let side = if edges[i].other_side.is_some() {
2077 let Some(side) = walked_seam_side(&edges[i], back, tol)? else {
2078 walk.clear();
2079 break;
2080 };
2081 Some(side)
2082 } else {
2083 None
2084 };
2085 walk.push(Some((i, back, side)));
2086 }
2087 let n = walk.len();
2088 for k in 0..n {
2089 let (Some((a, a_back, a_side)), Some((b, b_back, b_side))) =
2090 (&walk[k], &walk[(k + 1) % n])
2091 else {
2092 continue;
2093 };
2094 let end = |i: usize,
2098 back: bool,
2099 side: &Option<(PlanarCurve, (f64, f64))>,
2100 last: bool|
2101 -> OgeomResult<(Point2, (Point, f64))> {
2102 let e = &edges[i];
2103 let (pcurve, range) = side
2104 .as_ref()
2105 .map_or((&e.pcurve, e.prange), |(c, r)| (c, *r));
2106 let at_start = back == last;
2107 let t = if at_start { range.0 } else { range.1 };
2108 Ok((pcurve.point_at(t, tol)?, e.ends[usize::from(!at_start)]))
2109 };
2110 let ((from, (pa, ra)), (to, (pb, rb))) = (
2111 end(*a, *a_back, a_side, true)?,
2112 end(*b, *b_back, b_side, false)?,
2113 );
2114 if from.distance(to) <= PARAM_SNAP {
2115 continue;
2116 }
2117 let reach = ra.max(rb).max(tol.confusion());
2118 if pa.distance(pb) > reach {
2119 continue;
2120 }
2121 let along_u = (to.x - from.x).abs() >= (to.y - from.y).abs();
2122 let (length, offset, width, closing) = if along_u {
2123 (
2124 (to.x - from.x).abs(),
2125 (to.y - from.y).abs(),
2126 (u1 - u0).abs(),
2127 closes[0],
2128 )
2129 } else {
2130 (
2131 (to.y - from.y).abs(),
2132 (to.x - from.x).abs(),
2133 (v1 - v0).abs(),
2134 closes[1],
2135 )
2136 };
2137 if closing && length >= width - PARAM_SNAP {
2138 continue;
2139 }
2140 let mut collapses = true;
2141 for s in [0.0, 0.5, 1.0] {
2142 let q = Point2::new(from.x + (to.x - from.x) * s, from.y + (to.y - from.y) * s);
2143 let Ok((du, dv)) = surface.d1_at(q.x, q.y, tol) else {
2144 collapses = false;
2145 break;
2146 };
2147 let (rate, across) = if along_u {
2148 (du.magnitude(), dv.magnitude())
2149 } else {
2150 (dv.magnitude(), du.magnitude())
2151 };
2152 if !(rate * width <= reach && across * offset <= reach) {
2153 collapses = false;
2154 break;
2155 }
2156 }
2157 if !collapses {
2158 continue;
2159 }
2160 let run = to - from;
2162 let middle = Point2::midpoint(from, to);
2163 let towards = centre - middle;
2164 if run.x * towards.y - run.y * towards.x <= 0.0 {
2165 continue;
2166 }
2167 let Ok(at) = surface.point_at(from.x, from.y, tol) else {
2168 continue;
2169 };
2170 let line = ogeom_geom::Line2d::segment(from, to, tol)?;
2171 let prange = ogeom_geom::Curve2d::domain(&line);
2172 poles.push(PoleEdge {
2173 point: at,
2174 pcurve: line.into(),
2175 prange,
2176 });
2177 }
2178 }
2179 Ok(())
2180}
2181
2182fn bring_trim_home(
2194 surface: &SurfaceGeometry,
2195 edges: &mut [BoundaryEdge],
2196 poles: &mut [PoleEdge],
2197 tol: Tolerances,
2198) -> OgeomResult<bool> {
2199 if !(surface.is_periodic_u() || surface.is_periodic_v()) {
2200 return Ok(false);
2201 }
2202 let mut low = Point2::new(f64::INFINITY, f64::INFINITY);
2203 let mut high = Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
2204 let mut take = |pc: &PlanarCurve, range: (f64, f64)| -> OgeomResult<()> {
2205 for k in 0..=4 {
2206 let t = range.0 + (range.1 - range.0) * f64::from(k) / 4.0;
2207 let p = pc.point_at(t, tol)?;
2208 low = Point2::new(low.x.min(p.x), low.y.min(p.y));
2209 high = Point2::new(high.x.max(p.x), high.y.max(p.y));
2210 }
2211 Ok(())
2212 };
2213 for e in edges.iter() {
2214 take(&e.pcurve, e.prange)?;
2215 if let Some((other, range)) = &e.other_side {
2216 take(other, *range)?;
2217 }
2218 }
2219 for p in poles.iter() {
2220 take(&p.pcurve, p.prange)?;
2221 }
2222 let ((ua, ub), (va, vb)) = surface.domain();
2223 let turns = |periodic: bool, lo: f64, hi: f64, start: f64, end: f64| -> f64 {
2226 let span = end - start;
2227 if !periodic
2228 || span <= 0.0
2229 || !lo.is_finite()
2230 || !hi.is_finite()
2231 || (lo >= start - span && hi <= end + span)
2232 {
2233 return 0.0;
2234 }
2235 -((f64::midpoint(lo, hi) - start) / span).floor() * span
2236 };
2237 let shift = ogeom_math::Vector2::new(
2238 turns(surface.is_periodic_u(), low.x, high.x, ua, ub),
2239 turns(surface.is_periodic_v(), low.y, high.y, va, vb),
2240 );
2241 if shift.x == 0.0 && shift.y == 0.0 {
2242 return Ok(false);
2243 }
2244 let moved = ogeom_math::Transform2::translation(shift);
2245 for e in edges.iter_mut() {
2246 e.pcurve = e.pcurve.transformed(&moved, tol)?;
2247 if let Some((other, _)) = &mut e.other_side {
2248 *other = other.transformed(&moved, tol)?;
2249 }
2250 if let Some((forward, reversed)) = &mut e.seam {
2251 *forward = forward.transformed(&moved, tol)?;
2252 *reversed = reversed.transformed(&moved, tol)?;
2253 }
2254 }
2255 for p in poles.iter_mut() {
2256 p.pcurve = p.pcurve.transformed(&moved, tol)?;
2257 }
2258 Ok(true)
2259}
2260
2261fn onto_range(t: f64, curve: &Curve, range: (f64, f64), tol: Tolerances) -> f64 {
2270 if !curve.is_periodic() {
2271 return t;
2272 }
2273 let (a, b) = curve.domain();
2274 let period = b - a;
2275 if period <= 0.0 {
2276 return t;
2277 }
2278 for k in [0.0, -1.0, 1.0, -2.0, 2.0] {
2279 let shifted = period.mul_add(k, t);
2280 if shifted >= range.0 - tol.parametric() && shifted <= range.1 + tol.parametric() {
2281 return shifted;
2282 }
2283 }
2284 t
2285}
2286
2287type OverlapPart = ((f64, f64), [Option<f64>; 2]);
2290
2291fn overlap_within(
2304 overlap: &ogeom_intersect::Overlap,
2305 range: (f64, f64),
2306 curve: &Curve,
2307 tol: Tolerances,
2308) -> Vec<OverlapPart> {
2309 let ordered = |r: (f64, f64)| if r.0 <= r.1 { r } else { (r.1, r.0) };
2310 let span_a = overlap.on_a.1 - overlap.on_a.0;
2311 let span_b = overlap.on_b.1 - overlap.on_b.0;
2312 if span_b.abs() <= f64::MIN_POSITIVE {
2313 return Vec::new();
2314 }
2315 let to_a = |t: f64| overlap.on_a.0 + span_a * (t - overlap.on_b.0) / span_b;
2316 let (from_start, from_end) = (to_a(range.0), to_a(range.1));
2317 let ((wlo, elo), (whi, ehi)) = if from_start <= from_end {
2318 ((from_start, range.0), (from_end, range.1))
2319 } else {
2320 ((from_end, range.1), (from_start, range.0))
2321 };
2322 let (lo, hi) = ordered(overlap.on_a);
2323 let period = if curve.is_periodic() {
2324 let (a, b) = curve.domain();
2325 (b - a) * (span_a / span_b).abs()
2328 } else {
2329 0.0
2330 };
2331 let turns: &[f64] = if period == 0.0 {
2332 &[0.0]
2333 } else {
2334 &[-2.0, -1.0, 0.0, 1.0, 2.0]
2335 };
2336 turns
2337 .iter()
2338 .map(|&k| {
2339 let (start, end) = (period.mul_add(k, wlo), period.mul_add(k, whi));
2340 (
2341 (lo.max(start), hi.min(end)),
2342 [(start > lo).then_some(elo), (end < hi).then_some(ehi)],
2343 )
2344 })
2345 .filter(|((a, b), _)| b - a > tol.parametric())
2346 .collect()
2347}
2348
2349fn rescale(t: f64, from: (f64, f64), to: (f64, f64)) -> f64 {
2351 let span = from.1 - from.0;
2352 if span.abs() <= f64::MIN_POSITIVE {
2353 return to.0;
2354 }
2355 to.0 + (to.1 - to.0) * (t - from.0) / span
2356}
2357
2358fn exact(c: &Curve) -> bool {
2361 match c {
2362 Curve::Line(_)
2363 | Curve::Circle(_)
2364 | Curve::Ellipse(_)
2365 | Curve::Hyperbola(_)
2366 | Curve::Parabola(_) => true,
2367 Curve::Trimmed(t) => exact(t.basis()),
2368 _ => false,
2369 }
2370}
2371
2372fn measuring_width(curve: &Curve, tolerance: f64, e: &BoundaryEdge, tol: Tolerances) -> f64 {
2382 let floor = if exact(curve) && exact(&e.curve) {
2383 tol.confusion() * 10.0
2384 } else {
2385 tol.confusion() * 1e3
2386 };
2387 (tolerance.max(e.tolerance) * 2.0).max(floor)
2388}
2389
2390fn grows_in_proportion(
2398 off: impl Fn(f64) -> OgeomResult<f64>,
2399 meet: (f64, f64),
2400 t: f64,
2401) -> OgeomResult<bool> {
2402 let rise = off(t)? - meet.1;
2403 let half = off(f64::midpoint(meet.0, t))? - meet.1;
2404 Ok(rise > 0.0 && half >= rise * core::f64::consts::FRAC_1_SQRT_2 / 2.0)
2405}
2406
2407fn crosses_shallowly(
2422 curve: &Curve,
2423 crange: (f64, f64),
2424 tolerance: f64,
2425 e: &BoundaryEdge,
2426 (on_a, on_b): (f64, f64),
2427 tol: Tolerances,
2428) -> OgeomResult<bool> {
2429 let bending =
2430 |c: &Curve, t: f64| -> OgeomResult<Option<(ogeom_math::Vector, ogeom_math::Vector)>> {
2431 let d = c.derivatives_at(t, 2, tol)?;
2432 let speed = d[1].magnitude();
2433 if speed <= tol.confusion() {
2434 return Ok(None);
2435 }
2436 let unit = d[1] / speed;
2437 let normal = d[2] - unit * unit.dot(d[2]);
2438 Ok(Some((d[1], normal / (speed * speed))))
2439 };
2440 let (Some((da, ka)), Some((db, kb))) = (bending(curve, on_a)?, bending(&e.curve, on_b)?) else {
2441 return Ok(false);
2442 };
2443 let (ma, mb) = (da.magnitude(), db.magnitude());
2444 let turn = da.cross(db).magnitude() / (ma * mb);
2445 let width = measuring_width(curve, tolerance, e, tol);
2446 if turn * turn <= (ka - kb).magnitude() * width {
2447 return Ok(false);
2448 }
2449 let step = width / turn / ma;
2450 let off = |t: f64| -> OgeomResult<f64> {
2451 distance_to_edge_curve(&e.curve, e.crange, curve.point_at(t, tol)?, tol)
2452 };
2453 let meet = (on_a, off(on_a)?);
2454 for t in [(on_a - step).max(crange.0), (on_a + step).min(crange.1)] {
2455 if (t - on_a).abs() <= tol.parametric() || !grows_in_proportion(off, meet, t)? {
2456 return Ok(false);
2457 }
2458 }
2459 Ok(true)
2460}
2461
2462fn measured_overlaps(
2471 curve: &Curve,
2472 crange: (f64, f64),
2473 tolerance: f64,
2474 e: &BoundaryEdge,
2475 parted: Option<f64>,
2476 tol: Tolerances,
2477) -> OgeomResult<Vec<ogeom_intersect::Overlap>> {
2478 const SAMPLES: usize = 48;
2479 let width = measuring_width(curve, tolerance, e, tol);
2480 let mut near: Vec<bool> = Vec::with_capacity(SAMPLES + 1);
2481 for i in 0..=SAMPLES {
2482 #[allow(clippy::cast_precision_loss)]
2483 let t = crange.0 + (crange.1 - crange.0) * (i as f64) / (SAMPLES as f64);
2484 let p = curve.point_at(t, tol)?;
2485 near.push(
2486 e.bound.expanded(width).contains(p)
2487 && distance_to_edge_curve(&e.curve, e.crange, p, tol)? <= width,
2488 );
2489 }
2490 let mut out = Vec::new();
2491 let mut i = 0;
2492 while i <= SAMPLES {
2493 if !near[i] {
2494 i += 1;
2495 continue;
2496 }
2497 let start = i;
2498 while i < SAMPLES && near[i + 1] {
2499 i += 1;
2500 }
2501 let end = i;
2502 i += 1;
2503 if end - start < 2 {
2505 continue;
2506 }
2507 #[allow(clippy::cast_precision_loss)]
2508 let at = |k: usize| crange.0 + (crange.1 - crange.0) * (k as f64) / (SAMPLES as f64);
2509 let refine = |inside: f64, outside: f64| -> OgeomResult<f64> {
2511 let (mut a, mut b) = (inside, outside);
2512 for _ in 0..24 {
2513 let m = f64::midpoint(a, b);
2514 let p = curve.point_at(m, tol)?;
2515 if e.bound.expanded(width).contains(p)
2516 && distance_to_edge_curve(&e.curve, e.crange, p, tol)? <= width
2517 {
2518 a = m;
2519 } else {
2520 b = m;
2521 }
2522 }
2523 Ok(a)
2524 };
2525 let lo = if start == 0 {
2526 at(0)
2527 } else {
2528 refine(at(start), at(start - 1))?
2529 };
2530 let hi = if end == SAMPLES {
2531 at(SAMPLES)
2532 } else {
2533 refine(at(end), at(end + 1))?
2534 };
2535 let at_vertex = |t: f64| -> OgeomResult<f64> {
2541 let p = curve.point_at(t, tol)?;
2542 for end in [e.crange.0, e.crange.1] {
2543 let vertex = e.curve.point_at(end, tol)?;
2544 if vertex.distance(p) > width * 2.0 {
2545 continue;
2546 }
2547 let foot = ogeom_algo::project_on_curve(curve, vertex, 64, tol)?;
2548 let (clo, chi) = (crange.0.min(crange.1), crange.0.max(crange.1));
2549 if foot.distance <= width && foot.parameter >= clo && foot.parameter <= chi {
2550 return Ok(foot.parameter);
2551 }
2552 }
2553 Ok(t)
2554 };
2555 let lo = if start == 0 { lo } else { at_vertex(lo)? };
2556 let hi = if end == SAMPLES { hi } else { at_vertex(hi)? };
2557 if hi - lo <= tol.parametric() {
2558 continue;
2559 }
2560 let ends_there = |t: f64, own_end: bool| -> OgeomResult<bool> {
2566 if own_end {
2567 return Ok(true);
2568 }
2569 let p = curve.point_at(t, tol)?;
2570 for end in [e.crange.0, e.crange.1] {
2571 if e.curve.point_at(end, tol)?.distance(p) <= width * 2.0 {
2572 return Ok(true);
2573 }
2574 }
2575 Ok(false)
2576 };
2577 let (ends_lo, ends_hi) = (ends_there(lo, start == 0)?, ends_there(hi, end == SAMPLES)?);
2578 let (lo, hi) = if exact(curve) && exact(&e.curve) {
2579 if !(ends_lo && ends_hi) {
2580 continue;
2581 }
2582 (lo, hi)
2583 } else if ends_lo && ends_hi {
2584 (lo, hi)
2585 } else {
2586 let off = |t: f64| -> OgeomResult<f64> {
2594 distance_to_edge_curve(&e.curve, e.crange, curve.point_at(t, tol)?, tol)
2595 };
2596 let mut meet = (lo, off(lo)?);
2597 for k in 1..=SAMPLES {
2598 #[allow(clippy::cast_precision_loss)]
2599 let t = lo + (hi - lo) * (k as f64) / (SAMPLES as f64);
2600 let d = off(t)?;
2601 if d < meet.1 {
2602 meet = (t, d);
2603 }
2604 }
2605 let mut crossing = false;
2606 for (t, ends) in [(lo, ends_lo), (hi, ends_hi)] {
2607 if !ends {
2608 crossing |= grows_in_proportion(off, meet, t)?;
2609 }
2610 }
2611 if crossing {
2612 continue;
2613 }
2614 (lo, hi)
2615 };
2616 if hi - lo <= tol.parametric() {
2617 continue;
2618 }
2619 let off = |t: f64| -> OgeomResult<f64> {
2627 distance_to_edge_curve(&e.curve, e.crange, curve.point_at(t, tol)?, tol)
2628 };
2629 let runs = match parted {
2630 Some(within) if !(exact(curve) && exact(&e.curve)) => {
2631 runs_within(&off, (lo, hi), within, SAMPLES)?.unwrap_or_else(|| vec![(lo, hi)])
2632 }
2633 _ => vec![(lo, hi)],
2634 };
2635 for (lo, hi) in runs {
2636 if hi - lo <= tol.parametric() {
2637 continue;
2638 }
2639 let on_edge = |t: f64| -> OgeomResult<f64> {
2643 let p = curve.point_at(t, tol)?;
2644 if parted.is_some() {
2645 return Ok(nearest_on_edge_curve(&e.curve, e.crange, p, tol)?.1);
2646 }
2647 let foot = ogeom_algo::project_on_curve(&e.curve, p, 64, tol)?;
2648 Ok(onto_range(foot.parameter, &e.curve, e.crange, tol))
2649 };
2650 if *DEBUG_WIRE {
2651 eprintln!(
2652 "CONTACT along edge {} measured over ({lo:.6}, {hi:.6}) within {width:.2e}",
2653 e.node.index()
2654 );
2655 }
2656 out.push(ogeom_intersect::Overlap {
2657 on_a: (lo, hi),
2658 on_b: (on_edge(lo)?, on_edge(hi)?),
2659 });
2660 }
2661 }
2662 if out.is_empty()
2668 && let Some(overlap) = edge_along_curve(curve, crange, e, width, tol)?
2669 {
2670 out.push(overlap);
2671 }
2672 Ok(out)
2673}
2674
2675fn held_radius(
2678 held: &ogeom_core::FastMap<EdgeKey, (f64, &BoundaryEdge)>,
2679 node: EdgeKey,
2680 tolerance: f64,
2681) -> f64 {
2682 held.get(&node)
2683 .map_or(tolerance, |&(r, _)| r)
2684 .max(tolerance)
2685}
2686
2687enum End {
2689 OfEdge { on_contact: f64, gap: f64 },
2692 OfContact { on_edge: f64, gap: f64 },
2694}
2695
2696fn shallow_at_end(
2708 contact: &ContactRec,
2709 e: &BoundaryEdge,
2710 on: (f64, f64),
2711 width: f64,
2712 tol: Tolerances,
2713) -> OgeomResult<Option<End>> {
2714 let (da, db) = (contact.curve.d1_at(on.0, tol)?, e.curve.d1_at(on.1, tol)?);
2715 let (ma, mb) = (da.magnitude(), db.magnitude());
2716 if ma <= tol.confusion() || mb <= tol.confusion() {
2717 return Ok(None);
2718 }
2719 let turn = da.cross(db).magnitude() / (ma * mb);
2720 if turn <= f64::EPSILON {
2721 return Ok(None);
2722 }
2723 let along = width / turn;
2724 let at = contact.curve.point_at(on.0, tol)?;
2725 let mut best: Option<(f64, End)> = None;
2726 for end in [e.crange.0, e.crange.1] {
2727 let vertex = e.curve.point_at(end, tol)?;
2728 let away = vertex.distance(at);
2729 if away > along {
2730 continue;
2731 }
2732 let (gap, t) = nearest_on_edge_curve(&contact.curve, contact.crange, vertex, tol)?;
2733 if gap <= width && best.as_ref().is_none_or(|b| away < b.0) {
2734 best = Some((away, End::OfEdge { on_contact: t, gap }));
2735 }
2736 }
2737 for end in [contact.crange.0, contact.crange.1] {
2738 let vertex = contact.curve.point_at(end, tol)?;
2739 let away = vertex.distance(at);
2740 if away > along {
2741 continue;
2742 }
2743 let (gap, t) = nearest_on_edge_curve(&e.curve, e.crange, vertex, tol)?;
2744 if gap <= width && best.as_ref().is_none_or(|b| away < b.0) {
2745 best = Some((away, End::OfContact { on_edge: t, gap }));
2746 }
2747 }
2748 Ok(best.map(|(_, end)| end))
2749}
2750
2751fn runs_within(
2755 off: &impl Fn(f64) -> OgeomResult<f64>,
2756 span: (f64, f64),
2757 within: f64,
2758 samples: usize,
2759) -> OgeomResult<Option<Vec<(f64, f64)>>> {
2760 #[allow(clippy::cast_precision_loss)]
2761 let at = |k: usize| span.0 + (span.1 - span.0) * (k as f64) / (samples as f64);
2762 let near: Vec<bool> = (0..=samples)
2763 .map(|k| off(at(k)).map(|d| d <= within))
2764 .collect::<OgeomResult<_>>()?;
2765 if !near.contains(&true) {
2766 return Ok(None);
2767 }
2768 let parting = |inside: f64, outside: f64| -> OgeomResult<f64> {
2769 let (mut a, mut b) = (inside, outside);
2770 for _ in 0..24 {
2771 let m = f64::midpoint(a, b);
2772 if off(m)? <= within {
2773 a = m;
2774 } else {
2775 b = m;
2776 }
2777 }
2778 Ok(a)
2779 };
2780 let mut runs = Vec::new();
2781 let mut k = 0;
2782 while k <= samples {
2783 if !near[k] {
2784 k += 1;
2785 continue;
2786 }
2787 let first = k;
2788 while k < samples && near[k + 1] {
2789 k += 1;
2790 }
2791 let lo = if first == 0 {
2792 span.0
2793 } else {
2794 parting(at(first), at(first - 1))?
2795 };
2796 let hi = if k == samples {
2797 span.1
2798 } else {
2799 parting(at(k), at(k + 1))?
2800 };
2801 runs.push((lo, hi));
2802 k += 1;
2803 }
2804 Ok(Some(runs))
2805}
2806
2807fn shared_overlaps(
2820 contact: &ContactRec,
2821 e: &BoundaryEdge,
2822 held: &ogeom_core::FastMap<EdgeKey, (f64, &BoundaryEdge)>,
2823 shared: &mut ogeom_core::FastMap<(EdgeKey, EdgeKey), Vec<ogeom_intersect::Overlap>>,
2824 tol: Tolerances,
2825) -> OgeomResult<Vec<ogeom_intersect::Overlap>> {
2826 if let Some(found) = shared.get(&(contact.node, e.node)) {
2827 return Ok(found.clone());
2828 }
2829 let swap = |overlaps: &[ogeom_intersect::Overlap]| -> Vec<ogeom_intersect::Overlap> {
2830 overlaps
2831 .iter()
2832 .map(|o| ogeom_intersect::Overlap {
2833 on_a: o.on_b,
2834 on_b: o.on_a,
2835 })
2836 .collect()
2837 };
2838 let own = held.get(&contact.node).copied();
2839 let held_by_contact = held_radius(held, contact.node, contact.tolerance);
2840 let held_by_edge = held_radius(held, e.node, e.tolerance);
2841 let within = (held_by_contact + held_by_edge).max(tol.confusion());
2842 let owner = own
2845 .map(|(_, owner)| owner)
2846 .filter(|owner| owner.crange == contact.crange && *owner.curve == contact.curve);
2847 let from_contact = measured_overlaps(
2848 &contact.curve,
2849 contact.crange,
2850 contact.tolerance,
2851 e,
2852 Some(within),
2853 tol,
2854 )?;
2855 let from_edge = match owner {
2856 Some(owner) => swap(&measured_overlaps(
2857 &e.curve,
2858 e.crange,
2859 e.tolerance,
2860 owner,
2861 Some(within),
2862 tol,
2863 )?),
2864 None => Vec::new(),
2865 };
2866 let (mut found, other) = if held_by_edge < held_by_contact {
2871 (from_edge, from_contact)
2872 } else {
2873 (from_contact, from_edge)
2874 };
2875 let ordered = |r: (f64, f64)| if r.0 <= r.1 { r } else { (r.1, r.0) };
2876 let unseen: Vec<ogeom_intersect::Overlap> = other
2877 .into_iter()
2878 .filter(|o| {
2879 let (lo, hi) = ordered(o.on_a);
2880 found.iter().all(|f| {
2881 let (flo, fhi) = ordered(f.on_a);
2882 hi < flo || lo > fhi
2883 })
2884 })
2885 .collect();
2886 found.extend(unseen);
2887 shared.insert((e.node, contact.node), swap(&found));
2888 shared.insert((contact.node, e.node), found.clone());
2889 Ok(found)
2890}
2891
2892fn edge_along_curve(
2896 curve: &Curve,
2897 crange: (f64, f64),
2898 e: &BoundaryEdge,
2899 width: f64,
2900 tol: Tolerances,
2901) -> OgeomResult<Option<ogeom_intersect::Overlap>> {
2902 const SAMPLES: u32 = 48;
2903 let at =
2904 |i: u32| (e.crange.1 - e.crange.0).mul_add(f64::from(i) / f64::from(SAMPLES), e.crange.0);
2905 let order = [0, SAMPLES, SAMPLES / 2]
2908 .into_iter()
2909 .chain((1..SAMPLES).filter(|&i| i != SAMPLES / 2));
2910 let mut feet = [0.0; 2];
2911 for i in order {
2912 let p = e.curve.point_at(at(i), tol)?;
2913 let (off, t) = nearest_on_edge_curve(curve, crange, p, tol)?;
2914 if off > width {
2915 return Ok(None);
2916 }
2917 if i == 0 {
2918 feet[0] = t;
2919 } else if i == SAMPLES {
2920 feet[1] = t;
2921 }
2922 }
2923 let (on_a, on_b) = if feet[0] <= feet[1] {
2924 ((feet[0], feet[1]), e.crange)
2925 } else {
2926 ((feet[1], feet[0]), (e.crange.1, e.crange.0))
2927 };
2928 Ok((on_a.1 - on_a.0 > tol.parametric()).then_some(ogeom_intersect::Overlap { on_a, on_b }))
2929}
2930
2931fn pcurve_polyline(
2933 pcurve: &PlanarCurve,
2934 prange: (f64, f64),
2935 crange: (f64, f64),
2936 sub: (f64, f64),
2937 surface: &SurfaceGeometry,
2938 tol: Tolerances,
2939) -> OgeomResult<Vec<Point2>> {
2940 let sagged = matches!(surface, SurfaceGeometry::Plane(_));
2941 chart_polyline(pcurve, prange, crange, sub, surface, sagged, tol)
2942}
2943
2944fn pcurve_polyline_within_sag(
2947 pcurve: &PlanarCurve,
2948 prange: (f64, f64),
2949 crange: (f64, f64),
2950 sub: (f64, f64),
2951 surface: &SurfaceGeometry,
2952 tol: Tolerances,
2953) -> OgeomResult<Vec<Point2>> {
2954 chart_polyline(pcurve, prange, crange, sub, surface, true, tol)
2955}
2956
2957fn chart_polyline(
2960 pcurve: &PlanarCurve,
2961 prange: (f64, f64),
2962 crange: (f64, f64),
2963 sub: (f64, f64),
2964 surface: &SurfaceGeometry,
2965 sagged: bool,
2966 tol: Tolerances,
2967) -> OgeomResult<Vec<Point2>> {
2968 let lo = rescale(sub.0, crange, prange);
2969 let hi = rescale(sub.1, crange, prange);
2970 let mut count = match pcurve {
2974 PlanarCurve::Line(_) => 1,
2975 _ => {
2976 let span = (hi - lo).abs().max(SCAFFOLD_CHORD);
2977 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2978 let n = (span / SCAFFOLD_CHORD).sqrt().ceil() as usize;
2979 n.clamp(8, 256)
2980 }
2981 };
2982 let ((ua, ub), (va, vb)) = surface.domain();
2990 let (first, last) = (pcurve.point_at(lo, tol)?, pcurve.point_at(hi, tol)?);
2991 for (periodic, period, reach) in [
2992 (surface.is_periodic_u(), ub - ua, (last.x - first.x).abs()),
2993 (surface.is_periodic_v(), vb - va, (last.y - first.y).abs()),
2994 ] {
2995 if !periodic || period <= 0.0 || !reach.is_finite() {
2996 continue;
2997 }
2998 let steps = (reach / (period / 3.0)).ceil();
2999 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
3000 let steps = steps.clamp(1.0, 4096.0) as usize;
3001 count = count.max(steps);
3002 }
3003 if sagged && !matches!(pcurve, PlanarCurve::Line(_)) {
3004 count = match circle_sag_count(pcurve, lo, hi, count) {
3005 Some(count) => count,
3006 None => sag_count(&|t| pcurve.point_at(t, tol), lo, hi, count)?.0,
3007 };
3008 }
3009 let mut out = Vec::with_capacity(count + 1);
3010 for i in 0..=count {
3011 #[allow(clippy::cast_precision_loss)]
3012 let t = lo + (hi - lo) * i as f64 / count as f64;
3013 out.push(pcurve.point_at(t, tol)?);
3014 }
3015 Ok(out)
3016}
3017
3018struct SectionRec {
3022 curve: Curve,
3023 pc_a: PlanarCurve,
3025 pc_b: PlanarCurve,
3027 face_a: usize,
3028 face_b: usize,
3029 closed: bool,
3030 tolerance: f64,
3035}
3036
3037fn seamed_at_a_stop(
3048 section: &SectionRec,
3049 fa: &GFace,
3050 fb: &GFace,
3051 tol: Tolerances,
3052) -> Option<SectionRec> {
3053 use core::f64::consts::TAU;
3054 if !section.closed || section.tolerance > 0.0 {
3055 return None;
3056 }
3057 let Curve::Circle(c) = §ion.curve else {
3058 return None;
3059 };
3060 let circle = c.circle();
3061 let (centre, radius) = (circle.centre(), circle.radius());
3062 let frame = circle.frame();
3063 let sweep = if c.is_reversed() { -1.0 } else { 1.0 };
3065 let angle_of = |p: Point| {
3066 let local = frame.to_local(p);
3067 (sweep * local.y.atan2(local.x)).rem_euclid(TAU)
3068 };
3069 let mut nearest: Option<f64> = None;
3070 for face in [fa, fb] {
3071 for e in &face.edges {
3072 for end in [e.crange.0, e.crange.1] {
3073 let Ok(p) = e.curve.point_at(end, tol) else {
3074 continue;
3075 };
3076 let local = frame.to_local(p);
3077 let off = local.z.hypot(local.x.hypot(local.y) - radius);
3078 if off > tol.confusion() * 10.0 {
3079 continue;
3080 }
3081 let a = angle_of(p);
3082 let shift = if a > TAU / 2.0 { a - TAU } else { a };
3083 if nearest.is_none_or(|n| shift.abs() < n.abs()) {
3084 nearest = Some(shift);
3085 }
3086 }
3087 }
3088 }
3089 let shift = nearest?;
3090 if shift.abs() <= tol.parametric() || shift.abs() > 1e-2 {
3091 return None;
3092 }
3093 let start = c.point_at(shift, tol).ok()?;
3094 let x = ogeom_math::Direction::new(start - centre, tol).ok()?;
3095 let z = if c.is_reversed() {
3096 -frame.z()
3097 } else {
3098 frame.z()
3099 };
3100 let turned = ogeom_math::Frame::new(centre, z, x, tol).ok()?;
3101 let curve: Curve =
3102 ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(turned, radius, tol).ok()?).into();
3103 let pc_a = ogeom_intersect::exact_pcurve_of(&curve, &fa.surface, tol)?;
3104 let pc_b = ogeom_intersect::exact_pcurve_of(&curve, &fb.surface, tol)?;
3105 Some(SectionRec {
3106 curve,
3107 pc_a,
3108 pc_b,
3109 face_a: section.face_a,
3110 face_b: section.face_b,
3111 closed: true,
3112 tolerance: section.tolerance,
3113 })
3114}
3115
3116fn reseamed_section(
3131 section: &SectionRec,
3132 u: f64,
3133 surfaces: [&SurfaceGeometry; 2],
3134 tol: Tolerances,
3135) -> Option<SectionRec> {
3136 let Curve::BSpline(spline) = §ion.curve else {
3137 return None;
3138 };
3139 if spline.is_periodic() {
3140 return None;
3141 }
3142 let (start, end) = spline.domain();
3143 if spline
3144 .point_at(start, tol)
3145 .ok()?
3146 .distance(spline.point_at(end, tol).ok()?)
3147 > tol.confusion()
3148 {
3149 return None;
3150 }
3151 let image = |pcurve: &PlanarCurve, surface: &SurfaceGeometry| -> Option<PlanarCurve> {
3152 let PlanarCurve::BSpline(image) = pcurve else {
3153 return None;
3154 };
3155 let domain = image.domain();
3156 if (domain.0 - start).abs() > tol.parametric() || (domain.1 - end).abs() > tol.parametric()
3157 {
3158 return None;
3159 }
3160 let (head, tail) = (
3161 image.point_at(start, tol).ok()?,
3162 image.point_at(end, tol).ok()?,
3163 );
3164 let climb = [tail.x - head.x, tail.y - head.y];
3167 let mut lap = [0.0; 2];
3168 for (k, span) in chart_spans(surface, tol).into_iter().enumerate() {
3169 if let Some(span) = span {
3170 lap[k] = (climb[k] / span).round() * span;
3171 }
3172 }
3173 image
3174 .reseamed_at(u, ogeom_math::Vector2::new(lap[0], lap[1]), tol)
3175 .ok()
3176 .map(PlanarCurve::BSpline)
3177 };
3178 Some(SectionRec {
3179 curve: Curve::BSpline(spline.reseamed_at(u, tol).ok()?),
3180 pc_a: image(§ion.pc_a, surfaces[0])?,
3181 pc_b: image(§ion.pc_b, surfaces[1])?,
3182 face_a: section.face_a,
3183 face_b: section.face_b,
3184 closed: true,
3185 tolerance: section.tolerance,
3186 })
3187}
3188
3189fn outline_refs(face: &GFace, tol: Tolerances) -> OgeomResult<Vec<&[Point2]>> {
3191 let Some(lines) = face.outline(tol) else {
3192 ogeom_bail!(
3193 Construction,
3194 "a face's boundary would not polyline in its chart"
3195 );
3196 };
3197 Ok(lines.iter().map(Vec::as_slice).collect())
3198}
3199
3200fn contact_intervals(
3212 fused: &GeneralFused,
3213 contact: &TangentRec,
3214 tol: Tolerances,
3215) -> OgeomResult<Vec<(f64, f64)>> {
3216 let refs_a = outline_refs(&fused.a.faces[contact.face_a], tol)?;
3220 let refs_b = outline_refs(&fused.b.faces[contact.face_b], tol)?;
3221
3222 const STATIONS: usize = 64;
3223 let domain = contact.curve.domain();
3224 let span = domain.1 - domain.0;
3225 let mut runs: Vec<(f64, f64)> = Vec::new();
3226 let mut open: Option<f64> = None;
3227 for k in 0..=STATIONS {
3228 #[expect(
3229 clippy::cast_precision_loss,
3230 reason = "a station index, far below the mantissa"
3231 )]
3232 let t = span.mul_add(k as f64 / STATIONS as f64, domain.0);
3233 let held = matches!(
3236 (
3237 contact.pc_a.point_at(t, tol),
3238 contact.pc_b.point_at(t, tol)
3239 ),
3240 (Ok(pa), Ok(pb))
3241 if arrange::inside_many(
3242 &refs_a,
3243 fold_point_into_chart(pa, &fused.a.faces[contact.face_a].surface, tol),
3244 ) && arrange::inside_many(
3245 &refs_b,
3246 fold_point_into_chart(pb, &fused.b.faces[contact.face_b].surface, tol),
3247 )
3248 );
3249 match (held, open) {
3250 (true, None) => open = Some(t),
3251 (false, Some(from)) => {
3252 if t - from > tol.parametric() {
3253 runs.push((from, t));
3254 }
3255 open = None;
3256 }
3257 _ => {}
3258 }
3259 }
3260 if let Some(from) = open
3261 && domain.1 - from > tol.parametric()
3262 {
3263 runs.push((from, domain.1));
3264 }
3265 Ok(runs)
3266}
3267
3268struct TangentRec {
3275 curve: Curve,
3276 pc_a: PlanarCurve,
3277 pc_b: PlanarCurve,
3278 face_a: usize,
3279 face_b: usize,
3280}
3281
3282fn shared_line(ends: &[Point], target: &GFace, weld: f64, tol: Tolerances) -> OgeomResult<bool> {
3285 for t in &target.edges {
3286 if !matches!(&*t.curve, Curve::Line(_)) {
3287 continue;
3288 }
3289 let (a, b) = (
3290 t.curve.point_at(t.crange.0, tol)?,
3291 t.curve.point_at(t.crange.1, tol)?,
3292 );
3293 let along = (b.x - a.x, b.y - a.y, b.z - a.z);
3294 let length2 = along.0 * along.0 + along.1 * along.1 + along.2 * along.2;
3295 if length2 <= 0.0 {
3296 continue;
3297 }
3298 let beside = |p: &Point| {
3299 let s =
3300 ((p.x - a.x) * along.0 + (p.y - a.y) * along.1 + (p.z - a.z) * along.2) / length2;
3301 let s = s.clamp(0.0, 1.0);
3302 p.distance(Point::new(
3303 a.x + s * along.0,
3304 a.y + s * along.1,
3305 a.z + s * along.2,
3306 ))
3307 };
3308 if ends.iter().all(|p| beside(p) <= weld) {
3309 return Ok(true);
3310 }
3311 }
3312 Ok(false)
3313}
3314
3315fn one_side_of(
3321 face: &GFace,
3322 plane: ogeom_math::quadric::Plane,
3323 tol: Tolerances,
3324) -> OgeomResult<Option<bool>> {
3325 if face.edges.is_empty() || !face.poles.is_empty() {
3326 return Ok(None);
3327 }
3328 let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
3329 let mut edge_in = false;
3330 for e in &face.edges {
3331 if !matches!(&*e.curve, Curve::Line(_)) {
3332 return Ok(None);
3333 }
3334 let mut ends_in = true;
3335 for t in [e.crange.0, e.crange.1] {
3336 let d = plane.signed_distance_to(e.curve.point_at(t, tol)?);
3337 (low, high) = (low.min(d), high.max(d));
3338 ends_in &= d.abs() <= tol.confusion();
3339 }
3340 edge_in |= ends_in;
3341 }
3342 let past = tol.confusion() * 1e-3;
3347 Ok((low >= -past || high <= past).then_some(edge_in))
3348}
3349
3350fn honest(tolerance: f64, tol: Tolerances) -> f64 {
3355 tolerance.min(tol.confusion() * 1e4)
3356}
3357
3358const GRAZE_REACH: f64 = 10.0;
3361
3362fn graze_contacts(
3394 ga: &GSolid,
3395 gb: &GSolid,
3396 sections: &[SectionRec],
3397 contacts: &[ContactRec],
3398 crossings: Crossings,
3399 tol: Tolerances,
3400) -> OgeomResult<Vec<ContactRec>> {
3401 let reach = tol.confusion() * GRAZE_REACH;
3402 let weld = tol.confusion() * 1e2;
3405 let mut out: Vec<ContactRec> = Vec::new();
3406 for section in sections.iter().filter(|s| !s.closed) {
3407 let (lo, hi) = section.curve.domain();
3408 let ends = [
3409 section.curve.point_at(lo, tol)?,
3410 section.curve.point_at(hi, tol)?,
3411 ];
3412 let middle = section.curve.point_at(f64::midpoint(lo, hi), tol)?;
3413 let near = tol.confusion().max(honest(section.tolerance, tol));
3414 for (owner, target, target_from_a, target_face) in [
3415 (
3416 &ga.faces[section.face_a],
3417 &gb.faces[section.face_b],
3418 false,
3419 section.face_b,
3420 ),
3421 (
3422 &gb.faces[section.face_b],
3423 &ga.faces[section.face_a],
3424 true,
3425 section.face_a,
3426 ),
3427 ] {
3428 for e in &owner.edges {
3429 let held = |c: &ContactRec| {
3430 c.node == e.node
3431 && c.target_from_a == target_from_a
3432 && c.target_face == target_face
3433 };
3434 let reached = near.max(e.standoff);
3435 let crossed = if crossings != Crossings::Off
3442 && ends.iter().all(|p| e.bound.distance_to(*p) > reached)
3443 {
3444 section_crossings(§ion.curve, e, &owner.surface, crossings, reached, tol)?
3445 } else {
3446 None
3447 };
3448 let (ends, middle) = match crossed {
3449 Some(Crossed::Once(at, along)) => {
3450 let length = e
3451 .curve
3452 .point_at(e.crange.0, tol)?
3453 .distance(e.curve.point_at(e.crange.1, tol)?);
3454 ([at, ends[1]], at + along * length)
3455 }
3456 Some(Crossed::Twice(first, second)) => {
3457 ([first, second], first.midpoint(second))
3458 }
3459 None => (ends, middle),
3460 };
3461 let crossed = crossed.is_some() && !matches!(&*e.curve, Curve::Line(_));
3462 if ends.iter().all(|p| e.bound.distance_to(*p) > reached)
3463 || contacts.iter().chain(&out).any(held)
3464 {
3465 continue;
3466 }
3467 let mut feet: [Option<f64>; 2] = [None, None];
3468 for (k, end) in ends.iter().enumerate() {
3469 if e.bound.distance_to(*end) > reached {
3470 continue;
3471 }
3472 let foot = ogeom_algo::project_on_curve(&e.curve, *end, 64, tol)?;
3473 if foot.distance <= reached
3474 && foot.parameter >= e.crange.0 - tol.parametric()
3475 && foot.parameter <= e.crange.1 + tol.parametric()
3476 {
3477 feet[k] = Some(foot.parameter.clamp(e.crange.0, e.crange.1));
3478 }
3479 }
3480 if feet.iter().all(Option::is_none)
3484 || ogeom_algo::project_on_curve(&e.curve, middle, 64, tol)?.distance <= near
3485 {
3486 continue;
3487 }
3488 let closed = e
3489 .curve
3490 .point_at(e.crange.0, tol)?
3491 .distance(e.curve.point_at(e.crange.1, tol)?)
3492 <= tol.confusion();
3493 let mut ways: Vec<Vec<(f64, f64)>> = Vec::new();
3497 if let [Some(f0), Some(f1)] = feet {
3498 let (t0, t1) = (f0.min(f1), f0.max(f1));
3499 if t1 - t0 > tol.parametric() {
3500 ways.push(vec![(t0, t1)]);
3501 if closed {
3502 ways.push(
3503 [(t1, e.crange.1), (e.crange.0, t0)]
3504 .into_iter()
3505 .filter(|(a, b)| b - a > tol.parametric())
3506 .collect(),
3507 );
3508 }
3509 }
3510 } else if !closed {
3511 for t in feet.into_iter().flatten() {
3512 ways.push(vec![(e.crange.0, t)]);
3513 ways.push(vec![(t, e.crange.1)]);
3514 }
3515 }
3516 let mut stretches = None;
3517 for way in ways {
3518 let mut lies = !way.is_empty();
3519 'way: for &(a, b) in &way {
3520 let span = e
3521 .curve
3522 .point_at(a, tol)?
3523 .distance(e.curve.point_at(b, tol)?);
3524 if b - a <= tol.parametric() || span <= weld {
3525 lies = false;
3526 break;
3527 }
3528 for i in 0..=16 {
3529 let t = a + (b - a) * f64::from(i) / 16.0;
3530 let p = e.curve.point_at(t, tol)?;
3531 if ogeom_algo::project_on_surface(&target.surface, p, 16, tol)?.distance
3532 > reach
3533 {
3534 lies = false;
3535 break 'way;
3536 }
3537 }
3538 }
3539 if lies {
3540 stretches = Some(way);
3541 break;
3542 }
3543 }
3544 let Some(stretches) = stretches else {
3545 continue;
3546 };
3547 for crange in stretches {
3548 let Ok((pcurve, _, true, ..)) = ogeom_algo::pcurve_fit::fit_projected_pcurve(
3549 &e.curve,
3550 crange,
3551 &target.surface,
3552 tol,
3553 ) else {
3554 continue;
3555 };
3556 if *DEBUG_WIRE {
3557 eprintln!(
3558 "GRAZE edge {} over {crange:?} onto face {target_face} (from_a {target_from_a})",
3559 e.node.index()
3560 );
3561 }
3562 out.push(ContactRec {
3563 curve: (*e.curve).clone(),
3564 crange,
3565 pcurve,
3566 prange: crange,
3567 node: e.node,
3568 tolerance: e.tolerance.max(owner.tolerance + target.tolerance),
3569 target_from_a,
3570 target_face,
3571 bound: e.bound,
3572 crossed,
3573 });
3574 }
3575 }
3576 }
3577 }
3578 Ok(out)
3579}
3580
3581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3584enum Crossings {
3585 Off,
3587 Lines,
3589 Curves,
3591}
3592
3593enum Crossed {
3596 Once(Point, ogeom_math::Vector),
3598 Twice(Point, Point),
3600}
3601
3602fn section_crossings(
3618 section: &Curve,
3619 e: &BoundaryEdge,
3620 owner: &SurfaceGeometry,
3621 crossings: Crossings,
3622 reach: f64,
3623 tol: Tolerances,
3624) -> OgeomResult<Option<Crossed>> {
3625 if matches!(&*e.curve, Curve::Line(_)) {
3626 return Ok(
3627 lines_crossing(section, &e.curve, tol)?.map(|(at, along)| Crossed::Once(at, along))
3628 );
3629 }
3630 if crossings != Crossings::Curves {
3631 return Ok(None);
3632 }
3633 let (Curve::Line(_), SurfaceGeometry::Plane(plane)) = (section, owner) else {
3634 return Ok(None);
3635 };
3636 let p = section.point_at(0.0, tol)?;
3637 let u = section.point_at(1.0, tol)? - p;
3638 let length = u.magnitude();
3639 if length <= 0.0 {
3640 return Ok(None);
3641 }
3642 let u = u / length;
3643 let mut found: Vec<Point> = Vec::new();
3644 if let Curve::Circle(c) = &*e.curve {
3645 let circle = c.circle();
3646 let axis = circle.frame().z().vector();
3647 let w = p - circle.centre();
3648 let w = w - axis * w.dot(axis);
3649 let v = u - axis * u.dot(axis);
3650 let (square, half, rest) = (
3651 v.dot(v),
3652 w.dot(v),
3653 w.dot(w) - circle.radius() * circle.radius(),
3654 );
3655 let discriminant = half * half - square * rest;
3656 if square <= 0.0 || discriminant <= 0.0 {
3657 return Ok(None);
3658 }
3659 let q = -(half + discriminant.sqrt().copysign(half));
3660 if q == 0.0 {
3661 return Ok(None);
3662 }
3663 found.extend([p + u * (q / square), p + u * (rest / q)]);
3664 } else {
3665 let across = u.cross(plane.plane().normal().vector());
3666 let size = across.magnitude();
3667 if size <= 0.0 {
3668 return Ok(None);
3669 }
3670 let across = across / size;
3671 let side = |q: Point| (q - p).dot(across);
3672 let corners = e.bound.expanded(reach).corners();
3673 if corners.iter().all(|q| side(*q) > 0.0) || corners.iter().all(|q| side(*q) < 0.0) {
3674 return Ok(None);
3675 }
3676 let (lo, hi) = e.crange;
3677 let at = |t: f64| -> OgeomResult<f64> { Ok(side(e.curve.point_at(t, tol)?)) };
3678 let mut before = (lo, at(lo)?);
3679 for i in 1..=64 {
3680 let t = lo + (hi - lo) * f64::from(i) / 64.0;
3681 let now = (t, at(t)?);
3682 if (before.1 < 0.0) != (now.1 < 0.0) {
3683 let (mut a, mut b, mut fa) = (before.0, now.0, before.1);
3684 for _ in 0..64 {
3685 let m = f64::midpoint(a, b);
3686 if m <= a || m >= b {
3687 break;
3688 }
3689 let fm = at(m)?;
3690 if (fa < 0.0) == (fm < 0.0) {
3691 (a, fa) = (m, fm);
3692 } else {
3693 b = m;
3694 }
3695 }
3696 let q = e.curve.point_at(f64::midpoint(a, b), tol)?;
3697 found.push(p + u * (q - p).dot(u));
3698 }
3699 before = now;
3700 }
3701 }
3702 let mut kept: Vec<Point> = Vec::new();
3703 for q in found {
3704 let foot = ogeom_algo::project_on_curve(&e.curve, q, 64, tol)?;
3705 if foot.distance <= reach
3706 && foot.parameter >= e.crange.0 - tol.parametric()
3707 && foot.parameter <= e.crange.1 + tol.parametric()
3708 {
3709 kept.push(q);
3710 }
3711 }
3712 Ok(match kept[..] {
3713 [at] => Some(Crossed::Once(at, u)),
3714 [first, second] => Some(Crossed::Twice(first, second)),
3715 _ => None,
3716 })
3717}
3718
3719fn lines_crossing(
3722 first: &Curve,
3723 second: &Curve,
3724 tol: Tolerances,
3725) -> OgeomResult<Option<(Point, ogeom_math::Vector)>> {
3726 if !matches!(first, Curve::Line(_)) || !matches!(second, Curve::Line(_)) {
3727 return Ok(None);
3728 }
3729 let (p, q) = (first.point_at(0.0, tol)?, second.point_at(0.0, tol)?);
3730 let (u, v) = (
3731 first.point_at(1.0, tol)? - p,
3732 second.point_at(1.0, tol)? - q,
3733 );
3734 let across = u.cross(v);
3735 let square = across.dot(across);
3736 if square <= 0.0 {
3737 return Ok(None);
3738 }
3739 let s = (q - p).cross(v).dot(across) / square;
3740 Ok(Some((p + u * s, u / u.magnitude())))
3741}
3742
3743fn tangent_contacts(
3759 ga: &GSolid,
3760 gb: &GSolid,
3761 tangents: &[TangentRec],
3762 contacts: &[ContactRec],
3763 tol: Tolerances,
3764) -> OgeomResult<Vec<ContactRec>> {
3765 let reach = tol.confusion() * GRAZE_REACH;
3766 let weld = tol.confusion() * 1e2;
3767 let mut out: Vec<ContactRec> = Vec::new();
3768 for contact in tangents {
3769 let (lo, hi) = contact.curve.domain();
3770 let mut contact_bound = ogeom_math::Aabb::EMPTY;
3771 for i in 0..=16 {
3772 let t = lo + (hi - lo) * f64::from(i) / 16.0;
3773 contact_bound = contact_bound.with_point(contact.curve.point_at(t, tol)?);
3774 }
3775 for (solid, owner, target, target_from_a, target_face) in [
3776 (
3777 ga,
3778 &ga.faces[contact.face_a],
3779 &gb.faces[contact.face_b],
3780 false,
3781 contact.face_b,
3782 ),
3783 (
3784 gb,
3785 &gb.faces[contact.face_b],
3786 &ga.faces[contact.face_a],
3787 true,
3788 contact.face_a,
3789 ),
3790 ] {
3791 for e in &owner.edges {
3792 let held = |c: &ContactRec| {
3793 c.node == e.node
3794 && c.target_from_a == target_from_a
3795 && c.target_face == target_face
3796 };
3797 let near = reach.max(e.tolerance);
3798 if e.other_side.is_some()
3799 || !e.bound.expanded(near).intersects(&contact_bound)
3800 || contacts.iter().chain(&out).any(held)
3801 {
3802 continue;
3803 }
3804 let span = e
3805 .curve
3806 .point_at(e.crange.0, tol)?
3807 .distance(e.curve.point_at(e.crange.1, tol)?);
3808 if span <= weld {
3809 continue;
3810 }
3811 let mut lies = true;
3814 for i in 0..=16 {
3815 let t = e.crange.0 + (e.crange.1 - e.crange.0) * f64::from(i) / 16.0;
3816 let p = e.curve.point_at(t, tol)?;
3817 if ogeom_algo::project_on_curve(&contact.curve, p, 64, tol)?.distance > near
3818 || ogeom_algo::project_on_surface(&target.surface, p, 16, tol)?.distance
3819 > near
3820 {
3821 lies = false;
3822 break;
3823 }
3824 }
3825 if !lies {
3826 continue;
3827 }
3828 let Some(across) = solid.faces.iter().find(|f| {
3834 !core::ptr::eq(*f, owner) && f.edges.iter().any(|x| x.node == e.node)
3835 }) else {
3836 continue;
3837 };
3838 let Some(across_edge) = across.edges.iter().find(|x| x.node == e.node) else {
3839 continue;
3840 };
3841 let (Some(own_side), Some(other_side)) = (
3842 side_off_edge(owner, e, &target.surface, near, tol)?,
3843 side_off_edge(across, across_edge, &target.surface, near, tol)?,
3844 ) else {
3845 continue;
3846 };
3847 if own_side.signum() == other_side.signum() {
3848 continue;
3849 }
3850 let Ok((pcurve, _, true, ..)) = ogeom_algo::pcurve_fit::fit_projected_pcurve(
3851 &e.curve,
3852 e.crange,
3853 &target.surface,
3854 tol,
3855 ) else {
3856 continue;
3857 };
3858 if *DEBUG_WIRE {
3859 eprintln!(
3860 "TANGENT edge {} over {:?} onto face {target_face} (from_a {target_from_a})",
3861 e.node.index(),
3862 e.crange
3863 );
3864 }
3865 out.push(ContactRec {
3866 curve: (*e.curve).clone(),
3867 crange: e.crange,
3868 pcurve,
3869 prange: e.crange,
3870 node: e.node,
3871 tolerance: e.tolerance.max(owner.tolerance + target.tolerance),
3872 target_from_a,
3873 target_face,
3874 bound: e.bound,
3875 crossed: false,
3876 });
3877 }
3878 }
3879 }
3880 Ok(out)
3881}
3882
3883fn side_off_edge(
3893 face: &GFace,
3894 e: &BoundaryEdge,
3895 other: &SurfaceGeometry,
3896 reach: f64,
3897 tol: Tolerances,
3898) -> OgeomResult<Option<f64>> {
3899 let (p0, p1) = e.prange;
3900 let tm = f64::midpoint(p0, p1);
3901 let h = (p1 - p0).abs() * 1e-3;
3902 if h <= 0.0 {
3903 return Ok(None);
3904 }
3905 let at = e.pcurve.point_at(tm, tol)?;
3906 let (before, after) = (
3907 e.pcurve.point_at(tm - h, tol)?,
3908 e.pcurve.point_at(tm + h, tol)?,
3909 );
3910 let along = (after.x - before.x, after.y - before.y);
3911 let length = along.0.hypot(along.1);
3912 if length <= f64::MIN_POSITIVE {
3913 return Ok(None);
3914 }
3915 let square = (-along.1 / length, along.0 / length);
3916 let (su, sv) = face.surface.d1_at(at.x, at.y, tol)?;
3917 let per_param = su.magnitude() * square.0.abs() + sv.magnitude() * square.1.abs();
3918 if per_param <= f64::MIN_POSITIVE {
3919 return Ok(None);
3920 }
3921 let span = e
3922 .curve
3923 .point_at(e.crange.0, tol)?
3924 .distance(e.curve.point_at(e.crange.1, tol)?);
3925 let mut step = span * 0.05;
3926 for _ in 0..8 {
3927 for sign in [1.0, -1.0] {
3928 let k = sign * step / per_param;
3929 let q2 = Point2::new(square.0.mul_add(k, at.x), square.1.mul_add(k, at.y));
3930 let Ok(q) = face.surface.point_at(q2.x, q2.y, tol) else {
3931 continue;
3932 };
3933 if chart_point_within(face, q, reach, tol).is_none() {
3934 continue;
3935 }
3936 let foot = ogeom_geom::project_on_surface(other, q, 16, tol)?;
3937 let normal = other.normal_at(foot.parameters.0, foot.parameters.1, tol)?;
3938 let side = (q - foot.point).dot(normal.vector());
3939 if side.abs() > tol.confusion() {
3940 return Ok(Some(side));
3941 }
3942 return Ok(None);
3943 }
3944 step *= 0.5;
3945 }
3946 Ok(None)
3947}
3948
3949#[derive(Debug, Clone, Copy)]
3951struct Pave {
3952 t: f64,
3954 honesty: f64,
3958 across: Option<(bool, usize)>,
3962 spread: f64,
3967}
3968
3969fn lean_spread(
3975 e: &BoundaryEdge,
3976 t: f64,
3977 surface: &SurfaceGeometry,
3978 at: Point,
3979 tol: Tolerances,
3980) -> f64 {
3981 if e.tolerance <= tol.confusion() * DRAWN_APART {
3982 return 0.0;
3983 }
3984 let Ok(along) = e.curve.d1_at(t, tol) else {
3985 return 0.0;
3986 };
3987 let length = along.magnitude();
3988 let Ok(foot) = ogeom_geom::project_on_surface(surface, at, 16, tol) else {
3989 return 0.0;
3990 };
3991 let Ok(normal) = surface.normal_at(foot.parameters.0, foot.parameters.1, tol) else {
3992 return 0.0;
3993 };
3994 if length <= 0.0 {
3995 return 0.0;
3996 }
3997 let sine = along.dot(normal.vector()).abs() / length;
3998 let most = e.tolerance * LEAN_SPREAD;
3999 if sine * most > e.tolerance {
4000 e.tolerance / sine
4001 } else {
4002 most
4003 }
4004}
4005
4006fn touch_spread(
4026 (curve, t): (&Curve, f64),
4027 surface: &SurfaceGeometry,
4028 at: Point,
4029 honesty: f64,
4030 tol: Tolerances,
4031) -> f64 {
4032 if honesty <= 0.0 {
4033 return 0.0;
4034 }
4035 let Ok(d) = curve.derivatives_at(t, 2, tol) else {
4036 return 0.0;
4037 };
4038 let (Some(&d1), Some(&d2)) = (d.get(1), d.get(2)) else {
4039 return 0.0;
4040 };
4041 let speed = d1.magnitude();
4042 let Ok(foot) = ogeom_geom::project_on_surface(surface, at, 16, tol) else {
4043 return 0.0;
4044 };
4045 let (u, v) = foot.parameters;
4046 let (Ok((_, su, sv)), Ok((suu, suv, svv))) =
4047 (surface.point_d1_at(u, v, tol), surface.d2_at(u, v, tol))
4048 else {
4049 return 0.0;
4050 };
4051 let normal = su.cross(sv);
4052 let area = normal.magnitude();
4053 if speed <= 0.0 || area <= 0.0 {
4054 return 0.0;
4055 }
4056 let normal = normal * (1.0 / area);
4057 let tangent = d1 * (1.0 / speed);
4058 let (e, f, g) = (su.dot(su), su.dot(sv), sv.dot(sv));
4062 let (tu, tv) = (tangent.dot(su), tangent.dot(sv));
4063 let du = g.mul_add(tu, -f * tv) / (area * area);
4064 let dv = e.mul_add(tv, -f * tu) / (area * area);
4065 let bends = (du * du).mul_add(
4066 suu.dot(normal),
4067 (2.0 * du * dv).mul_add(suv.dot(normal), dv * dv * svv.dot(normal)),
4068 );
4069 let lean = tangent.dot(normal).abs();
4070 let curving = (d2 - tangent * d2.dot(tangent)) * (1.0 / (speed * speed));
4071 let parting = (curving.dot(normal) - bends).abs();
4072 if parting <= 0.0 || lean * lean > 2.0 * parting * honesty {
4073 return 0.0;
4074 }
4075 (lean + lean.mul_add(lean, 2.0 * parting * honesty).sqrt()) / parting
4076}
4077
4078#[derive(Debug, Clone, Copy)]
4089struct PaveCluster {
4090 t: f64,
4093 at: Point,
4095 span: f64,
4097 honesty: f64,
4099 members: usize,
4101}
4102
4103fn cluster_paves(
4104 curve: &Curve,
4105 crange: (f64, f64),
4106 edge_tolerance: f64,
4107 paves: &[Pave],
4108 off: Option<OffFace<'_>>,
4109 along: Along<'_>,
4110 tol: Tolerances,
4111) -> OgeomResult<Vec<PaveCluster>> {
4112 let mut ts: Vec<Pave> = paves
4113 .iter()
4114 .copied()
4115 .filter(|p| p.t > crange.0 + tol.parametric() && p.t < crange.1 - tol.parametric())
4116 .collect();
4117 ts.sort_by(|x, y| x.t.partial_cmp(&y.t).unwrap_or(core::cmp::Ordering::Equal));
4118 ts.dedup_by(|x, y| {
4119 if (x.t - y.t).abs() <= tol.parametric() {
4120 y.honesty = y.honesty.max(x.honesty);
4121 true
4122 } else {
4123 false
4124 }
4125 });
4126 let floor = edge_tolerance.max(tol.confusion() * 10.0);
4127 let ends = [
4131 curve.point_at(crange.0, tol)?,
4132 curve.point_at(crange.1, tol)?,
4133 ];
4134 let mut clusters: Vec<PaveCluster> = Vec::new();
4135 let mut prev: Option<(Point, Pave)> = None;
4136 let mut held_at: Vec<Point> = Vec::new();
4138 for pave in ts {
4139 let at = curve.point_at(pave.t, tol)?;
4140 if ends
4141 .iter()
4142 .any(|e| e.distance(at) <= floor.max(pave.honesty))
4143 {
4144 continue;
4145 }
4146 let joined = prev.is_some_and(|(held, last): (Point, Pave)| {
4153 let apart = held.distance(at);
4154 apart <= floor.max(last.honesty + pave.honesty)
4155 || (apart <= last.spread + pave.spread
4156 && held_at
4157 .iter()
4158 .all(|p| p.distance(at) <= last.spread + pave.spread))
4159 });
4160 let touch = match (prev, off) {
4161 (Some((held, last)), Some(off)) if !joined => grazed(
4162 curve,
4163 (held, last),
4164 (at, pave),
4165 edge_tolerance,
4166 off,
4167 along,
4168 tol,
4169 )?,
4170 _ => None,
4171 };
4172 if (joined || touch.is_some())
4173 && let Some(cluster) = clusters.last_mut()
4174 {
4175 held_at.push(at);
4176 if let Some((t, point)) = touch {
4177 cluster.t = t;
4178 cluster.at = point;
4179 }
4180 cluster.span = held_at
4181 .iter()
4182 .map(|p| p.distance(cluster.at))
4183 .fold(0.0, f64::max);
4184 cluster.honesty = cluster.honesty.max(pave.honesty);
4185 cluster.members += 1;
4186 } else {
4187 held_at.clear();
4188 held_at.push(at);
4189 clusters.push(PaveCluster {
4190 t: pave.t,
4191 at,
4192 span: 0.0,
4193 honesty: pave.honesty,
4194 members: 1,
4195 });
4196 }
4197 prev = Some((at, pave));
4198 }
4199 Ok(clusters)
4200}
4201
4202type OffFace<'f> = &'f dyn Fn((bool, usize), Point) -> OgeomResult<f64>;
4205
4206type Stretch = ((f64, f64), (bool, usize));
4210
4211type Along<'h> = &'h [Stretch];
4213
4214type AlongEdges = ogeom_core::FastMap<EdgeKey, Vec<Stretch>>;
4216
4217fn crossed_along(contacts: &[ContactRec]) -> AlongEdges {
4219 let mut along = AlongEdges::default();
4220 for c in contacts.iter().filter(|c| c.crossed) {
4221 along
4222 .entry(c.node)
4223 .or_default()
4224 .push((c.crange, (c.target_from_a, c.target_face)));
4225 }
4226 along
4227}
4228
4229fn off_face(
4232 ga: &GSolid,
4233 gb: &GSolid,
4234 (from_a, fi): (bool, usize),
4235 p: Point,
4236 tol: Tolerances,
4237) -> OgeomResult<f64> {
4238 let face = if from_a { &ga.faces[fi] } else { &gb.faces[fi] };
4239 Ok(ogeom_algo::project_on_surface(&face.surface, p, 16, tol)?.distance)
4240}
4241
4242fn grazed(
4269 curve: &Curve,
4270 (from, first): (Point, Pave),
4271 (to, second): (Point, Pave),
4272 edge_tolerance: f64,
4273 off: OffFace<'_>,
4274 along: Along<'_>,
4275 tol: Tolerances,
4276) -> OgeomResult<Option<(f64, Point)>> {
4277 let (Some(face), Some(other)) = (first.across, second.across) else {
4278 return Ok(None);
4279 };
4280 let honesty = honest(first.honesty.max(second.honesty).max(edge_tolerance), tol);
4281 if face != other || from.distance(to) > honest(f64::INFINITY, tol) * 1e2 {
4282 return Ok(None);
4283 }
4284 let (lo, hi) = (first.t.min(second.t), first.t.max(second.t));
4285 if along.iter().any(|&((a, b), onto)| {
4286 onto == face && a <= lo + tol.parametric() && b >= hi - tol.parametric()
4287 }) {
4288 return Ok(None);
4289 }
4290 let mut nearest: Option<(f64, f64, Point)> = None;
4291 for k in 1..8 {
4292 let t = (second.t - first.t).mul_add(f64::from(k) / 8.0, first.t);
4293 let p = curve.point_at(t, tol)?;
4294 let d = off(face, p)?;
4295 if d > honesty {
4296 return Ok(None);
4297 }
4298 if nearest.is_none_or(|(best, ..)| d < best) {
4299 nearest = Some((d, t, p));
4300 }
4301 }
4302 Ok(nearest.map(|(_, t, p)| (t, p)))
4303}
4304
4305#[derive(Clone)]
4307struct SectionPiece {
4308 section: usize,
4309 range: (f64, f64),
4310 hugs: [bool; 2],
4315 hug_key: Option<(EdgeKey, bool, usize)>,
4320}
4321
4322struct ContactRec {
4326 curve: Curve,
4328 crange: (f64, f64),
4329 pcurve: PlanarCurve,
4333 prange: (f64, f64),
4334 node: EdgeKey,
4336 tolerance: f64,
4340 target_from_a: bool,
4342 target_face: usize,
4343 bound: ogeom_math::Aabb,
4346 crossed: bool,
4351}
4352
4353fn projected_into_shared_chart(
4370 curve: &Curve,
4371 range: (f64, f64),
4372 owner: &SurfaceGeometry,
4373 target: &SurfaceGeometry,
4374 tol: Tolerances,
4375) -> OgeomResult<Option<PlanarCurve>> {
4376 let against_target = ogeom_algo::pcurve_fit::fit_projected_pcurve(curve, range, target, tol);
4377 if let Ok(fitted) = &against_target
4378 && fitted.2
4379 {
4380 return Ok(Some(fitted.0.clone()));
4381 }
4382 let Ok(on_owner) = ogeom_algo::pcurve_fit::fit_projected_pcurve(curve, range, owner, tol)
4383 else {
4384 ogeom_bail!(
4385 NotDone,
4386 "same-domain contact whose edge could not be projected into the \
4387 shared surface's chart: {}",
4388 against_target
4389 .err()
4390 .map_or_else(String::new, |e| e.to_string())
4391 );
4392 };
4393 let ((ua, ub), (va, vb)) = target.domain();
4394 let reach = tol.confusion() * 1e4;
4395 let at = |i: u32| range.0 + (range.1 - range.0) * f64::from(i) / 16.0;
4396 let mut checked = 0usize;
4397 for i in 0..=16 {
4398 let t = at(i);
4399 let uv = on_owner.0.point_at(t, tol)?;
4400 if uv.x < ua || uv.x > ub || uv.y < va || uv.y > vb {
4401 continue;
4402 }
4403 let lifted = target.point_at(uv.x, uv.y, tol)?;
4404 if lifted.distance(curve.point_at(t, tol)?) > reach {
4405 ogeom_bail!(
4406 NotDone,
4407 "same-domain contact whose edge overhangs the shared surface's \
4408 window, on a chart the two surfaces do not share"
4409 );
4410 }
4411 checked += 1;
4412 }
4413 if checked == 0 {
4414 for i in 0..=16 {
4419 if on_window(curve.point_at(at(i), tol)?, target, reach, tol)? {
4420 ogeom_bail!(
4421 NotDone,
4422 "same-domain contact whose edge lies on the shared surface's \
4423 window, on a chart the two surfaces do not share"
4424 );
4425 }
4426 }
4427 return Ok(None);
4428 }
4429 Ok(Some(on_owner.0))
4430}
4431
4432fn on_window(
4436 p: Point,
4437 surface: &SurfaceGeometry,
4438 reach: f64,
4439 tol: Tolerances,
4440) -> OgeomResult<bool> {
4441 let ((ua, ub), (va, vb)) = surface.domain();
4442 let (mu, mv) = ((ub - ua).abs() * 1e-9, (vb - va).abs() * 1e-9);
4443 let Ok(foot) = ogeom_algo::project_on_surface(surface, p, 16, tol) else {
4444 return Ok(false);
4445 };
4446 let (u, v) = foot.parameters;
4447 Ok(foot.distance <= reach
4448 && (!u.is_finite() || !ua.is_finite() || (u > ua + mu && u < ub - mu))
4449 && (!v.is_finite() || !va.is_finite() || (v > va + mv && v < vb - mv)))
4450}
4451
4452fn clipped_into_chart(
4466 curve: &Curve,
4467 range: (f64, f64),
4468 target: &SurfaceGeometry,
4469 reach: f64,
4470 tol: Tolerances,
4471) -> OgeomResult<Vec<(PlanarCurve, (f64, f64))>> {
4472 const SAMPLES: u32 = 64;
4473 let on =
4474 |t: f64| -> OgeomResult<bool> { on_window(curve.point_at(t, tol)?, target, reach, tol) };
4475 let at = |i: u32| range.0 + (range.1 - range.0) * f64::from(i) / f64::from(SAMPLES);
4476 let mut inside = Vec::with_capacity(SAMPLES as usize + 1);
4477 for i in 0..=SAMPLES {
4478 inside.push(on(at(i))?);
4479 }
4480 let narrow = |mut good: f64, mut bad: f64| -> OgeomResult<f64> {
4483 for _ in 0..40 {
4484 let mid = f64::midpoint(good, bad);
4485 if on(mid)? {
4486 good = mid;
4487 } else {
4488 bad = mid;
4489 }
4490 }
4491 Ok(good)
4492 };
4493 let mut stretches = Vec::new();
4494 let mut next = 0usize;
4495 while let Some(offset) = inside[next..].iter().position(|&x| x) {
4496 let first = next + offset;
4497 let last = inside[first..]
4498 .iter()
4499 .position(|&x| !x)
4500 .map_or(inside.len() - 1, |gap| first + gap - 1);
4501 next = last + 1;
4502 #[allow(clippy::cast_possible_truncation, reason = "sample indices")]
4503 let lo = if first == 0 {
4504 range.0
4505 } else {
4506 narrow(at(first as u32), at(first as u32 - 1))?
4507 };
4508 #[allow(clippy::cast_possible_truncation, reason = "sample indices")]
4509 let hi = if last == SAMPLES as usize {
4510 range.1
4511 } else {
4512 narrow(at(last as u32), at(last as u32 + 1))?
4513 };
4514 if hi - lo <= tol.parametric() {
4515 continue;
4516 }
4517 let fitted = ogeom_algo::pcurve_fit::fit_projected_pcurve(curve, (lo, hi), target, tol)?;
4518 if !fitted.2 {
4519 ogeom_bail!(
4520 NotDone,
4521 "same-domain contact whose edge's stretch on the shared surface's \
4522 window has no image in its chart"
4523 );
4524 }
4525 stretches.push((fitted.0, (lo, hi)));
4526 }
4527 Ok(stretches)
4528}
4529
4530fn coaxial_chart_map(
4547 owner: &SurfaceGeometry,
4548 target: &SurfaceGeometry,
4549 slop: f64,
4550 tol: Tolerances,
4551) -> Option<ogeom_math::Transform2> {
4552 let reach = slop.max(tol.confusion());
4553 let mut cones = None;
4554 let (fa, fb) = match (owner, target) {
4555 (SurfaceGeometry::Cylinder(a), SurfaceGeometry::Cylinder(b)) => {
4556 let (a, b) = (a.cylinder(), b.cylinder());
4557 if (a.radius() - b.radius()).abs() > reach {
4558 return None;
4559 }
4560 (a.frame(), b.frame())
4561 }
4562 (SurfaceGeometry::Cone(a), SurfaceGeometry::Cone(b)) => {
4563 let (a, b) = (a.cone(), b.cone());
4564 cones = Some((a, b));
4565 (a.frame(), b.frame())
4566 }
4567 (SurfaceGeometry::Torus(a), SurfaceGeometry::Torus(b)) => {
4568 let (a, b) = (a.torus(), b.torus());
4569 if (a.major_radius() - b.major_radius()).abs() > reach
4570 || (a.minor_radius() - b.minor_radius()).abs() > reach
4571 || a.frame().origin().distance(b.frame().origin()) > reach
4572 {
4573 return None;
4574 }
4575 (a.frame(), b.frame())
4576 }
4577 _ => return None,
4578 };
4579 let (za, zb) = (fa.z().vector(), fb.z().vector());
4580 let axis = ogeom_math::Axis::new(fb.origin(), fb.z());
4581 let lever = match owner {
4587 SurfaceGeometry::Torus(t) => t.torus().major_radius() + t.torus().minor_radius(),
4588 _ => {
4589 let (_, (va, vb)) = owner.domain();
4590 va.abs().max(vb.abs())
4591 }
4592 };
4593 let tilt = za.cross(zb).magnitude();
4594 let tilted = if lever.is_finite() {
4595 tilt * lever > reach
4596 } else {
4597 tilt > tol.angular()
4598 };
4599 if tilted || axis.distance_to(fa.origin()) > reach {
4600 return None;
4601 }
4602 let s = if za.dot(zb) >= 0.0 { 1.0 } else { -1.0 };
4603 let sigma = s * fa.handedness().sign() * fb.handedness().sign();
4604 let x = fa.x().vector();
4605 let phi = x.dot(fb.y().vector()).atan2(x.dot(fb.x().vector()));
4606 let c = if matches!(target, SurfaceGeometry::Torus(_)) {
4607 0.0
4608 } else {
4609 (fa.origin() - fb.origin()).dot(zb)
4610 };
4611 if let Some((a, b)) = cones
4612 && ((a.half_angle() - s * b.half_angle()).abs() > tol.angular()
4613 || (a.reference_radius() - b.radius_at(c)).abs() > reach)
4614 {
4615 return None;
4616 }
4617 let slide = ogeom_math::Transform2::translation(ogeom_math::Vector2::new(phi, c));
4618 Some(match (sigma > 0.0, s > 0.0) {
4619 (true, true) => slide,
4620 (false, false) => {
4621 ogeom_math::Transform2::rotation(Point2::new(phi / 2.0, c / 2.0), core::f64::consts::PI)
4622 }
4623 (false, true) => {
4625 ogeom_math::Transform2::translation(ogeom_math::Vector2::new(0.0, c))
4626 * ogeom_math::Transform2::line_mirror(
4627 Point2::new(phi / 2.0, 0.0),
4628 ogeom_math::Direction2::X,
4629 )
4630 }
4631 (true, false) => {
4633 ogeom_math::Transform2::translation(ogeom_math::Vector2::new(phi, 0.0))
4634 * ogeom_math::Transform2::line_mirror(
4635 Point2::new(0.0, c / 2.0),
4636 ogeom_math::Direction2::Y,
4637 )
4638 }
4639 })
4640}
4641
4642fn carried_across(
4646 pcurve: &PlanarCurve,
4647 prange: (f64, f64),
4648 owner: &SurfaceGeometry,
4649 target: &SurfaceGeometry,
4650 slop: f64,
4651 tol: Tolerances,
4652) -> Option<PlanarCurve> {
4653 let map = coaxial_chart_map(owner, target, slop, tol)?;
4654 let carried = pcurve.transformed(&map, tol).ok()?;
4655 let middle = carried.point_at(0.5 * (prange.0 + prange.1), tol).ok()?;
4656 let ((ua, ub), (va, vb)) = target.domain();
4657 let turn = core::f64::consts::TAU;
4658 let turns = |at: f64, lo: f64, hi: f64| -> f64 {
4659 if at < lo {
4660 ((lo - at) / turn).ceil()
4661 } else if at > hi {
4662 -((at - hi) / turn).ceil()
4663 } else {
4664 0.0
4665 }
4666 };
4667 let along_v = if target.is_periodic_v() {
4670 turns(middle.y, va, vb)
4671 } else {
4672 0.0
4673 };
4674 let shift = ogeom_math::Vector2::new(turns(middle.x, ua, ub) * turn, along_v * turn);
4675 if shift.x == 0.0 && shift.y == 0.0 {
4676 return Some(carried);
4677 }
4678 carried
4679 .transformed(&ogeom_math::Transform2::translation(shift), tol)
4680 .ok()
4681}
4682
4683fn same_chart(a: &SurfaceGeometry, b: &SurfaceGeometry, tol: Tolerances) -> bool {
4692 use SurfaceGeometry as S;
4693 let frames = |fa: ogeom_math::Frame, fb: ogeom_math::Frame| -> bool {
4697 fa.handedness() == fb.handedness()
4698 && fa.origin().distance(fb.origin()) <= tol.confusion()
4699 && fa.z().vector().dot(fb.z().vector()) >= 1.0 - tol.angular()
4700 && fa.x().vector().dot(fb.x().vector()) >= 1.0 - tol.angular()
4701 };
4702 match (a, b) {
4703 (S::Plane(x), S::Plane(y)) => frames(x.plane().frame(), y.plane().frame()),
4704 (S::Cylinder(x), S::Cylinder(y)) => {
4705 frames(x.cylinder().frame(), y.cylinder().frame())
4706 && (x.cylinder().radius() - y.cylinder().radius()).abs() <= tol.confusion()
4707 }
4708 (S::Cone(x), S::Cone(y)) => {
4709 frames(x.cone().frame(), y.cone().frame())
4710 && (x.cone().reference_radius() - y.cone().reference_radius()).abs()
4711 <= tol.confusion()
4712 && (x.cone().half_angle() - y.cone().half_angle()).abs() <= tol.angular()
4713 }
4714 (S::Sphere(x), S::Sphere(y)) => {
4715 frames(x.sphere().frame(), y.sphere().frame())
4716 && (x.sphere().radius() - y.sphere().radius()).abs() <= tol.confusion()
4717 }
4718 (S::Torus(x), S::Torus(y)) => {
4719 frames(x.torus().frame(), y.torus().frame())
4720 && (x.torus().major_radius() - y.torus().major_radius()).abs() <= tol.confusion()
4721 && (x.torus().minor_radius() - y.torus().minor_radius()).abs() <= tol.confusion()
4722 }
4723 (S::BSpline(x), S::BSpline(y)) => x == y,
4728 _ => false,
4729 }
4730}
4731
4732#[derive(Clone)]
4734enum Tag {
4735 Boundary { edge: usize, range: (f64, f64) },
4737 Section { section: usize, range: (f64, f64) },
4739 Contact { contact: usize, range: (f64, f64) },
4742 Pole { pole: usize, range: (f64, f64) },
4746}
4747
4748#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4756enum PieceState {
4757 In,
4758 Out,
4759 OnAligned,
4763 OnOpposed,
4766 Unread,
4768}
4769
4770fn interior_of(line: &[Point2]) -> Point2 {
4775 if line.len() == 2 {
4776 Point2::new(
4777 f64::midpoint(line[0].x, line[1].x),
4778 f64::midpoint(line[0].y, line[1].y),
4779 )
4780 } else {
4781 line[line.len() / 2]
4782 }
4783}
4784
4785fn fold_into_chart(line: &mut [Point2], surface: &SurfaceGeometry, tol: Tolerances) {
4796 let ((ua, _), (va, _)) = surface.domain();
4797 if line.is_empty() {
4798 return;
4799 }
4800 let mid = interior_of(line);
4801 let [span_u, span_v] = chart_spans(surface, tol);
4802 if let Some(span) = span_u {
4803 let shift = (ua + (mid.x - ua).rem_euclid(span)) - mid.x;
4804 for p in line.iter_mut() {
4805 p.x += shift;
4806 }
4807 }
4808 if let Some(span) = span_v {
4809 let shift = (va + (mid.y - va).rem_euclid(span)) - mid.y;
4810 for p in line.iter_mut() {
4811 p.y += shift;
4812 }
4813 }
4814}
4815
4816fn chart_spans(surface: &SurfaceGeometry, tol: Tolerances) -> [Option<f64>; 2] {
4822 let ((ua, ub), (va, vb)) = surface.domain();
4823 [
4824 ((surface.is_periodic_u() || surface.is_closed_u(tol)) && ub > ua).then_some(ub - ua),
4825 ((surface.is_periodic_v() || surface.is_closed_v(tol)) && vb > va).then_some(vb - va),
4826 ]
4827}
4828
4829fn period_shifts(surface: &SurfaceGeometry, tol: Tolerances) -> Vec<(f64, f64)> {
4832 let mut out = vec![(0.0, 0.0)];
4833 let [span_u, span_v] = chart_spans(surface, tol);
4834 if let Some(span) = span_u {
4835 out.push((span, 0.0));
4836 out.push((-span, 0.0));
4837 }
4838 if let Some(span) = span_v {
4839 out.push((0.0, span));
4840 out.push((0.0, -span));
4841 }
4842 out
4843}
4844
4845fn shifts_toward(
4856 surface: &SurfaceGeometry,
4857 from: Point2,
4858 centre: Point2,
4859 tol: Tolerances,
4860) -> Vec<(f64, f64)> {
4861 let mut out = period_shifts(surface, tol);
4862 if let [Some(du), Some(dv)] = chart_spans(surface, tol) {
4863 let toward = (
4864 ((centre.x - from.x) / du).round() * du,
4865 ((centre.y - from.y) / dv).round() * dv,
4866 );
4867 if toward.0 != 0.0 && toward.1 != 0.0 {
4868 out.push(toward);
4869 }
4870 }
4871 out
4872}
4873
4874fn middle_of<'a>(lines: impl IntoIterator<Item = &'a [Point2]>) -> Point2 {
4876 let (low, high) = lines.into_iter().flatten().fold(
4877 (
4878 Point2::new(f64::INFINITY, f64::INFINITY),
4879 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
4880 ),
4881 |(low, high), p| {
4882 (
4883 Point2::new(low.x.min(p.x), low.y.min(p.y)),
4884 Point2::new(high.x.max(p.x), high.y.max(p.y)),
4885 )
4886 },
4887 );
4888 Point2::new(f64::midpoint(low.x, high.x), f64::midpoint(low.y, high.y))
4889}
4890
4891fn fold_inside(
4900 p: Point2,
4901 surface: &SurfaceGeometry,
4902 trim: &[&[Point2]],
4903 tol: Tolerances,
4904) -> Point2 {
4905 let folded = fold_point_into_chart(p, surface, tol);
4906 if trim.is_empty() || chart_spans(surface, tol).iter().all(Option::is_none) {
4908 return folded;
4909 }
4910 shifts_toward(surface, folded, middle_of(trim.iter().copied()), tol)
4911 .into_iter()
4912 .map(|(du, dv)| Point2::new(folded.x + du, folded.y + dv))
4913 .find(|q| inside_many(trim, *q))
4914 .unwrap_or(folded)
4915}
4916
4917fn fold_line_inside(
4921 line: &mut [Point2],
4922 surface: &SurfaceGeometry,
4923 trim: &arrange::Trim,
4924 tol: Tolerances,
4925) {
4926 fold_into_chart(line, surface, tol);
4927 if line.is_empty() || trim.is_empty() {
4928 return;
4929 }
4930 let mid = interior_of(line);
4934 if let Some((du, dv)) = shifts_toward(surface, mid, trim.middle(), tol)
4935 .into_iter()
4936 .find(|(du, dv)| trim.inside_slanted(Point2::new(mid.x + du, mid.y + dv)))
4937 {
4938 for p in line.iter_mut() {
4939 p.x += du;
4940 p.y += dv;
4941 }
4942 }
4943}
4944
4945fn face_trim_lines(face: &GFace, tol: Tolerances) -> Vec<Vec<Point2>> {
4948 let sample = |pcurve: &PlanarCurve, range: (f64, f64)| -> Vec<Point2> {
4949 (0..=16)
4950 .filter_map(|k| {
4951 pcurve
4952 .point_at(range.0 + (range.1 - range.0) * f64::from(k) / 16.0, tol)
4953 .ok()
4954 })
4955 .collect()
4956 };
4957 let mut out = Vec::new();
4958 for e in &face.edges {
4959 out.push(sample(&e.pcurve, e.prange));
4960 if let Some((other, range)) = &e.other_side {
4961 out.push(sample(other, *range));
4962 }
4963 }
4964 out
4965}
4966
4967fn unwrap_polyline(line: &mut [Point2], surface: &SurfaceGeometry, tol: Tolerances) {
4969 let [span_u, span_v] = chart_spans(surface, tol);
4973 let spans = (span_u.unwrap_or(0.0), span_v.unwrap_or(0.0));
4974 for i in 1..line.len() {
4975 if spans.0 > 0.0 {
4976 while line[i].x - line[i - 1].x > spans.0 * 0.5 {
4977 line[i].x -= spans.0;
4978 }
4979 while line[i].x - line[i - 1].x < -spans.0 * 0.5 {
4980 line[i].x += spans.0;
4981 }
4982 }
4983 if spans.1 > 0.0 {
4984 while line[i].y - line[i - 1].y > spans.1 * 0.5 {
4985 line[i].y -= spans.1;
4986 }
4987 while line[i].y - line[i - 1].y < -spans.1 * 0.5 {
4988 line[i].y += spans.1;
4989 }
4990 }
4991 }
4992}
4993
4994fn fold_point_into_chart(p: Point2, surface: &SurfaceGeometry, tol: Tolerances) -> Point2 {
4996 let mut one = [p];
4997 fold_into_chart(&mut one, surface, tol);
4998 one[0]
4999}
5000
5001fn folded_range(range: (f64, f64), domain: (f64, f64), closed: bool) -> (f64, f64) {
5011 if !closed {
5012 return range;
5013 }
5014 let length = range.1 - range.0;
5015 let mut f0 = fold(range.0, domain);
5016 if f0 + length - domain.1 > domain.1 - f0 {
5017 f0 = domain.0.max(f0 - (domain.1 - domain.0));
5018 }
5019 let f1 = (f0 + length).min(domain.1);
5020 (f0, f1)
5021}
5022
5023fn fold(t: f64, domain: (f64, f64)) -> f64 {
5025 let span = domain.1 - domain.0;
5026 if span <= 0.0 {
5027 return domain.0;
5028 }
5029 domain.0 + (t - domain.0).rem_euclid(span)
5030}
5031
5032fn at_param(t: f64, domain: (f64, f64), closed: bool) -> f64 {
5041 if closed { fold(t, domain) } else { t }
5042}
5043
5044fn doubt_of(face: &GFace) -> f64 {
5048 face.edges
5049 .iter()
5050 .fold(0.0_f64, |acc, e| {
5051 acc.max(e.standoff * 2.0)
5052 .max(e.ends_tolerance + e.tolerance)
5053 })
5054 .max(
5055 face.holes_aside
5056 .iter()
5057 .fold(0.0_f64, |acc, hole| acc.max(hole.doubt)),
5058 )
5059}
5060
5061fn doubt_at(face: &GFace, p: Point, tol: Tolerances) -> OgeomResult<f64> {
5069 let mut doubt = face
5070 .holes_aside
5071 .iter()
5072 .fold(face.tolerance, |acc, hole| acc.max(hole.doubt));
5073 for e in &face.edges {
5074 for (end, radius) in e.ends {
5075 let reach = radius + e.tolerance;
5076 if p.distance(end) <= reach {
5077 doubt = doubt.max(reach);
5078 }
5079 }
5080 let along = e.standoff * 2.0;
5081 if along > doubt
5082 && e.bound.distance_to(p) <= along
5083 && distance_to_edge_curve(&e.curve, e.crange, p, tol)? <= along
5084 {
5085 doubt = along;
5086 }
5087 }
5088 Ok(doubt.min(doubt_of(face)))
5089}
5090
5091fn share_hugs(
5112 pieces: &mut [SectionPiece],
5113 sections: &[SectionRec],
5114 solids: [&GSolid; 2],
5115 tol: Tolerances,
5116) -> OgeomResult<()> {
5117 let point_of = |section: &SectionRec, t: f64| -> OgeomResult<Point> {
5118 let domain = section.curve.domain();
5119 section
5120 .curve
5121 .point_at(at_param(t, domain, section.closed), tol)
5122 };
5123 let face_of = |s: &SectionRec, side: usize| if side == 0 { s.face_a } else { s.face_b };
5124 let mut weld: [Vec<f64>; 2] = [
5127 solids[0].faces.iter().map(doubt_of).collect(),
5128 solids[1].faces.iter().map(doubt_of).collect(),
5129 ];
5130 for s in sections {
5131 let (a, b) = (
5132 doubt_of(&solids[0].faces[s.face_a]),
5133 doubt_of(&solids[1].faces[s.face_b]),
5134 );
5135 weld[0][s.face_a] = weld[0][s.face_a].max(b);
5136 weld[1][s.face_b] = weld[1][s.face_b].max(a);
5137 }
5138 let hug_width = |p: &SectionPiece, side: usize| -> f64 {
5140 if p.hugs[side] {
5141 (tol.confusion() * 1e3).max(sections[p.section].tolerance * 3.0)
5142 } else {
5143 0.0
5144 }
5145 };
5146 let along_edge = |p: &SectionPiece,
5149 e: &BoundaryEdge,
5150 near: &dyn Fn(Point) -> OgeomResult<f64>|
5151 -> OgeomResult<bool> {
5152 let s = §ions[p.section];
5153 for i in 0..=4 {
5154 let t = p.range.0 + (p.range.1 - p.range.0) * f64::from(i) / 4.0;
5155 let at = point_of(s, t)?;
5156 let (drawn, drange, off) = e.drawn();
5157 let reach = near(at)?.max(off * 2.0);
5158 if distance_to_edge_curve(drawn, drange, at, tol)? > reach {
5159 return Ok(false);
5160 }
5161 }
5162 Ok(true)
5163 };
5164 let stretch =
5165 |s: &SectionRec, p: &SectionPiece| folded_range(p.range, s.curve.domain(), s.closed);
5166 let middles: Vec<Point> = pieces
5167 .iter()
5168 .map(|p| point_of(§ions[p.section], f64::midpoint(p.range.0, p.range.1)))
5169 .collect::<OgeomResult<_>>()?;
5170 let mut adopted: Vec<(usize, usize, EdgeKey)> = Vec::new();
5171 for side in 0..2 {
5172 let own = solids[side];
5173 for (x, px) in pieces.iter().enumerate() {
5174 if px.hugs[1 - side] {
5175 continue;
5176 }
5177 let sx = §ions[px.section];
5178 let (fx, crossed) = (face_of(sx, side), face_of(sx, 1 - side));
5179 for (y, py) in pieces.iter().enumerate().skip(x + 1) {
5180 if py.hugs[1 - side] || py.section == px.section || (px.hugs[side] && py.hugs[side])
5181 {
5182 continue;
5183 }
5184 let sy = §ions[py.section];
5185 let fy = face_of(sy, side);
5186 if fy == fx || face_of(sy, 1 - side) != crossed {
5187 continue;
5188 }
5189 let hugged = hug_width(px, side).max(hug_width(py, side));
5190 let widest = weld[1 - side][crossed].max(hugged);
5191 let (mx, my) = (middles[x], middles[y]);
5192 if mx.distance(my) > 2.0 * widest
5193 || distance_to_edge_curve(&sx.curve, stretch(sx, px), my, tol)? > widest
5194 || distance_to_edge_curve(&sy.curve, stretch(sy, py), mx, tol)? > widest
5195 {
5196 continue;
5197 }
5198 let faces = [
5199 &own.faces[fx],
5200 &own.faces[fy],
5201 &solids[1 - side].faces[crossed],
5202 ];
5203 let near = |at: Point| -> OgeomResult<f64> {
5204 let mut near = hugged;
5205 for face in faces {
5206 near = near.max(doubt_at(face, at, tol)?);
5207 }
5208 Ok(near)
5209 };
5210 let middle = near(mx)?.max(near(my)?);
5211 if mx.distance(my) > 2.0 * middle
5212 || distance_to_edge_curve(&sx.curve, stretch(sx, px), my, tol)? > middle
5213 || distance_to_edge_curve(&sy.curve, stretch(sy, py), mx, tol)? > middle
5214 {
5215 continue;
5216 }
5217 for ex in &own.faces[fx].edges {
5218 let Some(ey) = own.faces[fy].edges.iter().find(|e| e.node == ex.node) else {
5219 continue;
5220 };
5221 if along_edge(px, ex, &near)? && along_edge(py, ey, &near)? {
5222 if !px.hugs[side] {
5223 adopted.push((x, side, ex.node));
5224 }
5225 if !py.hugs[side] {
5226 adopted.push((y, side, ey.node));
5227 }
5228 break;
5229 }
5230 }
5231 }
5232 }
5233 }
5234 for (y, side, key) in adopted {
5235 let piece = &mut pieces[y];
5236 if piece.hugs[0] || piece.hugs[1] {
5237 continue;
5238 }
5239 let section = §ions[piece.section];
5240 if *DEBUG_WIRE {
5241 eprintln!(
5242 "PAVE s{}: ({:.6}, {:.6}) hugs the edge with the section beside it",
5243 piece.section, piece.range.0, piece.range.1
5244 );
5245 }
5246 piece.hugs[side] = true;
5247 piece.hug_key = Some(if side == 0 {
5248 (key, false, section.face_b)
5249 } else {
5250 (key, true, section.face_a)
5251 });
5252 }
5253 Ok(())
5254}
5255
5256fn drums_onto_planes(
5273 own: &GSolid,
5274 other: &GSolid,
5275 tol: Tolerances,
5276) -> Vec<Option<ogeom_math::Vector>> {
5277 let mut numbered: ogeom_core::FastMap<&(ogeom_topo::SurfaceId, Location), usize> =
5278 ogeom_core::FastMap::default();
5279 let mut asked: Vec<Vec<(ogeom_math::Vector, f64)>> = Vec::new();
5280 let mut of_face = Vec::with_capacity(own.faces.len());
5281 for face in &own.faces {
5282 let next = numbered.len();
5283 let at = *numbered.entry(&face.carrier).or_insert(next);
5284 if at == asked.len() {
5285 asked.push(Vec::new());
5286 }
5287 of_face.push(at);
5288 let SurfaceGeometry::Cylinder(drum) = &face.surface else {
5289 continue;
5290 };
5291 let drum = drum.cylinder();
5292 for theirs in &other.faces {
5293 let SurfaceGeometry::Plane(plane) = &theirs.surface else {
5294 continue;
5295 };
5296 if !face.filter.intersects(&theirs.filter) {
5297 continue;
5298 }
5299 let plane = plane.plane();
5300 if let Some(shift) = ogeom_intersect::drum_onto_plane(&plane, &drum, tol) {
5301 let normal = plane.normal().vector();
5302 asked[at].push((normal, shift.dot(normal)));
5303 }
5304 }
5305 }
5306 let moves: Vec<Option<ogeom_math::Vector>> = asked
5307 .iter()
5308 .map(|asked| {
5309 let &(n0, s0) = asked.first()?;
5310 let mut shift = n0 * s0;
5314 if let Some(&(n1, s1)) = asked
5315 .iter()
5316 .find(|(n, _)| n.cross(n0).magnitude() > tol.angular())
5317 {
5318 let (a, b, c) = (n0.dot(n0), n0.dot(n1), n1.dot(n1));
5319 let det = a.mul_add(c, -(b * b));
5320 shift = n0 * ((s0 * c - s1 * b) / det) + n1 * ((s1 * a - s0 * b) / det);
5321 }
5322 let agrees = asked
5323 .iter()
5324 .all(|&(n, s)| (n.dot(shift) - s).abs() <= tol.intersection());
5325 let longest = asked.iter().map(|(_, s)| s.abs()).fold(0.0, f64::max);
5326 (agrees && shift.magnitude() <= longest + tol.intersection()).then_some(shift)
5327 })
5328 .collect();
5329 of_face.into_iter().map(|at| moves[at]).collect()
5330}
5331
5332fn move_drums(
5342 solid: &mut GSolid,
5343 moves: &[Option<ogeom_math::Vector>],
5344 tol: Tolerances,
5345) -> OgeomResult<()> {
5346 if moves.iter().all(Option::is_none) {
5347 return Ok(());
5348 }
5349 let mut holders: ogeom_core::FastMap<EdgeKey, Vec<(usize, usize)>> =
5350 ogeom_core::FastMap::default();
5351 for (f, face) in solid.faces.iter().enumerate() {
5352 for (k, e) in face.edges.iter().enumerate() {
5353 holders.entry(e.node).or_default().push((f, k));
5354 }
5355 }
5356 let along_plane = |face: &GFace, shift: ogeom_math::Vector| match &face.surface {
5357 SurfaceGeometry::Plane(plane) => {
5358 plane.plane().normal().vector().dot(shift).abs() <= tol.angular() * shift.magnitude()
5359 }
5360 _ => false,
5361 };
5362 let mut shift_of: ogeom_core::FastMap<EdgeKey, ogeom_math::Vector> =
5363 ogeom_core::FastMap::default();
5364 for (key, held) in &holders {
5365 if solid.shared.contains_key(key) {
5366 continue;
5367 }
5368 let Some(shift) = held.iter().find_map(|&(f, _)| moves[f]) else {
5369 continue;
5370 };
5371 if held.iter().all(|&(f, _)| match moves[f] {
5372 Some(other) => other == shift,
5373 None => along_plane(&solid.faces[f], shift),
5374 }) {
5375 shift_of.insert(*key, shift);
5376 }
5377 }
5378 let staying: ogeom_core::FastSet<[u64; 3]> = solid
5380 .faces
5381 .iter()
5382 .flat_map(|face| &face.edges)
5383 .filter(|e| !shift_of.contains_key(&e.node))
5384 .flat_map(|e| e.vertices)
5385 .map(|p| [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()])
5386 .collect();
5387 let mut curves: ogeom_core::FastMap<EdgeKey, std::sync::Arc<Curve>> =
5388 ogeom_core::FastMap::default();
5389 for (face, own) in solid.faces.iter_mut().zip(moves) {
5390 let far = own
5391 .or_else(|| {
5392 face.edges
5393 .iter()
5394 .find_map(|e| shift_of.get(&e.node).copied())
5395 })
5396 .map_or(0.0, |shift| shift.magnitude());
5397 if far == 0.0 {
5398 continue;
5399 }
5400 if let Some(shift) = own {
5401 face.surface = face
5402 .surface
5403 .transformed(&ogeom_math::Transform::translation(*shift), tol)?;
5404 }
5405 face.bound = face.bound.expanded(far);
5406 face.filter = face.filter.expanded(far);
5407 let chart_shift = |shift: ogeom_math::Vector| match &face.surface {
5408 SurfaceGeometry::Plane(plane) => {
5409 let frame = plane.plane().frame();
5410 Some(ogeom_math::Transform2::translation(
5411 ogeom_math::Vector2::new(
5412 shift.dot(frame.x().vector()),
5413 shift.dot(frame.y().vector()),
5414 ),
5415 ))
5416 }
5417 _ => None,
5418 };
5419 for e in &mut face.edges {
5420 let moved = shift_of.get(&e.node).copied();
5421 if let Some(image) = &e.image
5424 && let Some(shift) = moved.or(*own)
5425 {
5426 e.image = Some(std::sync::Arc::new(
5427 image.transformed(&ogeom_math::Transform::translation(shift), tol)?,
5428 ));
5429 }
5430 let Some(shift) = moved else {
5431 continue;
5432 };
5433 let moving = ogeom_math::Transform::translation(shift);
5434 e.curve = match curves.get(&e.node) {
5435 Some(curve) => curve.clone(),
5436 None => {
5437 let curve = std::sync::Arc::new(e.curve.transformed(&moving, tol)?);
5438 curves.insert(e.node, curve.clone());
5439 curve
5440 }
5441 };
5442 if own.is_none()
5444 && let Some(by) = chart_shift(shift)
5445 {
5446 e.pcurve = e.pcurve.transformed(&by, tol)?;
5447 }
5448 for end in &mut e.ends {
5449 end.0 = moving.apply(end.0);
5450 }
5451 for at in &mut e.vertices {
5452 if !staying.contains(&[at.x.to_bits(), at.y.to_bits(), at.z.to_bits()]) {
5453 *at = moving.apply(*at);
5454 }
5455 }
5456 e.bound = e.bound.expanded(far);
5457 }
5458 }
5459 Ok(())
5460}
5461
5462#[allow(clippy::type_complexity)]
5465fn fill(
5466 ga: &GSolid,
5467 gb: &GSolid,
5468 admit_all: bool,
5469 neighbours: Option<&[ogeom_core::FastSet<usize>]>,
5470 tol: Tolerances,
5471) -> OgeomResult<(
5472 Vec<SectionRec>,
5473 Vec<SectionPiece>,
5474 Vec<ContactRec>,
5475 Vec<TangentRec>,
5476 Vec<Vec<(f64, f64)>>,
5477 ogeom_core::FastMap<EdgeKey, Vec<Pave>>,
5478 Vec<Vec<usize>>,
5479 Vec<Vec<usize>>,
5480 Vec<Junction>,
5481)> {
5482 use ogeom_intersect::{
5483 CurveCurveOptions, IntersectOptions, SurfaceIntersection, intersect_curves,
5484 intersect_surfaces_within,
5485 };
5486 let mut sections: Vec<SectionRec> = Vec::new();
5487 let mut contacts: Vec<ContactRec> = Vec::new();
5488 let mut tangents: Vec<TangentRec> = Vec::new();
5489 let mut same_pairs: Vec<(usize, usize)> = Vec::new();
5490 #[derive(Default)]
5499 struct PairFound {
5500 sections: Vec<SectionRec>,
5501 contacts: Vec<ContactRec>,
5502 tangents: Vec<TangentRec>,
5503 same_pairs: Vec<(usize, usize)>,
5504 rim_crossed: bool,
5505 #[cfg(feature = "test-hooks")]
5506 marched: usize,
5507 }
5508 let at_rims = AT_RIMS.get();
5509 let as_stated = AS_STATED.get();
5510 let mut pairs: Vec<(usize, usize)> = if admit_all {
5511 (0..ga.faces.len())
5512 .flat_map(|ia| (0..gb.faces.len()).map(move |ib| (ia, ib)))
5513 .collect()
5514 } else {
5515 let filters: Vec<ogeom_math::Aabb> = gb.faces.iter().map(|f| f.filter).collect();
5519 let tree = box_tree::BoxTree::new(&filters);
5520 let mut meeting = Vec::new();
5521 let mut pairs = Vec::new();
5522 for (ia, fa) in ga.faces.iter().enumerate() {
5523 tree.meeting(&fa.filter, &mut meeting);
5524 pairs.extend(meeting.iter().map(|&ib| (ia, ib)));
5525 }
5526 pairs
5527 };
5528 if let Some(neighbours) = neighbours {
5531 pairs.retain(|(ia, ib)| !neighbours[*ia].contains(ib));
5532 }
5533 #[cfg(feature = "test-hooks")]
5534 work::add(|w| w.face_pairs += pairs.len());
5535 let (carriers_a, carriers_b) = (carrier_windows(ga, tol), carrier_windows(gb, tol));
5539 type Met = std::sync::Arc<std::sync::OnceLock<OgeomResult<SurfaceIntersection>>>;
5540 let met_once: std::sync::Mutex<ogeom_core::FastMap<(usize, usize, u64), Met>> =
5541 std::sync::Mutex::default();
5542 let found = ogeom_core::parallel::map_ordered(
5543 &pairs,
5544 |_, &(ia, ib)| -> OgeomResult<PairFound> {
5545 ogeom_core::progress::checkpoint()?;
5546 let (fa, fb) = (&ga.faces[ia], &gb.faces[ib]);
5547 let ((carrier_a, window_a), (carrier_b, window_b)) = (carriers_a[ia], carriers_b[ib]);
5548 let admitted = fa.filter.intersects(&fb.filter);
5549 let mut out = PairFound::default();
5550 let scale = fa.chord_scale.min(fb.chord_scale);
5551 let chord = (scale * 1e-7).max(tol.confusion() * 0.5);
5552 let options = IntersectOptions {
5553 tolerance: chord,
5558 marching: ogeom_intersect::Marching {
5559 chord,
5560 ..ogeom_intersect::Marching::default()
5561 },
5562 };
5563 let at_rim = if at_rims {
5567 ogeom_intersect::plane_extrusion_at_rims(&fa.surface, &fb.surface, tol)
5568 } else {
5569 out.rim_crossed =
5570 ogeom_intersect::crosses_extrusion_rim(&fa.surface, &fb.surface, tol);
5571 None
5572 };
5573 let met = if coincide_as_stated(fa, fb, as_stated, tol) {
5577 SurfaceIntersection::Same
5578 } else if let Some(found) = at_rim {
5579 if found.is_empty() {
5580 SurfaceIntersection::Apart
5581 } else {
5582 SurfaceIntersection::Along(found)
5583 }
5584 } else {
5585 let once = met_once
5586 .lock()
5587 .unwrap_or_else(std::sync::PoisonError::into_inner)
5588 .entry((carrier_a, carrier_b, chord.to_bits()))
5589 .or_default()
5590 .clone();
5591 let met = once
5592 .get_or_init(|| {
5593 intersect_surfaces_within(
5594 &fa.surface,
5595 &fb.surface,
5596 (window_a, window_b),
5597 options,
5598 tol,
5599 )
5600 })
5601 .clone()?;
5602 let wide_a = reaching(&fa.surface, &fb.surface, &met, tol)?;
5603 let wide_b = reaching(&fb.surface, &fa.surface, &met, tol)?;
5604 if wide_a.is_some() || wide_b.is_some() {
5605 intersect_surfaces_within(
5608 wide_a.as_ref().unwrap_or(&fa.surface),
5609 wide_b.as_ref().unwrap_or(&fb.surface),
5610 (
5611 window_a.filter(|_| wide_a.is_none()),
5612 window_b.filter(|_| wide_b.is_none()),
5613 ),
5614 options,
5615 tol,
5616 )?
5617 } else {
5618 met
5619 }
5620 };
5621 match met {
5622 SurfaceIntersection::Apart => {}
5623 SurfaceIntersection::Same => {
5624 out.same_pairs.push((ia, ib));
5632 for (owner_from_a, owner, target_from_a, target, target_face) in
5633 [(false, fb, true, fa, ia), (true, fa, false, fb, ib)]
5634 {
5635 for e in &owner.edges {
5636 let slop = target
5644 .edges
5645 .iter()
5646 .map(|t| t.tolerance)
5647 .fold(e.tolerance, f64::max)
5648 .max(owner.tolerance + target.tolerance);
5649 let exact =
5650 ogeom_intersect::exact_pcurve_of(&e.curve, &target.surface, tol)
5651 .or_else(|| {
5652 if slop <= tol.confusion() {
5653 return None;
5654 }
5655 let loose = Tolerances::with_scale(
5656 tol.scale() * tol.confusion() / slop,
5657 )
5658 .ok()?;
5659 ogeom_intersect::exact_pcurve_of(
5660 &e.curve,
5661 &target.surface,
5662 loose,
5663 )
5664 });
5665 let mut clipped_contact = if exact.is_none()
5666 && !same_chart(&owner.surface, &target.surface, tol)
5667 && carried_across(
5668 &e.pcurve,
5669 e.prange,
5670 &owner.surface,
5671 &target.surface,
5672 slop,
5673 tol,
5674 )
5675 .is_none()
5676 && projected_into_shared_chart(
5677 &e.curve,
5678 e.crange,
5679 &owner.surface,
5680 &target.surface,
5681 tol,
5682 )
5683 .is_err()
5684 {
5685 let reach = slop
5686 .max(owner.tolerance + target.tolerance)
5687 .max(tol.confusion() * 1e2);
5688 let stretches = clipped_into_chart(
5689 &e.curve,
5690 e.crange,
5691 &target.surface,
5692 reach,
5693 tol,
5694 )?;
5695 if stretches.is_empty() {
5698 continue;
5699 }
5700 stretches
5701 } else {
5702 Vec::new()
5703 };
5704 let clipped = core::mem::take(&mut clipped_contact);
5705 let shared_chart = exact.is_none()
5706 && clipped.is_empty()
5707 && same_chart(&owner.surface, &target.surface, tol);
5708 let carried = if exact.is_none() && clipped.is_empty() && !shared_chart
5714 {
5715 carried_across(
5716 &e.pcurve,
5717 e.prange,
5718 &owner.surface,
5719 &target.surface,
5720 slop,
5721 tol,
5722 )
5723 .filter(|_| {
5724 projected_into_shared_chart(
5725 &e.curve,
5726 e.crange,
5727 &owner.surface,
5728 &target.surface,
5729 tol,
5730 )
5731 .is_err()
5732 })
5733 } else {
5734 None
5735 };
5736 let images: Vec<(PlanarCurve, (f64, f64), (f64, f64))> = match (
5737 exact,
5738 clipped.is_empty(),
5739 carried,
5740 ) {
5741 (Some(exact), _, _) => vec![(exact, e.crange, e.crange)],
5742 (None, false, _) => clipped
5747 .into_iter()
5748 .map(|(pcurve, range)| (pcurve, range, range))
5749 .collect(),
5750 (None, true, _) if shared_chart => {
5759 vec![(e.pcurve.clone(), e.prange, e.crange)]
5760 }
5761 (None, true, Some(carried)) => vec![(carried, e.prange, e.crange)],
5762 (None, true, None) => {
5771 let Some(pcurve) = projected_into_shared_chart(
5772 &e.curve,
5773 e.crange,
5774 &owner.surface,
5775 &target.surface,
5776 tol,
5777 )?
5778 else {
5779 continue;
5784 };
5785 if *DEBUG_WIRE {
5786 eprintln!(
5787 "SAME owner {} face {} (from_a {owner_from_a}) edge {} of kind {:?}: {:?} .. {:?} against target face {target_face}",
5788 if owner_from_a { "a" } else { "b" },
5789 if owner_from_a { ia } else { ib },
5790 e.node.index(),
5791 core::mem::discriminant(&*e.curve),
5792 e.curve.point_at(e.crange.0, tol).ok(),
5793 e.curve.point_at(e.crange.1, tol).ok()
5794 );
5795 }
5796 vec![(pcurve, e.crange, e.crange)]
5797 }
5798 };
5799 for (pcurve, prange, crange) in images {
5800 out.contacts.push(ContactRec {
5801 curve: (*e.curve).clone(),
5802 crange,
5803 pcurve,
5804 prange,
5805 node: e.node,
5806 tolerance: e.tolerance.max(owner.tolerance + target.tolerance),
5811 target_from_a,
5812 target_face,
5813 bound: e.bound,
5814 crossed: false,
5815 });
5816 }
5817 }
5818 }
5819 }
5820 SurfaceIntersection::Touching(_) => {}
5827 SurfaceIntersection::Along(curves) => {
5828 if let (SurfaceGeometry::Plane(pa), SurfaceGeometry::Plane(pb)) =
5836 (&fa.surface, &fb.surface)
5837 {
5838 let (na, nb) = (pa.plane().normal(), pb.plane().normal());
5839 if na.vector().cross(nb.vector()).magnitude() <= 1e-3 {
5840 for (owner, target, target_from_a, target_face) in
5841 [(fb, fa, true, ia), (fa, fb, false, ib)]
5842 {
5843 let SurfaceGeometry::Plane(on) = &target.surface else {
5844 continue;
5845 };
5846 for e in &owner.edges {
5847 if !matches!(&*e.curve, Curve::Line(_)) {
5848 continue;
5849 }
5850 let lies = [e.crange.0, e.crange.1].iter().all(|&t| {
5851 e.curve.point_at(t, tol).is_ok_and(|p| {
5852 on.plane().distance_to(p) <= tol.confusion()
5853 })
5854 });
5855 if !lies {
5856 continue;
5857 }
5858 let Some(pcurve) = ogeom_intersect::exact_pcurve_of(
5859 &e.curve,
5860 &target.surface,
5861 tol,
5862 ) else {
5863 continue;
5864 };
5865 out.contacts.push(ContactRec {
5866 curve: (*e.curve).clone(),
5867 crange: e.crange,
5868 pcurve,
5869 prange: e.crange,
5870 node: e.node,
5871 tolerance: e
5872 .tolerance
5873 .max(owner.tolerance + target.tolerance),
5874 target_from_a,
5875 target_face,
5876 bound: e.bound,
5877 crossed: false,
5878 });
5879 }
5880 }
5881 }
5882 }
5883 let mut replaced = false;
5904 if let (SurfaceGeometry::Plane(pa), SurfaceGeometry::Plane(pb)) =
5905 (&fa.surface, &fb.surface)
5906 {
5907 let sine = pa
5908 .plane()
5909 .normal()
5910 .vector()
5911 .cross(pb.plane().normal().vector())
5912 .magnitude();
5913 let lines: Vec<&Curve> = curves
5914 .iter()
5915 .filter(|c| !c.tangential)
5916 .map(|c| &c.curve)
5917 .collect();
5918 let weld = tol.confusion() * 1e2;
5919 let narrow = sine * fa.bound.diagonal().min(fb.bound.diagonal()) > weld;
5920 if (sine > 1e-3 || narrow)
5921 && let [solved] = lines.as_slice()
5922 && matches!(solved, Curve::Line(_))
5923 {
5924 for (owner, target, target_from_a, target_face) in
5925 [(fb, fa, true, ia), (fa, fb, false, ib)]
5926 {
5927 let SurfaceGeometry::Plane(on) = &target.surface else {
5928 continue;
5929 };
5930 for e in &owner.edges {
5931 if !matches!(&*e.curve, Curve::Line(_)) {
5932 continue;
5933 }
5934 let mut ends = Vec::with_capacity(2);
5935 for t in [e.crange.0, e.crange.1] {
5936 ends.push(e.curve.point_at(t, tol)?);
5937 }
5938 let stray = ends
5939 .iter()
5940 .map(|p| on.plane().distance_to(*p))
5941 .fold(0.0_f64, f64::max);
5942 if stray > weld {
5943 continue;
5944 }
5945 let mut off = 0.0_f64;
5946 for p in &ends {
5947 off = off.max(
5948 ogeom_algo::project_on_curve(solved, *p, 64, tol)?
5949 .distance,
5950 );
5951 }
5952 if off <= tol.confusion().max(stray * 2.0) || off > weld / sine
5958 {
5959 continue;
5960 }
5961 let Some(pcurve) = ogeom_intersect::exact_pcurve_of(
5962 &e.curve,
5963 &target.surface,
5964 tol,
5965 ) else {
5966 continue;
5967 };
5968 if sine <= 1e-3 && !shared_line(&ends, target, weld, tol)? {
5977 continue;
5978 }
5979 replaced = true;
5980 if sine <= 1e-3 && stray <= tol.confusion() {
5984 continue;
5985 }
5986 out.contacts.push(ContactRec {
5987 curve: (*e.curve).clone(),
5988 crange: e.crange,
5989 pcurve,
5990 prange: e.crange,
5991 node: e.node,
5992 tolerance: e
5993 .tolerance
5994 .max(owner.tolerance + target.tolerance),
5995 target_from_a,
5996 target_face,
5997 bound: e.bound,
5998 crossed: false,
5999 });
6000 }
6001 }
6002 }
6003 }
6004 if !replaced
6020 && let (SurfaceGeometry::Plane(pa), SurfaceGeometry::Plane(pb)) =
6021 (&fa.surface, &fb.surface)
6022 {
6023 let sine = pa
6024 .plane()
6025 .normal()
6026 .vector()
6027 .cross(pb.plane().normal().vector())
6028 .magnitude();
6029 for (face, plane) in [(fa, pb.plane()), (fb, pa.plane())] {
6030 if let Some(edge_in) = one_side_of(face, plane, tol)?
6031 && (sine <= 1e-3 || !edge_in)
6032 {
6033 replaced = true;
6034 }
6035 }
6036 }
6037 let before = out.sections.len();
6043 let mut marched = false;
6044 for sc in curves {
6045 if sc.tangential {
6051 if let (Some(pa), Some(pb)) = (sc.on_a, sc.on_b) {
6052 out.tangents.push(TangentRec {
6053 curve: sc.curve,
6054 pc_a: pa,
6055 pc_b: pb,
6056 face_a: ia,
6057 face_b: ib,
6058 });
6059 }
6060 continue;
6061 }
6062 if marched || replaced {
6063 continue;
6064 }
6065 if !sc.exact {
6074 let (length, reach) = length_and_reach(&sc.curve, tol)?;
6075 let turn =
6076 4.0 * fa.bound.diagonal().max(fb.bound.diagonal()).max(reach);
6077 if *DEBUG_WIRE {
6078 eprintln!(
6079 "SECTION CHECK faces {ia}/{ib} exact {} closed {} length {length:.4} turn {turn:.4} tol {:.2e}",
6080 sc.exact, sc.closed, sc.tolerance
6081 );
6082 }
6083 if length > turn {
6084 ogeom_bail!(
6085 NotDone,
6086 "a marched section of length {length} runs beyond a turn \
6087 round the faces it cuts ({turn}); its trace wandered \
6088 beside a chart's pole; see docs/CAPABILITIES.md, bool.booleans"
6089 );
6090 }
6091 }
6092 match (sc.on_a.clone(), sc.on_b.clone()) {
6093 (Some(pa), Some(pb)) => out.sections.push(SectionRec {
6094 curve: sc.curve,
6095 pc_a: pa,
6096 pc_b: pb,
6097 face_a: ia,
6098 face_b: ib,
6099 closed: sc.closed,
6100 tolerance: sc.tolerance,
6101 }),
6102 _ => {
6103 if let Some(split) = split_at_degeneracies(&sc.curve, fa, fb, tol)?
6112 {
6113 for (curve, pa, pb) in split {
6114 out.sections.push(SectionRec {
6115 curve,
6116 pc_a: pa,
6117 pc_b: pb,
6118 face_a: ia,
6119 face_b: ib,
6120 closed: false,
6121 tolerance: 0.0,
6122 });
6123 }
6124 continue;
6125 }
6126 let image = |surface: &SurfaceGeometry,
6137 have: Option<&PlanarCurve>|
6138 -> OgeomResult<Option<(PlanarCurve, f64)>> {
6139 if let Some(pc) = have {
6140 return Ok(Some((pc.clone(), 0.0)));
6141 }
6142 fitted_image(&sc.curve, surface, options.tolerance, tol)
6143 };
6144 if let (Some((pa, ea)), Some((pb, eb))) = (
6145 image(&fa.surface, sc.on_a.as_ref())?,
6146 image(&fb.surface, sc.on_b.as_ref())?,
6147 ) {
6148 out.sections.push(SectionRec {
6149 curve: sc.curve,
6150 pc_a: pa,
6151 pc_b: pb,
6152 face_a: ia,
6153 face_b: ib,
6154 closed: sc.closed,
6155 tolerance: sc.tolerance.max(ea).max(eb),
6156 });
6157 continue;
6158 }
6159 if *DEBUG_WIRE {
6162 eprintln!(
6163 "MARCH PAIR faces {ia}/{ib} a {:?} b {:?} admitted {admitted}",
6164 core::mem::discriminant(&fa.surface),
6165 core::mem::discriminant(&fb.surface)
6166 );
6167 }
6168 out.sections.truncate(before);
6169 marched = true;
6170 let shared = if admitted {
6171 fa.bound.intersection(&fb.bound)
6172 } else {
6173 fa.bound.union(&fb.bound)
6177 };
6178 let (wa, wb) = (
6179 windowed_to(&fa.surface, &shared),
6180 windowed_to(&fb.surface, &shared),
6181 );
6182 #[cfg(feature = "test-hooks")]
6183 {
6184 out.marched += 1;
6185 }
6186 let (marched, pole_reach) = march_pair(&wa, &wb, &options, tol)?;
6187 for fitted in marched {
6188 let budget =
6197 (options.marching.chord * 1e3).max(options.tolerance * 1e3);
6198 if fitted.fit_error > budget {
6199 ogeom_bail!(
6200 NotDone,
6201 "a marched section's fit misses its trace by {} \
6202 against a chord of {}; a branch passing beside a \
6203 chart's pole fits nothing yet; see docs/CAPABILITIES.md, \
6204 boolean.general",
6205 fitted.fit_error,
6206 options.marching.chord
6207 );
6208 }
6209 let curve: Curve = fitted.curve.into();
6210 let (length, reach) = length_and_reach(&curve, tol)?;
6211 let turn = 4.0
6212 * fa.bound.diagonal().max(fb.bound.diagonal()).max(reach);
6213 if length > turn {
6214 ogeom_bail!(
6215 NotDone,
6216 "a marched section of length {length} runs beyond a \
6217 turn round the faces it cuts ({turn}); its trace \
6218 wandered beside a chart's pole; see docs/CAPABILITIES.md, \
6219 boolean.general"
6220 );
6221 }
6222 out.sections.push(SectionRec {
6223 closed: curve.is_closed(tol)
6228 && !on_a_chart_pole(&curve, [&wa, &wb], tol)?,
6229 tolerance: (options.marching.chord + fitted.fit_error)
6233 .max(pole_reach),
6234 curve,
6235 pc_a: fitted.on_a.into(),
6236 pc_b: fitted.on_b.into(),
6237 face_a: ia,
6238 face_b: ib,
6239 });
6240 }
6241 }
6242 }
6243 }
6244 }
6245 }
6246
6247 Ok(out)
6248 },
6249 );
6250 let mut carried: ogeom_core::FastSet<(EdgeKey, bool, usize, [u64; 4])> =
6255 ogeom_core::FastSet::default();
6256 for pair in found {
6257 let pair = pair?;
6258 sections.extend(pair.sections);
6259 contacts.extend(pair.contacts.into_iter().filter(|c| {
6260 carried.insert((
6261 c.node,
6262 c.target_from_a,
6263 c.target_face,
6264 [
6265 c.crange.0.to_bits(),
6266 c.crange.1.to_bits(),
6267 c.prange.0.to_bits(),
6268 c.prange.1.to_bits(),
6269 ],
6270 ))
6271 }));
6272 tangents.extend(pair.tangents);
6273 same_pairs.extend(pair.same_pairs);
6274 if pair.rim_crossed {
6275 RIM_CROSSED.set(true);
6276 }
6277 #[cfg(feature = "test-hooks")]
6278 work::add(|w| w.marched_pairs += pair.marched);
6279 }
6280 for section in &mut sections {
6281 if let Some(rebased) = seamed_at_a_stop(
6282 section,
6283 &ga.faces[section.face_a],
6284 &gb.faces[section.face_b],
6285 tol,
6286 ) {
6287 *section = rebased;
6288 }
6289 }
6290 #[cfg(feature = "test-hooks")]
6291 work::add(|w| {
6292 w.widest_section = sections
6293 .iter()
6294 .fold(w.widest_section, |widest, s| widest.max(s.tolerance));
6295 });
6296 let grazed = graze_contacts(ga, gb, §ions, &contacts, FROM_CROSSINGS.get(), tol)?;
6297 contacts.extend(grazed);
6298 let touched = tangent_contacts(ga, gb, &tangents, &contacts, tol)?;
6299 contacts.extend(touched);
6300
6301 let outline = |face: &GFace| -> OgeomResult<Vec<Vec<Point2>>> {
6303 let mut lines = Vec::new();
6304 for e in &face.edges {
6305 lines.push(pcurve_polyline_within_sag(
6306 &e.pcurve,
6307 e.prange,
6308 e.crange,
6309 e.crange,
6310 &face.surface,
6311 tol,
6312 )?);
6313 if let Some((other, orange)) = &e.other_side {
6314 lines.push(pcurve_polyline_within_sag(
6315 other,
6316 *orange,
6317 e.crange,
6318 e.crange,
6319 &face.surface,
6320 tol,
6321 )?);
6322 }
6323 }
6324 weld_outline_ends(&mut lines, face, tol);
6325 bridge_outline_gaps(&mut lines);
6326 Ok(lines)
6327 };
6328 let crossed_a: ogeom_core::FastSet<usize> = sections.iter().map(|s| s.face_a).collect();
6332 let crossed_b: ogeom_core::FastSet<usize> = sections.iter().map(|s| s.face_b).collect();
6333 let mut outlines_a: Vec<std::borrow::Cow<'_, [Vec<Point2>]>> =
6336 Vec::with_capacity(ga.faces.len());
6337 let mut outlines_b: Vec<std::borrow::Cow<'_, [Vec<Point2>]>> =
6338 Vec::with_capacity(gb.faces.len());
6339 for (faces, crossed, outlines) in [
6340 (&ga.faces, &crossed_a, &mut outlines_a),
6341 (&gb.faces, &crossed_b, &mut outlines_b),
6342 ] {
6343 for (i, f) in faces.iter().enumerate() {
6344 outlines.push(if !crossed.contains(&i) {
6345 std::borrow::Cow::Owned(Vec::new())
6346 } else if let Some(lines) = f.outline(tol) {
6347 std::borrow::Cow::Borrowed(lines)
6348 } else {
6349 std::borrow::Cow::Owned(outline(f)?)
6350 });
6351 }
6352 }
6353
6354 let mut paves: ogeom_core::FastMap<EdgeKey, Vec<Pave>> = ogeom_core::FastMap::default();
6357 let mut pieces: Vec<SectionPiece> = Vec::new();
6358 type SectionWork = (
6364 Vec<(EdgeKey, Pave)>,
6365 Vec<SectionPiece>,
6366 Vec<Junction>,
6367 Option<SectionRec>,
6368 );
6369 let mut hug_junctions: Vec<Junction> = Vec::new();
6370 let section_bounds: Vec<Option<ogeom_math::Aabb>> = sections
6374 .iter()
6375 .map(|s| {
6376 ogeom_algo::curve_bounds(&s.curve, tol)
6377 .ok()
6378 .map(|b| b.expanded(s.tolerance))
6379 })
6380 .collect();
6381 let at_stated = AT_STATED.get();
6384 let paved: Vec<OgeomResult<SectionWork>> = ogeom_core::parallel::map_ordered(
6385 §ions,
6386 |si, section: &SectionRec| {
6387 ogeom_core::progress::checkpoint()?;
6388 let mut paves: Vec<(EdgeKey, Pave)> = Vec::new();
6389 let mut pieces: Vec<SectionPiece> = Vec::new();
6390 let mut junctions: Vec<Junction> = Vec::new();
6391 let reach = tol.confusion().max(section.tolerance * 2.0);
6394 let cc = CurveCurveOptions {
6395 gap: reach.max(CurveCurveOptions::default().gap),
6396 ..CurveCurveOptions::default()
6397 };
6398 let domain = section.curve.domain();
6399 let mut trim_ts: Vec<f64> = Vec::new();
6400 let mut hits: Vec<(usize, EdgeKey, f64, f64, f64, bool, f64)> = Vec::new();
6403 let mut along: [Vec<(f64, f64, usize)>; 2] = [Vec::new(), Vec::new()];
6417 for (side, own) in [
6418 (0_usize, &ga.faces[section.face_a]),
6419 (1, &gb.faces[section.face_b]),
6420 ] {
6421 let across = if side == 0 {
6425 &gb.faces[section.face_b]
6426 } else {
6427 &ga.faces[section.face_a]
6428 };
6429 for (ei, e) in own.edges.iter().enumerate() {
6430 if !admit_all && !e.bound.expanded(reach).intersects(&across.bound) {
6431 continue;
6432 }
6433 let (drawn, drange, off) = e.drawn();
6445 let reach = if section.tolerance > 0.0 {
6446 reach + off
6447 } else {
6448 reach
6449 };
6450 let planar = matches!(own.surface, SurfaceGeometry::Plane(_));
6451 let widened = planar || section.tolerance <= 0.0;
6452 let edge_reach = if widened { reach + off } else { reach };
6453 let cc = CurveCurveOptions {
6454 gap: cc.gap.max(edge_reach),
6455 ..cc
6456 };
6457 let found = intersect_curves(§ion.curve, drawn, cc, tol)?;
6458 let near_an_end = |c: &ogeom_intersect::Crossing<Point>| -> OgeomResult<bool> {
6473 for end in [e.crange.0, e.crange.1] {
6474 if e.curve.point_at(end, tol)?.distance(c.point) <= c.reach {
6475 return Ok(true);
6476 }
6477 }
6478 Ok(false)
6479 };
6480 let mut crossings = found.crossings.clone();
6481 if section.tolerance <= 0.0
6482 && e.image.is_none()
6483 && exact(§ion.curve)
6484 && exact(drawn)
6485 && edge_reach > reach
6486 {
6487 let mut touches = Vec::new();
6488 for c in &found.crossings {
6489 if c.reach > 0.0 && near_an_end(c)? {
6490 touches.push(*c);
6491 }
6492 }
6493 if !touches.is_empty() {
6494 let narrow = intersect_curves(
6495 §ion.curve,
6496 drawn,
6497 CurveCurveOptions {
6498 gap: reach.max(CurveCurveOptions::default().gap),
6499 ..cc
6500 },
6501 tol,
6502 )?;
6503 let met: Vec<ogeom_intersect::Crossing<Point>> = narrow
6504 .crossings
6505 .into_iter()
6506 .filter(|n| {
6507 n.reach <= 0.0
6508 && n.gap <= reach
6509 && touches
6510 .iter()
6511 .any(|t| t.point.distance(n.point) <= t.reach)
6512 })
6513 .collect();
6514 if !met.is_empty() {
6515 crossings.retain(|c| !touches.contains(c));
6516 crossings.extend(met);
6517 }
6518 }
6519 }
6520 for crossing in &crossings {
6521 if crossing.gap > edge_reach {
6522 continue;
6523 }
6524 if !planar
6532 && crossing.gap > reach
6533 && tangential(§ion.curve, crossing.on_a, drawn, crossing.on_b, tol)?
6534 {
6535 continue;
6536 }
6537 let mut on_b =
6538 onto_range(e.onto_curve(crossing.on_b), &e.curve, e.crange, tol);
6539 let mut honesty = honest(section.tolerance, tol);
6540 let mut on_a = crossing.on_a;
6547 let exact_pair = section.tolerance <= 0.0
6563 && e.image.is_none()
6564 && exact(§ion.curve)
6565 && exact(drawn);
6566 let weld = if exact_pair {
6567 (reach + e.tolerance.max(e.ends_tolerance)).max(tol.confusion() * 10.0)
6568 } else {
6569 (reach + honest(section.tolerance, tol)).max(tol.confusion() * 1e2)
6570 };
6571 let mut at_end: Option<(f64, Point)> = None;
6572 for end in [e.crange.0, e.crange.1] {
6573 let vertex = e.curve.point_at(end, tol)?;
6574 if vertex.distance(crossing.point) <= weld + crossing.reach {
6575 at_end = Some((end, vertex));
6576 let snapped =
6577 ogeom_algo::project_on_curve(§ion.curve, vertex, 64, tol)?;
6578 if snapped.distance <= weld + crossing.reach {
6579 on_a = snapped.parameter;
6580 on_b = end;
6588 }
6589 break;
6590 }
6591 }
6592 let touch = crossing.reach > 0.0
6602 || tangential(
6603 §ion.curve,
6604 crossing.on_a,
6605 drawn,
6606 crossing.on_b,
6607 tol,
6608 )?;
6609 let mut ended = false;
6610 if touch && !section.closed {
6613 let half = (domain.1 - domain.0) * 0.5;
6614 for end in [domain.0, domain.1] {
6615 if (crossing.on_a - end).abs() > half {
6616 continue;
6617 }
6618 let tip = section
6619 .curve
6620 .point_at(at_param(end, domain, section.closed), tol)?;
6621 if crossing.reach > 0.0
6622 && tip.distance(crossing.point) > crossing.reach + weld
6623 {
6624 continue;
6625 }
6626 let foot = ogeom_algo::project_on_curve(drawn, tip, 64, tol)?;
6631 let width =
6632 (crossing.reach + honest(weld, tol)).max(tol.confusion() * 1e3);
6633 if *DEBUG_WIRE {
6634 eprintln!(
6635 "PAVE s{si}: touch on edge {} at {:.6}: tip {:.3e} off the edge, width {width:.2e}",
6636 e.node.index(),
6637 crossing.on_a,
6638 foot.distance
6639 );
6640 }
6641 if foot.distance <= width {
6642 ended = true;
6643 on_a = end;
6644 on_b = onto_range(
6645 e.onto_curve(foot.parameter),
6646 &e.curve,
6647 e.crange,
6648 tol,
6649 );
6650 if let Some((end_b, vertex)) = at_end
6658 && tip.distance(vertex) <= width.min(tol.confusion() * 1e5)
6659 {
6660 on_b = end_b;
6661 let gap = tip.distance(vertex);
6662 if gap > tol.confusion() * 1e2 {
6663 junctions.push(Junction {
6664 at: vertex,
6665 reach: gap + tol.confusion() * 1e2,
6666 onto_vertex: true,
6667 });
6668 }
6669 }
6670 let wander = if section.tolerance > 0.0 {
6684 crossing.reach.min(tol.confusion() * 1e5)
6685 } else {
6686 0.0
6687 };
6688 honesty = honesty.max(foot.distance).max(wander);
6689 }
6690 break;
6691 }
6692 }
6693 if touch
6705 && !ended
6706 && at_end.is_none()
6707 && let Some(touched) = ogeom_intersect::touch_curve_surface(
6708 drawn,
6709 &across.surface,
6710 crossing.on_b,
6711 tol,
6712 )
6713 && touched.point.distance(crossing.point) <= crossing.reach + weld
6714 {
6715 let foot = ogeom_algo::project_on_curve(
6716 §ion.curve,
6717 touched.point,
6718 64,
6719 tol,
6720 )?;
6721 if foot.distance <= reach {
6722 on_a = foot.parameter;
6723 on_b = onto_range(
6724 e.onto_curve(touched.on_curve),
6725 &e.curve,
6726 e.crange,
6727 tol,
6728 );
6729 }
6730 }
6731 if at_end.is_none()
6743 && !ended
6744 && e.seam.is_none()
6745 && (on_b < e.crange.0 - tol.parametric()
6746 || on_b > e.crange.1 + tol.parametric())
6747 && distance_to_edge_curve(drawn, drange, crossing.point, tol)?
6748 > weld + crossing.reach
6749 {
6750 continue;
6751 }
6752 if at_end.is_none()
6778 && let SurfaceGeometry::Plane(plane) = &across.surface
6779 {
6780 let along_edge = e.curve.d1_at(on_b, tol)?;
6781 let length = along_edge.magnitude();
6782 if length > 0.0 {
6783 let sine =
6784 along_edge.dot(plane.plane().normal().vector()).abs() / length;
6785 if sine > 0.0 {
6786 let off = plane
6787 .plane()
6788 .signed_distance_to(e.curve.point_at(on_b, tol)?)
6789 .abs();
6790 let leaning = off / sine;
6791 honesty =
6792 honesty.max(if crossing.gap <= tol.confusion() * 1e2 {
6793 leaning.min(tol.confusion() * 2e4)
6794 } else {
6795 honest(leaning, tol)
6796 });
6797 }
6798 }
6799 }
6800 if *DEBUG_WIRE {
6801 eprintln!(
6802 "PAVE s{si}: side {side} edge {} crossing at {on_a:.6} (edge {on_b:.6}) gap {:.2e} reach {:.2e} honesty {honesty:.2e} at {:?}",
6803 e.node.index(),
6804 crossing.gap,
6805 crossing.reach,
6806 crossing.point
6807 );
6808 }
6809 let spread = if at_end.is_none() {
6820 lean_spread(e, on_b, &across.surface, crossing.point, tol).max(
6821 touch_spread(
6822 (drawn, crossing.on_b),
6823 &across.surface,
6824 crossing.point,
6825 honest(section.tolerance, tol),
6826 tol,
6827 ),
6828 )
6829 } else {
6830 0.0
6831 };
6832 hits.push((side, e.node, on_b, on_a, honesty, at_end.is_none(), spread));
6833 }
6834 for overlap in &found.overlaps {
6835 for ((lo, hi), ends) in overlap_within(overlap, drange, drawn, tol) {
6842 let (olo, ohi) = if overlap.on_a.0 <= overlap.on_a.1 {
6857 overlap.on_a
6858 } else {
6859 (overlap.on_a.1, overlap.on_a.0)
6860 };
6861 let mut window = [lo, hi];
6862 for (t, end) in window.iter_mut().zip(ends) {
6863 let Some(end) = end else {
6864 continue;
6865 };
6866 let vertex = drawn.point_at(end, tol)?;
6867 let foot =
6868 ogeom_algo::project_on_curve(§ion.curve, vertex, 64, tol)?;
6869 let mut f = foot.parameter;
6870 if section.closed {
6871 let period = domain.1 - domain.0;
6872 f += period * ((*t - f) / period).round();
6873 }
6874 if foot.distance <= cc.gap
6875 && f >= olo - tol.parametric()
6876 && f <= ohi + tol.parametric()
6877 {
6878 *t = f;
6879 }
6880 }
6881 let [lo, hi] = window;
6882 if hi - lo <= tol.parametric() {
6883 continue;
6884 }
6885 if *DEBUG_WIRE {
6886 eprintln!("PAVE s{si}: side {side} overlap ({lo:.6}, {hi:.6})");
6887 }
6888 for t in [lo, hi] {
6901 let at = section
6902 .curve
6903 .point_at(at_param(t, domain, section.closed), tol)?;
6904 let foot = ogeom_algo::project_on_curve(drawn, at, 64, tol)?;
6905 if foot.distance <= reach.max(tol.confusion() * 1e3) {
6906 let on_b = onto_range(
6907 e.onto_curve(foot.parameter),
6908 &e.curve,
6909 e.crange,
6910 tol,
6911 );
6912 let (split, gap) = if e.image.is_some() {
6915 let split = e.curve.point_at(on_b, tol)?;
6916 (split, at.distance(split))
6917 } else {
6918 (foot.point, foot.distance)
6919 };
6920 hits.push((
6921 side,
6922 e.node,
6923 on_b,
6924 t,
6925 honest(section.tolerance, tol).max(gap),
6926 false,
6927 0.0,
6928 ));
6929 if gap > tol.confusion() * 1e2 {
6930 junctions.push(Junction {
6931 at: split,
6932 reach: gap + tol.confusion() * 1e2,
6933 onto_vertex: false,
6934 });
6935 }
6936 }
6937 }
6938 trim_ts.push(lo);
6939 trim_ts.push(hi);
6940 along[side].push((lo, hi, ei));
6941 }
6942 }
6943 }
6944 }
6945 hits.retain(|(side, _, _, on_a, _, _, _)| {
6954 !along[*side].iter().any(|(lo, hi, _)| {
6955 *on_a > lo + tol.parametric() && *on_a < hi - tol.parametric()
6956 })
6957 });
6958 #[allow(clippy::type_complexity)]
6959 let edge_hits: Vec<(
6960 EdgeKey,
6961 f64,
6962 f64,
6963 f64,
6964 Option<(bool, usize)>,
6965 f64,
6966 usize,
6967 )> = hits
6968 .iter()
6969 .map(|(side, node, on_b, on_a, honesty, crossed, spread)| {
6970 let across = crossed.then(|| {
6971 if *side == 0 {
6972 (false, section.face_b)
6973 } else {
6974 (true, section.face_a)
6975 }
6976 });
6977 (*node, *on_b, *on_a, *honesty, across, *spread, *side)
6978 })
6979 .collect();
6980 trim_ts.extend(hits.iter().map(|(_, _, _, on_a, _, _, _)| *on_a));
6981 let mut cross_ts: Vec<f64> = Vec::new();
6982 for (sj, other) in sections.iter().enumerate() {
6983 if sj == si {
6984 continue;
6985 }
6986 if other.face_a != section.face_a && other.face_b != section.face_b {
6987 continue;
6988 }
6989 let both = reach.max(tol.confusion().max(other.tolerance * 2.0));
6990 if !admit_all
6991 && let (Some(x), Some(y)) = (§ion_bounds[si], §ion_bounds[sj])
6992 && !x.expanded(both).intersects(y)
6993 {
6994 continue;
6995 }
6996 if !admit_all {
7003 let apart = |x: &ogeom_math::Aabb, y: &ogeom_math::Aabb| {
7004 !x.expanded(both).intersects(&y.expanded(both))
7005 };
7006 let unshared_a = other.face_a != section.face_a
7007 && apart(
7008 &ga.faces[section.face_a].filter,
7009 &ga.faces[other.face_a].filter,
7010 );
7011 let unshared_b = other.face_b != section.face_b
7012 && apart(
7013 &gb.faces[section.face_b].filter,
7014 &gb.faces[other.face_b].filter,
7015 );
7016 if unshared_a || unshared_b {
7017 continue;
7018 }
7019 }
7020 let cc2 = CurveCurveOptions {
7021 gap: both.max(CurveCurveOptions::default().gap),
7022 ..CurveCurveOptions::default()
7023 };
7024 let found = intersect_curves(§ion.curve, &other.curve, cc2, tol)?;
7025 for crossing in &found.crossings {
7026 if crossing.gap > both {
7027 continue;
7028 }
7029 let mut at = crossing.on_a;
7030 let touch = crossing.reach > 0.0
7034 || tangential(
7035 §ion.curve,
7036 crossing.on_a,
7037 &other.curve,
7038 crossing.on_b,
7039 tol,
7040 )?;
7041 if touch {
7042 let half = (domain.1 - domain.0) * 0.5;
7043 for end in [domain.0, domain.1] {
7044 if (crossing.on_a - end).abs() > half {
7045 continue;
7046 }
7047 let tip = section
7048 .curve
7049 .point_at(at_param(end, domain, section.closed), tol)?;
7050 let foot = ogeom_algo::project_on_curve(&other.curve, tip, 64, tol)?;
7051 let width =
7052 (crossing.reach + honest(both, tol)).max(tol.confusion() * 1e3);
7053 if foot.distance <= width {
7054 at = end;
7055 }
7056 break;
7057 }
7058 }
7059 let shared = |edges: &[BoundaryEdge], node: EdgeKey| {
7068 edges.iter().any(|e| e.node == node)
7069 };
7070 let apart_on = if other.face_a == section.face_a {
7071 Some(&gb.faces[other.face_b].edges)
7072 } else if other.face_b == section.face_b {
7073 Some(&ga.faces[other.face_a].edges)
7074 } else {
7075 None
7076 };
7077 if let Some(edges) = apart_on {
7078 let close = both.max(tol.confusion() * 1e2);
7079 let mut best: Option<(f64, f64)> = None;
7080 for (node, _, on_a, ..) in &edge_hits {
7081 if !shared(edges, *node)
7082 || best.is_some_and(|(bd, _)| (on_a - at).abs() >= bd)
7083 {
7084 continue;
7085 }
7086 let mut together = true;
7087 for k in 1..8 {
7088 let t = (on_a - at).mul_add(f64::from(k) / 8.0, at);
7089 let p = section
7090 .curve
7091 .point_at(at_param(t, domain, section.closed), tol)?;
7092 if ogeom_algo::project_on_curve(&other.curve, p, 16, tol)?.distance
7093 > close
7094 {
7095 together = false;
7096 break;
7097 }
7098 }
7099 if together {
7100 best = Some(((on_a - at).abs(), *on_a));
7101 }
7102 }
7103 if let Some((_, on_a)) = best {
7104 at = on_a;
7105 } else if let (Some(own), Some(beside)) = if other.face_a == section.face_a
7106 {
7107 (gb.faces.get(section.face_b), gb.faces.get(other.face_b))
7108 } else {
7109 (ga.faces.get(section.face_a), ga.faces.get(other.face_a))
7110 } {
7111 let loose = beside
7129 .edges
7130 .iter()
7131 .filter(|e| {
7132 e.tolerance > tol.confusion() * DRAWN_APART
7133 && own.edges.iter().any(|o| o.node == e.node)
7134 })
7135 .map(|e| e.tolerance)
7136 .fold(None, |most: Option<f64>, t| {
7137 Some(most.map_or(t, |m| m.max(t)))
7138 });
7139 let doubt = doubt_at(beside, crossing.point, tol)?
7140 .max(both)
7141 .max(tol.confusion() * 1e3)
7142 + loose.unwrap_or(0.0);
7143 if !within_face(beside, crossing.point, doubt, tol)? {
7144 if *DEBUG_WIRE {
7145 eprintln!(
7146 "PAVE s{si}: cross s{sj} at {at:.6} off s{sj}'s face, no cut"
7147 );
7148 }
7149 continue;
7150 }
7151 }
7152 }
7153 if *DEBUG_WIRE {
7154 eprintln!(
7155 "PAVE s{si}: cross s{sj} at {at:.6} gap {:.2e}",
7156 crossing.gap
7157 );
7158 }
7159 cross_ts.push(at);
7160 }
7161 }
7162
7163 trim_ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
7166 trim_ts.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric());
7167 let mut speaks_for: Vec<(f64, f64, bool)> = Vec::with_capacity(trim_ts.len());
7179 {
7180 let floor = (honest(section.tolerance, tol) * 3.0).max(tol.confusion() * 10.0);
7181 let mut kept: Vec<f64> = Vec::with_capacity(trim_ts.len());
7182 let mut held: Option<Point> = None;
7183 for t in &trim_ts {
7184 let at = section
7185 .curve
7186 .point_at(at_param(*t, domain, section.closed), tol)?;
7187 if held.is_none_or(|h: Point| h.distance(at) > floor) {
7188 kept.push(*t);
7189 held = Some(at);
7190 }
7191 if let (Some(&first), Some(h)) = (kept.last(), held) {
7192 speaks_for.push((*t, first, h.distance(at) > tol.confusion()));
7193 }
7194 }
7195 trim_ts = kept;
7196 }
7197 let hug_floor = if section.tolerance > 0.0 && !at_stated {
7214 tol.confusion() * 1e3
7215 } else {
7216 tol.confusion() * 10.0
7217 };
7218 let mut vertex_stops: Vec<f64> = Vec::new();
7228 if section.tolerance <= 0.0 {
7229 let width = hug_floor;
7230 let near_stop = |t: f64, at: Point| -> OgeomResult<bool> {
7231 Ok(section
7232 .curve
7233 .point_at(at_param(t, domain, section.closed), tol)?
7234 .distance(at)
7235 <= width)
7236 };
7237 for own in [&ga.faces[section.face_a], &gb.faces[section.face_b]] {
7238 for e in &own.edges {
7239 for end in [e.crange.0, e.crange.1] {
7240 let vertex = e.curve.point_at(end, tol)?;
7241 if section_bounds[si]
7242 .as_ref()
7243 .is_some_and(|b| !b.expanded(width).contains(vertex))
7244 {
7245 continue;
7246 }
7247 let foot =
7248 ogeom_algo::project_on_curve(§ion.curve, vertex, 64, tol)?;
7249 if foot.distance > width
7250 || (!section.closed
7251 && (foot.parameter <= domain.0 + tol.parametric()
7252 || foot.parameter >= domain.1 - tol.parametric()))
7253 {
7254 continue;
7255 }
7256 let mut taken = false;
7257 for &t in trim_ts.iter().chain(&vertex_stops) {
7258 if near_stop(t, foot.point)? {
7259 taken = true;
7260 break;
7261 }
7262 }
7263 if taken {
7264 continue;
7265 }
7266 if *DEBUG_WIRE {
7267 eprintln!(
7268 "PAVE s{si}: stop at {:.6} beside the vertex {vertex:?}, {:.2e} off",
7269 foot.parameter, foot.distance
7270 );
7271 }
7272 vertex_stops.push(foot.parameter);
7273 }
7274 }
7275 }
7276 trim_ts.extend_from_slice(&vertex_stops);
7277 trim_ts.sort_by(f64::total_cmp);
7278 }
7279 let mut candidates: Vec<(f64, f64)> = Vec::new();
7280 if section.closed {
7281 let period = domain.1 - domain.0;
7282 if trim_ts.is_empty() {
7283 candidates.push(domain);
7284 } else {
7285 for i in 0..trim_ts.len() {
7286 let lo = trim_ts[i];
7287 let hi = if i + 1 < trim_ts.len() {
7288 trim_ts[i + 1]
7289 } else {
7290 trim_ts[0] + period
7291 };
7292 candidates.push((lo, hi));
7293 }
7294 }
7295 } else {
7296 let mut stops = vec![domain.0];
7297 stops.extend(
7298 trim_ts
7299 .iter()
7300 .copied()
7301 .filter(|t| *t > domain.0 && *t < domain.1),
7302 );
7303 stops.push(domain.1);
7304 for pair in stops.windows(2) {
7305 candidates.push((pair[0], pair[1]));
7306 }
7307 }
7308
7309 let inside_over = |lo: f64, hi: f64| -> OgeomResult<[bool; 2]> {
7317 let la: Vec<&[Point2]> = outlines_a[section.face_a]
7318 .iter()
7319 .map(Vec::as_slice)
7320 .collect();
7321 let lb: Vec<&[Point2]> = outlines_b[section.face_b]
7322 .iter()
7323 .map(Vec::as_slice)
7324 .collect();
7325 let face_a = &ga.faces[section.face_a];
7326 let face_b = &gb.faces[section.face_b];
7327 let at = |t: f64| -> OgeomResult<(Point2, Point2)> {
7328 let tf = if section.closed { fold(t, domain) } else { t };
7329 Ok((
7330 fold_inside(section.pc_a.point_at(tf, tol)?, &face_a.surface, &la, tol),
7331 fold_inside(section.pc_b.point_at(tf, tol)?, &face_b.surface, &lb, tol),
7332 ))
7333 };
7334 let mid = f64::midpoint(lo, hi);
7335 let (ua, ub) = at(mid)?;
7336 let mut held = [
7337 inside_plane_exactly(face_a, ua, tol),
7338 inside_plane_exactly(face_b, ub, tol),
7339 ];
7340 for k in [3.0, 5.0, 2.0, 6.0, 1.0, 7.0] {
7341 if held.iter().all(Option::is_some) {
7342 break;
7343 }
7344 let (va, vb) = at(lo + (hi - lo) * k / 8.0)?;
7345 if held[0].is_none() {
7346 held[0] = inside_plane_exactly(face_a, va, tol);
7347 }
7348 if held[1].is_none() {
7349 held[1] = inside_plane_exactly(face_b, vb, tol);
7350 }
7351 }
7352 Ok([
7353 held[0].unwrap_or_else(|| inside_many(&la, ua)),
7354 held[1].unwrap_or_else(|| inside_many(&lb, ub)),
7355 ])
7356 };
7357
7358 let mut bridges: Vec<(f64, f64, [Vec<usize>; 2])> = Vec::new();
7361 let mut kept_stops: Vec<f64> = Vec::new();
7362 let mut reseamed: Option<SectionRec> = None;
7363 for (lo, hi) in candidates {
7364 if hi - lo <= tol.parametric() {
7365 continue;
7366 }
7367 let mid = f64::midpoint(lo, hi);
7368 let mid_folded = if section.closed {
7369 fold(mid, domain)
7370 } else {
7371 mid
7372 };
7373 let held = inside_over(lo, hi)?;
7374 if *DEBUG_WIRE {
7375 let tf = if section.closed {
7376 fold(mid, domain)
7377 } else {
7378 mid
7379 };
7380 eprintln!(
7381 "PAVE s{si}: candidate ({lo:.6}, {hi:.6}) inside {held:?} at a {:?} b {:?}",
7382 section
7383 .pc_a
7384 .point_at(tf, tol)
7385 .ok()
7386 .map(|q| fold_point_into_chart(
7387 q,
7388 &ga.faces[section.face_a].surface,
7389 tol
7390 )),
7391 section
7392 .pc_b
7393 .point_at(tf, tol)
7394 .ok()
7395 .map(|q| fold_point_into_chart(
7396 q,
7397 &gb.faces[section.face_b].surface,
7398 tol
7399 ))
7400 );
7401 }
7402 let mut hugs = [false; 2];
7414 let mut hugged: [Vec<usize>; 2] = [Vec::new(), Vec::new()];
7419 for (side, own, side_from_a, side_face) in [
7420 (0_usize, &ga.faces[section.face_a], true, section.face_a),
7421 (1, &gb.faces[section.face_b], false, section.face_b),
7422 ] {
7423 if let Some((_, _, ei)) = along[side]
7424 .iter()
7425 .find(|(alo, ahi, _)| mid_folded >= *alo && mid_folded <= *ahi)
7426 {
7427 hugs[side] = true;
7428 hugged[side].push(*ei);
7429 continue;
7430 }
7431 let mut all_near = true;
7432 let mut votes: Vec<usize> = vec![0; own.edges.len()];
7433 let mut contact_votes: Vec<usize> = vec![0; contacts.len()];
7434 let samples = (0..=4)
7435 .map(|i| {
7436 let t = lo + (hi - lo) * f64::from(i) / 4.0;
7437 let tf = if section.closed { fold(t, domain) } else { t };
7438 section.curve.point_at(tf, tol)
7439 })
7440 .collect::<OgeomResult<Vec<Point>>>()?;
7441 let stub: f64 = samples.windows(2).map(|w| w[0].distance(w[1])).sum();
7449 let held_to = |width: f64, own: f64| -> f64 {
7450 if stub < width {
7451 (stub * 0.25).max(own).max(tol.confusion() * 10.0)
7452 } else {
7453 width
7454 }
7455 };
7456 for (i, &at) in samples.iter().enumerate() {
7457 let mut near = false;
7458 for (ei, e) in own.edges.iter().enumerate() {
7459 let (drawn, drange, off) = e.drawn();
7460 let radii = if at_stated && section.tolerance > 0.0 {
7461 section.tolerance + e.tolerance.max(e.residual)
7462 } else {
7463 0.0
7464 };
7465 let width =
7466 held_to(reach.max(hug_floor).max(radii).max(off * 2.0), off * 2.0);
7467 if !e.bound.expanded(width).contains(at) {
7470 continue;
7471 }
7472 let d = distance_to_edge_curve(drawn, drange, at, tol)?;
7473 if d <= width {
7474 if *DEBUG_WIRE {
7475 eprintln!(
7476 "PAVE s{si}: side {side} sample {i} hugs edge {} at {d:.2e} (width {width:.2e}) range {:?} point {at:?}",
7477 e.node.index(),
7478 e.crange
7479 );
7480 }
7481 near = true;
7488 votes[ei] += 1;
7489 }
7490 }
7491 let width = reach.max(hug_floor);
7492 for (ci, c) in contacts.iter().enumerate() {
7499 if c.target_from_a != side_from_a || c.target_face != side_face {
7500 continue;
7501 }
7502 let held = held_to(width.max(c.tolerance * 2.0), c.tolerance * 2.0);
7503 if c.bound.expanded(held).contains(at)
7504 && distance_to_edge_curve(&c.curve, c.crange, at, tol)? <= held
7505 {
7506 near = true;
7507 contact_votes[ci] += 1;
7508 }
7509 }
7510 if !near {
7511 all_near = false;
7512 break;
7513 }
7514 }
7515 let one_line = votes.iter().chain(&contact_votes).any(|&v| v == 5);
7520 hugs[side] = all_near && one_line;
7521 if all_near {
7522 hugged[side].extend(
7523 votes
7524 .iter()
7525 .enumerate()
7526 .filter(|(_, c)| **c > 0)
7527 .map(|(ei, _)| ei),
7528 );
7529 }
7530 }
7531 if *DEBUG_WIRE {
7532 eprintln!("PAVE s{si}: candidate ({lo:.6}, {hi:.6}) hugs {hugs:?}");
7533 }
7534 let mut hug_key: Option<(EdgeKey, bool, usize)> = None;
7542 let admitted = if held[0] && held[1] {
7543 if hugs[0] != hugs[1] {
7551 let hugging = usize::from(hugs[1]);
7552 let (own, other_from_a, other_face) = if hugging == 0 {
7553 (&ga.faces[section.face_a], false, section.face_b)
7554 } else {
7555 (&gb.faces[section.face_b], true, section.face_a)
7556 };
7557 hug_key = hugged[hugging]
7558 .first()
7559 .map(|&ei| (own.edges[ei].node, other_from_a, other_face));
7560 }
7561 true
7562 } else if hugs[0] != hugs[1] {
7563 let hugging = usize::from(hugs[1]);
7564 let other = 1 - hugging;
7565 let (own, other_from_a, other_face) = if hugging == 0 {
7566 (&ga.faces[section.face_a], false, section.face_b)
7567 } else {
7568 (&gb.faces[section.face_b], true, section.face_a)
7569 };
7570 let carried = hugged[hugging].iter().any(|&ei| {
7571 contacts.iter().any(|c| {
7572 c.node == own.edges[ei].node
7573 && c.target_from_a == other_from_a
7574 && c.target_face == other_face
7575 })
7576 });
7577 if held[other] && !carried {
7578 hug_key = hugged[hugging]
7579 .first()
7580 .map(|&ei| (own.edges[ei].node, other_from_a, other_face));
7581 true
7582 } else {
7583 false
7584 }
7585 } else {
7586 false
7587 };
7588 if hugs[0] && hugs[1] {
7589 bridges.push((lo, hi, hugged.clone()));
7590 }
7591 if !admitted {
7592 continue;
7593 }
7594 if hugs[0] && hugs[1] {
7595 continue;
7598 }
7599 kept_stops.extend([lo, hi]);
7600 let speaker = |on_section: f64| {
7603 speaks_for
7604 .iter()
7605 .find(|(t, ..)| (t - on_section).abs() <= tol.parametric())
7606 .map_or((on_section, false), |&(_, first, moved)| (first, moved))
7607 };
7608 for (node, on_edge, on_section, honesty, across, spread, side) in &edge_hits {
7609 let (s, moved) = speaker(*on_section);
7610 let near = |x: f64| {
7611 (s - x).abs() <= tol.parametric()
7612 || (section.closed
7613 && ((s + (domain.1 - domain.0)) - x).abs() <= tol.parametric())
7614 };
7615 if !(near(lo) || near(hi)) {
7616 continue;
7617 }
7618 if !moved {
7619 paves.push((
7620 *node,
7621 Pave {
7622 t: *on_edge,
7623 honesty: *honesty,
7624 across: *across,
7625 spread: *spread,
7626 },
7627 ));
7628 continue;
7629 }
7630 if edge_hits.iter().any(|(_, _, other, .., other_side)| {
7633 other_side == side && speaker(*other) == (s, false)
7634 }) {
7635 continue;
7636 }
7637 let own = if *side == 0 {
7638 &ga.faces[section.face_a]
7639 } else {
7640 &gb.faces[section.face_b]
7641 };
7642 let Some(e) = own.edges.iter().find(|e| e.node == *node) else {
7643 continue;
7644 };
7645 let at = section
7646 .curve
7647 .point_at(at_param(s, domain, section.closed), tol)?;
7648 let foot = ogeom_algo::project_on_curve(e.drawn().0, at, 64, tol)?;
7649 if *DEBUG_WIRE {
7650 eprintln!(
7651 "PAVE s{si}: side {side} edge {} crossing at {on_section:.6} handed to the stop at {s:.6}, {:.2e} off",
7652 node.index(),
7653 foot.distance
7654 );
7655 }
7656 paves.push((
7657 *node,
7658 Pave {
7659 t: onto_range(e.onto_curve(foot.parameter), &e.curve, e.crange, tol),
7660 honesty: honesty.max(foot.distance),
7661 across: *across,
7662 spread: *spread,
7663 },
7664 ));
7665 }
7666 let mut cuts = vec![lo];
7669 if section.closed
7673 && domain.1 > lo + tol.parametric()
7674 && domain.1 < hi - tol.parametric()
7675 {
7676 cuts.push(domain.1);
7677 }
7678 for &c in &cross_ts {
7679 let c2 = if section.closed && c < lo {
7680 c + (domain.1 - domain.0)
7681 } else {
7682 c
7683 };
7684 if c2 > lo + tol.parametric() && c2 < hi - tol.parametric() {
7685 cuts.push(c2);
7686 }
7687 }
7688 cuts.push(hi);
7689 for side in 0..2 {
7694 if !hugs[side] {
7695 continue;
7696 }
7697 let own = if side == 0 {
7698 &ga.faces[section.face_a]
7699 } else {
7700 &gb.faces[section.face_b]
7701 };
7702 for &ei in &hugged[side] {
7703 let e = &own.edges[ei];
7704 for end in [e.crange.0, e.crange.1] {
7705 let p = e.curve.point_at(end, tol)?;
7706 let foot = ogeom_algo::project_on_curve(§ion.curve, p, 64, tol)?;
7707 if *DEBUG_WIRE {
7708 eprintln!(
7709 "PAVE s{si}: side {side} hugged edge {} end {end:.6} lands at {:.6} off {:.2e} against ({lo:.6}, {hi:.6})",
7710 e.node.index(),
7711 foot.parameter,
7712 foot.distance
7713 );
7714 }
7715 if foot.distance > reach.max(tol.confusion() * 1e3) {
7716 continue;
7717 }
7718 let mut t = foot.parameter;
7719 if section.closed {
7720 let period = domain.1 - domain.0;
7721 while t < lo {
7722 t += period;
7723 }
7724 while t - period >= lo {
7725 t -= period;
7726 }
7727 }
7728 if t > lo + tol.parametric() && t < hi - tol.parametric() {
7729 if *DEBUG_WIRE {
7730 eprintln!(
7731 "PAVE s{si}: side {side} cut at {t:.6} where hugged edge {} ends",
7732 e.node.index()
7733 );
7734 }
7735 cuts.push(t);
7736 }
7737 }
7738 }
7739 }
7740 cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
7741 {
7746 let floor = (honest(section.tolerance, tol) * 3.0).max(tol.confusion() * 10.0);
7747 let mut kept: Vec<f64> = Vec::with_capacity(cuts.len());
7748 let mut held: Option<Point> = None;
7749 for (index, c) in cuts.iter().enumerate() {
7750 let at = section
7751 .curve
7752 .point_at(at_param(*c, domain, section.closed), tol)?;
7753 let last = index + 1 == cuts.len();
7754 if index == 0 || last || held.is_none_or(|h: Point| h.distance(at) > floor)
7755 {
7756 kept.push(*c);
7757 held = Some(at);
7758 }
7759 }
7760 cuts = kept;
7761 }
7762 if section.tolerance > 0.0
7771 && let Some(at) = cuts.iter().position(|c| *c == domain.1)
7772 && at > 0
7773 && at + 1 < cuts.len()
7774 {
7775 let span = |from: f64, to: f64| -> OgeomResult<f64> {
7776 let mut length = 0.0;
7777 let mut last = section.curve.point_at(at_param(from, domain, true), tol)?;
7778 for k in 1..=16 {
7779 let t = from + (to - from) * f64::from(k) / 16.0;
7780 let p = section.curve.point_at(at_param(t, domain, true), tol)?;
7781 length += last.distance(p);
7782 last = p;
7783 }
7784 Ok(length)
7785 };
7786 let shortest = span(cuts[at - 1], cuts[at])?.min(span(cuts[at], cuts[at + 1])?);
7787 if shortest <= tol.confusion() * 1e3 {
7788 reseamed = reseamed_section(
7789 section,
7790 lo,
7791 [
7792 &ga.faces[section.face_a].surface,
7793 &gb.faces[section.face_b].surface,
7794 ],
7795 tol,
7796 );
7797 if *DEBUG_WIRE {
7798 eprintln!(
7799 "PAVE s{si}: the domain-end split of ({lo:.6}, {hi:.6}) leaves {shortest:.3e}; {}",
7800 if reseamed.is_some() {
7801 "the seam moves to the interval's start"
7802 } else {
7803 "it stays"
7804 }
7805 );
7806 }
7807 if reseamed.is_some() {
7808 cuts.remove(at);
7809 }
7810 }
7811 }
7812 for side in 0..2 {
7817 if !hugs[side] {
7818 continue;
7819 }
7820 let own = if side == 0 {
7821 &ga.faces[section.face_a]
7822 } else {
7823 &gb.faces[section.face_b]
7824 };
7825 for &ei in &hugged[side] {
7826 let e = &own.edges[ei];
7827 for c in &cuts {
7832 let at = section
7833 .curve
7834 .point_at(at_param(*c, domain, section.closed), tol)?;
7835 let foot = ogeom_algo::project_on_curve(e.drawn().0, at, 64, tol)?;
7836 if foot.distance > reach.max(tol.confusion() * 1e3) {
7841 continue;
7842 }
7843 let on_e =
7844 onto_range(e.onto_curve(foot.parameter), &e.curve, e.crange, tol);
7845 if on_e <= e.crange.0.min(e.crange.1) + tol.parametric()
7846 || on_e >= e.crange.0.max(e.crange.1) - tol.parametric()
7847 {
7848 continue;
7849 }
7850 if *DEBUG_WIRE {
7851 eprintln!(
7852 "PAVE s{si}: side {side} hugged edge {} paved at {on_e:.6} for the cut at {c:.6}",
7853 e.node.index()
7854 );
7855 }
7856 paves.push((
7857 e.node,
7858 Pave {
7859 t: on_e,
7860 honesty: honest(section.tolerance, tol).max(foot.distance),
7861 across: None,
7862 spread: 0.0,
7863 },
7864 ));
7865 }
7866 }
7867 }
7868 for pair in cuts.windows(2) {
7869 let (lo2, hi2) = (pair[0], pair[1]);
7870 let from = section
7871 .curve
7872 .point_at(at_param(lo2, domain, section.closed), tol)?;
7873 let to = section
7874 .curve
7875 .point_at(at_param(hi2, domain, section.closed), tol)?;
7876 if from.distance(to) <= tol.confusion() {
7877 let mid = f64::midpoint(lo2, hi2);
7880 pieces.push(SectionPiece {
7881 section: si,
7882 range: (lo2, mid),
7883 hugs,
7884 hug_key,
7885 });
7886 pieces.push(SectionPiece {
7887 section: si,
7888 range: (mid, hi2),
7889 hugs,
7890 hug_key,
7891 });
7892 } else {
7893 pieces.push(SectionPiece {
7894 section: si,
7895 range: (lo2, hi2),
7896 hugs,
7897 hug_key,
7898 });
7899 }
7900 }
7901 }
7902 for (lo, hi, hugged) in &bridges {
7915 for stop in [*lo, *hi] {
7916 if !kept_stops
7917 .iter()
7918 .chain(&vertex_stops)
7919 .any(|k| (k - stop).abs() <= tol.parametric())
7920 {
7921 continue;
7922 }
7923 let at = section
7924 .curve
7925 .point_at(at_param(stop, domain, section.closed), tol)?;
7926 for (side, own) in [&ga.faces[section.face_a], &gb.faces[section.face_b]]
7927 .into_iter()
7928 .enumerate()
7929 {
7930 for &ei in &hugged[side] {
7931 let e = &own.edges[ei];
7932 if edge_hits.iter().any(|(node, _, on_section, ..)| {
7933 *node == e.node && (on_section - stop).abs() <= tol.parametric()
7934 }) {
7935 continue;
7936 }
7937 let foot = ogeom_algo::project_on_curve(e.drawn().0, at, 64, tol)?;
7938 if foot.distance > reach.max(tol.confusion() * 1e3) {
7939 continue;
7940 }
7941 let on_e =
7942 onto_range(e.onto_curve(foot.parameter), &e.curve, e.crange, tol);
7943 if on_e <= e.crange.0.min(e.crange.1) + tol.parametric()
7944 || on_e >= e.crange.0.max(e.crange.1) - tol.parametric()
7945 {
7946 continue;
7947 }
7948 if *DEBUG_WIRE {
7949 eprintln!(
7950 "PAVE s{si}: side {side} edge {} along ({lo:.6}, {hi:.6}) paved at {on_e:.6} for the stop at {stop:.6}, {:.2e} off",
7951 e.node.index(),
7952 foot.distance
7953 );
7954 }
7955 paves.push((
7956 e.node,
7957 Pave {
7958 t: on_e,
7959 honesty: honest(section.tolerance, tol).max(foot.distance),
7960 across: None,
7961 spread: 0.0,
7962 },
7963 ));
7964 }
7965 }
7966 }
7967 }
7968 Ok((paves, pieces, junctions, reseamed))
7969 },
7970 );
7971 let mut reseams: Vec<(usize, SectionRec)> = Vec::new();
7972 for (si, work) in paved.into_iter().enumerate() {
7973 let (found, made, hugged, reseamed) = work?;
7974 hug_junctions.extend(hugged);
7975 for (node, at) in found {
7976 paves.entry(node).or_default().push(at);
7977 }
7978 pieces.extend(made);
7979 if let Some(section) = reseamed {
7980 reseams.push((si, section));
7981 }
7982 }
7983 for (si, section) in reseams {
7987 let domain = section.curve.domain();
7988 for piece in pieces.iter_mut().filter(|p| p.section == si) {
7989 let start = fold(piece.range.0, domain);
7990 piece.range = (start, start + (piece.range.1 - piece.range.0));
7991 }
7992 sections[si] = section;
7993 }
7994 share_hugs(&mut pieces, §ions, [ga, gb], tol)?;
7995 for _ in 0..2 {
8004 let mut settled: Vec<SectionPiece> = Vec::with_capacity(pieces.len());
8005 for piece in pieces.drain(..) {
8006 if !(piece.hugs[0] || piece.hugs[1]) {
8007 settled.push(piece);
8008 continue;
8009 }
8010 let section = §ions[piece.section];
8011 let domain = section.curve.domain();
8012 let period = domain.1 - domain.0;
8013 let (lo, hi) = piece.range;
8014 let at = |t: f64| {
8015 section
8016 .curve
8017 .point_at(at_param(t, domain, section.closed), tol)
8018 };
8019 let width = (tol.confusion() * 1e3).max(section.tolerance * 3.0);
8020 let mid = at(f64::midpoint(lo, hi))?;
8021 let mut hugged: Vec<&BoundaryEdge> = Vec::new();
8022 for (side, &hugs) in piece.hugs.iter().enumerate().take(2) {
8023 if !hugs {
8024 continue;
8025 }
8026 let face = if side == 0 {
8027 &ga.faces[section.face_a]
8028 } else {
8029 &gb.faces[section.face_b]
8030 };
8031 for e in &face.edges {
8032 let (drawn, drange, off) = e.drawn();
8033 let near = width.max(off * 2.0);
8034 if e.bound.expanded(near).contains(mid)
8035 && distance_to_edge_curve(drawn, drange, mid, tol)? <= near
8036 {
8037 hugged.push(e);
8038 }
8039 }
8040 }
8041 let mut cuts: Vec<f64> = Vec::new();
8042 for e in &hugged {
8043 let mut on_edge: Vec<f64> = vec![e.crange.0, e.crange.1];
8044 if let Some(list) = paves.get(&e.node) {
8045 on_edge.extend(list.iter().map(|pave| pave.t));
8046 }
8047 let (drawn, _, off) = e.drawn();
8048 for t in on_edge {
8049 let q = drawn.point_at(e.onto_drawn(t), tol)?;
8050 let foot = ogeom_algo::project_on_curve(§ion.curve, q, 64, tol)?;
8051 if foot.distance > width.max(off * 2.0) {
8052 continue;
8053 }
8054 let mut f = foot.parameter;
8055 if section.closed && period > 0.0 {
8056 while f < lo {
8057 f += period;
8058 }
8059 while f - period >= lo {
8060 f -= period;
8061 }
8062 }
8063 if f > lo + tol.parametric() && f < hi - tol.parametric() {
8064 cuts.push(f);
8065 }
8066 }
8067 }
8068 cuts.push(lo);
8069 cuts.push(hi);
8070 cuts.sort_by(f64::total_cmp);
8071 cuts.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric());
8072 for e in &hugged {
8074 let (drawn, _, off) = e.drawn();
8075 for &c in &cuts {
8076 let q = at(c)?;
8077 let foot = ogeom_algo::project_on_curve(drawn, q, 64, tol)?;
8078 if foot.distance > width.max(off * 2.0) {
8079 continue;
8080 }
8081 let on_e = onto_range(e.onto_curve(foot.parameter), &e.curve, e.crange, tol);
8082 if on_e <= e.crange.0.min(e.crange.1) + tol.parametric()
8083 || on_e >= e.crange.0.max(e.crange.1) - tol.parametric()
8084 {
8085 continue;
8086 }
8087 let list = paves.entry(e.node).or_default();
8088 if list
8089 .iter()
8090 .all(|pave| (pave.t - on_e).abs() > tol.parametric())
8091 {
8092 list.push(Pave {
8093 t: on_e,
8094 honesty: honest(section.tolerance, tol).max(foot.distance),
8095 across: None,
8096 spread: 0.0,
8097 });
8098 }
8099 }
8100 }
8101 for pair in cuts.windows(2) {
8102 settled.push(SectionPiece {
8103 section: piece.section,
8104 range: (pair[0], pair[1]),
8105 hugs: piece.hugs,
8106 hug_key: piece.hug_key,
8107 });
8108 }
8109 }
8110 pieces = settled;
8111 }
8112 {
8120 let width = tol.confusion() * 1e3;
8121 let mut drop: Vec<usize> = Vec::new();
8122 let hugged_onto: Vec<usize> = pieces
8123 .iter()
8124 .enumerate()
8125 .filter(|(_, p)| p.hug_key.is_some())
8126 .map(|(i, _)| i)
8127 .collect();
8128 let mid_of = |p: &SectionPiece| -> OgeomResult<Point> {
8129 let section = §ions[p.section];
8130 let domain = section.curve.domain();
8131 section.curve.point_at(
8132 at_param(f64::midpoint(p.range.0, p.range.1), domain, section.closed),
8133 tol,
8134 )
8135 };
8136 let length_of = |p: &SectionPiece| -> OgeomResult<f64> {
8137 let section = §ions[p.section];
8138 let domain = section.curve.domain();
8139 let a = section
8140 .curve
8141 .point_at(at_param(p.range.0, domain, section.closed), tol)?;
8142 let b = section
8143 .curve
8144 .point_at(at_param(p.range.1, domain, section.closed), tol)?;
8145 Ok(a.distance(b))
8146 };
8147 for &x in &hugged_onto {
8152 let px = &pieces[x];
8153 let Some((_, target_from_a, target_face)) = px.hug_key else {
8154 continue;
8155 };
8156 for (y, py) in pieces.iter().enumerate() {
8157 if y == x || py.section == px.section || drop.contains(&x) || drop.contains(&y) {
8158 continue;
8159 }
8160 let sy = §ions[py.section];
8161 let on_target = if target_from_a {
8162 sy.face_a == target_face && !py.hugs[0]
8163 } else {
8164 sy.face_b == target_face && !py.hugs[1]
8165 };
8166 if !on_target {
8167 continue;
8168 }
8169 let sx = §ions[px.section];
8170 let stretch = |section: &SectionRec, range: (f64, f64)| -> (f64, f64) {
8174 folded_range(range, section.curve.domain(), section.closed)
8175 };
8176 let on_y =
8177 distance_to_edge_curve(&sy.curve, stretch(sy, py.range), mid_of(px)?, tol)?
8178 <= width;
8179 let on_x =
8180 distance_to_edge_curve(&sx.curve, stretch(sx, px.range), mid_of(py)?, tol)?
8181 <= width;
8182 if !(on_x && on_y) {
8183 continue;
8184 }
8185 let loser = if py.hug_key.is_some() && length_of(px)? > length_of(py)? {
8186 y
8187 } else {
8188 x
8189 };
8190 if *DEBUG_WIRE {
8191 eprintln!(
8192 "PAVE: s{} and s{} split one face along one line; s{} speaks for it",
8193 px.section,
8194 py.section,
8195 pieces[if loser == x { y } else { x }].section
8196 );
8197 }
8198 drop.push(loser);
8199 if loser == x {
8200 break;
8201 }
8202 }
8203 }
8204 let mut index = 0_usize;
8205 pieces.retain(|_| {
8206 let keep = !drop.contains(&index);
8207 index += 1;
8208 keep
8209 });
8210 }
8211 if admit_all {
8225 for piece in &pieces {
8226 let section = §ions[piece.section];
8227 let (fa, fb) = (&ga.faces[section.face_a], &gb.faces[section.face_b]);
8228 assert!(
8229 fa.filter.intersects(&fb.filter),
8230 "bound filter audit: faces {}/{} were dropped by the bound \
8231 filter, yet their section paved a surviving piece over \
8232 {:?}; the filter under-approximates",
8233 section.face_a,
8234 section.face_b,
8235 piece.range,
8236 );
8237 }
8238 }
8239 let mut contact_along: Vec<Vec<(f64, f64)>> = vec![Vec::new(); contacts.len()];
8243 let mut held: ogeom_core::FastMap<EdgeKey, (f64, &BoundaryEdge)> =
8247 ogeom_core::FastMap::default();
8248 if at_stated {
8249 for e in ga
8250 .faces
8251 .iter()
8252 .chain(&gb.faces)
8253 .flat_map(|f| f.edges.iter())
8254 {
8255 held.entry(e.node)
8256 .and_modify(|(most, _)| *most = most.max(e.residual))
8257 .or_insert((e.residual, e));
8258 }
8259 }
8260 let mut shared: ogeom_core::FastMap<(EdgeKey, EdgeKey), Vec<ogeom_intersect::Overlap>> =
8263 ogeom_core::FastMap::default();
8264 for (ci, contact) in contacts.iter().enumerate() {
8265 let target = if contact.target_from_a {
8266 &ga.faces[contact.target_face]
8267 } else {
8268 &gb.faces[contact.target_face]
8269 };
8270 let reach = tol.confusion().max(contact.tolerance * 2.0);
8275 let cc = CurveCurveOptions {
8276 gap: reach.max(CurveCurveOptions::default().gap),
8277 ..CurveCurveOptions::default()
8278 };
8279 for e in &target.edges {
8280 if !contact.bound.expanded(reach).intersects(&e.bound) {
8281 continue;
8282 }
8283 let found = intersect_curves(&contact.curve, &e.curve, cc, tol)?;
8284 for crossing in &found.crossings {
8285 if crossing.gap > reach {
8286 continue;
8287 }
8288 let on_b = onto_range(crossing.on_b, &e.curve, e.crange, tol);
8296 if (on_b < e.crange.0 - tol.parametric() || on_b > e.crange.1 + tol.parametric())
8297 && distance_to_edge_curve(&e.curve, e.crange, crossing.point, tol)?
8298 > reach + crossing.reach + e.tolerance.max(e.ends_tolerance)
8299 {
8300 continue;
8301 }
8302 let at_end = crossing.on_a < contact.crange.0 + tol.parametric()
8306 || crossing.on_a > contact.crange.1 - tol.parametric();
8307 if at_end
8308 && (crossing.on_a < contact.crange.0 - tol.parametric()
8309 || crossing.on_a > contact.crange.1 + tol.parametric())
8310 {
8311 continue;
8312 }
8313 let mut reach = crossing.reach;
8323 let mut onto_end = None;
8324 if tangential(&contact.curve, crossing.on_a, &e.curve, crossing.on_b, tol)? {
8325 let shallow = at_stated
8326 && crosses_shallowly(
8327 &contact.curve,
8328 contact.crange,
8329 contact.tolerance,
8330 e,
8331 (crossing.on_a, crossing.on_b),
8332 tol,
8333 )?;
8334 if !shallow {
8335 if at_end {
8343 let end = if crossing.on_a
8344 < f64::midpoint(contact.crange.0, contact.crange.1)
8345 {
8346 contact.crange.0
8347 } else {
8348 contact.crange.1
8349 };
8350 let vertex = contact.curve.point_at(end, tol)?;
8351 let (gap, on_edge) =
8352 nearest_on_edge_curve(&e.curve, e.crange, vertex, tol)?;
8353 let width = held_radius(&held, contact.node, contact.tolerance)
8354 + held_radius(&held, e.node, e.tolerance);
8355 if gap <= width
8356 && on_edge > e.crange.0 + tol.parametric()
8357 && on_edge < e.crange.1 - tol.parametric()
8358 {
8359 paves.entry(e.node).or_default().push(Pave {
8360 t: on_edge,
8361 honesty: honest(contact.tolerance, tol).max(gap),
8362 across: None,
8363 spread: 0.0,
8364 });
8365 }
8366 }
8367 continue;
8368 }
8369 reach = 0.0;
8370 let width = held_radius(&held, contact.node, contact.tolerance)
8371 + held_radius(&held, e.node, e.tolerance);
8372 onto_end =
8373 shallow_at_end(contact, e, (crossing.on_a, crossing.on_b), width, tol)?;
8374 }
8375 let honesty = honest(contact.tolerance, tol).max(reach);
8376 match onto_end {
8377 Some(End::OfEdge { on_contact, gap }) => {
8378 if on_contact > contact.crange.0 + tol.parametric()
8379 && on_contact < contact.crange.1 - tol.parametric()
8380 {
8381 paves.entry(contact.node).or_default().push(Pave {
8382 t: on_contact,
8383 honesty: honesty.max(gap),
8384 across: None,
8385 spread: 0.0,
8386 });
8387 }
8388 continue;
8389 }
8390 Some(End::OfContact { on_edge, gap }) => {
8391 if on_edge > e.crange.0 + tol.parametric()
8392 && on_edge < e.crange.1 - tol.parametric()
8393 {
8394 paves.entry(e.node).or_default().push(Pave {
8395 t: on_edge,
8396 honesty: honesty.max(gap),
8397 across: None,
8398 spread: 0.0,
8399 });
8400 }
8401 continue;
8402 }
8403 None => {}
8404 }
8405 if !at_end {
8406 paves.entry(contact.node).or_default().push(Pave {
8407 t: crossing.on_a,
8408 honesty,
8409 across: None,
8410 spread: 0.0,
8411 });
8412 }
8413 if on_b > e.crange.0 + tol.parametric() && on_b < e.crange.1 - tol.parametric() {
8414 paves.entry(e.node).or_default().push(Pave {
8415 t: on_b,
8416 honesty,
8417 across: None,
8418 spread: 0.0,
8419 });
8420 }
8421 }
8422 let survives = |overlap: &ogeom_intersect::Overlap| -> bool {
8435 let (lo, hi) = if overlap.on_a.0 <= overlap.on_a.1 {
8436 overlap.on_a
8437 } else {
8438 (overlap.on_a.1, overlap.on_a.0)
8439 };
8440 let carried = if contact.curve.is_periodic() {
8441 let (dlo, dhi) = contact.curve.domain();
8442 let period = dhi - dlo;
8443 let turn = ((contact.crange.0 - lo) / period).floor();
8444 [turn, turn + 1.0, turn - 1.0]
8445 .into_iter()
8446 .map(|k| (k.mul_add(period, lo), k.mul_add(period, hi)))
8447 .map(|(a, b)| b.min(contact.crange.1) - a.max(contact.crange.0))
8448 .fold(f64::NEG_INFINITY, f64::max)
8449 } else {
8450 hi.min(contact.crange.1) - lo.max(contact.crange.0)
8451 };
8452 carried > tol.parametric()
8453 };
8454 let clipped: Vec<ogeom_intersect::Overlap> = found
8463 .overlaps
8464 .iter()
8465 .flat_map(|overlap| {
8466 let span = overlap.on_a.1 - overlap.on_a.0;
8467 let parts = if span.abs() <= f64::MIN_POSITIVE {
8468 Vec::new()
8469 } else {
8470 overlap_within(overlap, e.crange, &contact.curve, tol)
8471 .into_iter()
8472 .map(|(part, _)| part)
8473 .collect()
8474 };
8475 let to_b = move |t: f64| {
8476 overlap.on_b.0
8477 + (overlap.on_b.1 - overlap.on_b.0) * (t - overlap.on_a.0) / span
8478 };
8479 parts
8480 .into_iter()
8481 .map(move |(lo, hi)| ogeom_intersect::Overlap {
8482 on_a: (lo, hi),
8483 on_b: (to_b(lo), to_b(hi)),
8484 })
8485 })
8486 .collect();
8487 let conic = |c: &Curve| {
8493 matches!(
8494 c,
8495 Curve::Circle(_) | Curve::Ellipse(_) | Curve::Hyperbola(_) | Curve::Parabola(_)
8496 )
8497 };
8498 let straight_on_conic = (matches!(contact.curve, Curve::Line(_)) && conic(&e.curve))
8499 || (conic(&contact.curve) && matches!(*e.curve, Curve::Line(_)));
8500 let fitted = !(exact(&contact.curve) && exact(&e.curve));
8504 let clipped = if at_stated && fitted {
8505 Vec::new()
8506 } else {
8507 clipped
8508 };
8509 let measured: Vec<ogeom_intersect::Overlap> =
8510 if clipped.iter().any(survives) || straight_on_conic {
8511 Vec::new()
8512 } else if at_stated {
8513 shared_overlaps(contact, e, &held, &mut shared, tol)?
8514 } else {
8515 measured_overlaps(
8516 &contact.curve,
8517 contact.crange,
8518 contact.tolerance,
8519 e,
8520 None,
8521 tol,
8522 )?
8523 };
8524 for overlap in clipped.iter().chain(measured.iter()) {
8525 let ordered = |r: (f64, f64)| if r.0 <= r.1 { r } else { (r.1, r.0) };
8526 let (lo, hi) = ordered(overlap.on_a);
8527 let turns: Vec<f64> = if contact.curve.is_periodic() {
8540 let (dlo, dhi) = contact.curve.domain();
8541 let period = dhi - dlo;
8542 let turn = ((contact.crange.0 - lo) / period).floor();
8543 [turn - 1.0, turn, turn + 1.0]
8544 .into_iter()
8545 .map(|k| k * period)
8546 .collect()
8547 } else {
8548 vec![0.0]
8549 };
8550 for shift in turns {
8551 let (lo, hi) = (
8552 (lo + shift).max(contact.crange.0),
8553 (hi + shift).min(contact.crange.1),
8554 );
8555 if hi - lo <= tol.parametric() {
8556 continue;
8557 }
8558 let span = overlap.on_a.1 - overlap.on_a.0;
8559 let carry = |t: f64| -> f64 {
8560 if span.abs() <= f64::MIN_POSITIVE {
8561 overlap.on_b.0
8562 } else {
8563 overlap.on_b.0
8564 + (overlap.on_b.1 - overlap.on_b.0) * (t - overlap.on_a.0) / span
8565 }
8566 };
8567 let honesty = |t: f64| -> OgeomResult<f64> {
8572 let own = honest(contact.tolerance, tol);
8573 if !at_stated {
8574 return Ok(own);
8575 }
8576 let gap = contact
8577 .curve
8578 .point_at(t, tol)?
8579 .distance(e.curve.point_at(carry(t), tol)?);
8580 Ok(own.max(gap))
8581 };
8582 for t in [lo, hi] {
8583 paves.entry(contact.node).or_default().push(Pave {
8584 t,
8585 honesty: honesty(t)?,
8586 across: None,
8587 spread: 0.0,
8588 });
8589 }
8590 if *DEBUG_WIRE {
8591 eprintln!(
8592 "CONTACT c{ci} along edge {} over ({lo:.6}, {hi:.6}) of {:?}: {:?} .. {:?}",
8593 e.node.index(),
8594 contact.crange,
8595 contact.curve.point_at(lo, tol).ok(),
8596 contact.curve.point_at(hi, tol).ok()
8597 );
8598 }
8599 contact_along[ci].push((lo, hi));
8600 let mut apart = 0.0_f64;
8601 for t in [lo, f64::midpoint(lo, hi), hi] {
8602 apart = apart.max(
8603 contact
8604 .curve
8605 .point_at(t, tol)?
8606 .distance(e.curve.point_at(carry(t), tol)?),
8607 );
8608 }
8609 if apart > contact.tolerance.max(e.tolerance) {
8610 ALONG_APART.set(ALONG_APART.get().max(apart));
8611 }
8612 let (mut tlo, mut thi) = ((carry(lo), honesty(lo)?), (carry(hi), honesty(hi)?));
8620 if tlo.0 > thi.0 {
8621 core::mem::swap(&mut tlo, &mut thi);
8622 }
8623 let target_domain = e.curve.domain();
8624 let periodic = e.curve.is_periodic();
8625 for (t, honesty) in [tlo, thi] {
8626 let t = if periodic { fold(t, target_domain) } else { t };
8630 if t > e.crange.0 + tol.parametric() && t < e.crange.1 - tol.parametric() {
8631 paves.entry(e.node).or_default().push(Pave {
8632 t,
8633 honesty,
8634 across: None,
8635 spread: 0.0,
8636 });
8637 }
8638 }
8639 }
8640 }
8641 }
8642 }
8643
8644 let mut same_a: Vec<Vec<usize>> = vec![Vec::new(); ga.faces.len()];
8645 let mut same_b: Vec<Vec<usize>> = vec![Vec::new(); gb.faces.len()];
8646 for (ia, ib) in same_pairs {
8647 same_a[ia].push(ib);
8648 same_b[ib].push(ia);
8649 }
8650 Ok((
8651 sections,
8652 pieces,
8653 contacts,
8654 tangents,
8655 contact_along,
8656 paves,
8657 same_a,
8658 same_b,
8659 hug_junctions,
8660 ))
8661}
8662
8663fn split_at_degeneracies(
8675 curve: &Curve,
8676 fa: &GFace,
8677 fb: &GFace,
8678 tol: Tolerances,
8679) -> OgeomResult<Option<Vec<(Curve, PlanarCurve, PlanarCurve)>>> {
8680 let mut stops: Vec<f64> = Vec::new();
8681 let domain = curve.domain();
8682 let mut candidates: Vec<Point> = Vec::new();
8688 for face in [fa, fb] {
8689 for pole in &face.poles {
8690 candidates.push(pole.point);
8691 }
8692 match &face.surface {
8693 SurfaceGeometry::Sphere(s) => {
8694 let sphere = s.sphere();
8695 let axis = sphere.frame().z().vector();
8696 candidates.push(sphere.centre() + axis * sphere.radius());
8697 candidates.push(sphere.centre() - axis * sphere.radius());
8698 }
8699 SurfaceGeometry::Cone(c) => {
8700 candidates.push(c.cone().apex());
8701 }
8702 _ => {}
8703 }
8704 }
8705 for point in candidates {
8706 let found = ogeom_algo::project_on_curve(curve, point, 256, tol)?;
8707 if found.distance <= tol.confusion() {
8708 stops.push(found.parameter);
8709 }
8710 }
8711 if stops.is_empty() {
8712 return Ok(None);
8713 }
8714 stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
8715 stops.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric());
8716
8717 let closed = curve.is_closed(tol) || curve.is_periodic();
8720 let mut arcs: Vec<(f64, f64)> = Vec::new();
8721 if closed {
8722 if stops.len() < 2 {
8723 let period = domain.1 - domain.0;
8726 arcs.push((stops[0], stops[0] + period));
8727 } else {
8728 let period = domain.1 - domain.0;
8729 for i in 0..stops.len() {
8730 let lo = stops[i];
8731 let hi = if i + 1 < stops.len() {
8732 stops[i + 1]
8733 } else {
8734 stops[0] + period
8735 };
8736 arcs.push((lo, hi));
8737 }
8738 }
8739 } else {
8740 let mut cuts = vec![domain.0];
8741 cuts.extend(
8742 stops
8743 .iter()
8744 .copied()
8745 .filter(|t| *t > domain.0 + tol.parametric() && *t < domain.1 - tol.parametric()),
8746 );
8747 cuts.push(domain.1);
8748 for pair in cuts.windows(2) {
8749 arcs.push((pair[0], pair[1]));
8750 }
8751 }
8752
8753 let mut out = Vec::with_capacity(arcs.len());
8754 for (lo, hi) in arcs {
8755 if hi - lo <= tol.parametric() {
8756 continue;
8757 }
8758 let (Some(pa), Some(pb)) = (
8759 ogeom_intersect::exact_pcurve_over(curve, (lo, hi), &fa.surface, tol),
8760 ogeom_intersect::exact_pcurve_over(curve, (lo, hi), &fb.surface, tol),
8761 ) else {
8762 return Ok(None);
8763 };
8764 let Ok(piece) = ogeom_geom::TrimmedCurve::new(curve.clone(), lo, hi, tol) else {
8765 return Ok(None);
8766 };
8767 out.push((piece.into(), pa, pb));
8768 }
8769 if out.is_empty() {
8770 return Ok(None);
8771 }
8772 Ok(Some(out))
8773}
8774
8775fn on_a_chart_pole(
8777 curve: &Curve,
8778 surfaces: [&SurfaceGeometry; 2],
8779 tol: Tolerances,
8780) -> OgeomResult<bool> {
8781 let start = curve.point_at(curve.domain().0, tol)?;
8782 Ok(surfaces.iter().any(|surface| {
8783 ogeom_intersect::march::chart_poles(surface, tol)
8784 .iter()
8785 .any(|&(_, _, pole)| pole.distance(start) <= tol.confusion())
8786 }))
8787}
8788
8789fn carrier_windows(
8798 g: &GSolid,
8799 tol: Tolerances,
8800) -> Vec<(usize, Option<ogeom_intersect::ParameterWindow>)> {
8801 let mut numbered: ogeom_core::FastMap<&(ogeom_topo::SurfaceId, Location), usize> =
8802 ogeom_core::FastMap::default();
8803 let empty = (
8806 Point2::new(f64::INFINITY, f64::INFINITY),
8807 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
8808 );
8809 let mut reach: Vec<Option<(Point2, Point2)>> = Vec::new();
8810 let mut of_face = Vec::with_capacity(g.faces.len());
8811 for face in &g.faces {
8812 let next = numbered.len();
8813 let at = *numbered.entry(&face.carrier).or_insert(next);
8814 if at == reach.len() {
8815 reach.push(Some(empty));
8816 }
8817 of_face.push(at);
8818 let Some((low, high)) = reach[at].as_mut() else {
8819 continue;
8820 };
8821 let mut points: Vec<Point2> = face.trim_lines(tol).iter().flatten().copied().collect();
8822 for pole in &face.poles {
8823 for k in 0..=4 {
8824 let t = pole.prange.0 + (pole.prange.1 - pole.prange.0) * f64::from(k) / 4.0;
8825 if let Ok(p) = pole.pcurve.point_at(t, tol) {
8826 points.push(p);
8827 }
8828 }
8829 }
8830 if points.is_empty() || points.iter().any(|p| !p.x.is_finite() || !p.y.is_finite()) {
8831 reach[at] = None;
8832 continue;
8833 }
8834 for p in points {
8835 *low = Point2::new(low.x.min(p.x), low.y.min(p.y));
8836 *high = Point2::new(high.x.max(p.x), high.y.max(p.y));
8837 }
8838 }
8839 let grow = |lo: f64, hi: f64| {
8840 let margin = (hi - lo) * 0.25;
8841 (lo - margin, hi + margin)
8842 };
8843 let windows: Vec<Option<ogeom_intersect::ParameterWindow>> = reach
8844 .into_iter()
8845 .map(|bound| bound.map(|(low, high)| (grow(low.x, high.x), grow(low.y, high.y))))
8846 .collect();
8847 of_face.into_iter().map(|at| (at, windows[at])).collect()
8848}
8849
8850fn windowed_to(surface: &SurfaceGeometry, bound: &ogeom_math::Aabb) -> SurfaceGeometry {
8860 let corners = bound.corners();
8861 if corners.is_empty() {
8862 return surface.clone();
8863 }
8864 match surface {
8865 SurfaceGeometry::Plane(p) => {
8866 let frame = p.plane().frame();
8867 let (mut u0, mut u1, mut v0, mut v1) = (
8868 f64::INFINITY,
8869 f64::NEG_INFINITY,
8870 f64::INFINITY,
8871 f64::NEG_INFINITY,
8872 );
8873 for c in &corners {
8874 let local = frame.to_local(*c);
8875 u0 = u0.min(local.x);
8876 u1 = u1.max(local.x);
8877 v0 = v0.min(local.y);
8878 v1 = v1.max(local.y);
8879 }
8880 let margin = ((u1 - u0) + (v1 - v0)).mul_add(0.25, 1.0);
8881 let (want_u, want_v) = ((u0 - margin, u1 + margin), (v0 - margin, v1 + margin));
8882 let (have_u, have_v) = ogeom_geom::Surface::domain(p);
8883 if have_u.1 - have_u.0 <= want_u.1 - want_u.0
8884 && have_v.1 - have_v.0 <= want_v.1 - want_v.0
8885 {
8886 return surface.clone();
8887 }
8888 ogeom_geom::PlaneSurface::over(p.plane(), want_u, want_v)
8889 .map_or_else(|_| surface.clone(), Into::into)
8890 }
8891 SurfaceGeometry::Cylinder(c) => {
8892 let frame = c.cylinder().frame();
8893 let (mut h0, mut h1) = (f64::INFINITY, f64::NEG_INFINITY);
8894 for corner in &corners {
8895 let h = (*corner - frame.origin()).dot(frame.z().vector());
8896 h0 = h0.min(h);
8897 h1 = h1.max(h);
8898 }
8899 let margin = (h1 - h0).mul_add(0.25, 1.0);
8900 let want = (h0 - margin, h1 + margin);
8901 let have = ogeom_geom::Surface::domain(c).1;
8902 if have.1 - have.0 <= want.1 - want.0 {
8903 return surface.clone();
8904 }
8905 ogeom_geom::CylinderSurface::new(c.cylinder(), want)
8906 .map_or_else(|_| surface.clone(), Into::into)
8907 }
8908 other => other.clone(),
8909 }
8910}
8911
8912fn reaching(
8925 surface: &SurfaceGeometry,
8926 other: &SurfaceGeometry,
8927 met: &ogeom_intersect::SurfaceIntersection,
8928 tol: Tolerances,
8929) -> OgeomResult<Option<SurfaceGeometry>> {
8930 let SurfaceGeometry::Cylinder(c) = surface else {
8931 return Ok(None);
8932 };
8933 if !matches!(other, SurfaceGeometry::BSpline(_)) {
8934 return Ok(None);
8935 }
8936 let ogeom_intersect::SurfaceIntersection::Along(curves) = met else {
8937 return Ok(None);
8938 };
8939 let frame = c.cylinder().frame();
8940 let have = ogeom_geom::Surface::domain(c).1;
8941 let past = (have.1 - have.0) * 0.02;
8942 let short = |gap: f64| gap > tol.confusion() && gap <= past;
8943 let (mut low, mut high) = (false, false);
8944 for section in curves {
8945 let (t0, t1) = section.curve.domain();
8946 for t in [t0, t1] {
8947 let at = section.curve.point_at(t, tol)?;
8948 let h = (at - frame.origin()).dot(frame.z().vector());
8949 low |= short(h - have.0);
8950 high |= short(have.1 - h);
8951 }
8952 }
8953 if !low && !high {
8954 return Ok(None);
8955 }
8956 let want = (
8957 if low { have.0 - past } else { have.0 },
8958 if high { have.1 + past } else { have.1 },
8959 );
8960 Ok(ogeom_geom::CylinderSurface::new(c.cylinder(), want)
8961 .ok()
8962 .map(Into::into))
8963}
8964
8965fn march_pair(
8969 a: &SurfaceGeometry,
8970 b: &SurfaceGeometry,
8971 options: &ogeom_intersect::IntersectOptions,
8972 tol: Tolerances,
8973) -> OgeomResult<(Vec<ogeom_intersect::IntersectionCurve>, f64)> {
8974 use ogeom_intersect::{approximate_branch, branches_cut_at_poles, passes_a_pole};
8975 let mut pole_reach = 0.0;
8979 let out = loop {
8980 let traced = branches_cut_at_poles(a, b, options.marching, pole_reach, tol)?;
8981 let mut out = Vec::new();
8982 let mut again: Option<f64> = None;
8983 for branch in &traced {
8984 let fitted = approximate_branch(a, b, branch, options.tolerance, tol)?;
8985 let stated = options.marching.chord + fitted.fit_error;
8986 if pole_reach == 0.0 && passes_a_pole(&fitted.curve.clone().into(), [a, b], stated, tol)
8987 {
8988 again = Some(again.map_or(stated, |r: f64| r.max(stated)));
8989 }
8990 out.push(fitted);
8991 }
8992 match again {
8993 Some(reach) => pole_reach = reach,
8994 None => break out,
8995 }
8996 };
8997 if out.is_empty() {
8998 if surfaces_stand_apart(a, b, tol) {
9005 return Ok((out, pole_reach));
9006 }
9007 ogeom_bail!(
9008 NotDone,
9009 "an exact section has no closed-form pcurve and marching resolved \
9010 no branch; the configuration is beyond the intersector's current \
9011 reach"
9012 );
9013 }
9014 Ok((out, pole_reach))
9015}
9016
9017fn surfaces_stand_apart(a: &SurfaceGeometry, b: &SurfaceGeometry, tol: Tolerances) -> bool {
9026 use ogeom_geom::Surface as _;
9027 let extent_of = |s: &SurfaceGeometry| -> f64 {
9028 let ((ua, ub), (va, vb)) = s.domain();
9029 (ub - ua).abs().max((vb - va).abs())
9030 };
9031 let (sample, against) = if extent_of(a) <= extent_of(b) {
9032 (a, b)
9033 } else {
9034 (b, a)
9035 };
9036 let span = extent_of(sample);
9037 if !span.is_finite() || span > 1e4 {
9038 return false;
9039 }
9040 let ((ua, ub), (va, vb)) = sample.domain();
9041 const GRID: usize = 9;
9042 let Ok(seeds) = ogeom_algo::SurfaceSeeds::over(against, 16, tol) else {
9047 return false;
9048 };
9049 let mut clearance = f64::INFINITY;
9050 for i in 0..=GRID {
9051 for j in 0..=GRID {
9052 #[allow(clippy::cast_precision_loss)]
9053 let u = ua + (ub - ua) * i as f64 / GRID as f64;
9054 #[allow(clippy::cast_precision_loss)]
9055 let v = va + (vb - va) * j as f64 / GRID as f64;
9056 let Ok(p) = sample.point_at(u, v, tol) else {
9057 return false;
9058 };
9059 let Ok(projection) = seeds.project(against, p, tol) else {
9060 return false;
9061 };
9062 clearance = clearance.min(projection.distance);
9063 if clearance <= tol.confusion() * 1e3 {
9064 return false;
9065 }
9066 }
9067 }
9068 clearance > tol.confusion() * 1e3
9069}
9070
9071fn coincide_as_stated(fa: &GFace, fb: &GFace, as_stated: bool, tol: Tolerances) -> bool {
9087 const GRID: usize = 8;
9088 const EVIDENCE: usize = 4;
9089 let reach = (fa.tolerance + fb.tolerance).max(tol.confusion() * 2.0);
9093 if reach <= ogeom_core::tolerance::CONFUSION * 10.0 && !as_stated {
9094 return false;
9095 }
9096 let kind = |s: &SurfaceGeometry| match s {
9099 SurfaceGeometry::Plane(_) => Some(0),
9100 SurfaceGeometry::Cylinder(_) => Some(1),
9101 SurfaceGeometry::Cone(_) => Some(2),
9102 SurfaceGeometry::Sphere(_) => Some(3),
9103 SurfaceGeometry::Torus(_) => Some(4),
9104 _ => None,
9105 };
9106 if let (Some(a), Some(b)) = (kind(&fa.surface), kind(&fb.surface))
9107 && a != b
9108 {
9109 return false;
9110 }
9111 let (sampled, against) = if fa.bound.diagonal() <= fb.bound.diagonal() {
9112 (fa, fb)
9113 } else {
9114 (fb, fa)
9115 };
9116 let Some(lines) = sampled.outline(tol) else {
9117 return false;
9118 };
9119 let borrowed: Vec<&[Point2]> = lines.iter().map(Vec::as_slice).collect();
9120 let (lo, hi) = lines.iter().flatten().fold(
9121 (
9122 Point2::new(f64::INFINITY, f64::INFINITY),
9123 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
9124 ),
9125 |(lo, hi), p| {
9126 (
9127 Point2::new(lo.x.min(p.x), lo.y.min(p.y)),
9128 Point2::new(hi.x.max(p.x), hi.y.max(p.y)),
9129 )
9130 },
9131 );
9132 if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) {
9133 return false;
9134 }
9135 let search = reach.max(sampled.bound.diagonal().max(against.bound.diagonal()) * 1e-3);
9138 let mut evidence = 0_usize;
9139 for i in 0..GRID {
9140 for j in 0..GRID {
9141 #[allow(clippy::cast_precision_loss)]
9142 let at = Point2::new(
9143 lo.x + (hi.x - lo.x) * ((i as f64 + 0.5) / GRID as f64),
9144 lo.y + (hi.y - lo.y) * ((j as f64 + 0.5) / GRID as f64),
9145 );
9146 if !arrange::inside_many(&borrowed, at) {
9147 continue;
9148 }
9149 let Ok(p) = sampled.surface.point_at(at.x, at.y, tol) else {
9150 return false;
9151 };
9152 if !against.bound.expanded(search).contains(p) {
9153 continue;
9154 }
9155 let Some(uv) = chart_point_within(against, p, search, tol) else {
9156 continue;
9157 };
9158 let Ok(foot) = against.surface.point_at(uv.x, uv.y, tol) else {
9159 return false;
9160 };
9161 if foot.distance(p) > reach {
9162 return false;
9163 }
9164 evidence += 1;
9165 }
9166 }
9167 evidence >= EVIDENCE
9168}
9169
9170fn partner_reach(face: &GFace, partner: &GFace, tol: Tolerances) -> f64 {
9174 (face.tolerance + partner.tolerance).max(tol.confusion() * 10.0)
9175}
9176
9177const FINE: usize = 1024;
9179
9180const FINE_BLOCK: usize = 32;
9183
9184struct FineRings<'a> {
9193 rings: Vec<Vec<FinePart<'a>>>,
9194 tol: Tolerances,
9195}
9196
9197enum FinePart<'a> {
9199 Points(Vec<Point2>),
9201 Arc(FineArc<'a>),
9203}
9204
9205struct FineArc<'a> {
9208 curve: &'a PlanarCurve,
9209 circle: ogeom_math::Circle2,
9210 from: f64,
9211 to: f64,
9212 wrap: Option<(f64, f64)>,
9213 reversed: bool,
9214 drawn: core::cell::RefCell<Vec<Option<Point2>>>,
9216}
9217
9218impl FineArc<'_> {
9219 fn parameter(&self, i: usize) -> f64 {
9221 #[allow(clippy::cast_precision_loss)]
9222 let t = self.from + (self.to - self.from) * (i as f64 / FINE as f64);
9223 self.wrap.map_or(t, |domain| fold(t, domain))
9224 }
9225
9226 fn point(&self, i: usize, tol: Tolerances) -> Point2 {
9228 if let Some(Some(p)) = self.drawn.borrow().get(i) {
9229 return *p;
9230 }
9231 let p = self
9232 .curve
9233 .point_at(self.parameter(i), tol)
9234 .unwrap_or(Point2::new(f64::NAN, f64::NAN));
9235 let mut drawn = self.drawn.borrow_mut();
9236 if drawn.is_empty() {
9237 drawn.resize(FINE + 1, None);
9238 }
9239 drawn[i] = Some(p);
9240 p
9241 }
9242
9243 fn slack(&self) -> f64 {
9246 let centre = self.circle.centre();
9247 1e-9 * (1.0 + self.circle.radius() + centre.x.abs() + centre.y.abs())
9248 }
9249
9250 fn may_cross(&self, lo: usize, hi: usize, p: Point2, tol: Tolerances) -> bool {
9258 let (t0, t1) = (self.parameter(lo), self.parameter(hi));
9259 #[allow(clippy::cast_precision_loss)]
9260 let step = (self.to - self.from) * ((hi - lo) as f64 / FINE as f64);
9261 let unfolded = self
9262 .wrap
9263 .is_none_or(|(a, b)| ((t1 - t0) - step).abs() <= 0.5 * (b - a).abs());
9264 let start = self.point(lo, tol);
9265 let reach = self.circle.radius() * (t1 - t0).abs() + self.slack();
9266 if !(unfolded && reach.is_finite() && start.y.is_finite() && p.y.is_finite()) {
9267 return true;
9268 }
9269 start.y - reach <= p.y && start.y + reach >= p.y
9270 }
9271
9272 fn extent(&self) -> (f64, f64, f64) {
9276 let frame = self.circle.frame();
9277 let (x, y) = (frame.x().vector(), frame.y().vector());
9278 let r = self.circle.radius();
9279 let centre = self.circle.centre();
9280 let across = r * (x.x.abs() + y.x.abs()) + self.slack();
9281 let up = r * (x.y.abs() + y.y.abs()) + self.slack();
9282 (centre.x + across, centre.y - up, centre.y + up)
9283 }
9284}
9285
9286impl<'a> FineRings<'a> {
9287 fn of(
9291 face: &'a GFace,
9292 rings: &[Vec<Traversal<Tag>>],
9293 sections: &'a [SectionRec],
9294 from_a: bool,
9295 only: Option<&[bool]>,
9296 tol: Tolerances,
9297 ) -> Option<Self> {
9298 if !matches!(face.surface, SurfaceGeometry::Plane(_)) {
9299 return None;
9300 }
9301 let mut lines: Vec<Vec<FinePart<'a>>> = Vec::with_capacity(rings.len());
9302 for (k, ring) in rings.iter().enumerate() {
9303 let mut line: Vec<FinePart<'a>> = Vec::new();
9304 if only.is_some_and(|wanted| !wanted.get(k).copied().unwrap_or(true)) {
9305 lines.push(line);
9306 continue;
9307 }
9308 for traversal in ring {
9309 let (curve, a, b, wrap) = fine_strand(face, sections, from_a, &traversal.tag)?;
9310 if let PlanarCurve::Circle(circle) = curve {
9311 line.push(FinePart::Arc(FineArc {
9312 curve,
9313 circle: circle.circle(),
9314 from: a,
9315 to: b,
9316 wrap,
9317 reversed: traversal.reversed,
9318 drawn: core::cell::RefCell::new(Vec::new()),
9319 }));
9320 continue;
9321 }
9322 let count = if matches!(curve, PlanarCurve::Line(_)) {
9323 1
9324 } else {
9325 FINE
9326 };
9327 let mut points: Vec<Point2> = Vec::with_capacity(count + 1);
9328 for i in 0..=count {
9329 #[allow(clippy::cast_precision_loss)]
9330 let t = a + (b - a) * (i as f64 / count as f64);
9331 let t = wrap.map_or(t, |domain| fold(t, domain));
9332 points.push(curve.point_at(t, tol).ok()?);
9333 }
9334 if traversal.reversed {
9335 points.reverse();
9336 }
9337 line.push(FinePart::Points(points));
9338 }
9339 lines.push(line);
9340 }
9341 Some(Self { rings: lines, tol })
9342 }
9343
9344 fn inside(&self, p: Point2) -> bool {
9347 self.rings
9348 .iter()
9349 .fold(false, |acc, ring| acc != self.ring_inside(ring, p))
9350 }
9351
9352 fn ring_inside(&self, ring: &[FinePart<'_>], p: Point2) -> bool {
9356 let tol = self.tol;
9357 let mut inside = false;
9358 let mut flip = |a: Point2, b: Point2| {
9359 if arrange::ray_crosses(a, b, p) {
9360 inside = !inside;
9361 }
9362 };
9363 let mut first: Option<Point2> = None;
9364 let mut last: Option<Point2> = None;
9365 for part in ring {
9366 let (head, tail) = match part {
9367 FinePart::Points(points) => {
9368 for w in points.windows(2) {
9369 flip(w[0], w[1]);
9370 }
9371 (points[0], points[points.len() - 1])
9372 }
9373 FinePart::Arc(arc) => {
9374 let mut lo = 0;
9375 while lo < FINE {
9376 let hi = (lo + FINE_BLOCK).min(FINE);
9377 if arc.may_cross(lo, hi, p, tol) {
9378 for i in lo..hi {
9379 flip(arc.point(i, tol), arc.point(i + 1, tol));
9380 }
9381 }
9382 lo = hi;
9383 }
9384 let ends = (arc.point(0, tol), arc.point(FINE, tol));
9385 if arc.reversed { (ends.1, ends.0) } else { ends }
9386 }
9387 };
9388 if let Some(at) = last {
9389 flip(at, head);
9390 }
9391 first.get_or_insert(head);
9392 last = Some(tail);
9393 }
9394 if let (Some(first), Some(last)) = (first, last) {
9395 flip(last, first);
9396 }
9397 inside
9398 }
9399
9400 fn out_of_reach(ring: &[FinePart<'_>], p: Point2) -> bool {
9404 let (mut right, mut low, mut high) = (f64::NEG_INFINITY, f64::INFINITY, f64::NEG_INFINITY);
9405 let mut reach = |x: f64, y: f64| {
9406 right = right.max(x);
9407 low = low.min(y);
9408 high = high.max(y);
9409 x.is_finite() && y.is_finite()
9410 };
9411 for part in ring {
9412 let finite = match part {
9413 FinePart::Points(points) => points.iter().all(|q| reach(q.x, q.y)),
9414 FinePart::Arc(arc) => {
9415 let (x, lo, hi) = arc.extent();
9416 arc.from.is_finite() && arc.to.is_finite() && reach(x, lo) && reach(x, hi)
9417 }
9418 };
9419 if !finite {
9420 return false;
9421 }
9422 }
9423 ring.is_empty() || p.y < low || p.y > high || p.x > right
9424 }
9425
9426 fn inside_beside(&self, p: Point2, reached: &[bool]) -> Option<bool> {
9432 let mut inside = false;
9433 for (k, ring) in self.rings.iter().enumerate() {
9434 let counted = reached.get(k).copied().unwrap_or(true);
9435 if !counted && Self::out_of_reach(ring, p) {
9436 continue;
9437 }
9438 let crossed = self.ring_inside(ring, p);
9439 if !counted && crossed {
9440 return None;
9441 }
9442 inside ^= crossed;
9443 }
9444 Some(inside)
9445 }
9446
9447 fn points(&self) -> Vec<Vec<Point2>> {
9449 self.rings
9450 .iter()
9451 .map(|ring| {
9452 let mut line = Vec::new();
9453 for part in ring {
9454 match part {
9455 FinePart::Points(points) => line.extend_from_slice(points),
9456 FinePart::Arc(arc) => {
9457 let start = line.len();
9458 line.extend((0..=FINE).map(|i| arc.point(i, self.tol)));
9459 if arc.reversed {
9460 line[start..].reverse();
9461 }
9462 }
9463 }
9464 }
9465 line
9466 })
9467 .collect()
9468 }
9469}
9470
9471type FineStrand<'a> = (&'a PlanarCurve, f64, f64, Option<(f64, f64)>);
9474
9475fn fine_strand<'a>(
9479 face: &'a GFace,
9480 sections: &'a [SectionRec],
9481 from_a: bool,
9482 tag: &Tag,
9483) -> Option<FineStrand<'a>> {
9484 Some(match *tag {
9485 Tag::Boundary { edge, range } => {
9486 let e = face.edges.get(edge)?;
9487 (
9488 &e.pcurve,
9489 rescale(range.0, e.crange, e.prange),
9490 rescale(range.1, e.crange, e.prange),
9491 None,
9492 )
9493 }
9494 Tag::Section { section, range } => {
9495 let record = sections.get(section)?;
9496 let pcurve = if from_a { &record.pc_a } else { &record.pc_b };
9497 let wrap = record.closed.then(|| record.curve.domain());
9498 (pcurve, range.0, range.1, wrap)
9499 }
9500 Tag::Contact { .. } | Tag::Pole { .. } => return None,
9501 })
9502}
9503
9504fn rings_near(outlines: &[Vec<Point2>], probes: &[Point2]) -> Vec<bool> {
9509 outlines
9510 .iter()
9511 .map(|line| {
9512 let (mut low, mut high) = (
9513 Point2::new(f64::INFINITY, f64::INFINITY),
9514 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
9515 );
9516 for p in line {
9517 low = Point2::new(low.x.min(p.x), low.y.min(p.y));
9518 high = Point2::new(high.x.max(p.x), high.y.max(p.y));
9519 }
9520 if low
9521 .x
9522 .partial_cmp(&high.x)
9523 .is_none_or(core::cmp::Ordering::is_gt)
9524 {
9525 return true;
9526 }
9527 let margin = 1e-3 + 1e-2 * low.distance(high);
9528 probes
9529 .iter()
9530 .any(|p| p.y >= low.y - margin && p.y <= high.y + margin && p.x <= high.x + margin)
9531 })
9532 .collect()
9533}
9534
9535fn inside_plane_exactly(face: &GFace, p: Point2, tol: Tolerances) -> Option<bool> {
9544 if !matches!(face.surface, SurfaceGeometry::Plane(_))
9546 || face
9547 .edges
9548 .iter()
9549 .all(|e| matches!(e.pcurve, PlanarCurve::Line(_)))
9550 {
9551 return None;
9552 }
9553 let at = face.surface.point_at(p.x, p.y, tol).ok()?;
9559 for e in &face.edges {
9560 let doubt = (e.standoff * 2.0).max(tol.confusion() * 75.0);
9561 if !e.bound.expanded(doubt).contains(at) {
9562 continue;
9563 }
9564 if distance_to_edge_curve(&e.curve, e.crange, at, tol).ok()? <= doubt {
9565 return None;
9566 }
9567 }
9568 let reach = face
9569 .edges
9570 .iter()
9571 .map(|e| e.bound.diagonal())
9572 .sum::<f64>()
9573 .max(1.0)
9574 * 4.0;
9575 let mut curves: Vec<(PlanarCurve, (f64, f64))> = Vec::new();
9576 for e in &face.edges {
9577 curves.push((e.pcurve.clone(), e.prange));
9578 if let Some((other, range)) = &e.other_side {
9579 curves.push((other.clone(), *range));
9580 }
9581 }
9582 for pole in &face.poles {
9583 curves.push((pole.pcurve.clone(), pole.prange));
9584 }
9585 let options = ogeom_intersect::CurveCurveOptions::default();
9586 'directions: for (dx, dy) in [(1.0, 0.0137), (-0.31, 1.0), (-1.0, -0.47), (0.53, -1.0)] {
9587 let length = f64::hypot(dx, dy);
9588 let far = Point2::new(p.x + dx / length * reach, p.y + dy / length * reach);
9589 let Ok(ray) = ogeom_geom::Line2d::segment(p, far, tol) else {
9590 return None;
9591 };
9592 let ray: PlanarCurve = ray.into();
9593 let mut crossings = 0_usize;
9594 for (curve, (a, b)) in &curves {
9595 let (lo, hi) = (a.min(*b), a.max(*b));
9596 if hi - lo <= tol.parametric() {
9597 continue;
9598 }
9599 let Ok(piece) = ogeom_geom::Trimmed2d::new(curve.clone(), lo, hi, tol) else {
9600 continue 'directions;
9601 };
9602 let piece: PlanarCurve = piece.into();
9603 let Ok(met) = ogeom_intersect::intersect_curves_2d(&ray, &piece, options, tol) else {
9604 continue 'directions;
9605 };
9606 if !met.overlaps.is_empty() {
9607 continue 'directions;
9608 }
9609 let span = hi - lo;
9610 for hit in &met.crossings {
9611 let at_start = hit.on_a <= tol.confusion();
9612 let at_end =
9613 (hit.on_b - lo).abs() <= span * 1e-9 || (hi - hit.on_b).abs() <= span * 1e-9;
9614 if at_start || at_end || hit.reach > 0.0 {
9615 continue 'directions;
9616 }
9617 crossings += 1;
9618 }
9619 }
9620 return Some(crossings % 2 == 1);
9621 }
9622 None
9623}
9624
9625struct FacePiece {
9629 from_a: bool,
9631 face: usize,
9632 rings: Vec<Vec<Traversal<Tag>>>,
9633 outlines: Vec<Vec<Point2>>,
9635 probe: Point,
9637 state: PieceState,
9638 covered: bool,
9650 whole: bool,
9653}
9654
9655struct GeneralFused {
9656 a: GSolid,
9657 b: GSolid,
9658 sections: Vec<SectionRec>,
9659 contacts: Vec<ContactRec>,
9660 tangents: Vec<TangentRec>,
9664 pieces: Vec<FacePiece>,
9665 coincident: Vec<(usize, usize)>,
9669 junctions: Vec<Junction>,
9672}
9673
9674#[derive(Debug, Clone, Copy)]
9687struct Junction {
9688 at: Point,
9690 reach: f64,
9693 onto_vertex: bool,
9697}
9698
9699fn seams_through_junctions(
9711 solid: &GSolid,
9712 from_a: bool,
9713 (sections, pieces): (&[SectionRec], &[SectionPiece]),
9714 junctions: &[Junction],
9715 paves: &mut ogeom_core::FastMap<EdgeKey, Vec<Pave>>,
9716 tol: Tolerances,
9717) -> OgeomResult<()> {
9718 for (fi, face) in solid.faces.iter().enumerate() {
9719 let seams: Vec<&BoundaryEdge> = face
9720 .edges
9721 .iter()
9722 .filter(|e| e.other_side.is_some())
9723 .collect();
9724 if seams.is_empty() {
9725 continue;
9726 }
9727 let mut ends: Vec<(Point, Point2)> = Vec::new();
9729 for sp in pieces {
9730 let section = §ions[sp.section];
9731 let (face_of, pcurve, hugs) = if from_a {
9732 (section.face_a, §ion.pc_a, sp.hugs[0])
9733 } else {
9734 (section.face_b, §ion.pc_b, sp.hugs[1])
9735 };
9736 if face_of != fi || hugs || section.closed {
9737 continue;
9738 }
9739 for t in [sp.range.0, sp.range.1] {
9740 ends.push((section.curve.point_at(t, tol)?, pcurve.point_at(t, tol)?));
9741 }
9742 }
9743 for e in seams {
9744 let Some((other, orange)) = &e.other_side else {
9745 continue;
9746 };
9747 for junction in junctions {
9748 if !e.bound.expanded(junction.reach).contains(junction.at) {
9749 continue;
9750 }
9751 let (gap, t) = nearest_on_edge_curve(&e.curve, e.crange, junction.at, tol)?;
9752 if gap > junction.reach {
9753 continue;
9754 }
9755 let paved = paves.get(&e.node).is_some_and(|list| {
9756 list.iter().any(|p| {
9757 e.curve
9758 .point_at(p.t, tol)
9759 .is_ok_and(|q| junction.at.distance(q) <= junction.reach)
9760 })
9761 });
9762 if paved {
9763 continue;
9764 }
9765 let columns = [
9766 e.pcurve.point_at(rescale(t, e.crange, e.prange), tol)?,
9767 other.point_at(rescale(t, e.crange, *orange), tol)?,
9768 ];
9769 let mut sides = [false; 2];
9770 for (at, chart) in &ends {
9771 if junction.at.distance(*at) <= junction.reach {
9772 let nearer =
9773 usize::from(chart.distance(columns[1]) < chart.distance(columns[0]));
9774 sides[nearer] = true;
9775 }
9776 }
9777 if sides == [true; 2] {
9778 if *DEBUG_WIRE {
9779 eprintln!(
9780 "PAVE seam e{} at {t:.6}: a junction at {:?} holds ends on both sides",
9781 e.node.index(),
9782 junction.at
9783 );
9784 }
9785 paves.entry(e.node).or_default().push(Pave {
9786 t,
9787 honesty: gap,
9788 across: None,
9789 spread: 0.0,
9790 });
9791 }
9792 }
9793 }
9794 }
9795 Ok(())
9796}
9797
9798fn merge_junctions(junctions: Vec<Junction>) -> Vec<Junction> {
9802 let n = junctions.len();
9803 let mut parent: Vec<usize> = (0..n).collect();
9804 fn root(parent: &mut [usize], mut i: usize) -> usize {
9805 while parent[i] != i {
9806 parent[i] = parent[parent[i]];
9807 i = parent[i];
9808 }
9809 i
9810 }
9811 let widest = junctions.iter().map(|j| j.reach).fold(0.0_f64, f64::max);
9817 let mut order: Vec<usize> = (0..n).collect();
9818 order.sort_by(|&x, &y| junctions[x].at.x.total_cmp(&junctions[y].at.x));
9819 for (k, &i) in order.iter().enumerate() {
9820 let limit = junctions[i].at.x + junctions[i].reach + widest;
9821 for &j in &order[k + 1..] {
9822 if junctions[j].at.x > limit {
9823 break;
9824 }
9825 if junctions[i].at.distance(junctions[j].at) <= junctions[i].reach + junctions[j].reach
9826 {
9827 let (ri, rj) = (root(&mut parent, i), root(&mut parent, j));
9828 if ri != rj {
9829 parent[rj] = ri;
9830 }
9831 }
9832 }
9833 }
9834 let mut groups: Vec<Vec<usize>> = Vec::new();
9835 let mut of_root: Vec<Option<usize>> = vec![None; n];
9836 for i in 0..n {
9837 let r = root(&mut parent, i);
9838 match of_root[r] {
9839 Some(g) => groups[g].push(i),
9840 None => {
9841 of_root[r] = Some(groups.len());
9842 groups.push(vec![i]);
9843 }
9844 }
9845 }
9846 let mut out = Vec::with_capacity(groups.len());
9852 for members in groups {
9853 if members.len() == 1 {
9854 out.push(junctions[members[0]]);
9855 continue;
9856 }
9857 let mut sum = ogeom_math::Vector::ZERO;
9858 for &m in &members {
9859 sum += junctions[m].at - Point::ORIGIN;
9860 }
9861 #[allow(clippy::cast_precision_loss)]
9862 let at = Point::ORIGIN + sum / (members.len() as f64);
9863 let reach = members
9864 .iter()
9865 .map(|&m| at.distance(junctions[m].at) + junctions[m].reach)
9866 .fold(0.0_f64, f64::max);
9867 let widest = members
9868 .iter()
9869 .map(|&m| junctions[m].reach)
9870 .fold(0.0_f64, f64::max);
9871 if reach > widest * 4.0 {
9872 out.extend(members.iter().map(|&m| junctions[m]));
9873 } else {
9874 out.push(Junction {
9875 at,
9876 reach,
9877 onto_vertex: members.iter().any(|&m| junctions[m].onto_vertex),
9878 });
9879 }
9880 }
9881 out
9882}
9883
9884fn pave_junctions(
9887 ga: &GSolid,
9888 gb: &GSolid,
9889 paves: &ogeom_core::FastMap<EdgeKey, Vec<Pave>>,
9890 along: &AlongEdges,
9891 tol: Tolerances,
9892) -> OgeomResult<Vec<Junction>> {
9893 let mut seen: ogeom_core::FastSet<EdgeKey> = ogeom_core::FastSet::default();
9894 let mut junctions = Vec::new();
9895 for e in ga
9896 .faces
9897 .iter()
9898 .chain(gb.faces.iter())
9899 .flat_map(|f| f.edges.iter())
9900 {
9901 if !seen.insert(e.node) {
9902 continue;
9903 }
9904 let Some(ts) = paves.get(&e.node) else {
9905 continue;
9906 };
9907 let tolerant = e.tolerance > tol.confusion() * 1e2;
9912 let floor = if tolerant {
9913 e.tolerance * 2.0
9914 } else {
9915 e.tolerance
9916 }
9917 .max(tol.confusion() * 10.0);
9918 let off = |face, p| off_face(ga, gb, face, p, tol);
9919 let along = along.get(&e.node).map_or(&[][..], Vec::as_slice);
9920 for cluster in cluster_paves(&e.curve, e.crange, e.tolerance, ts, Some(&off), along, tol)? {
9921 if cluster.members > 1 || cluster.honesty > tol.confusion() * 1e2 || tolerant {
9922 junctions.push(Junction {
9923 at: cluster.at,
9924 reach: cluster.span + cluster.honesty.max(floor),
9925 onto_vertex: false,
9926 });
9927 }
9928 }
9929 }
9930 Ok(junctions)
9931}
9932
9933const QUIET_EDGE: f64 = 1e-2;
9936
9937fn quiet_piece(
9945 model: &Model,
9946 from_a: bool,
9947 fi: usize,
9948 face: &GFace,
9949 boundary: Option<&ogeom_algo::SolidBoundary>,
9950 tol: Tolerances,
9951) -> OgeomResult<Option<FacePiece>> {
9952 let forward = face.face.oriented(ogeom_topo::Orientation::Forward);
9953 let indexed: Option<ogeom_core::FastMap<EdgeKey, usize>> = (face.edges.len() > 16).then(|| {
9955 let mut first =
9956 ogeom_core::FastMap::with_capacity_and_hasher(face.edges.len(), Default::default());
9957 for (index, e) in face.edges.iter().enumerate() {
9958 first.entry(e.node).or_insert(index);
9959 }
9960 first
9961 });
9962 let aside: ogeom_core::FastSet<ogeom_topo::TShapeId> = face
9963 .holes_aside
9964 .iter()
9965 .map(|hole| hole.wire.node())
9966 .collect();
9967 let mut rings = Vec::new();
9968 for wire in model.ordered_children_of(&forward)? {
9969 if aside.contains(&wire.node()) {
9970 continue;
9971 }
9972 let mut ring = Vec::new();
9973 for edge in model.ordered_children_of(&wire)? {
9974 let key = EdgeKey::of(&edge);
9975 if face.slits.contains(&key) {
9976 continue;
9977 }
9978 let found = match &indexed {
9979 Some(first) => first.get(&key).copied(),
9980 None => face.edges.iter().position(|e| e.node == key),
9981 };
9982 let Some(index) = found else {
9983 return Ok(None);
9984 };
9985 ring.push(Traversal {
9986 tag: Tag::Boundary {
9987 edge: index,
9988 range: face.edges[index].crange,
9989 },
9990 reversed: edge.orientation() == ogeom_topo::Orientation::Reversed,
9991 nodes: None,
9992 });
9993 }
9994 rings.push(ring);
9995 }
9996 let Some(e) = face.edges.first() else {
9997 return Ok(None);
9998 };
9999 let probe = e
10000 .curve
10001 .point_at(f64::midpoint(e.crange.0, e.crange.1), tol)?;
10002 let Some(boundary) = boundary else {
10003 return Ok(Some(FacePiece {
10004 from_a,
10005 face: fi,
10006 rings,
10007 outlines: Vec::new(),
10008 probe,
10009 state: PieceState::Unread,
10010 covered: false,
10011 whole: true,
10012 }));
10013 };
10014 let state = match boundary.holds(model, probe, tol) {
10015 Ok(Containment::In) => PieceState::In,
10016 Ok(Containment::Out) => PieceState::Out,
10017 Ok(Containment::On) | Err(ogeom_core::OgeomError::NotDone(_)) => return Ok(None),
10018 Err(e) => return Err(e),
10019 };
10020 Ok(Some(FacePiece {
10021 from_a,
10022 face: fi,
10023 rings,
10024 outlines: Vec::new(),
10025 probe,
10026 state,
10027 covered: false,
10028 whole: true,
10029 }))
10030}
10031
10032type Apart = (Vec<Strand<Tag>>, Vec<usize>, Vec<arrange::Lone<Tag>>);
10035
10036fn lone_holes(
10044 model: &Model,
10045 face: &GFace,
10046 strands: &[Strand<Tag>],
10047 snap: f64,
10048) -> OgeomResult<Option<Apart>> {
10049 const FEW: usize = 64;
10052 if strands.len() < FEW
10053 || !matches!(face.surface, SurfaceGeometry::Plane(_))
10054 || !face.poles.is_empty()
10055 {
10056 return Ok(None);
10057 }
10058 let index: ogeom_core::FastMap<EdgeKey, usize> = face
10060 .edges
10061 .iter()
10062 .enumerate()
10063 .map(|(i, e)| (e.node, i))
10064 .collect();
10065 let mut wire_of: Vec<Option<Option<usize>>> = vec![None; face.edges.len()];
10066 let wires = model.ordered_children_of(&face.face.oriented(ogeom_topo::Orientation::Forward))?;
10067 for (w, wire) in wires.iter().enumerate() {
10068 for edge in model.ordered_children_of(wire)? {
10069 if let Some(&i) = index.get(&EdgeKey::of(&edge)) {
10070 wire_of[i] = match wire_of[i] {
10071 None => Some(Some(w)),
10072 Some(_) => Some(None),
10073 };
10074 }
10075 }
10076 }
10077 let strand_wire: Vec<Option<usize>> = strands
10078 .iter()
10079 .map(|st| match st.tag {
10080 Tag::Boundary { edge, .. } if st.boundary => {
10081 wire_of.get(edge).copied().flatten().flatten()
10082 }
10083 _ => None,
10084 })
10085 .collect();
10086 let mut by_wire: Vec<Vec<usize>> = vec![Vec::new(); wires.len()];
10087 for (k, w) in strand_wire.iter().enumerate() {
10088 if let Some(w) = w {
10089 by_wire[*w].push(k);
10090 }
10091 }
10092 let boxed = |ks: &[usize]| -> (Point2, Point2) {
10093 let mut lo = Point2::new(f64::INFINITY, f64::INFINITY);
10094 let mut hi = Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
10095 for &k in ks {
10096 for p in &strands[k].polyline {
10097 lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
10098 hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
10099 }
10100 }
10101 (lo, hi)
10102 };
10103 let Some(outer) = by_wire.first().filter(|ks| !ks.is_empty()) else {
10104 return Ok(None);
10105 };
10106 let (outer_lo, outer_hi) = boxed(outer);
10107 let mut ends: Vec<(f64, f64, usize)> = Vec::with_capacity(strands.len() * 2);
10110 let mut loose: Vec<(Point2, Point2)> = Vec::new();
10111 for (k, st) in strands.iter().enumerate() {
10112 for p in [st.polyline.first(), st.polyline.last()]
10113 .into_iter()
10114 .flatten()
10115 {
10116 ends.push((p.x, p.y, k));
10117 }
10118 if strand_wire[k].is_none() {
10119 loose.push(boxed(&[k]));
10120 }
10121 }
10122 ends.sort_by(|a, b| a.0.total_cmp(&b.0));
10123 let margin = snap * 4.0;
10124 let mut lone = Vec::new();
10125 let mut taken = vec![false; strands.len()];
10126 for (w, ks) in by_wire.iter().enumerate().skip(1) {
10127 if ks.is_empty() {
10128 continue;
10129 }
10130 let (lo, hi) = boxed(ks);
10131 let (lo, hi) = (
10132 Point2::new(lo.x - margin, lo.y - margin),
10133 Point2::new(hi.x + margin, hi.y + margin),
10134 );
10135 if !(lo.x > outer_lo.x && lo.y > outer_lo.y && hi.x < outer_hi.x && hi.y < outer_hi.y) {
10136 continue;
10137 }
10138 let first = ends.partition_point(|e| e.0 < lo.x);
10139 let crowded = ends[first..]
10140 .iter()
10141 .take_while(|e| e.0 <= hi.x)
10142 .any(|&(_, y, k)| y >= lo.y && y <= hi.y && strand_wire[k] != Some(w));
10143 let crossed = loose
10144 .iter()
10145 .any(|(a, b)| a.x <= hi.x && b.x >= lo.x && a.y <= hi.y && b.y >= lo.y);
10146 if crowded || crossed {
10147 continue;
10148 }
10149 let mine: Vec<Strand<Tag>> = ks.iter().map(|&k| strands[k].clone()).collect();
10150 let Some(ring) = arrange::lone_ring(&mine, ks, snap) else {
10151 continue;
10152 };
10153 for &k in ks {
10154 taken[k] = true;
10155 }
10156 lone.push(ring);
10157 }
10158 if lone.is_empty() {
10159 return Ok(None);
10160 }
10161 let (rest, places): (Vec<Strand<Tag>>, Vec<usize>) = strands
10162 .iter()
10163 .enumerate()
10164 .filter(|(k, _)| !taken[*k])
10165 .map(|(k, st)| (st.clone(), k))
10166 .unzip();
10167 Ok(Some((rest, places, lone)))
10168}
10169
10170fn outward_normal(face: &GFace, at: Point2, tol: Tolerances) -> OgeomResult<ogeom_math::Vector> {
10173 let n = face.surface.normal_at(at.x, at.y, tol)?.vector();
10174 Ok(
10175 if face.face.orientation() == ogeom_topo::Orientation::Reversed {
10176 -n
10177 } else {
10178 n
10179 },
10180 )
10181}
10182
10183fn outline_snap(face: &GFace, tol: Tolerances) -> f64 {
10187 face.edges
10188 .iter()
10189 .fold(tol.confusion() * 1e2, |acc, e| acc.max(e.tolerance * 2.0))
10190 .max(
10191 if face.edges.iter().any(|e| e.tolerance > tol.confusion()) {
10192 tol.confusion() * 1e3
10193 } else {
10194 0.0
10195 },
10196 )
10197}
10198
10199struct EndsByX {
10202 order: Vec<usize>,
10204 xs: Vec<f64>,
10205}
10206
10207impl EndsByX {
10208 fn new(ends: &[(usize, bool, Point2)]) -> Self {
10209 let mut order: Vec<usize> = (0..ends.len()).collect();
10210 order.sort_by(|&a, &b| ends[a].2.x.total_cmp(&ends[b].2.x));
10211 let xs = order.iter().map(|&k| ends[k].2.x).collect();
10212 Self { order, xs }
10213 }
10214
10215 fn near(&self, p: Point2, reach: f64) -> Vec<usize> {
10218 let lo = self.xs.partition_point(|&x| x < p.x - reach);
10219 let hi = self.xs.partition_point(|&x| x <= p.x + reach);
10220 let mut out = self.order[lo..hi.max(lo)].to_vec();
10221 out.sort_unstable();
10222 out
10223 }
10224}
10225
10226fn bridge_outline_gaps(lines: &mut Vec<Vec<Point2>>) {
10237 let ends: Vec<(usize, bool, Point2)> = lines
10238 .iter()
10239 .enumerate()
10240 .filter(|(_, line)| line.len() >= 2)
10241 .flat_map(|(i, line)| [(i, false, line[0]), (i, true, line[line.len() - 1])])
10242 .collect();
10243 let by_x = EndsByX::new(&ends);
10248 let mut joins: Vec<Vec<Point2>> = Vec::new();
10249 for (k, &(i, end_i, p)) in ends.iter().enumerate() {
10250 for m in by_x.near(p, PARAM_SNAP) {
10251 let (j, end_j, q) = ends[m];
10252 let d = p.distance(q);
10253 if m > k && (j, end_j) != (i, end_i) && d > 0.0 && d <= PARAM_SNAP {
10254 joins.push(vec![p, q]);
10255 }
10256 }
10257 }
10258 let open: Vec<Point2> = ends
10259 .iter()
10260 .filter(|&&(i, end_i, p)| {
10261 !by_x.near(p, PARAM_SNAP).into_iter().any(|m| {
10262 let (j, end_j, q) = ends[m];
10263 (j, end_j) != (i, end_i) && p.distance(q) <= PARAM_SNAP
10264 })
10265 })
10266 .map(|&(_, _, p)| p)
10267 .collect();
10268 let (mut lo, mut hi) = (
10271 Point2::new(f64::INFINITY, f64::INFINITY),
10272 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
10273 );
10274 for p in lines.iter().flatten() {
10275 lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
10276 hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
10277 }
10278 let reach = lo.distance(hi) * 1e-3;
10279 let nearest = |k: usize| {
10280 (0..open.len()).filter(|&m| m != k).min_by(|&a, &b| {
10281 open[k]
10282 .distance(open[a])
10283 .total_cmp(&open[k].distance(open[b]))
10284 })
10285 };
10286 for k in 0..open.len() {
10287 if let Some(m) = nearest(k)
10288 && k < m
10289 && nearest(m) == Some(k)
10290 && open[k].distance(open[m]) <= reach
10291 {
10292 lines.push(vec![open[k], open[m]]);
10293 }
10294 }
10295 lines.extend(joins);
10296}
10297
10298fn weld_outline_ends(lines: &mut [Vec<Point2>], face: &GFace, tol: Tolerances) {
10318 let snap = outline_snap(face, tol);
10319 if snap <= 0.0 {
10320 return;
10321 }
10322 let owner: Vec<usize> = face
10324 .edges
10325 .iter()
10326 .enumerate()
10327 .flat_map(|(i, e)| std::iter::repeat_n(i, 1 + usize::from(e.other_side.is_some())))
10328 .collect();
10329 if owner.len() != lines.len() {
10330 return;
10331 }
10332 let lift = |p: Point2| face.surface.point_at(p.x, p.y, tol).ok();
10333 let mut within = snap;
10338 for line in lines.iter() {
10339 let points: Vec<Point> = line.iter().filter_map(|p| lift(*p)).collect();
10340 let length: f64 = points.windows(2).map(|w| w[0].distance(w[1])).sum();
10341 if points.len() == line.len() && length > tol.confusion() {
10342 within = within.min(length / 2.0);
10343 }
10344 }
10345 let ends: Vec<(usize, bool, Point2)> = lines
10348 .iter()
10349 .enumerate()
10350 .flat_map(|(i, line)| {
10351 let first = line.first().map(|p| (i, false, *p));
10352 let last = line.last().map(|p| (i, true, *p));
10353 first.into_iter().chain(last)
10354 })
10355 .collect();
10356 let lifted: Vec<Option<Point>> = ends.iter().map(|&(_, _, p)| lift(p)).collect();
10357 let by_x = EndsByX::new(&ends);
10362 let mut moves: Vec<(usize, bool, Point2)> = Vec::new();
10363 for (k, &(i, end_i, p)) in ends.iter().enumerate() {
10364 let Some(at) = lifted[k] else {
10365 continue;
10366 };
10367 let nearest = by_x
10368 .near(p, snap)
10369 .into_iter()
10370 .filter_map(|m| {
10371 let (j, end_j, q) = ends[m];
10372 let d = p.distance(q);
10373 if (j, end_j) >= (i, end_i) || owner[j] == owner[i] || d <= PARAM_SNAP || d > snap {
10378 return None;
10379 }
10380 (lifted[m]?.distance(at) <= within).then_some((d, q))
10381 })
10382 .min_by(|a, b| a.0.total_cmp(&b.0));
10383 if let Some((_, q)) = nearest {
10384 moves.push((i, end_i, q));
10385 }
10386 }
10387 for (i, end_i, q) in moves {
10388 let slot = if end_i {
10389 lines[i].last_mut()
10390 } else {
10391 lines[i].first_mut()
10392 };
10393 if let Some(p) = slot {
10394 *p = q;
10395 }
10396 }
10397}
10398
10399fn length_and_reach(curve: &Curve, tol: Tolerances) -> OgeomResult<(f64, f64)> {
10403 use ogeom_geom::Curve3d as _;
10404 let (lo, hi) = curve.domain();
10405 let mut length = 0.0_f64;
10406 let mut last: Option<Point> = None;
10407 let mut span = ogeom_math::Aabb::default();
10408 for k in 0..=64 {
10409 let t = if k == 64 {
10410 hi
10411 } else {
10412 lo + (hi - lo) * f64::from(k) / 64.0
10413 };
10414 let p = curve.point_at(t, tol)?;
10415 if let Some(q) = last {
10416 length += q.distance(p);
10417 }
10418 last = Some(p);
10419 span = span.with_point(p);
10420 }
10421 Ok((length, span.diagonal()))
10422}
10423
10424fn within_face(face: &GFace, p: Point, margin: f64, tol: Tolerances) -> OgeomResult<bool> {
10427 if chart_point_within(face, p, margin.max(tol.confusion() * 1e2), tol).is_some() {
10428 return Ok(true);
10429 }
10430 for e in &face.edges {
10431 if !e.bound.expanded(margin).contains(p) {
10432 continue;
10433 }
10434 let (drawn, drange, _) = e.drawn();
10435 if distance_to_edge_curve(drawn, drange, p, tol)? <= margin {
10436 return Ok(true);
10437 }
10438 }
10439 Ok(false)
10440}
10441
10442fn chart_point_within(face: &GFace, p: Point, reach: f64, tol: Tolerances) -> Option<Point2> {
10445 use ogeom_math::elementary;
10452 let raw = match &face.surface {
10453 SurfaceGeometry::Plane(x) => {
10454 let local = x.plane().frame().to_local(p);
10455 if local.z.abs() > reach {
10456 return None;
10457 }
10458 Point2::new(local.x, local.y)
10459 }
10460 SurfaceGeometry::Cylinder(x) => {
10461 let cylinder = x.cylinder();
10462 if cylinder.distance_to(p) > reach {
10463 return None;
10464 }
10465 let (u, v) = elementary::cylinder_parameters(&cylinder, p, tol).ok()?;
10466 Point2::new(u, v)
10467 }
10468 SurfaceGeometry::Cone(x) => {
10469 let cone = x.cone();
10470 if cone.distance_to(p) > reach {
10471 return None;
10472 }
10473 let (u, v) = elementary::cone_parameters(&cone, p, tol).ok()?;
10474 Point2::new(u, v)
10475 }
10476 SurfaceGeometry::Sphere(x) => {
10477 let sphere = x.sphere();
10478 if sphere.distance_to(p) > reach {
10479 return None;
10480 }
10481 let (u, v) = elementary::sphere_parameters(&sphere, p, tol).ok()?;
10482 Point2::new(u, v)
10483 }
10484 SurfaceGeometry::Torus(x) => {
10485 let torus = x.torus();
10486 if torus.distance_to(p) > reach {
10487 return None;
10488 }
10489 let (u, v) = elementary::torus_parameters(&torus, p, tol).ok()?;
10490 Point2::new(u, v)
10491 }
10492 other => {
10496 let foot = ogeom_algo::project_on_surface(other, p, 24, tol).ok()?;
10497 if foot.distance > reach {
10498 return None;
10499 }
10500 Point2::new(foot.parameters.0, foot.parameters.1)
10501 }
10502 };
10503 let at = fold_point_into_chart(raw, &face.surface, tol);
10504 let lines = face.outline(tol)?;
10505 let trim = face.outline_trim(tol)?;
10506 let mut shifts = vec![0.0];
10512 if face.surface.is_periodic_u() {
10513 let ((ua, ub), _) = face.surface.domain();
10514 if ub > ua {
10515 let period = ub - ua;
10516 let (sum, count) = lines
10517 .iter()
10518 .flatten()
10519 .fold((0.0_f64, 0_usize), |(s, n), q| (s + q.x, n + 1));
10520 #[allow(clippy::cast_precision_loss)]
10521 let centre = if count > 0 { sum / count as f64 } else { at.x };
10522 let home = ((centre - at.x) / period).round() * period;
10523 shifts = vec![home, home + period, home - period];
10524 }
10525 }
10526 for shift in shifts {
10527 let shifted = Point2::new(at.x + shift, at.y);
10528 if trim.inside(shifted) {
10529 return Some(shifted);
10530 }
10531 }
10532 None
10533}
10534
10535fn inside_beside_edge(face: &GFace, p: Point, tol: Tolerances) -> OgeomResult<Option<bool>> {
10543 let mut nearest: Option<(f64, usize, f64)> = None;
10544 for (k, e) in face.edges.iter().enumerate() {
10545 let (off, t) = nearest_on_edge_curve(&e.curve, e.crange, p, tol)?;
10546 if nearest.is_none_or(|n| off < n.0) {
10547 nearest = Some((off, k, t));
10548 }
10549 }
10550 let Some((off, k, t)) = nearest else {
10551 return Ok(None);
10552 };
10553 let e = &face.edges[k];
10554 let (lo, hi) = (e.crange.0.min(e.crange.1), e.crange.0.max(e.crange.1));
10555 let Some(forward) = e.walked else {
10556 return Ok(None);
10557 };
10558 if off <= e.tolerance.max(tol.confusion())
10559 || t <= lo + tol.parametric()
10560 || t >= hi - tol.parametric()
10561 {
10562 return Ok(None);
10563 }
10564 let at = e.pcurve.point_at(rescale(t, e.crange, e.prange), tol)?;
10565 let tangent = e.curve.d1_at(t, tol)?;
10566 let walked = if forward { tangent } else { -tangent };
10567 let left = outward_normal(face, at, tol)?.cross(walked);
10568 Ok(Some((p - e.curve.point_at(t, tol)?).dot(left) > 0.0))
10569}
10570
10571enum Thin {
10573 Wide,
10576 Undecided,
10578 Reads(PieceState),
10580 Disagree,
10582}
10583
10584fn thin_by_strands(
10603 face: &GFace,
10604 rings: &[Vec<Traversal<Tag>>],
10605 outlines: &[Vec<Point2>],
10606 contacts: &[ContactRec],
10607 theirs: &GSolid,
10608 tol: Tolerances,
10609) -> OgeomResult<Thin> {
10610 if !matches!(face.surface, SurfaceGeometry::Plane(_)) {
10611 return Ok(Thin::Wide);
10612 }
10613 let (mut area, mut perimeter) = (0.0, 0.0);
10616 for (k, ring) in outlines.iter().enumerate() {
10617 let n = ring.len();
10618 let mut twice = 0.0;
10619 for i in 0..n {
10620 let (a, b) = (ring[i], ring[(i + 1) % n]);
10621 twice += a.x * b.y - b.x * a.y;
10622 perimeter += (b - a).magnitude();
10623 }
10624 area += if k == 0 { twice.abs() } else { -twice.abs() } / 2.0;
10625 }
10626 if area > tol.confusion() * 1e4 * perimeter / 2.0 {
10627 return Ok(Thin::Wide);
10628 }
10629 let mut read: Option<PieceState> = None;
10630 let mut spoke = false;
10631 for traversal in rings.iter().flatten() {
10632 let (contact, range) = match &traversal.tag {
10633 Tag::Contact { contact, range } => (&contacts[*contact], *range),
10634 Tag::Section { .. } => return Ok(Thin::Undecided),
10635 Tag::Boundary { .. } | Tag::Pole { .. } => continue,
10636 };
10637 let Some(state) = beyond_edge(face, contact, range, traversal.reversed, theirs, tol)?
10638 else {
10639 return Ok(Thin::Undecided);
10640 };
10641 spoke = true;
10642 match read {
10643 Some(said) if said != state => return Ok(Thin::Disagree),
10644 _ => read = Some(state),
10645 }
10646 }
10647 Ok(match read {
10648 Some(state) if spoke => Thin::Reads(state),
10649 _ => Thin::Undecided,
10650 })
10651}
10652
10653fn beyond_edge(
10657 face: &GFace,
10658 contact: &ContactRec,
10659 range: (f64, f64),
10660 reversed: bool,
10661 theirs: &GSolid,
10662 tol: Tolerances,
10663) -> OgeomResult<Option<PieceState>> {
10664 let t = f64::midpoint(range.0, range.1);
10665 let p = contact.curve.point_at(t, tol)?;
10666 let along = contact.curve.d1_at(t, tol)?;
10667 let walked = if (range.1 < range.0) != reversed {
10668 -along
10669 } else {
10670 along
10671 };
10672 let uv = contact
10673 .pcurve
10674 .point_at(rescale(t, contact.crange, contact.prange), tol)?;
10675 let chart = face.surface.normal_at(uv.x, uv.y, tol)?.vector();
10676 let Ok(left) = chart.cross(walked).normalized(tol) else {
10677 return Ok(None);
10678 };
10679 let mut sides = Vec::with_capacity(2);
10682 for other in &theirs.faces {
10683 for e in other.edges.iter().filter(|e| e.node == contact.node) {
10684 let Some(forward) = e.walked else {
10685 return Ok(None);
10686 };
10687 let (_, te) = nearest_on_edge_curve(&e.curve, e.crange, p, tol)?;
10688 let at = e.pcurve.point_at(rescale(te, e.crange, e.prange), tol)?;
10689 let normal = outward_normal(other, at, tol)?;
10690 let tangent = e.curve.d1_at(te, tol)?;
10691 let tangent = if forward { tangent } else { -tangent };
10692 let Ok(into) = normal.cross(tangent).normalized(tol) else {
10693 return Ok(None);
10694 };
10695 let Ok(normal) = normal.normalized(tol) else {
10696 return Ok(None);
10697 };
10698 sides.push((into, normal));
10699 }
10700 }
10701 let [(into_a, normal_a), (into_b, normal_b)] = sides[..] else {
10702 return Ok(None);
10703 };
10704 let along_this = |into: ogeom_math::Vector| into.cross(left).magnitude() < TANGENT_TURN;
10707 let ((into_g, normal_g), into_h) = match (along_this(into_a), along_this(into_b)) {
10708 (true, false) => ((into_a, normal_a), into_b),
10709 (false, true) => ((into_b, normal_b), into_a),
10710 _ => return Ok(None),
10711 };
10712 if into_g.dot(left) > 0.0 {
10713 return Ok(None);
10714 }
10715 let turn = into_h.dot(normal_g);
10716 if turn.abs() < TANGENT_TURN {
10717 return Ok(None);
10718 }
10719 Ok(Some(if turn < 0.0 {
10720 PieceState::Out
10721 } else {
10722 PieceState::In
10723 }))
10724}
10725
10726pub(crate) const TANGENT_TURN: f64 = 2e-2;
10731
10732fn tangential(a: &Curve, ta: f64, b: &Curve, tb: f64, tol: Tolerances) -> OgeomResult<bool> {
10735 let da = a.d1_at(ta, tol)?;
10736 let db = b.d1_at(tb, tol)?;
10737 let (ma, mb) = (da.magnitude(), db.magnitude());
10738 if ma <= tol.confusion() || mb <= tol.confusion() {
10739 return Ok(true);
10740 }
10741 Ok(da.cross(db).magnitude() / (ma * mb) < TANGENT_TURN)
10742}
10743
10744fn fitted_image(
10749 curve: &Curve,
10750 surface: &SurfaceGeometry,
10751 budget: f64,
10752 tol: Tolerances,
10753) -> OgeomResult<Option<(PlanarCurve, f64)>> {
10754 use ogeom_geom::Surface as _;
10755 const SAMPLES: usize = 96;
10756 let (lo, hi) = curve.domain();
10757 if !hi.is_finite() || !lo.is_finite() || hi <= lo {
10758 return Ok(None);
10759 }
10760 let ((ua, ub), (va, vb)) = surface.domain();
10761 let periods = (
10762 if surface.is_periodic_u() {
10763 ub - ua
10764 } else {
10765 0.0
10766 },
10767 if surface.is_periodic_v() {
10768 vb - va
10769 } else {
10770 0.0
10771 },
10772 );
10773 let invert = |t: f64, guess: Option<(f64, f64)>| -> OgeomResult<Option<((f64, f64), f64)>> {
10779 let p = curve.point_at(t, tol)?;
10780 if let SurfaceGeometry::Plane(plane) = surface {
10785 let local = plane.plane().frame().to_local(p);
10786 let back = plane
10787 .plane()
10788 .frame()
10789 .to_world(ogeom_math::Point::new(local.x, local.y, 0.0));
10790 return Ok(Some(((local.x, local.y), back.distance(p))));
10791 }
10792 if let SurfaceGeometry::Sphere(ball) = surface {
10798 let frame = ball.sphere().frame();
10799 let local = frame.to_local(p);
10800 let flat = local.x.hypot(local.y);
10801 let half = core::f64::consts::FRAC_PI_2 - 1e-6;
10805 let v = local.z.atan2(flat).clamp(-half, half);
10806 let u = local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU);
10807 let back = surface.point_at(u, v, tol)?;
10808 if back.distance(p) <= budget.max(tol.confusion() * 1e2) {
10809 return Ok(Some(((u, v), back.distance(p))));
10810 }
10811 }
10812 if let SurfaceGeometry::Cone(cone) = surface
10817 && let Ok((u, v)) = ogeom_math::elementary::cone_parameters(&cone.cone(), p, tol)
10818 {
10819 let back = ogeom_math::elementary::cone_at(&cone.cone(), u, v).point;
10820 if back.distance(p) <= budget.max(tol.confusion() * 1e2) {
10821 return Ok(Some(((u, v), back.distance(p))));
10822 }
10823 }
10824 let mut foot = match guess {
10825 Some(g) => ogeom_algo::project_on_surface_from(surface, p, g, tol)?,
10826 None => ogeom_algo::project_on_surface(surface, p, 24, tol)?,
10827 };
10828 if foot.distance > budget.max(tol.confusion() * 1e2) && guess.is_some() {
10829 foot = ogeom_algo::project_on_surface(surface, p, 48, tol)?;
10830 }
10831 if foot.distance > budget.max(tol.confusion() * 1e2) {
10832 if *DEBUG_WIRE {
10833 eprintln!(
10834 "IMAGE FIT: a point sits {:.2e} off the surface; no image",
10835 foot.distance
10836 );
10837 }
10838 return Ok(None);
10839 }
10840 Ok(Some((foot.parameters, foot.distance)))
10841 };
10842 let unwrap = |uv: (f64, f64), before: Option<Point2>| -> Point2 {
10843 let mut uv = Point2::new(uv.0, uv.1);
10844 if let Some(prev) = before {
10845 for (coord, period, before) in [
10846 (&mut uv.x, periods.0, prev.x),
10847 (&mut uv.y, periods.1, prev.y),
10848 ] {
10849 if period > 0.0 {
10850 while *coord - before > period / 2.0 {
10851 *coord -= period;
10852 }
10853 while before - *coord > period / 2.0 {
10854 *coord += period;
10855 }
10856 }
10857 }
10858 }
10859 uv
10860 };
10861 let mut samples: Vec<(f64, Point2)> = Vec::with_capacity(SAMPLES + 1);
10862 let mut guess: Option<(f64, f64)> = None;
10863 for k in 0..=SAMPLES {
10864 #[allow(clippy::cast_precision_loss)]
10865 let t = lo + (hi - lo) * (k as f64) / (SAMPLES as f64);
10866 let Some((uv, _)) = invert(t, guess)? else {
10867 return Ok(None);
10868 };
10869 guess = Some(uv);
10870 let before = samples.last().map(|(_, p)| *p);
10871 samples.push((t, unwrap(uv, before)));
10872 }
10873 let step = {
10874 let (su, sv) = (
10875 if periods.0 > 0.0 { periods.0 } else { ub - ua },
10876 if periods.1 > 0.0 { periods.1 } else { vb - va },
10877 );
10878 su.min(sv) * 0.01
10879 };
10880 const CAP: usize = 4096;
10881 let mut refined = true;
10882 while refined && samples.len() < CAP {
10883 refined = false;
10884 let mut next: Vec<(f64, Point2)> = Vec::with_capacity(samples.len() * 2);
10885 for pair in samples.windows(2) {
10886 let ((t0, p0), (t1, p1)) = (pair[0], pair[1]);
10887 next.push((t0, p0));
10888 if p0.distance(p1) > step && t1 - t0 > (hi - lo) * 1e-7 && next.len() < CAP {
10889 let tm = f64::midpoint(t0, t1);
10890 let Some((uv, _)) = invert(tm, Some((p0.x, p0.y)))? else {
10891 return Ok(None);
10892 };
10893 next.push((tm, unwrap(uv, Some(p0))));
10894 refined = true;
10895 }
10896 }
10897 next.push(samples[samples.len() - 1]);
10898 samples = next;
10899 }
10900 let params: Vec<f64> = samples.iter().map(|(t, _)| *t).collect();
10901 let image: Vec<Point2> = samples.iter().map(|(_, p)| *p).collect();
10902 let fitted =
10903 ogeom_geom::fit::fit_points_2d_at(¶ms, &image, 3, tol.confusion() * 10.0, tol)?;
10904 let planar: PlanarCurve = fitted.curve.into();
10905 let mut off = 0.0_f64;
10913 let mut checks: Vec<f64> = Vec::with_capacity(params.len() * 2);
10914 for pair in params.windows(2) {
10915 checks.push(pair[0]);
10916 checks.push(f64::midpoint(pair[0], pair[1]));
10917 }
10918 if let Some(&t) = params.last() {
10919 checks.push(t);
10920 }
10921 let lift = |u: f64, v: f64| match surface {
10927 SurfaceGeometry::Cone(cone) => {
10928 Ok(ogeom_math::elementary::cone_at(&cone.cone(), u, v).point)
10929 }
10930 SurfaceGeometry::Cylinder(drum) => surface
10931 .point_at(u, v, tol)
10932 .or_else(|_| Ok(ogeom_math::elementary::cylinder_at(&drum.cylinder(), u, v).point)),
10933 _ => surface.point_at(u, v, tol),
10934 };
10935 for t in checks {
10936 let uv = planar.point_at(t, tol)?;
10937 let Ok(on_surface) = lift(uv.x, uv.y) else {
10938 if *DEBUG_WIRE {
10939 eprintln!("IMAGE FIT: the image leaves the chart at {uv:?}; no image");
10940 }
10941 return Ok(None);
10942 };
10943 off = off.max(on_surface.distance(curve.point_at(t, tol)?));
10944 }
10945 if *DEBUG_WIRE {
10946 eprintln!(
10947 "IMAGE FIT: image off the curve by {off:.2e} against {:.2e} (fit error {:.2e} met {})",
10948 budget.max(tol.confusion() * 1e3),
10949 fitted.error,
10950 fitted.met
10951 );
10952 }
10953 if off > budget.max(tol.confusion() * 1e3) {
10954 return Ok(None);
10955 }
10956 Ok(Some((planar, off)))
10957}
10958
10959fn distance_to_edge_curve(
10962 curve: &Curve,
10963 crange: (f64, f64),
10964 p: Point,
10965 tol: Tolerances,
10966) -> OgeomResult<f64> {
10967 Ok(nearest_on_edge_curve(curve, crange, p, tol)?.0)
10968}
10969
10970fn nearest_on_edge_curve(
10972 curve: &Curve,
10973 crange: (f64, f64),
10974 p: Point,
10975 tol: Tolerances,
10976) -> OgeomResult<(f64, f64)> {
10977 let scan = |lo: f64, hi: f64, steps: u32| -> OgeomResult<(f64, f64)> {
10981 let mut best = f64::INFINITY;
10982 let mut best_t = lo;
10983 let mut previous: Option<(f64, Point)> = None;
10984 for i in 0..=steps {
10985 let t = lo + (hi - lo) * f64::from(i) / f64::from(steps);
10986 let at = curve.point_at(t, tol)?;
10987 if let Some((t0, last)) = previous {
10988 let d = at - last;
10989 let len2 = d.dot(d);
10990 let s = if len2 > 0.0 {
10991 ((p - last).dot(d) / len2).clamp(0.0, 1.0)
10992 } else {
10993 0.0
10994 };
10995 let dist = p.distance(last + d * s);
10996 if dist < best {
10997 best = dist;
10998 best_t = (t - t0).mul_add(s, t0);
10999 }
11000 }
11001 previous = Some((t, at));
11002 }
11003 Ok((best, best_t))
11004 };
11005 let span = crange.1 - crange.0;
11006 let (_, t1) = scan(crange.0, crange.1, 48)?;
11007 let step = span / 48.0;
11008 let (_, t2) = scan((t1 - step).max(crange.0), (t1 + step).min(crange.1), 16)?;
11009 let fine = span / (48.0 * 8.0);
11010 scan((t2 - fine).max(crange.0), (t2 + fine).min(crange.1), 16)
11011}
11012
11013fn mark_covered_coincidences(
11028 ga: &GSolid,
11029 gb: &GSolid,
11030 pieces: &mut [FacePiece],
11031 tol: Tolerances,
11032) -> Vec<(usize, usize)> {
11033 let on = |state: PieceState| matches!(state, PieceState::OnAligned | PieceState::OnOpposed);
11034 let from_a: Vec<(usize, usize, PieceState)> = pieces
11036 .iter()
11037 .enumerate()
11038 .filter(|(_, p)| p.from_a && on(p.state))
11039 .map(|(i, p)| (i, p.face, p.state))
11040 .collect();
11041 if from_a.is_empty() {
11042 return Vec::new();
11043 }
11044 let mut pairs: Vec<(usize, usize)> = Vec::new();
11045 for (index, piece) in pieces.iter().enumerate() {
11046 if piece.from_a || !on(piece.state) {
11047 continue;
11048 }
11049 for &(other, face_a, state) in &from_a {
11050 if state != piece.state {
11051 continue;
11052 }
11053 let host = &ga.faces[face_a];
11054 let reach = partner_reach(&gb.faces[piece.face], host, tol);
11058 let Some(at) = chart_point_within(host, piece.probe, reach, tol) else {
11059 continue;
11060 };
11061 if inside_rings(&pieces[other].outlines, at) {
11069 pairs.push((other, index));
11070 break;
11071 }
11072 }
11073 }
11074 for &(_, index) in &pairs {
11075 if pieces[index].state == PieceState::OnAligned {
11076 pieces[index].covered = true;
11077 }
11078 }
11079 pairs
11080}
11081
11082static AUDIT_BOUNDS: std::sync::LazyLock<bool> =
11089 std::sync::LazyLock::new(|| std::env::var("OGEOM_BOOL_AUDIT_BOUNDS").is_ok());
11090static DEBUG_WIRE: std::sync::LazyLock<bool> =
11094 std::sync::LazyLock::new(|| std::env::var("OGEOM_DEBUG_WIRE").is_ok());
11095static DEBUG_STRANDS: std::sync::LazyLock<bool> =
11096 std::sync::LazyLock::new(|| std::env::var("OGEOM_DEBUG_STRANDS").is_ok());
11097thread_local! {
11098 static SETTLE_FROM_BOTH_SIDES: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
11104 static PROBE_AT_CENTRE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
11108 static ALONG_APART: std::cell::Cell<f64> = const { std::cell::Cell::new(0.0) };
11114 static SEW_ACROSS: std::cell::Cell<f64> = const { std::cell::Cell::new(0.0) };
11118 static AT_STATED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
11122 static FROM_CROSSINGS: std::cell::Cell<Crossings> = const { std::cell::Cell::new(Crossings::Off) };
11128 static MEMBRANE_WITHIN: std::cell::Cell<f64> = const { std::cell::Cell::new(f64::INFINITY) };
11135 static AT_RIMS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
11141 static RIM_CROSSED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
11144 static AS_STATED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
11149}
11150
11151fn settled(
11225 model: &mut Model,
11226 tol: Tolerances,
11227 mut attempt: impl FnMut(&mut Model, Tolerances) -> OgeomResult<Built>,
11228) -> OgeomResult<Built> {
11229 RIM_CROSSED.set(false);
11230 ALONG_APART.set(0.0);
11231 let refusal = match attempt(model, tol) {
11232 Ok(built) => return Ok(built),
11233 Err(e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_))) => {
11234 return Err(e);
11235 }
11236 Err(e) => e,
11237 };
11238 let apart = ALONG_APART.get();
11239 if let Some(built) = resettled(model, tol, &refusal, &mut attempt)? {
11240 return Ok(built);
11241 }
11242 if apart > 0.0
11243 && apart + tol.confusion() <= weld_limit(tol)
11244 && refusal.to_string().contains("did not close into a shell")
11245 {
11246 struct Plain;
11247 impl Drop for Plain {
11248 fn drop(&mut self) {
11249 SEW_ACROSS.set(0.0);
11250 }
11251 }
11252 SEW_ACROSS.set(apart + tol.confusion());
11253 let _plain = Plain;
11254 match attempt(model, tol) {
11255 Ok(built) => return Ok(built),
11256 Err(e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_))) => {
11257 return Err(e);
11258 }
11259 Err(_) => {}
11260 }
11261 }
11262 if refusal.to_string().contains("no coincident partner face") {
11263 struct Plain;
11264 impl Drop for Plain {
11265 fn drop(&mut self) {
11266 AS_STATED.set(false);
11267 }
11268 }
11269 AS_STATED.set(true);
11270 let _plain = Plain;
11271 match attempt(model, tol) {
11272 Ok(built) => return Ok(built),
11273 Err(e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_))) => {
11274 return Err(e);
11275 }
11276 Err(_) => {}
11277 }
11278 }
11279 Err(refusal)
11280}
11281
11282fn resettled(
11285 model: &mut Model,
11286 tol: Tolerances,
11287 refusal: &ogeom_core::OgeomError,
11288 attempt: &mut impl FnMut(&mut Model, Tolerances) -> OgeomResult<Built>,
11289) -> OgeomResult<Option<Built>> {
11290 if let Some(built) = settled_after(model, tol, refusal, attempt)? {
11291 return Ok(Some(built));
11292 }
11293 if !RIM_CROSSED.get() {
11294 return Ok(None);
11295 }
11296 struct Rims;
11297 impl Drop for Rims {
11298 fn drop(&mut self) {
11299 AT_RIMS.set(false);
11300 }
11301 }
11302 AT_RIMS.set(true);
11303 let _rims = Rims;
11304 let limit = weld_limit(tol);
11305 let at_rims = match attempt(model, tol) {
11306 Ok(built) if welded_within(model, &built.shape, limit)? => return Ok(Some(built)),
11307 Err(e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_))) => {
11308 return Err(e);
11309 }
11310 Ok(_) => return Ok(None),
11311 Err(e) => e,
11312 };
11313 if let Some(built) = settled_after(model, tol, &at_rims, attempt)?
11314 && welded_within(model, &built.shape, limit)?
11315 {
11316 return Ok(Some(built));
11317 }
11318 Ok(None)
11319}
11320
11321fn settled_after(
11325 model: &mut Model,
11326 tol: Tolerances,
11327 refusal: &ogeom_core::OgeomError,
11328 attempt: &mut impl FnMut(&mut Model, Tolerances) -> OgeomResult<Built>,
11329) -> OgeomResult<Option<Built>> {
11330 struct Reset;
11331 impl Drop for Reset {
11332 fn drop(&mut self) {
11333 SETTLE_FROM_BOTH_SIDES.set(false);
11334 }
11335 }
11336 SETTLE_FROM_BOTH_SIDES.set(true);
11337 let _reset = Reset;
11338 if let Some(built) = retried(model, tol, refusal, attempt)? {
11339 return Ok(Some(built));
11340 }
11341 if refusal.to_string().contains("no coincident partner face") {
11342 struct Plain;
11343 impl Drop for Plain {
11344 fn drop(&mut self) {
11345 FROM_CROSSINGS.set(Crossings::Off);
11346 }
11347 }
11348 let _plain = Plain;
11349 FROM_CROSSINGS.set(Crossings::Lines);
11350 if let Some(built) = retried(model, tol, refusal, attempt)? {
11351 return Ok(Some(built));
11352 }
11353 FROM_CROSSINGS.set(Crossings::Curves);
11354 if let Some(built) = retried(model, tol, refusal, attempt)?
11355 && welded_within(model, &built.shape, weld_limit(tol))?
11356 {
11357 return Ok(Some(built));
11358 }
11359 }
11360 Ok(None)
11361}
11362
11363fn retried(
11366 model: &mut Model,
11367 tol: Tolerances,
11368 refusal: &ogeom_core::OgeomError,
11369 attempt: &mut impl FnMut(&mut Model, Tolerances) -> OgeomResult<Built>,
11370) -> OgeomResult<Option<Built>> {
11371 if refusal.to_string().contains("no coincident partner face") {
11372 match attempt(model, tol) {
11373 Ok(built) => return Ok(Some(built)),
11374 Err(ogeom_core::OgeomError::Cancelled) => {
11375 return Err(ogeom_core::OgeomError::Cancelled);
11376 }
11377 Err(_) => {}
11378 }
11379 }
11380 if refusal.to_string().contains("did not close into a shell") {
11381 struct Clear;
11382 impl Drop for Clear {
11383 fn drop(&mut self) {
11384 PROBE_AT_CENTRE.set(false);
11385 }
11386 }
11387 PROBE_AT_CENTRE.set(true);
11388 let _clear = Clear;
11389 match attempt(model, tol) {
11390 Ok(built) => return Ok(Some(built)),
11391 Err(ogeom_core::OgeomError::Cancelled) => {
11392 return Err(ogeom_core::OgeomError::Cancelled);
11393 }
11394 Err(_) => {}
11395 }
11396 }
11397 let limit = weld_limit(tol);
11398 struct Restore(f64);
11399 impl Drop for Restore {
11400 fn drop(&mut self) {
11401 MEMBRANE_WITHIN.set(self.0);
11402 }
11403 }
11404 let restore = Restore(MEMBRANE_WITHIN.replace(limit.min(MEMBRANE_WITHIN.get())));
11405 for widen in [10.0, 100.0] {
11406 let Ok(wider) = Tolerances::with_scale(tol.scale() / widen) else {
11407 continue;
11408 };
11409 match attempt(model, wider) {
11410 Ok(built) if welded_within(model, &built.shape, limit)? => return Ok(Some(built)),
11411 Err(ogeom_core::OgeomError::Cancelled) => {
11412 return Err(ogeom_core::OgeomError::Cancelled);
11413 }
11414 _ => {}
11415 }
11416 }
11417 drop(restore);
11418 {
11419 struct Off;
11420 impl Drop for Off {
11421 fn drop(&mut self) {
11422 AT_STATED.set(false);
11423 }
11424 }
11425 AT_STATED.set(true);
11426 let _off = Off;
11427 #[cfg(feature = "test-hooks")]
11428 work::add(|w| w.stated_retries += 1);
11429 match attempt(model, tol) {
11430 Ok(built) => return Ok(Some(built)),
11431 Err(ogeom_core::OgeomError::Cancelled) => {
11432 return Err(ogeom_core::OgeomError::Cancelled);
11433 }
11434 Err(_) => {}
11435 }
11436 }
11437 Ok(None)
11438}
11439
11440fn weld_limit(tol: Tolerances) -> f64 {
11443 (tol.confusion() * 1e2 + tol.confusion() * 10.0) * (1.0 + 1e-9)
11444}
11445
11446fn welded_within(model: &Model, shape: &Shape, weld: f64) -> OgeomResult<bool> {
11449 for kind in [ShapeType::Edge, ShapeType::Vertex] {
11450 for sub in explore_unique(model, shape, kind)? {
11451 if model.tolerance_of(&sub)?.is_some_and(|t| t.get() > weld) {
11452 return Ok(false);
11453 }
11454 }
11455 }
11456 Ok(true)
11457}
11458
11459static ARRANGE_DEBUG: std::sync::LazyLock<bool> =
11460 std::sync::LazyLock::new(|| std::env::var("OGEOM_ARRANGE_DEBUG").is_ok());
11461
11462fn audit_fill_equivalence(
11472 filtered: (&[SectionRec], &[SectionPiece]),
11473 unfiltered: (&[SectionRec], &[SectionPiece]),
11474 tol: Tolerances,
11475) {
11476 type Key = (usize, usize, [i64; 3]);
11477 let key_of = |section: &SectionRec, tol: Tolerances| -> Key {
11478 let (lo, hi) = section.curve.domain();
11479 let p = section
11480 .curve
11481 .point_at(f64::midpoint(lo, hi), tol)
11482 .unwrap_or(ogeom_math::Point::ORIGIN);
11483 let grid = (tol.confusion() * 100.0).max(1e-9);
11484 #[allow(clippy::cast_possible_truncation)]
11485 let q = |x: f64| (x / grid).round() as i64;
11486 (section.face_a, section.face_b, [q(p.x), q(p.y), q(p.z)])
11487 };
11488 let merged = |sections: &[SectionRec], pieces: &[SectionPiece], tol: Tolerances| {
11489 let mut kept: ogeom_core::FastMap<Key, Vec<(f64, f64)>> = ogeom_core::FastMap::default();
11490 for piece in pieces {
11491 kept.entry(key_of(§ions[piece.section], tol))
11492 .or_default()
11493 .push(piece.range);
11494 }
11495 let slop = tol.parametric() * 10.0;
11496 for ranges in kept.values_mut() {
11497 ranges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
11498 let mut out: Vec<(f64, f64)> = Vec::with_capacity(ranges.len());
11499 for &(lo, hi) in ranges.iter() {
11500 match out.last_mut() {
11501 Some(last) if lo <= last.1 + slop => last.1 = last.1.max(hi),
11502 _ => out.push((lo, hi)),
11503 }
11504 }
11505 *ranges = out;
11506 }
11507 kept
11508 };
11509 let a = merged(filtered.0, filtered.1, tol);
11510 let b = merged(unfiltered.0, unfiltered.1, tol);
11511 let slop = tol.parametric() * 20.0;
11512 let matches = |x: &Vec<(f64, f64)>, y: &Vec<(f64, f64)>| {
11513 x.len() == y.len()
11514 && x.iter()
11515 .zip(y)
11516 .all(|(p, q)| (p.0 - q.0).abs() <= slop && (p.1 - q.1).abs() <= slop)
11517 };
11518 for (key, ranges) in &b {
11519 let held = a.get(key);
11520 assert!(
11521 held.is_some_and(|r| matches(r, ranges)),
11522 "strict bound-filter audit: the unfiltered fill keeps {ranges:?} \
11523 on section {key:?}, the filtered fill keeps {held:?}; the \
11524 filter drops material"
11525 );
11526 }
11527 for (key, ranges) in &a {
11528 assert!(
11529 b.contains_key(key),
11530 "strict bound-filter audit: the filtered fill keeps {ranges:?} \
11531 on section {key:?} the unfiltered fill never made"
11532 );
11533 }
11534}
11535
11536fn general_fuse(
11537 model: &Model,
11538 a: &Shape,
11539 b: &Shape,
11540 aside: Option<&[aside::Aside; 2]>,
11541 tol: Tolerances,
11542) -> OgeomResult<GeneralFused> {
11543 general_fuse_as(model, a, b, Operands::Solids, aside, tol)
11544}
11545
11546fn general_fuse_classified(
11560 model: &Model,
11561 a: &Shape,
11562 b: &Shape,
11563 aside: Option<&[aside::Aside; 2]>,
11564 tol: Tolerances,
11565) -> OgeomResult<GeneralFused> {
11566 let fused = general_fuse(model, a, b, aside, tol)?;
11567 #[cfg(feature = "test-hooks")]
11568 work::add(|w| w.pieces += fused.pieces.len());
11569 if !fused.sections.is_empty()
11570 && fused.pieces.iter().all(|p| p.state == PieceState::Out)
11571 && !apart(model, a, b, tol)?
11572 {
11573 ogeom_bail!(
11574 NotDone,
11575 "the boundaries cross yet every piece of both reads outside the other, \
11576 though the solids overlap; a section was not arranged into the faces it \
11577 splits"
11578 );
11579 }
11580 Ok(fused)
11581}
11582
11583fn neighbouring_faces(
11586 model: &Model,
11587 solid: &GSolid,
11588) -> OgeomResult<Vec<ogeom_core::FastSet<usize>>> {
11589 let mut by_vertex: ogeom_core::FastMap<EdgeKey, Vec<usize>> = ogeom_core::FastMap::default();
11592 for (index, face) in solid.faces.iter().enumerate() {
11593 for vertex in explore_unique(model, &face.face, ShapeType::Vertex)? {
11594 by_vertex
11595 .entry(EdgeKey::of(&vertex))
11596 .or_default()
11597 .push(index);
11598 }
11599 }
11600 let mut neighbours: Vec<ogeom_core::FastSet<usize>> = (0..solid.faces.len())
11601 .map(|index| std::iter::once(index).collect())
11602 .collect();
11603 for faces in by_vertex.values() {
11604 for &i in faces {
11605 neighbours[i].extend(faces.iter().copied());
11606 }
11607 }
11608 Ok(neighbours)
11609}
11610
11611fn general_fuse_as(
11615 model: &Model,
11616 a: &Shape,
11617 b: &Shape,
11618 operands: Operands,
11619 aside: Option<&[aside::Aside; 2]>,
11620 tol: Tolerances,
11621) -> OgeomResult<GeneralFused> {
11622 let settle = SETTLE_FROM_BOTH_SIDES.get();
11625 let read_at_centre = PROBE_AT_CENTRE.get();
11626 let read_thin = AT_STATED.get();
11627 ogeom_core::progress::stage("boolean: gather");
11628 let mut ga = gather(
11629 model,
11630 a,
11631 matches!(operands, Operands::SheetBySolid | Operands::SheetBySheet),
11632 aside.map(|aside| &aside[0]),
11633 tol,
11634 )?;
11635 let mut gb = gather(
11636 model,
11637 b,
11638 operands == Operands::SheetBySheet,
11639 aside.map(|aside| &aside[1]),
11640 tol,
11641 )?;
11642 let (onto_a, onto_b) = (
11643 drums_onto_planes(&ga, &gb, tol),
11644 drums_onto_planes(&gb, &ga, tol),
11645 );
11646 move_drums(&mut ga, &onto_a, tol)?;
11647 move_drums(&mut gb, &onto_b, tol)?;
11648 #[cfg(feature = "test-hooks")]
11649 work::add(|w| w.faces_gathered += ga.faces.len() + gb.faces.len());
11650 let neighbours = if operands == Operands::Itself {
11651 Some(neighbouring_faces(model, &ga)?)
11652 } else {
11653 None
11654 };
11655 ogeom_core::progress::stage("boolean: intersect");
11656 let (
11657 sections,
11658 section_pieces,
11659 contacts,
11660 tangents,
11661 contact_along,
11662 mut paves,
11663 same_a,
11664 same_b,
11665 hugs,
11666 ) = fill(&ga, &gb, false, neighbours.as_deref(), tol)?;
11667 if operands == Operands::SheetBySheet && same_a.iter().any(|partners| !partners.is_empty()) {
11668 ogeom_bail!(
11669 NotDone,
11670 "the sheets lie on one surface where they overlap; a sheet split \
11671 by another lying along it is not resolved"
11672 );
11673 }
11674 for section in §ions {
11675 for (pcurve, face) in [
11676 (§ion.pc_a, &ga.faces[section.face_a]),
11677 (§ion.pc_b, &gb.faces[section.face_b]),
11678 ] {
11679 past_a_pole(section, pcurve, face, tol)?;
11680 }
11681 }
11682 let along = crossed_along(&contacts);
11683 let mut junctions = pave_junctions(&ga, &gb, &paves, &along, tol)?;
11684 if *DEBUG_WIRE {
11685 for j in &junctions {
11686 eprintln!("JUNCTION from paves at {:?} reach {:.3e}", j.at, j.reach);
11687 }
11688 for j in &hugs {
11689 eprintln!("JUNCTION from hugs at {:?} reach {:.3e}", j.at, j.reach);
11690 }
11691 }
11692 junctions.extend(hugs);
11693 let mut kept_bins = bins::Bins::new(tol.confusion() * 1e2);
11696 let kept_points: Vec<Point> = ga
11697 .shared_vertices
11698 .iter()
11699 .chain(&gb.shared_vertices)
11700 .map(|(at, _)| *at)
11701 .collect();
11702 for (index, at) in kept_points.iter().enumerate() {
11703 kept_bins.insert(*at, index);
11704 }
11705 let mut near_kept = Vec::new();
11706 for face in ga.faces.iter().chain(gb.faces.iter()) {
11707 for e in &face.edges {
11713 for (at, radius) in e.ends {
11714 let mut kept = || {
11715 !kept_points.is_empty()
11716 && if kept_bins.near_into(at, radius, &mut near_kept) {
11717 near_kept
11718 .iter()
11719 .any(|&i| kept_points[i].distance(at) <= radius)
11720 } else {
11721 kept_points.iter().any(|p| p.distance(at) <= radius)
11722 }
11723 };
11724 if radius > tol.confusion() * 10.0 && !kept() {
11725 if *DEBUG_WIRE {
11726 eprintln!("JUNCTION from vertex at {at:?} radius {radius:.3e}");
11727 }
11728 junctions.push(Junction {
11729 at,
11730 reach: radius,
11731 onto_vertex: false,
11732 });
11733 }
11734 }
11735 }
11736 }
11737 junctions = merge_junctions(junctions);
11745 for (from_a, gathered) in [(true, &ga), (false, &gb)] {
11746 seams_through_junctions(
11747 gathered,
11748 from_a,
11749 (§ions, §ion_pieces),
11750 &junctions,
11751 &mut paves,
11752 tol,
11753 )?;
11754 }
11755 if *DEBUG_WIRE {
11756 for j in &junctions {
11757 eprintln!("JUNCTION at {:?} reach {:.3e}", j.at, j.reach);
11758 }
11759 let kind = |sf: &SurfaceGeometry| match sf {
11760 SurfaceGeometry::Plane(_) => "plane",
11761 SurfaceGeometry::Cylinder(_) => "cyl",
11762 SurfaceGeometry::BSpline(_) => "bspline",
11763 _ => "other",
11764 };
11765 for (si, sec) in sections.iter().enumerate() {
11766 let fa = &ga.faces[sec.face_a];
11767 let fb = &gb.faces[sec.face_b];
11768 let d = sec.curve.domain();
11769 let mut worst_a: f64 = 0.0;
11770 let mut worst_b: f64 = 0.0;
11771 let mut len = 0.0;
11772 let mut prev: Option<Point> = None;
11773 for i in 0..=32 {
11774 let t = if i == 32 {
11775 d.1
11776 } else {
11777 d.0 + (d.1 - d.0) * f64::from(i) / 32.0
11778 };
11779 let at = sec.curve.point_at(t, tol)?;
11780 if let Some(p) = prev {
11781 len += p.distance(at);
11782 }
11783 prev = Some(at);
11784 let near = |f: &GFace| -> OgeomResult<f64> {
11785 let mut best = f64::INFINITY;
11786 for e in &f.edges {
11787 best = best.min(distance_to_edge_curve(&e.curve, e.crange, at, tol)?);
11788 }
11789 Ok(best)
11790 };
11791 worst_a = worst_a.max(near(fa)?);
11792 worst_b = worst_b.max(near(fb)?);
11793 }
11794 let pieces = section_pieces.iter().filter(|p| p.section == si).count();
11795 eprintln!(
11796 "SECTION s{si} a{}({}) x b{}({}) tol {:.2e} closed {} len {:.4} pieces {pieces} hug-dist a {:.3e} b {:.3e}",
11797 sec.face_a,
11798 kind(&fa.surface),
11799 sec.face_b,
11800 kind(&fb.surface),
11801 sec.tolerance,
11802 sec.closed,
11803 len,
11804 worst_a,
11805 worst_b
11806 );
11807 }
11808 eprintln!("TANGENTS {} CONTACTS {}", tangents.len(), contacts.len());
11809 }
11810 if *AUDIT_BOUNDS {
11816 match fill(&ga, &gb, true, neighbours.as_deref(), tol) {
11820 Ok((audit_sections, audit_pieces, ..)) => audit_fill_equivalence(
11821 (§ions, §ion_pieces),
11822 (&audit_sections, &audit_pieces),
11823 tol,
11824 ),
11825 Err(e @ ogeom_core::OgeomError::Cancelled) => return Err(e),
11826 Err(e) => eprintln!("bound-filter audit skipped: the unfiltered fill refused: {e}"),
11827 }
11828 }
11829
11830 ogeom_core::progress::stage("boolean: split");
11831 let mut pieces: Vec<FacePiece> = Vec::new();
11832 let chart_length =
11840 |line: &[Point2]| -> f64 { line.windows(2).map(|w| w[0].distance(w[1])).sum::<f64>() };
11841 let mut edge_vertices: ogeom_core::FastMap<EdgeKey, [(Point, f64); 2]> =
11845 ogeom_core::FastMap::default();
11846 for e in ga
11847 .faces
11848 .iter()
11849 .chain(gb.faces.iter())
11850 .flat_map(|f| f.edges.iter())
11851 {
11852 edge_vertices.entry(e.node).or_insert([
11853 (e.vertices[0], e.ends[0].1 + e.tolerance),
11854 (e.vertices[1], e.ends[1].1 + e.tolerance),
11855 ]);
11856 }
11857 let strands_of = |from_a: bool,
11858 fi: usize,
11859 face: &GFace|
11860 -> OgeomResult<(Vec<Strand<Tag>>, f64, Vec<Junction>)> {
11861 let mut strands: Vec<Strand<Tag>> = Vec::new();
11862 let mut gathered: Vec<(Point, Point2, f64)> = Vec::new();
11871 for (ei, e) in face.edges.iter().enumerate() {
11872 let mut stops = vec![e.crange.0];
11873 if let Some(ts) = paves.get(&e.node) {
11874 let off = |face, p| off_face(&ga, &gb, face, p, tol);
11877 let along = along.get(&e.node).map_or(&[][..], Vec::as_slice);
11878 for c in cluster_paves(&e.curve, e.crange, e.tolerance, ts, Some(&off), along, tol)?
11879 {
11880 if c.members > 1 {
11881 let chart = e.pcurve.point_at(rescale(c.t, e.crange, e.prange), tol)?;
11882 gathered.push((c.at, chart, c.span + c.honesty));
11883 }
11884 stops.push(c.t);
11885 }
11886 }
11887 stops.push(e.crange.1);
11888 let closed_edge = e
11891 .curve
11892 .point_at(e.crange.0, tol)?
11893 .distance(e.curve.point_at(e.crange.1, tol)?)
11894 <= tol.confusion();
11895 if closed_edge && stops.len() == 2 {
11896 stops.insert(1, f64::midpoint(e.crange.0, e.crange.1));
11897 }
11898 for pair in stops.windows(2) {
11899 let sub = (pair[0], pair[1]);
11900 if sub.1 - sub.0 <= tol.parametric() {
11901 continue;
11902 }
11903 strands.push(Strand {
11904 polyline: pcurve_polyline(
11905 &e.pcurve,
11906 e.prange,
11907 e.crange,
11908 sub,
11909 &face.surface,
11910 tol,
11911 )?,
11912 tag: Tag::Boundary {
11913 edge: ei,
11914 range: sub,
11915 },
11916 boundary: true,
11917 sag: f64::INFINITY,
11918 });
11919 if let Some((other_pc, orange)) = &e.other_side {
11920 strands.push(Strand {
11921 polyline: pcurve_polyline(
11922 other_pc,
11923 *orange,
11924 e.crange,
11925 sub,
11926 &face.surface,
11927 tol,
11928 )?,
11929 tag: Tag::Boundary {
11930 edge: ei,
11931 range: sub,
11932 },
11933 boundary: true,
11934 sag: f64::INFINITY,
11935 });
11936 }
11937 }
11938 }
11939 let pole_lines = face
11945 .poles
11946 .iter()
11947 .map(|pole| {
11948 pcurve_polyline(
11949 &pole.pcurve,
11950 pole.prange,
11951 pole.prange,
11952 pole.prange,
11953 &face.surface,
11954 tol,
11955 )
11956 })
11957 .collect::<OgeomResult<Vec<_>>>()?;
11958 let mut section_trim: Option<arrange::Trim> = None;
11959 for sp in §ion_pieces {
11960 let section = §ions[sp.section];
11961 let (belongs, pcurve) = if from_a {
11962 (section.face_a == fi && !sp.hugs[0], §ion.pc_a)
11963 } else {
11964 (section.face_b == fi && !sp.hugs[1], §ion.pc_b)
11965 };
11966 if !belongs {
11967 continue;
11968 }
11969 let domain = section.curve.domain();
11970 let sub = if section.closed {
11971 (
11972 fold(sp.range.0, domain),
11973 sp.range.1 - sp.range.0 + fold(sp.range.0, domain),
11974 )
11975 } else {
11976 sp.range
11977 };
11978 let fold_at = |t: f64| if section.closed { fold(t, domain) } else { t };
11981 let (count, sag) = if !matches!(face.surface, SurfaceGeometry::Plane(_)) {
11984 (32, f64::INFINITY)
11985 } else if matches!(pcurve, PlanarCurve::Line(_)) {
11986 (32, 0.0)
11987 } else {
11988 match circle_sag_count(pcurve, sub.0, sub.1, 32) {
11989 Some(count) => (
11990 count,
11991 circle_sag(pcurve, sub.0, sub.1, count).unwrap_or(f64::INFINITY),
11992 ),
11993 None => sag_count(&|t| pcurve.point_at(fold_at(t), tol), sub.0, sub.1, 32)?,
11994 }
11995 };
11996 let mut line = Vec::with_capacity(count + 1);
11997 for i in 0..=count {
11998 #[allow(clippy::cast_precision_loss)]
11999 let t = sub.0 + (sub.1 - sub.0) * i as f64 / count as f64;
12000 let tf = if section.closed { fold(t, domain) } else { t };
12001 line.push(pcurve.point_at(tf, tol)?);
12002 }
12003 unwrap_polyline(&mut line, &face.surface, tol);
12007 let trim = section_trim.get_or_insert_with(|| {
12008 arrange::Trim::new(
12009 strands
12010 .iter()
12011 .filter(|st| st.boundary)
12012 .map(|st| st.polyline.clone())
12013 .chain(pole_lines.iter().cloned())
12014 .collect(),
12015 )
12016 });
12017 fold_line_inside(&mut line, &face.surface, trim, tol);
12018 strands.push(Strand {
12019 polyline: line,
12020 tag: Tag::Section {
12021 section: sp.section,
12022 range: sp.range,
12023 },
12024 boundary: false,
12025 sag,
12026 });
12027 }
12028 for strand in &mut strands {
12039 let Tag::Section { section, .. } = strand.tag else {
12040 continue;
12041 };
12042 let s = §ions[section];
12043 let reach = tol.confusion().max(s.tolerance * 2.0);
12044 let window = (reach + honest(s.tolerance, tol)).max(tol.confusion() * 1e2);
12045 let last = strand.polyline.len() - 1;
12046 for end in [0, last] {
12047 let at = strand.polyline[end];
12048 let Ok(q) = face.surface.point_at(at.x, at.y, tol) else {
12049 continue;
12050 };
12051 for pole in &face.poles {
12052 let PlanarCurve::Line(line) = &pole.pcurve else {
12053 continue;
12054 };
12055 if q.distance(pole.point) > window {
12056 continue;
12057 }
12058 let axis = line.axis();
12059 let along = (at - axis.location)
12060 .dot(axis.direction.vector())
12061 .clamp(pole.prange.0, pole.prange.1);
12062 strand.polyline[end] = axis.point_at(along);
12063 }
12064 }
12065 }
12066 for (pi, pole) in face.poles.iter().enumerate() {
12071 let mut stops = vec![pole.prange.0, pole.prange.1];
12072 if let PlanarCurve::Line(line) = &pole.pcurve {
12073 let axis = line.axis();
12074 for strand in &strands {
12075 if strand.boundary {
12076 continue;
12077 }
12078 for end in [strand.polyline.first(), strand.polyline.last()]
12079 .into_iter()
12080 .flatten()
12081 {
12082 let along = (*end - axis.location).dot(axis.direction.vector());
12083 let foot = axis.point_at(along);
12084 if foot.distance(*end) <= PARAM_SNAP
12085 && along > pole.prange.0 + PARAM_SNAP
12086 && along < pole.prange.1 - PARAM_SNAP
12087 {
12088 stops.push(along);
12089 }
12090 }
12091 }
12092 }
12093 stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
12094 stops.dedup_by(|a, b| (*a - *b).abs() <= PARAM_SNAP);
12095 for pair in stops.windows(2) {
12096 let sub = (pair[0], pair[1]);
12097 strands.push(Strand {
12098 polyline: pcurve_polyline(
12099 &pole.pcurve,
12100 pole.prange,
12101 pole.prange,
12102 sub,
12103 &face.surface,
12104 tol,
12105 )?,
12106 tag: Tag::Pole {
12107 pole: pi,
12108 range: sub,
12109 },
12110 boundary: true,
12111 sag: f64::INFINITY,
12112 });
12113 }
12114 }
12115 let mut contact_trim: Option<arrange::Trim> = None;
12116 for (ci, contact) in contacts.iter().enumerate() {
12117 if contact.target_from_a != from_a || contact.target_face != fi {
12118 continue;
12119 }
12120 let mut stops = vec![contact.crange.0];
12124 if let Some(ts) = paves.get(&contact.node) {
12125 stops.extend(
12126 cluster_paves(
12127 &contact.curve,
12128 contact.crange,
12129 contact.tolerance,
12130 ts,
12131 None,
12132 &[],
12133 tol,
12134 )?
12135 .iter()
12136 .map(|c| c.t),
12137 );
12138 }
12139 stops.push(contact.crange.1);
12140 let closed_contact = contact
12141 .curve
12142 .point_at(contact.crange.0, tol)?
12143 .distance(contact.curve.point_at(contact.crange.1, tol)?)
12144 <= tol.confusion();
12145 if closed_contact && stops.len() == 2 {
12146 stops.insert(1, f64::midpoint(contact.crange.0, contact.crange.1));
12147 }
12148 for pair in stops.windows(2) {
12149 let sub = (pair[0], pair[1]);
12150 if sub.1 - sub.0 <= tol.parametric() {
12151 continue;
12152 }
12153 let mid_t = f64::midpoint(sub.0, sub.1);
12154 if contact_along[ci]
12155 .iter()
12156 .any(|(lo, hi)| mid_t >= *lo && mid_t <= *hi)
12157 {
12158 if *DEBUG_STRANDS {
12160 eprintln!("CONTACT c{ci} {sub:?} on fi={fi}: along boundary, skipped");
12161 }
12162 continue;
12163 }
12164 let mut line = pcurve_polyline(
12167 &contact.pcurve,
12168 contact.prange,
12169 contact.crange,
12170 sub,
12171 &face.surface,
12172 tol,
12173 )?;
12174 unwrap_polyline(&mut line, &face.surface, tol);
12175 let trim = contact_trim.get_or_insert_with(|| {
12176 arrange::Trim::new(
12177 strands
12178 .iter()
12179 .filter(|st| st.boundary)
12180 .map(|st| st.polyline.clone())
12181 .collect(),
12182 )
12183 });
12184 fold_line_inside(&mut line, &face.surface, trim, tol);
12185 let mid = interior_of(&line);
12186 let beside = if read_thin {
12191 inside_beside_edge(face, contact.curve.point_at(mid_t, tol)?, tol)?
12192 } else {
12193 None
12194 };
12195 if !beside.unwrap_or_else(|| trim.inside_slanted(mid)) {
12196 if *DEBUG_STRANDS {
12197 eprintln!(
12198 "CONTACT c{ci} {sub:?} on fi={fi}: outside the trim at {mid:?}, skipped"
12199 );
12200 }
12201 continue;
12202 }
12203 strands.push(Strand {
12204 polyline: line,
12205 tag: Tag::Contact {
12206 contact: ci,
12207 range: sub,
12208 },
12209 boundary: false,
12210 sag: f64::INFINITY,
12211 });
12212 }
12213 }
12214
12215 let mut spread = 0.0_f64;
12236 let mut spread_at: Vec<[f64; 2]> = vec![[0.0; 2]; strands.len()];
12237 for (si, strand) in strands
12238 .iter_mut()
12239 .enumerate()
12240 .filter(|(_, st)| !st.boundary)
12241 {
12242 let exact = matches!(strand.tag, Tag::Section { section, .. }
12243 if sections.get(section).is_some_and(|s| s.tolerance <= 0.0));
12244 let last = strand.polyline.len().saturating_sub(1);
12245 for (side, end) in [0, last].into_iter().enumerate() {
12246 let Some(&at) = strand.polyline.get(end) else {
12247 continue;
12248 };
12249 let Ok(q) = face.surface.point_at(at.x, at.y, tol) else {
12250 continue;
12251 };
12252 let landed = gathered
12253 .iter()
12254 .filter(|(junction, _, reach)| junction.distance(q) <= *reach)
12255 .min_by(|x, y| x.0.distance(q).total_cmp(&y.0.distance(q)));
12256 match landed {
12257 Some(&(_, chart, _)) if exact => {
12260 strand.polyline[end] = period_shifts(&face.surface, tol)
12261 .into_iter()
12262 .map(|(du, dv)| Point2::new(chart.x + du, chart.y + dv))
12263 .min_by(|p, r| p.distance(at).total_cmp(&r.distance(at)))
12264 .unwrap_or(chart);
12265 }
12266 Some(&(_, _, reach)) => {
12267 spread = spread.max(reach);
12268 spread_at[si][side] = reach;
12269 }
12270 None => {}
12271 }
12272 }
12273 }
12274 let everywhere = |face: &GFace| -> f64 {
12281 face.holes_aside
12282 .iter()
12283 .fold(face.tolerance, |acc, hole| acc.max(hole.doubt))
12284 .min(doubt_of(face))
12285 };
12286 let near = contacts
12287 .iter()
12288 .filter(|c| c.target_from_a == from_a && c.target_face == fi)
12289 .fold(PARAM_SNAP, |acc, c| acc.max(c.tolerance * 2.0))
12290 .max(tol.confusion() * 2.0)
12291 .max(everywhere(face))
12292 .max(if read_thin { 0.0 } else { spread })
12293 .max(
12294 if !read_thin
12295 && sections.iter().any(|s| {
12296 s.tolerance > 0.0
12297 && if from_a {
12298 s.face_a == fi
12299 } else {
12300 s.face_b == fi
12301 }
12302 })
12303 {
12304 tol.confusion() * 1e3
12305 } else {
12306 0.0
12307 },
12308 );
12309 let other = |section: usize| -> &GFace {
12311 let s = §ions[section];
12312 if from_a {
12313 &gb.faces[s.face_b]
12314 } else {
12315 &ga.faces[s.face_a]
12316 }
12317 };
12318 let face_snap = near.max(
12319 (0..sections.len())
12320 .filter(|&k| {
12321 if from_a {
12322 sections[k].face_a == fi
12323 } else {
12324 sections[k].face_b == fi
12325 }
12326 })
12327 .map(|k| everywhere(other(k)))
12328 .fold(0.0_f64, f64::max),
12329 );
12330 let mut ends: Vec<(usize, usize, Point2, Point, f64)> = Vec::new();
12344 for (si, st) in strands.iter().enumerate() {
12345 if st.polyline.len() < 2 || matches!(st.tag, Tag::Pole { .. }) {
12346 continue;
12347 }
12348 let last = st.polyline.len() - 1;
12349 for end in [0, last] {
12350 let at = st.polyline[end];
12351 let Ok(q) = face.surface.point_at(at.x, at.y, tol) else {
12352 continue;
12353 };
12354 let mut weld = doubt_at(face, q, tol)?.max(near);
12355 if let Tag::Section { section, .. } = st.tag {
12356 weld = weld.max(doubt_at(other(section), q, tol)?);
12357 let s = §ions[section];
12362 if s.tolerance <= 0.0 && exact(&s.curve) {
12363 let reach = tol.confusion();
12364 for e in face
12365 .edges
12366 .iter()
12367 .filter(|e| e.image.is_none() && exact(&e.curve))
12368 {
12369 let read_at = (reach + e.tolerance.max(e.ends_tolerance))
12370 .max(tol.confusion() * 10.0);
12371 for end_t in [e.crange.0, e.crange.1] {
12372 let gap = e.curve.point_at(end_t, tol)?.distance(q);
12373 if gap <= read_at {
12374 weld = weld.max(gap + tol.confusion());
12375 }
12376 }
12377 }
12378 }
12379 }
12380 if let Tag::Contact { contact, .. } = st.tag
12383 && let Some(vertices) = edge_vertices.get(&contacts[contact].node)
12384 {
12385 for &(v, radius) in vertices {
12386 if q.distance(v) <= radius {
12387 weld = weld.max(radius);
12388 }
12389 }
12390 }
12391 if read_thin {
12397 weld = weld.max(spread_at[si][usize::from(end != 0)]);
12398 if let Tag::Section { section, .. } = st.tag {
12399 weld = weld.max(honest(sections[section].tolerance, tol) * 2.0);
12400 }
12401 }
12402 ends.push((si, end, at, q, weld));
12403 }
12404 }
12405 let stretch = strands
12413 .iter()
12414 .flat_map(|st| st.polyline.iter())
12415 .step_by(8)
12416 .filter_map(|p| face.surface.d1_at(p.x, p.y, tol).ok())
12417 .map(|(du, dv)| du.magnitude().min(dv.magnitude()))
12418 .fold(f64::INFINITY, f64::min);
12419 let in_chart = |length: f64| -> f64 {
12420 if stretch.is_finite() && stretch > 1.0 {
12421 (length / stretch).max(PARAM_SNAP)
12422 } else {
12423 length
12424 }
12425 };
12426 let face_snap = in_chart(face_snap);
12427 let mut reach: Vec<f64> = ends.iter().map(|e| in_chart(e.4)).collect();
12428 let own_doubt = |si: usize| -> f64 {
12435 let at_ends = ends
12436 .iter()
12437 .filter(|e| e.0 == si)
12438 .fold(near, |acc, e| acc.max(e.4));
12439 match strands[si].tag {
12440 Tag::Section { section, .. } => at_ends.max(everywhere(other(section))),
12441 Tag::Pole { .. } => f64::INFINITY,
12442 Tag::Boundary { .. } | Tag::Contact { .. } => at_ends,
12443 }
12444 };
12445 let loosest = reach.iter().copied().fold(face_snap, f64::max);
12446 let kept: Vec<(Point2, Point2, f64)> = strands
12447 .iter()
12448 .enumerate()
12449 .filter(|(_, st)| st.polyline.len() >= 2 && chart_length(&st.polyline) <= loosest)
12450 .filter_map(|(si, st)| {
12451 let points: Vec<Point> = st
12452 .polyline
12453 .iter()
12454 .filter_map(|p| face.surface.point_at(p.x, p.y, tol).ok())
12455 .collect();
12456 let space: f64 = points.windows(2).map(|w| w[0].distance(w[1])).sum();
12457 (space > own_doubt(si)).then(|| {
12458 (
12459 st.polyline[0],
12460 st.polyline[st.polyline.len() - 1],
12461 chart_length(&st.polyline) / 2.0,
12462 )
12463 })
12464 })
12465 .collect();
12466 let face_snap = kept
12467 .iter()
12468 .filter(|k| k.2 * 2.0 <= face_snap)
12469 .fold(face_snap, |acc, k| acc.min(k.2))
12470 .max(PARAM_SNAP);
12471 for (e, r) in ends.iter().zip(reach.iter_mut()) {
12472 for &(a, b, half) in &kept {
12473 if half * 2.0 <= *r && (e.2.distance(a) <= *r || e.2.distance(b) <= *r) {
12474 *r = r.min(half);
12475 }
12476 }
12477 *r = r.max(face_snap);
12478 }
12479 let mut class: Vec<usize> = (0..ends.len()).collect();
12485 fn root(class: &mut [usize], mut i: usize) -> usize {
12486 while class[i] != i {
12487 class[i] = class[class[i]];
12488 i = class[i];
12489 }
12490 i
12491 }
12492 let mut welds: Vec<Junction> = Vec::new();
12493 let mut welded = false;
12494 let poled = strands.iter().any(|st| matches!(st.tag, Tag::Pole { .. }));
12509 let one_vertex = |i: usize, j: usize| -> bool {
12510 let (a, b) = (ends[i].3, ends[j].3);
12511 let (from, to) = (ends[i].2, ends[j].2);
12512 if a.distance(b) <= tol.confusion() {
12513 return false;
12514 }
12515 let close = from.distance(to) <= reach[i] + reach[j];
12516 let contact_vertices = [i, j]
12517 .into_iter()
12518 .filter_map(|k| match strands[ends[k].0].tag {
12519 Tag::Contact { contact, .. } => edge_vertices.get(&contacts[contact].node),
12520 _ => None,
12521 })
12522 .flatten()
12523 .copied();
12524 face.edges
12525 .iter()
12526 .flat_map(|e| {
12527 e.vertices
12528 .iter()
12529 .zip(e.ends)
12530 .map(|(&v, (_, radius))| (v, radius + e.tolerance))
12531 })
12532 .chain(contact_vertices)
12533 .any(|(v, within)| {
12534 a.distance(v) <= within
12535 && b.distance(v) <= within
12536 && (close
12537 || !poled
12538 && (1..16).all(|k| {
12539 let t = f64::from(k) / 16.0;
12540 let at = Point2::new(
12541 from.x + (to.x - from.x) * t,
12542 from.y + (to.y - from.y) * t,
12543 );
12544 face.surface
12545 .point_at(at.x, at.y, tol)
12546 .is_ok_and(|q| q.distance(v) <= within)
12547 }))
12548 })
12549 };
12550 for i in 0..ends.len() {
12551 if reach[i] <= face_snap {
12552 continue;
12553 }
12554 for j in 0..ends.len() {
12555 if j != i && (ends[i].2.distance(ends[j].2) <= reach[i] || one_vertex(i, j)) {
12556 let (ri, rj) = (root(&mut class, i), root(&mut class, j));
12557 if ri != rj {
12558 class[ri.max(rj)] = ri.min(rj);
12559 welded = true;
12560 }
12561 }
12562 }
12563 }
12564 if welded {
12565 let off_vertex = |q: Point| -> f64 {
12566 face.edges
12567 .iter()
12568 .flat_map(|e| e.ends.iter())
12569 .map(|(v, _)| v.distance(q))
12570 .fold(f64::INFINITY, f64::min)
12571 };
12572 let rank = |m: usize| {
12573 (
12574 u8::from(!strands[ends[m].0].boundary),
12575 off_vertex(ends[m].3),
12576 )
12577 };
12578 let mut node: Vec<Option<usize>> = vec![None; ends.len()];
12579 let mut weld_of = vec![0.0_f64; ends.len()];
12580 for (i, &weld) in reach.iter().enumerate() {
12581 let r = root(&mut class, i);
12582 weld_of[r] = weld_of[r].max(weld);
12583 let better = node[r].is_none_or(|k| {
12584 let (x, y) = (rank(i), rank(k));
12585 x.0.cmp(&y.0).then(x.1.total_cmp(&y.1)).is_lt()
12586 });
12587 if better {
12588 node[r] = Some(i);
12589 }
12590 }
12591 let mut spread_of = vec![0.0_f64; ends.len()];
12595 let mut held = vec![true; ends.len()];
12596 for i in 0..ends.len() {
12597 let r = root(&mut class, i);
12598 if let Some(k) = node[r] {
12599 spread_of[r] = spread_of[r].max(ends[k].3.distance(ends[i].3));
12600 held[r] &= junctions.iter().any(|j| {
12601 j.at.distance(ends[k].3) <= j.reach && j.at.distance(ends[i].3) <= j.reach
12602 });
12603 }
12604 }
12605 for (r, k) in node.iter().enumerate() {
12606 if let Some(k) = *k
12607 && spread_of[r] > tol.confusion() * 1e2
12608 && !held[r]
12609 {
12610 welds.push(Junction {
12611 at: ends[k].3,
12612 reach: spread_of[r] + tol.confusion() * 1e2,
12613 onto_vertex: false,
12614 });
12615 }
12616 }
12617 let lengths: Vec<f64> = strands
12618 .iter()
12619 .map(|st| chart_length(&st.polyline))
12620 .collect();
12621 let mut landed: Vec<[Option<usize>; 2]> = vec![[None, None]; strands.len()];
12622 for i in 0..ends.len() {
12623 let r = root(&mut class, i);
12624 let Some(k) = node[r] else {
12625 continue;
12626 };
12627 let (si, end, at, _, _) = ends[i];
12628 let target = ends[k].2;
12629 strands[si].polyline[end] = period_shifts(&face.surface, tol)
12632 .into_iter()
12633 .map(|(du, dv)| Point2::new(target.x + du, target.y + dv))
12634 .min_by(|p, q| p.distance(at).total_cmp(&q.distance(at)))
12635 .unwrap_or(target);
12636 landed[si][usize::from(end != 0)] = Some(r);
12637 }
12638 for (si, st) in strands.iter_mut().enumerate() {
12639 if let [Some(a), Some(b)] = landed[si]
12640 && a == b
12641 && lengths[si] <= weld_of[a]
12642 {
12643 if *DEBUG_STRANDS {
12644 eprintln!(
12645 "WELD from_a={from_a} fi={fi}: strand {si} is dust at its ends' weld {:.3e}",
12646 weld_of[a]
12647 );
12648 }
12649 st.polyline = vec![st.polyline[0], st.polyline[0]];
12650 }
12651 }
12652 }
12653 let mut anchors: Vec<(Option<usize>, Point2, Point)> = strands
12663 .iter()
12664 .filter(|st| st.boundary && st.polyline.len() >= 2)
12665 .flat_map(|st| [st.polyline[0], st.polyline[st.polyline.len() - 1]])
12666 .filter_map(|p| {
12667 face.surface
12668 .point_at(p.x, p.y, tol)
12669 .ok()
12670 .map(|q| (None, p, q))
12671 })
12672 .collect();
12673 for st in strands
12674 .iter_mut()
12675 .filter(|st| matches!(st.tag, Tag::Section { .. }) && st.polyline.len() >= 2)
12676 {
12677 let last = st.polyline.len() - 1;
12678 let mut moved_into: Option<usize> = None;
12679 for at in [0, last] {
12680 let p = st.polyline[at];
12681 if anchors
12682 .iter()
12683 .any(|(held, n, _)| held.is_none() && n.distance(p) <= face_snap)
12684 {
12685 continue;
12686 }
12687 let Ok(q) = face.surface.point_at(p.x, p.y, tol) else {
12688 continue;
12689 };
12690 let Some((ji, junction)) = junctions
12691 .iter()
12692 .enumerate()
12693 .filter(|(_, j)| j.at.distance(q) <= j.reach)
12694 .min_by(|(a, x), (b, y)| {
12695 x.at.distance(q).total_cmp(&y.at.distance(q)).then(a.cmp(b))
12696 })
12697 else {
12698 continue;
12699 };
12700 let anchor = anchors
12705 .iter()
12706 .filter(|(held, _, s)| {
12707 held.is_none_or(|h| h == ji) && junction.at.distance(*s) <= junction.reach
12708 })
12709 .filter(|(_, n, s)| {
12710 face.surface
12711 .point_at((n.x + p.x) / 2.0, (n.y + p.y) / 2.0, tol)
12712 .is_ok_and(|m| m.distance(q.midpoint(*s)) <= junction.reach)
12713 })
12714 .min_by(|a, b| {
12715 a.2.distance(q)
12716 .partial_cmp(&b.2.distance(q))
12717 .unwrap_or(core::cmp::Ordering::Equal)
12718 })
12719 .map(|(_, n, _)| *n);
12720 match anchor {
12721 Some(n) => {
12722 st.polyline[at] = n;
12723 moved_into = Some(ji);
12724 }
12725 None => anchors.push((Some(ji), p, q)),
12726 }
12727 }
12728 if let Some(ji) = moved_into
12736 && st.polyline[0].distance(st.polyline[last]) <= face_snap
12737 {
12738 let junction = &junctions[ji];
12739 let mut inside = true;
12740 for p in &st.polyline {
12741 inside &= face
12742 .surface
12743 .point_at(p.x, p.y, tol)
12744 .is_ok_and(|q| junction.at.distance(q) <= junction.reach);
12745 }
12746 if inside {
12747 st.polyline = vec![st.polyline[0], st.polyline[0]];
12748 }
12749 }
12750 }
12751 if *DEBUG_STRANDS {
12752 eprintln!(
12753 "FACE-SNAP from_a={from_a} fi={fi}: snap {face_snap:.3e} edges {:?}",
12754 face.edges
12755 .iter()
12756 .map(|e| (
12757 format!("{:.2e}", e.tolerance),
12758 format!("{:.2e}", e.ends_tolerance)
12759 ))
12760 .collect::<Vec<_>>()
12761 );
12762 for (si, st) in strands.iter().enumerate() {
12763 let tag = match st.tag {
12764 Tag::Boundary { edge, range } => format!("Boundary e{edge} {range:?}"),
12765 Tag::Contact { contact, range } => format!("Contact c{contact} {range:?}"),
12766 Tag::Section { section, range } => format!("Section s{section} {range:?}"),
12767 Tag::Pole { pole, range } => format!("Pole p{pole} {range:?}"),
12768 };
12769 let (a, b) = (st.polyline[0], st.polyline[st.polyline.len() - 1]);
12770 eprintln!(
12771 "STRAND from_a={from_a} fi={fi} {si}: boundary={} {tag} pts={} {a:?} .. {b:?}",
12772 st.boundary,
12773 st.polyline.len()
12774 );
12775 }
12776 }
12777 Ok((strands, face_snap, welds))
12778 };
12779 let node_of = |face: &GFace, tag: &Tag| -> Option<(usize, usize, (f64, f64))> {
12785 match tag {
12786 Tag::Boundary { edge, range } => {
12787 Some((0, face.edges[*edge].node.index() as usize, *range))
12788 }
12789 Tag::Contact { contact, range } => {
12790 Some((1, contacts[*contact].node.index() as usize, *range))
12791 }
12792 Tag::Section { section, range } => Some((2, *section, *range)),
12793 Tag::Pole { .. } => None,
12794 }
12795 };
12796 type Prepared = Option<(Vec<Strand<Tag>>, f64, Vec<Junction>)>;
12799 let mut quiet: [Vec<bool>; 2] = [vec![true; ga.faces.len()], vec![true; gb.faces.len()]];
12806 for s in §ions {
12807 quiet[0][s.face_a] = false;
12808 quiet[1][s.face_b] = false;
12809 }
12810 for c in &contacts {
12811 quiet[usize::from(!c.target_from_a)][c.target_face] = false;
12812 }
12813 for t in &tangents {
12814 quiet[0][t.face_a] = false;
12815 quiet[1][t.face_b] = false;
12816 }
12817 for (side, solid, same) in [(0_usize, &ga, &same_a), (1, &gb, &same_b)] {
12818 for (fi, face) in solid.faces.iter().enumerate() {
12819 if !same[fi].is_empty()
12820 || !face.poles.is_empty()
12821 || face.edges.iter().any(|e| {
12822 paves.contains_key(&e.node)
12823 || e.ends[0].0.distance(e.ends[1].0) <= QUIET_EDGE
12824 || e.bound.diagonal() <= QUIET_EDGE
12825 })
12826 {
12827 quiet[side][fi] = false;
12828 }
12829 }
12830 }
12831 let mut prepared: [Vec<Prepared>; 2] = [Vec::new(), Vec::new()];
12832 let mut dust: Vec<(usize, usize, (f64, f64))> = Vec::new();
12833 for (side, solid, from_a) in [(0_usize, &ga, true), (1, &gb, false)] {
12834 if side == 1 && operands != Operands::Solids {
12836 break;
12837 }
12838 for (fi, face) in solid.faces.iter().enumerate() {
12839 ogeom_core::progress::checkpoint()?;
12840 if quiet[side][fi] {
12841 prepared[side].push(Some((Vec::new(), 0.0, Vec::new())));
12842 continue;
12843 }
12844 let (strands, snap, welds) = strands_of(from_a, fi, face)?;
12845 for st in &strands {
12846 if st.polyline.len() >= 2
12847 && chart_length(&st.polyline) <= snap
12848 && let Some(key) = node_of(face, &st.tag)
12849 {
12850 dust.push(key);
12851 }
12852 }
12853 prepared[side].push(Some((strands, snap, welds)));
12854 }
12855 }
12856 ogeom_core::progress::stage("boolean: split arrange");
12857 let same_key = |a: &(usize, usize, (f64, f64)), b: &(usize, usize, (f64, f64))| {
12858 a.0 == b.0
12859 && a.1 == b.1
12860 && (a.2.0 - b.2.0).abs() <= tol.parametric()
12861 && (a.2.1 - b.2.1).abs() <= tol.parametric()
12862 };
12863 let boundaries = [
12870 if matches!(operands, Operands::SheetBySheet | Operands::Itself) {
12871 None
12872 } else {
12873 Some(ogeom_algo::SolidBoundary::of_kept(
12874 model,
12875 &gb.solid,
12876 tol.confusion() * 1e4,
12877 gb.closed,
12878 tol,
12879 )?)
12880 },
12881 if operands == Operands::Solids {
12884 Some(ogeom_algo::SolidBoundary::of_kept(
12885 model,
12886 &ga.solid,
12887 tol.confusion() * 1e4,
12888 ga.closed,
12889 tol,
12890 )?)
12891 } else {
12892 None
12893 },
12894 ];
12895 type FaceWork = std::sync::Mutex<Prepared>;
12901 let mut work: Vec<(bool, usize, FaceWork)> = Vec::new();
12902 for (side, slots) in prepared.iter_mut().enumerate() {
12903 for (fi, slot) in slots.iter_mut().enumerate() {
12904 let Some(prepared_face) = slot.take() else {
12905 ogeom_bail!(Construction, "a face was prepared twice");
12906 };
12907 work.push((side == 0, fi, std::sync::Mutex::new(Some(prepared_face))));
12908 }
12909 }
12910 let found = ogeom_core::parallel::map_ordered(
12911 &work,
12912 |_, (from_a, fi, prepared_face)| -> OgeomResult<(Vec<FacePiece>, Vec<Junction>)> {
12913 ogeom_core::progress::checkpoint()?;
12914 let (from_a, fi) = (*from_a, *fi);
12915 let (own, other, boundary) = if from_a {
12916 (&ga, &gb.solid, boundaries[0].as_ref())
12917 } else {
12918 (&gb, &ga.solid, boundaries[1].as_ref())
12919 };
12920 let face = &own.faces[fi];
12921 let mut junctions: Vec<Junction> = Vec::new();
12922 let Some((mut strands, mut face_snap, welds)) = prepared_face
12923 .lock()
12924 .map_err(|_| ogeom_core::ogeom_err!(Construction, "a face's work was poisoned"))?
12925 .take()
12926 else {
12927 ogeom_bail!(Construction, "a face was prepared twice");
12928 };
12929 junctions.extend(welds);
12930 if quiet[usize::from(!from_a)][fi] {
12931 if let Some(piece) = quiet_piece(model, from_a, fi, face, boundary, tol)? {
12932 return Ok((vec![piece], Vec::new()));
12933 }
12934 let welds;
12936 (strands, face_snap, welds) = strands_of(from_a, fi, face)?;
12937 junctions.extend(welds);
12938 }
12939 let mut pieces: Vec<FacePiece> = Vec::new();
12940 let space_ends = |face: &GFace, tag: &Tag| -> OgeomResult<Option<(Point, Point)>> {
12942 Ok(Some(match tag {
12943 Tag::Boundary { edge, range } => {
12944 let e = &face.edges[*edge];
12945 (
12946 e.curve.point_at(range.0, tol)?,
12947 e.curve.point_at(range.1, tol)?,
12948 )
12949 }
12950 Tag::Contact { contact, range } => {
12951 let c = &contacts[*contact];
12952 (
12953 c.curve.point_at(range.0, tol)?,
12954 c.curve.point_at(range.1, tol)?,
12955 )
12956 }
12957 Tag::Section { section, range } => {
12958 let sec = §ions[*section];
12959 let domain = sec.curve.domain();
12960 (
12961 sec.curve
12962 .point_at(at_param(range.0, domain, sec.closed), tol)?,
12963 sec.curve
12964 .point_at(at_param(range.1, domain, sec.closed), tol)?,
12965 )
12966 }
12967 Tag::Pole { .. } => return Ok(None),
12968 }))
12969 };
12970 for st in &strands {
12976 if st.polyline.len() >= 2
12977 && chart_length(&st.polyline) <= face_snap
12978 && let Some((from, to)) = space_ends(face, &st.tag)?
12979 && from.distance(to) > tol.confusion() * 1e2
12980 {
12981 junctions.push(Junction {
12982 at: from.midpoint(to),
12983 reach: from.distance(to) / 2.0 + tol.confusion() * 1e2,
12984 onto_vertex: false,
12985 });
12986 }
12987 }
12988 let forced: Vec<usize> = strands
12993 .iter()
12994 .enumerate()
12995 .filter(|(_, st)| {
12996 st.polyline.len() >= 2
12997 && chart_length(&st.polyline) > face_snap
12998 && node_of(face, &st.tag)
12999 .is_some_and(|k| dust.iter().any(|d| same_key(d, &k)))
13000 })
13001 .map(|(i, _)| i)
13002 .collect();
13003 if !forced.is_empty() {
13004 let mut reps: Vec<(Point2, usize)> = Vec::new();
13005 let canon = |p: Point2, reps: &mut Vec<(Point2, usize)>| -> usize {
13006 match reps.iter().position(|(q, _)| q.distance(p) <= face_snap) {
13007 Some(i) => i,
13008 None => {
13009 reps.push((p, reps.len()));
13010 reps.len() - 1
13011 }
13012 }
13013 };
13014 fn root(reps: &mut [(Point2, usize)], mut i: usize) -> usize {
13015 while reps[i].1 != i {
13016 reps[i].1 = reps[reps[i].1].1;
13017 i = reps[i].1;
13018 }
13019 i
13020 }
13021 for &i in &forced {
13022 let st = &strands[i];
13023 let a = canon(st.polyline[0], &mut reps);
13024 let b = canon(st.polyline[st.polyline.len() - 1], &mut reps);
13025 let (ra, rb) = (root(&mut reps, a), root(&mut reps, b));
13026 if ra != rb {
13027 reps[rb].1 = ra;
13028 }
13029 let Some((from, to)) = space_ends(face, &st.tag)? else {
13030 continue;
13031 };
13032 if *DEBUG_STRANDS {
13033 eprintln!(
13034 "DUST from_a={from_a} fi={fi}: strand {i} collapses with its partners, {from:?} -> {to:?}"
13035 );
13036 }
13037 junctions.push(Junction {
13038 at: from.midpoint(to),
13039 reach: from.distance(to) / 2.0 + tol.confusion() * 1e2,
13040 onto_vertex: false,
13041 });
13042 }
13043 for (i, st) in strands.iter_mut().enumerate() {
13044 if forced.contains(&i) {
13045 continue;
13046 }
13047 let last = st.polyline.len() - 1;
13048 for end in [0, last] {
13049 if let Some(k) = reps
13050 .iter()
13051 .position(|(q, _)| q.distance(st.polyline[end]) <= face_snap)
13052 {
13053 let r = root(&mut reps, k);
13054 st.polyline[end] = reps[r].0;
13055 }
13056 }
13057 }
13058 let mut index = 0_usize;
13059 strands.retain(|_| {
13060 let keep = !forced.contains(&index);
13061 index += 1;
13062 keep
13063 });
13064 }
13065 let lone = lone_holes(model, face, &strands, face_snap)?;
13069 let every: Vec<usize>;
13070 let (walked, places, lone): (&[Strand<Tag>], &[usize], &[arrange::Lone<Tag>]) =
13071 match &lone {
13072 Some((rest, places, lone)) => (rest, places, lone),
13073 None => {
13074 every = (0..strands.len()).collect();
13075 (&strands, &every, &[])
13076 }
13077 };
13078 let split = match arrange_pieces(walked, places, face_snap, lone) {
13079 Ok(split) => split,
13080 Err(err) => {
13081 if *ARRANGE_DEBUG {
13082 eprintln!("ARRANGE from_a={from_a} fi={fi} failed: {err}");
13083 for (si, st) in strands.iter().enumerate() {
13084 let (a, b) = (st.polyline[0], st.polyline[st.polyline.len() - 1]);
13085 let describe = |curve: &Curve, range: (f64, f64)| -> String {
13086 let mid = curve
13087 .point_at(f64::midpoint(range.0, range.1), tol)
13088 .map(|p| format!("({:.4},{:.4},{:.4})", p.x, p.y, p.z))
13089 .unwrap_or_default();
13090 match curve {
13091 Curve::Circle(c) => format!(
13092 "circle centre {:?} r {:.4} mid {mid}",
13093 c.circle().centre(),
13094 c.circle().radius()
13095 ),
13096 other => {
13097 format!("{:?} mid {mid}", core::mem::discriminant(other))
13098 }
13099 }
13100 };
13101 let geometry = match st.tag {
13102 Tag::Boundary { edge, range } => {
13103 describe(&face.edges[edge].curve, range)
13104 }
13105 Tag::Contact { contact, range } => {
13106 format!(
13107 "{} window {:?} along {:?} node {}",
13108 describe(&contacts[contact].curve, range),
13109 contacts[contact].crange,
13110 contact_along[contact],
13111 contacts[contact].node.index()
13112 )
13113 }
13114 _ => String::new(),
13115 };
13116 eprintln!(" {geometry}");
13117 eprintln!(
13118 " strand {si} boundary={} {:?} {a:?} .. {b:?}",
13119 st.boundary,
13120 match st.tag {
13121 Tag::Boundary { edge, range } =>
13122 format!("Boundary e{edge} {range:?}"),
13123 Tag::Contact { contact, range } =>
13124 format!("Contact c{contact} {range:?}"),
13125 Tag::Section { section, range } =>
13126 format!("Section s{section} {range:?}"),
13127 Tag::Pole { pole, range } => format!("Pole p{pole} {range:?}"),
13128 }
13129 );
13130 }
13131 }
13132 return Err(err);
13133 }
13134 };
13135 for mut piece in split {
13136 let Some(boundary) = boundary else {
13137 let probe = face.surface.point_at(piece.first.x, piece.first.y, tol)?;
13138 pieces.push(FacePiece {
13139 from_a,
13140 face: fi,
13141 rings: piece.rings,
13142 outlines: piece.outlines,
13143 probe,
13144 state: PieceState::Unread,
13145 covered: false,
13146 whole: false,
13147 });
13148 continue;
13149 };
13150 let partners = if from_a { &same_a[fi] } else { &same_b[fi] };
13170 let mut chosen = None;
13171 let mut unread = None;
13172 let ask = |candidate: Point2| -> OgeomResult<(Point, OgeomResult<Containment>)> {
13175 let at = face.surface.point_at(candidate.x, candidate.y, tol)?;
13176 let shared = !partners.is_empty()
13177 && partners.iter().any(|&pi| {
13178 let partner = if from_a { &gb.faces[pi] } else { &ga.faces[pi] };
13179 chart_point_within(partner, at, partner_reach(face, partner, tol), tol)
13180 .is_some()
13181 });
13182 if shared {
13183 return Ok((at, Ok(Containment::On)));
13184 }
13185 match boundary.holds(model, at, tol) {
13186 Err(e @ ogeom_core::OgeomError::NotDone(_)) => Ok((at, Err(e))),
13187 Err(e) => Err(e),
13188 says => Ok((at, says)),
13189 }
13190 };
13191 let first_kept = match face.surface {
13200 SurfaceGeometry::Plane(_) => {
13201 let drawn = piece.rings.iter().flatten().all(|t| {
13202 fine_strand(face, §ions, from_a, &t.tag).is_some_and(|s| {
13203 matches!(s.0, PlanarCurve::Line(_) | PlanarCurve::Circle(_))
13204 })
13205 });
13206 drawn
13207 .then(|| {
13208 FineRings::of(face, &piece.rings, §ions, from_a, None, tol)
13209 })
13210 .flatten()
13211 .and_then(|every| {
13212 let reached = rings_near(&piece.outlines, &[piece.first]);
13213 every.inside_beside(piece.first, &reached)
13214 })
13215 }
13216 _ => Some(true),
13217 };
13218 if first_kept == Some(true)
13219 && let (at, Ok(says @ (Containment::In | Containment::Out))) = ask(piece.first)?
13220 {
13221 chosen = Some((piece.first, at, says));
13222 }
13223 let mut fine = None;
13230 if chosen.is_none() {
13231 let mut interiors: Vec<Point2> = piece.interiors().to_vec();
13232 let reached = rings_near(&piece.outlines, &interiors);
13233 fine =
13234 FineRings::of(face, &piece.rings, §ions, from_a, Some(&reached), tol);
13235 if let Some(fine) = &fine {
13236 interiors.retain(|p| fine.inside(*p));
13237 if interiors.is_empty()
13238 && let Some(every) =
13239 FineRings::of(face, &piece.rings, §ions, from_a, None, tol)
13240 {
13241 interiors =
13242 arrange::interior_points_of(&every.points(), tol.parametric());
13243 }
13244 }
13245 for candidate in &interiors {
13246 let says = match ask(*candidate)? {
13247 (at, Ok(says)) => (at, says),
13248 (_, Err(e)) => {
13249 unread = Some(e);
13250 continue;
13251 }
13252 };
13253 if chosen.is_none() || !matches!(says.1, Containment::On) {
13254 chosen = Some((*candidate, says.0, says.1));
13255 }
13256 if !matches!(says.1, Containment::On) {
13257 break;
13258 }
13259 }
13260 }
13261 if read_at_centre
13267 && matches!(chosen, Some((_, _, Containment::In | Containment::Out)))
13268 && let Some(centre) = arrange::centroid_of(&piece.outlines)
13269 && arrange::inside_rings(&piece.outlines, centre)
13270 {
13271 if fine.is_none() {
13272 let reached = rings_near(&piece.outlines, &[centre]);
13273 fine = FineRings::of(
13274 face,
13275 &piece.rings,
13276 §ions,
13277 from_a,
13278 Some(&reached),
13279 tol,
13280 );
13281 }
13282 if fine.as_ref().is_none_or(|f| f.inside(centre)) {
13283 let at = face.surface.point_at(centre.x, centre.y, tol)?;
13284 if let Ok(says @ (Containment::In | Containment::Out)) =
13285 boundary.holds(model, at, tol)
13286 {
13287 chosen = Some((centre, at, says));
13288 }
13289 }
13290 }
13291 let Some((interior, probe, said)) = chosen else {
13292 if let Some(e) = unread {
13293 return Err(e);
13294 }
13295 ogeom_bail!(
13296 Construction,
13297 "a piece of a face has no interior point to classify at"
13298 );
13299 };
13300 let thin = if read_thin && matches!(said, Containment::On) {
13304 let theirs = if from_a { &gb } else { &ga };
13305 thin_by_strands(face, &piece.rings, &piece.outlines, &contacts, theirs, tol)?
13306 } else {
13307 Thin::Wide
13308 };
13309 match thin {
13310 Thin::Reads(state) => {
13311 pieces.push(FacePiece {
13312 from_a,
13313 face: fi,
13314 rings: piece.rings,
13315 outlines: piece.outlines,
13316 probe,
13317 state,
13318 covered: false,
13319 whole: false,
13320 });
13321 continue;
13322 }
13323 Thin::Disagree => ogeom_bail!(
13324 NotDone,
13325 "a piece thinner than the boolean reads the other solid \
13326 within lies beside that solid's edges on sides that \
13327 disagree on whether it is inside: it is dust the \
13328 boolean does not resolve"
13329 ),
13330 Thin::Wide | Thin::Undecided => {}
13331 }
13332 let state = match said {
13333 Containment::In => PieceState::In,
13334 Containment::Out => PieceState::Out,
13335 Containment::On => {
13336 let own_normal = outward_normal(face, interior, tol)?;
13340 let mut resolved = None;
13341 for &pi in partners {
13342 let partner = if from_a { &gb.faces[pi] } else { &ga.faces[pi] };
13343 let Some(uv) = chart_point_within(
13344 partner,
13345 probe,
13346 partner_reach(face, partner, tol),
13347 tol,
13348 ) else {
13349 continue;
13350 };
13351 let theirs = outward_normal(partner, uv, tol)?;
13352 resolved = Some(if own_normal.dot(theirs) > 0.0 {
13353 PieceState::OnAligned
13354 } else {
13355 PieceState::OnOpposed
13356 });
13357 break;
13358 }
13359 let Some(state) = resolved else {
13360 match ogeom_algo::classify_in_solid_exact_banded(
13366 model,
13367 other,
13368 probe,
13369 tol.confusion() * 10.0,
13370 tol,
13371 )? {
13372 Containment::In => {
13373 pieces.push(FacePiece {
13374 from_a,
13375 face: fi,
13376 rings: piece.rings,
13377 outlines: piece.outlines,
13378 probe,
13379 state: PieceState::In,
13380 covered: false,
13381 whole: false,
13382 });
13383 continue;
13384 }
13385 Containment::Out => {
13386 pieces.push(FacePiece {
13387 from_a,
13388 face: fi,
13389 rings: piece.rings,
13390 outlines: piece.outlines,
13391 probe,
13392 state: PieceState::Out,
13393 covered: false,
13394 whole: false,
13395 });
13396 continue;
13397 }
13398 Containment::On => {}
13399 }
13400 let mut off_contact = None;
13412 for candidate in
13413 arrange::off_contact_points(&piece.outlines, tol.parametric())
13414 {
13415 if !arrange::inside_rings(&piece.outlines, candidate)
13416 || fine.as_ref().is_some_and(|f| !f.inside(candidate))
13417 {
13418 continue;
13419 }
13420 let at = face.surface.point_at(candidate.x, candidate.y, tol)?;
13421 match boundary.holds(model, at, tol) {
13422 Ok(Containment::In) => {
13423 off_contact = Some(PieceState::In);
13424 break;
13425 }
13426 Ok(Containment::Out) => {
13427 off_contact = Some(PieceState::Out);
13428 break;
13429 }
13430 Ok(Containment::On)
13431 | Err(ogeom_core::OgeomError::NotDone(_)) => {}
13432 Err(e) => return Err(e),
13433 }
13434 }
13435 if let Some(state) = off_contact {
13436 pieces.push(FacePiece {
13437 from_a,
13438 face: fi,
13439 rings: piece.rings,
13440 outlines: piece.outlines,
13441 probe,
13442 state,
13443 covered: false,
13444 whole: false,
13445 });
13446 continue;
13447 }
13448 if settle {
13460 let off = own_normal * (tol.confusion() * 1e3);
13461 let side = |q: Point| {
13462 ogeom_algo::classify_in_solid_exact_banded(
13463 model,
13464 other,
13465 q,
13466 tol.confusion() * 10.0,
13467 tol,
13468 )
13469 };
13470 let (beyond, within) = (side(probe + off)?, side(probe - off)?);
13471 let read = match (beyond, within) {
13472 (Containment::In, Containment::In) => {
13473 Some((PieceState::In, false))
13474 }
13475 (Containment::Out, Containment::Out) => {
13476 Some((PieceState::Out, false))
13477 }
13478 (Containment::Out, Containment::In) => {
13479 Some((PieceState::OnAligned, !from_a))
13480 }
13481 (Containment::In, Containment::Out) => {
13482 Some((PieceState::OnOpposed, false))
13483 }
13484 _ => None,
13485 };
13486 if let Some((state, covered)) = read {
13487 pieces.push(FacePiece {
13488 from_a,
13489 face: fi,
13490 rings: piece.rings,
13491 outlines: piece.outlines,
13492 probe,
13493 state,
13494 covered,
13495 whole: false,
13496 });
13497 continue;
13498 }
13499 }
13500 if *DEBUG_WIRE {
13501 eprintln!(
13502 "ON-CONTACT piece of {} face {fi} probe {probe:?} partners {partners:?}",
13503 if from_a { "A" } else { "B" }
13504 );
13505 }
13506 ogeom_bail!(
13507 NotDone,
13508 "a piece lies on the other solid's boundary \
13509 at every point it was asked at, with no \
13510 coincident partner face to compare sides \
13511 against; a contact over a region of faces not \
13512 recognised as coincident is refused; see the \
13513 open items in docs/PLAN.md"
13514 );
13515 };
13516 state
13517 }
13518 };
13519 pieces.push(FacePiece {
13520 from_a,
13521 face: fi,
13522 rings: piece.rings,
13523 outlines: piece.outlines,
13524 probe,
13525 state,
13526 covered: false,
13527 whole: false,
13528 });
13529 }
13530 let untouched = strands.iter().all(|st| match &st.tag {
13533 Tag::Boundary { edge, range } => {
13534 let full = face.edges[*edge].crange;
13535 (range.0.min(range.1) - full.0.min(full.1)).abs() <= tol.parametric()
13536 && (range.0.max(range.1) - full.0.max(full.1)).abs() <= tol.parametric()
13537 }
13538 Tag::Pole { .. } => true,
13539 Tag::Section { .. } | Tag::Contact { .. } => false,
13540 });
13541 if untouched
13542 && let [piece] = pieces.as_mut_slice()
13543 && matches!(
13544 piece.state,
13545 PieceState::In | PieceState::Out | PieceState::Unread
13546 )
13547 {
13548 piece.whole = true;
13549 }
13550
13551 Ok((pieces, junctions))
13552 },
13553 );
13554 for face in found {
13555 let (face_pieces, face_junctions) = face?;
13556 pieces.extend(face_pieces);
13557 junctions.extend(face_junctions);
13558 }
13559 let coincident = mark_covered_coincidences(&ga, &gb, &mut pieces, tol);
13560 if *ARRANGE_DEBUG {
13561 for (fi, partners) in same_a.iter().enumerate() {
13562 if !partners.is_empty() {
13563 eprintln!("SAME a{fi} with b{partners:?}");
13564 }
13565 }
13566 for (ci, contact) in contacts.iter().enumerate() {
13567 eprintln!(
13568 "CONTACT c{ci} onto {}{} over {:?} prange {:?} node {} pcurve {:?}",
13569 if contact.target_from_a { "a" } else { "b" },
13570 contact.target_face,
13571 contact.crange,
13572 contact.prange,
13573 contact.node.index(),
13574 contact.pcurve
13575 );
13576 }
13577 for (i, p) in pieces.iter().enumerate() {
13578 let own = if p.from_a {
13579 &ga.faces[p.face]
13580 } else {
13581 &gb.faces[p.face]
13582 };
13583 let kind = match &own.surface {
13584 SurfaceGeometry::Plane(_) => "plane",
13585 SurfaceGeometry::Cylinder(_) => "cyl",
13586 SurfaceGeometry::Torus(_) => "torus",
13587 SurfaceGeometry::BSpline(_) => "bspline",
13588 _ => "other",
13589 };
13590 eprintln!(
13591 "PIECE {i} from_a={} face={} {kind} state={:?} covered={} probe={:?} rings {:?}",
13592 p.from_a,
13593 p.face,
13594 p.state,
13595 p.covered,
13596 p.probe,
13597 p.rings.iter().map(Vec::len).collect::<Vec<_>>()
13598 );
13599 }
13600 }
13601 ogeom_core::progress::stage("boolean: classified");
13602 let junctions = merge_junctions(junctions);
13608 Ok(GeneralFused {
13609 a: ga,
13610 b: gb,
13611 sections,
13612 contacts,
13613 tangents,
13614 pieces,
13615 coincident,
13616 junctions,
13617 })
13618}
13619
13620struct Rebuild<'m> {
13624 model: &'m mut Model,
13625 surfaces_a: Vec<Option<ogeom_topo::SurfaceId>>,
13627 surfaces_b: Vec<Option<ogeom_topo::SurfaceId>>,
13628 vertices: Vec<(Point, Shape)>,
13632 vertex_bins: bins::Bins,
13634 near: Vec<usize>,
13636 widest: f64,
13640 weld: f64,
13645 junctions: Vec<(Junction, Option<Shape>)>,
13648 junction_bins: bins::Bins,
13650 junction_reach: f64,
13652 onto_vertex: ogeom_core::FastSet<ogeom_topo::TShapeId>,
13655 whole_edges: ogeom_core::FastMap<EdgeKey, Shape>,
13659 kept: ogeom_core::FastMap<ogeom_topo::TShapeId, Point>,
13662 strained: bool,
13665}
13666
13667impl Rebuild<'_> {
13668 fn vertex(&mut self, p: Point, tol: Tolerances) -> Shape {
13669 let slack = self.weld.max(tol.confusion() * 1e2);
13675 if *DEBUG_WIRE {
13676 eprintln!("VERTEX ask {p:?}");
13677 }
13678 let reach = self.junction_reach;
13679 let inside = |slot: &usize| {
13680 let j = &self.junctions[*slot].0;
13681 j.at.distance(p) <= j.reach + slack
13682 };
13683 let mut near = core::mem::take(&mut self.near);
13684 let first = if self.junction_bins.near_into(p, reach + slack, &mut near) {
13685 near.iter().copied().find(inside)
13686 } else {
13687 (0..self.junctions.len()).find(inside)
13688 };
13689 if let Some(slot) = first {
13690 let junction = self.junctions[slot].0;
13691 let shape = match &self.junctions[slot].1 {
13692 Some(shape) => shape.clone(),
13693 None => {
13694 let shape = make_vertex(self.model, junction.at).shape;
13695 self.junctions[slot].1 = Some(shape.clone());
13696 self.remember(junction.at, &shape);
13697 shape
13698 }
13699 };
13700 self.near = near;
13701 if junction.onto_vertex {
13702 self.onto_vertex.insert(shape.node());
13703 }
13704 let gap = junction.at.distance(p);
13707 if let Some(node) = self.model.node_mut(&shape)
13708 && let ogeom_topo::NodeData::Vertex(data) = node.data_mut()
13709 {
13710 data.tolerance = data
13714 .tolerance
13715 .widen_to(junction.reach.max(gap) + tol.confusion());
13716 }
13717 self.note_tolerance(&shape);
13718 return shape;
13719 }
13720 let floor = self.weld.max(tol.confusion() * 1e2);
13738 if !self
13739 .vertex_bins
13740 .near_into(p, floor + self.widest, &mut near)
13741 {
13742 near.extend(0..self.vertices.len());
13743 }
13744 let found = near
13745 .iter()
13746 .filter_map(|&index| {
13747 let (q, shape) = &self.vertices[index];
13748 let own = self
13749 .model
13750 .node(shape)
13751 .and_then(|n| n.data().as_vertex())
13752 .map_or(0.0, |d| d.tolerance.get());
13753 let gap = q.distance(p);
13754 (gap <= floor.max(own + floor)).then_some((gap, index))
13755 })
13756 .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal))
13757 .map(|(gap, index)| (gap, self.vertices[index].1.clone()));
13758 self.near = near;
13759 if let Some((gap, shape)) = found {
13760 let (at, own) = self
13765 .model
13766 .node(&shape)
13767 .and_then(|n| n.data().as_vertex())
13768 .map_or((p, 0.0), |d| (d.point, d.tolerance.get()));
13769 let off = at.distance(p);
13770 if let Some(stands) = self.kept.get(&shape.node()) {
13771 let off = stands.distance(p);
13774 self.strained |= off > tol.confusion() && off + tol.confusion() > own;
13775 } else if off > tol.confusion()
13776 && let Some(node) = self.model.node_mut(&shape)
13777 && let ogeom_topo::NodeData::Vertex(data) = node.data_mut()
13778 {
13779 data.tolerance = data.tolerance.widen_to(off + tol.confusion());
13780 }
13781 self.note_tolerance(&shape);
13782 if gap > tol.confusion() {
13783 self.remember(p, &shape);
13784 }
13785 return shape;
13786 }
13787 let shape = make_vertex(self.model, p).shape;
13788 self.remember(p, &shape);
13789 shape
13790 }
13791
13792 fn remember(&mut self, p: Point, shape: &Shape) {
13794 self.vertex_bins.insert(p, self.vertices.len());
13795 self.vertices.push((p, shape.clone()));
13796 self.note_tolerance(shape);
13797 }
13798
13799 fn note_tolerance(&mut self, shape: &Shape) {
13801 if let Some(data) = self.model.node(shape).and_then(|n| n.data().as_vertex()) {
13802 self.widest = self.widest.max(data.tolerance.get());
13803 }
13804 }
13805
13806 fn widen(&mut self, vertex: &Shape, to: f64) -> OgeomResult<()> {
13809 if self.kept.contains_key(&vertex.node()) {
13810 let own = self
13811 .model
13812 .node(vertex)
13813 .and_then(|n| n.data().as_vertex())
13814 .map_or(0.0, |d| d.tolerance.get());
13815 self.strained |= own < to;
13816 return Ok(());
13817 }
13818 self.model.widen(vertex, ogeom_core::Tolerance::new(to)?)?;
13819 self.note_tolerance(vertex);
13820 Ok(())
13821 }
13822
13823 fn surface_id(
13824 &mut self,
13825 fused: &GeneralFused,
13826 from_a: bool,
13827 face: usize,
13828 ) -> ogeom_topo::SurfaceId {
13829 let slot = if from_a {
13830 &mut self.surfaces_a[face]
13831 } else {
13832 &mut self.surfaces_b[face]
13833 };
13834 if let Some(id) = slot {
13835 return *id;
13836 }
13837 let own = if from_a {
13838 fused.a.faces[face].own_surface
13839 } else {
13840 fused.b.faces[face].own_surface
13841 };
13842 if let Some(id) = own {
13843 *slot = Some(id);
13844 return id;
13845 }
13846 let surface = if from_a {
13847 fused.a.faces[face].surface.clone()
13848 } else {
13849 fused.b.faces[face].surface.clone()
13850 };
13851 let id = self.model.geometry_mut().add_surface(surface);
13852 *slot = Some(id);
13853 id
13854 }
13855}
13856
13857type StrandEdges = ogeom_core::FastMap<(usize, u8), Vec<((f64, f64), Shape)>>;
13860
13861fn build_piece(
13863 rebuild: &mut Rebuild,
13864 fused: &GeneralFused,
13865 piece: &FacePiece,
13866 tol: Tolerances,
13867) -> OgeomResult<Option<Shape>> {
13868 let own = if piece.from_a { &fused.a } else { &fused.b };
13869 let face = &own.faces[piece.face];
13870 let surface_id = rebuild.surface_id(fused, piece.from_a, piece.face);
13871
13872 let mut cache: StrandEdges = ogeom_core::FastMap::default();
13876 let mut wires = Vec::new();
13877 for ring in &piece.rings {
13878 let mut edges = Vec::with_capacity(ring.len());
13879 let mut taken: Vec<(EdgeKey, bool)> = Vec::new();
13882 for traversal in ring {
13883 if let Tag::Boundary { edge, .. } = &traversal.tag
13888 && let Some(kept) = own.shared.get(&face.edges[*edge].node)
13889 {
13890 if face.own_surface != Some(surface_id) {
13891 rebuild.strained = true;
13892 }
13893 let key = (face.edges[*edge].node, traversal.reversed);
13894 if !taken.contains(&key) {
13895 taken.push(key);
13896 edges.push(if traversal.reversed {
13897 kept.reversed()
13898 } else {
13899 kept.clone()
13900 });
13901 }
13902 continue;
13903 }
13904 let (key_edge, key_kind, range) = match &traversal.tag {
13905 Tag::Boundary { edge, range } => (*edge, 0_u8, *range),
13906 Tag::Section { section, range } => (*section, 1, *range),
13907 Tag::Contact { contact, range } => (*contact, 2, *range),
13908 Tag::Pole { pole, range } => (*pole, 3, *range),
13909 };
13910 let near = |a: (f64, f64), b: (f64, f64)| {
13911 (a.0 - b.0).abs() <= tol.parametric() && (a.1 - b.1).abs() <= tol.parametric()
13912 };
13913 if *DEBUG_WIRE && (range.1 - range.0).abs() < 1e-4 {
13914 let kind = match &traversal.tag {
13915 Tag::Boundary { .. } => "boundary",
13916 Tag::Section { .. } => "section",
13917 Tag::Contact { .. } => "contact",
13918 Tag::Pole { .. } => "pole",
13919 };
13920 eprintln!(
13921 "DUST {kind} {key_edge} range {range:?} in piece from_a={} face={}",
13922 piece.from_a, piece.face
13923 );
13924 }
13925 let built = if let Some((_, shape)) = cache
13926 .get(&(key_edge, key_kind))
13927 .and_then(|built| built.iter().find(|(r, _)| near(*r, range)))
13928 {
13929 shape.clone()
13930 } else {
13931 let shape = build_sub_edge(
13932 rebuild,
13933 fused,
13934 piece.from_a,
13935 face,
13936 surface_id,
13937 (&traversal.tag, traversal.nodes),
13938 tol,
13939 )?;
13940 cache
13941 .entry((key_edge, key_kind))
13942 .or_default()
13943 .push((range, shape.clone()));
13944 shape
13945 };
13946 if !matches!(traversal.tag, Tag::Pole { .. })
13952 && let Some((v0, v1)) = ogeom_algo::edge_vertices(rebuild.model, &built)?
13953 && v0.node() == v1.node()
13954 && let Some(vd) = rebuild
13955 .model
13956 .node(&v0)
13957 .and_then(|n| n.data().as_vertex())
13958 .map(|d| (d.point, d.tolerance.get()))
13959 && let Some(ed) = rebuild.model.node(&built).and_then(|n| n.data().as_edge())
13960 && let Some(ogeom_topo::EdgeRepr::Curve3d {
13961 curve, range: r3, ..
13962 }) = ed.curve3d()
13963 && let Some(g) = rebuild.model.geometry().curve(*curve)
13964 && g.point_at(f64::midpoint(r3.0, r3.1), tol)?.distance(vd.0)
13965 <= vd.1.max(tol.confusion() * 1e4)
13966 {
13967 if *DEBUG_WIRE {
13968 eprintln!(
13969 "DUST piece closing on one vertex at {:?} dropped from piece from_a={} face={}",
13970 vd.0, piece.from_a, piece.face
13971 );
13972 }
13973 continue;
13974 }
13975 edges.push(if traversal.reversed {
13976 built.reversed()
13977 } else {
13978 built
13979 });
13980 }
13981 if edges.is_empty() {
13985 if wires.is_empty() {
13986 if *DEBUG_WIRE {
13987 eprintln!(
13988 "DUST piece from_a={} face={}: its boundary closes on one point",
13989 piece.from_a, piece.face
13990 );
13991 }
13992 return Ok(None);
13993 }
13994 continue;
13995 }
13996 let wire = match make_wire(rebuild.model, &edges, tol) {
13997 Ok(w) => w.shape,
13998 Err(e) => {
13999 if *DEBUG_WIRE {
14000 eprintln!(
14001 "WIRE FAIL piece from_a={} face={}",
14002 piece.from_a, piece.face
14003 );
14004 for built in &edges {
14005 if let Some((a, b)) = ogeom_algo::edge_vertices(rebuild.model, built)? {
14006 let at = |v: &Shape| {
14007 rebuild
14008 .model
14009 .node(v)
14010 .and_then(|n| n.data().as_vertex())
14011 .map(|d| (d.point, d.tolerance.get()))
14012 };
14013 eprintln!(
14014 " built {:?}{}: v{} {:?} -> v{} {:?}",
14015 built.node(),
14016 if built.orientation() == ogeom_topo::Orientation::Reversed {
14017 " rev"
14018 } else {
14019 ""
14020 },
14021 a.node().index(),
14022 at(&a),
14023 b.node().index(),
14024 at(&b)
14025 );
14026 }
14027 }
14028 for t in ring {
14029 let tag = match &t.tag {
14030 Tag::Boundary { edge, range } => format!(
14031 "Boundary e{edge} {range:?} tol {:.2e}",
14032 face.edges[*edge].tolerance
14033 ),
14034 Tag::Contact { contact, range } => format!(
14035 "Contact c{contact} {range:?} tol {:.2e}",
14036 fused.contacts[*contact].tolerance
14037 ),
14038 Tag::Section { section, range } => format!(
14039 "Section s{section} {range:?} tol {:.2e}",
14040 fused.sections[*section].tolerance
14041 ),
14042 Tag::Pole { pole, range } => format!("Pole p{pole} {range:?}"),
14043 };
14044 eprintln!(" {tag} reversed={}", t.reversed);
14045 }
14046 }
14047 return Err(e);
14048 }
14049 };
14050 wires.push(wire);
14051 }
14052 let built = match make_face_on(rebuild.model, surface_id, &wires, tol) {
14053 Ok(b) => b.shape,
14054 Err(e) => {
14055 if *DEBUG_WIRE {
14056 eprintln!(
14057 "FACE FAIL piece from_a={} face={}: {e}",
14058 piece.from_a, piece.face
14059 );
14060 for (wi, wire) in wires.iter().enumerate() {
14061 for edge in rebuild.model.ordered_children_of(wire)? {
14062 if let Some((a, b)) = ogeom_algo::edge_vertices(rebuild.model, &edge)? {
14063 let at = |v: &Shape| {
14064 rebuild
14065 .model
14066 .node(v)
14067 .and_then(|n| n.data().as_vertex())
14068 .map(|d| d.point)
14069 };
14070 eprintln!(
14071 " wire {wi} edge {:?}{}: v{} {:?} -> v{} {:?}",
14072 edge.node(),
14073 if edge.orientation() == ogeom_topo::Orientation::Reversed {
14074 " rev"
14075 } else {
14076 ""
14077 },
14078 a.node().index(),
14079 at(&a),
14080 b.node().index(),
14081 at(&b)
14082 );
14083 }
14084 }
14085 }
14086 }
14087 return Err(e);
14088 }
14089 };
14090 Ok(Some(
14091 if face.face.orientation() == ogeom_topo::Orientation::Reversed {
14092 built.reversed()
14093 } else {
14094 built
14095 },
14096 ))
14097}
14098
14099fn build_sub_edge(
14102 rebuild: &mut Rebuild,
14103 fused: &GeneralFused,
14104 from_a: bool,
14105 face: &GFace,
14106 surface_id: ogeom_topo::SurfaceId,
14107 (tag, nodes): (&Tag, Option<[Point2; 2]>),
14108 tol: Tolerances,
14109) -> OgeomResult<Shape> {
14110 match tag {
14111 Tag::Pole { pole, range } => {
14112 let p = &face.poles[*pole];
14116 let at = rebuild.vertex(p.point, tol);
14117 let model = &mut *rebuild.model;
14118 let mut data = ogeom_topo::EdgeData::new();
14119 data.degenerate = true;
14120 let built = model.add_edge(data, &[at.clone(), at])?;
14121 ogeom_algo::attach_pcurve(
14122 model,
14123 &built,
14124 p.pcurve.clone(),
14125 surface_id,
14126 Location::identity(),
14127 *range,
14128 )?;
14129 Ok(built)
14130 }
14131 Tag::Boundary { edge, range } => {
14132 let e = &face.edges[*edge];
14133 let whole = e.other_side.is_none()
14134 && (range.0 - e.crange.0).abs() <= tol.parametric()
14135 && (range.1 - e.crange.1).abs() <= tol.parametric();
14136 let seam = match &e.seam {
14141 Some(pair) if e.other_side.is_none() && face.own_surface == Some(surface_id) => {
14142 Some(pair)
14143 }
14144 _ => None,
14145 };
14146 if whole && let Some(built) = rebuild.whole_edges.get(&e.node).cloned() {
14147 let attached = rebuild
14148 .model
14149 .node(&built)
14150 .and_then(|n| n.data().as_edge())
14151 .is_some_and(|d| d.pcurve_on(surface_id).is_some());
14152 match seam {
14153 Some(_) if attached => {}
14156 Some((forward, reversed)) => ogeom_algo::attach_seam(
14157 rebuild.model,
14158 &built,
14159 forward.clone(),
14160 reversed.clone(),
14161 surface_id,
14162 Location::identity(),
14163 e.prange,
14164 )?,
14165 None => ogeom_algo::attach_pcurve(
14166 rebuild.model,
14167 &built,
14168 e.pcurve.clone(),
14169 surface_id,
14170 Location::identity(),
14171 e.prange,
14172 )?,
14173 }
14174 return Ok(built);
14175 }
14176 let from = e.curve.point_at(range.0, tol)?;
14177 let to = e.curve.point_at(range.1, tol)?;
14178 let v0 = rebuild.vertex(from, tol);
14179 let v1 = rebuild.vertex(to, tol);
14180 let model = &mut *rebuild.model;
14181 let built = make_edge_between(model, (*e.curve).clone(), *range, &v0, &v1, tol)?.shape;
14182 if e.tolerance > ogeom_core::Tolerance::MIN.get() {
14192 if let Some(node) = model.node_mut(&built)
14193 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
14194 {
14195 data.tolerance = data.tolerance.widen_to(e.tolerance);
14196 }
14197 for v in [&v0, &v1] {
14198 rebuild.widen(v, e.tolerance)?;
14199 }
14200 }
14201 let model = &mut *rebuild.model;
14202 let sub_p = (
14203 rescale(range.0, e.crange, e.prange),
14204 rescale(range.1, e.crange, e.prange),
14205 );
14206 match (&e.other_side, seam) {
14207 (None, Some((forward, reversed))) => ogeom_algo::attach_seam(
14208 model,
14209 &built,
14210 forward.clone(),
14211 reversed.clone(),
14212 surface_id,
14213 Location::identity(),
14214 sub_p,
14215 )?,
14216 (None, None) => ogeom_algo::attach_pcurve(
14217 model,
14218 &built,
14219 e.pcurve.clone(),
14220 surface_id,
14221 Location::identity(),
14222 sub_p,
14223 )?,
14224 (Some((other, _)), _) => {
14225 ogeom_algo::attach_seam(
14229 model,
14230 &built,
14231 e.pcurve.clone(),
14232 other.clone(),
14233 surface_id,
14234 Location::identity(),
14235 sub_p,
14236 )?;
14237 }
14238 }
14239 if !whole
14245 && let Some(gap) = ogeom_algo::edge_pcurve_gap(model, &built, tol)?
14246 && let Some(data) = model.node(&built).and_then(|n| n.data().as_edge())
14247 && gap > data.tolerance.get()
14248 {
14249 let widened = ogeom_core::Tolerance::new(gap * (1.0 + 1e-6))?;
14250 if let Some(node) = model.node_mut(&built)
14251 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
14252 {
14253 data.tolerance = data.tolerance.widen(widened);
14254 }
14255 for v in [&v0, &v1] {
14256 rebuild.widen(v, widened.get())?;
14257 }
14258 }
14259 if whole {
14260 rebuild.whole_edges.insert(e.node, built.clone());
14261 }
14262 Ok(built)
14263 }
14264 Tag::Contact { contact, range } => {
14265 let c = &fused.contacts[*contact];
14266 let from = c.curve.point_at(range.0, tol)?;
14267 let to = c.curve.point_at(range.1, tol)?;
14268 let v0 = rebuild.vertex(from, tol);
14269 let v1 = rebuild.vertex(to, tol);
14270 let model = &mut *rebuild.model;
14271 let built = make_edge_between(model, c.curve.clone(), *range, &v0, &v1, tol)?.shape;
14272 if c.tolerance > ogeom_core::Tolerance::MIN.get() {
14276 if let Some(node) = model.node_mut(&built)
14277 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
14278 {
14279 data.tolerance = data.tolerance.widen_to(c.tolerance);
14280 }
14281 for v in [&v0, &v1] {
14282 rebuild.widen(v, c.tolerance)?;
14283 }
14284 }
14285 let model = &mut *rebuild.model;
14286 let sub_p = (
14289 rescale(range.0, c.crange, c.prange),
14290 rescale(range.1, c.crange, c.prange),
14291 );
14292 let mid = c.pcurve.point_at(f64::midpoint(sub_p.0, sub_p.1), tol)?;
14293 let trim: Vec<&[Point2]> = face.trim_lines(tol).iter().map(Vec::as_slice).collect();
14294 let folded = fold_inside(mid, &face.surface, &trim, tol);
14295 let shifted = c
14296 .pcurve
14297 .transformed(&ogeom_math::Transform2::translation(folded - mid), tol)?;
14298 ogeom_algo::attach_pcurve(
14299 model,
14300 &built,
14301 shifted,
14302 surface_id,
14303 Location::identity(),
14304 sub_p,
14305 )?;
14306 if let Some(gap) = ogeom_algo::edge_pcurve_gap(model, &built, tol)?
14311 && let Some(data) = model.node(&built).and_then(|n| n.data().as_edge())
14312 && gap > data.tolerance.get()
14313 {
14314 let widened = ogeom_core::Tolerance::new(gap * (1.0 + 1e-6))?;
14315 if let Some(node) = model.node_mut(&built)
14316 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
14317 {
14318 data.tolerance = data.tolerance.widen(widened);
14319 }
14320 for v in [&v0, &v1] {
14321 rebuild.widen(v, widened.get())?;
14322 }
14323 }
14324 Ok(built)
14325 }
14326 Tag::Section { section, range } => {
14327 let s = &fused.sections[*section];
14328 let domain = s.curve.domain();
14329 let (f0, f1) = folded_range(*range, domain, s.closed);
14330 let from = s.curve.point_at(at_param(f0, domain, s.closed), tol)?;
14331 let to = s.curve.point_at(at_param(f1, domain, s.closed), tol)?;
14332 let v0 = rebuild.vertex(from, tol);
14333 let v1 = rebuild.vertex(to, tol);
14334 let model = &mut *rebuild.model;
14335 let built = make_edge_between(model, s.curve.clone(), (f0, f1), &v0, &v1, tol)?.shape;
14336 let at = |v: &Shape| {
14342 model
14343 .node(v)
14344 .and_then(|n| n.data().as_vertex())
14345 .map(|d| d.point)
14346 };
14347 let ends = [at(&v0), at(&v1)];
14348 let welded = [(ends[0], from), (ends[1], to)]
14349 .iter()
14350 .filter_map(|(v, p)| v.map(|v| v.distance(*p)))
14351 .fold(0.0, f64::max);
14352 let own = s.tolerance.max(welded);
14353 if own > ogeom_core::Tolerance::MIN.get() {
14358 if let Some(node) = model.node_mut(&built)
14359 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
14360 {
14361 data.tolerance = data.tolerance.widen_to(own);
14362 }
14363 for v in [&v0, &v1] {
14364 rebuild.widen(v, own)?;
14365 }
14366 }
14367 let model = &mut *rebuild.model;
14368 let pcurve = if from_a { &s.pc_a } else { &s.pc_b };
14376 let mid = pcurve.point_at(f64::midpoint(f0, f1), tol)?;
14377 let trim: Vec<&[Point2]> = face.trim_lines(tol).iter().map(Vec::as_slice).collect();
14378 let folded = fold_inside(mid, &face.surface, &trim, tol);
14379 let shifted =
14380 pcurve.transformed(&ogeom_math::Transform2::translation(folded - mid), tol)?;
14381 let loose = s.tolerance.max(tol.confusion() * 1e2);
14386 let bent = [
14387 rebuild.onto_vertex.contains(&v0.node()),
14388 rebuild.onto_vertex.contains(&v1.node()),
14389 ];
14390 let targets = [
14391 ends[0].filter(|v| bent[0] && v.distance(from) > loose),
14392 ends[1].filter(|v| bent[1] && v.distance(to) > loose),
14393 ];
14394 let mut onto = [None, None];
14405 for (k, ((target, t), at)) in targets.iter().zip([f0, f1]).zip([from, to]).enumerate() {
14406 let end = shifted.point_at(t, tol)?;
14407 if let Some(target) = target {
14408 onto[k] = Some(chart_foot(&face.surface, *target, end, tol)?);
14409 continue;
14410 }
14411 let Some(node) = nodes.map(|n| n[k]) else {
14412 continue;
14413 };
14414 let node = period_shifts(&face.surface, tol)
14415 .into_iter()
14416 .map(|(du, dv)| Point2::new(node.x + du, node.y + dv))
14417 .min_by(|p, q| p.distance(end).total_cmp(&q.distance(end)))
14418 .unwrap_or(node);
14419 let vertex = if k == 0 { &v0 } else { &v1 };
14420 let radius = model.tolerance_of(vertex)?.map_or(0.0, |r| r.get());
14421 if node.distance(end) > PARAM_SNAP
14422 && !on_collapsed_row(&face.surface, node, radius, tol)
14423 && face.surface.point_at(node.x, node.y, tol)?.distance(at)
14424 < face.surface.point_at(end.x, end.y, tol)?.distance(at)
14425 {
14426 onto[k] = Some(node);
14427 }
14428 }
14429 let shifted = if onto.iter().any(Option::is_some) {
14430 pcurve_onto_ends(&shifted, (f0, f1), &face.surface, onto, tol)?
14431 } else {
14432 shifted
14433 };
14434 ogeom_algo::attach_pcurve(
14435 model,
14436 &built,
14437 shifted,
14438 surface_id,
14439 Location::identity(),
14440 (f0, f1),
14441 )?;
14442 if let Some(gap) = ogeom_algo::edge_pcurve_gap(model, &built, tol)?
14447 && let Some(data) = model.node(&built).and_then(|n| n.data().as_edge())
14448 && gap > data.tolerance.get()
14449 {
14450 let widened = ogeom_core::Tolerance::new(gap * (1.0 + 1e-6))?;
14451 if let Some(node) = model.node_mut(&built)
14452 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
14453 {
14454 data.tolerance = data.tolerance.widen(widened);
14455 }
14456 for v in [&v0, &v1] {
14457 rebuild.widen(v, widened.get())?;
14458 }
14459 }
14460 Ok(built)
14461 }
14462 }
14463}
14464
14465fn chart_foot(
14471 surface: &SurfaceGeometry,
14472 target: Point,
14473 end: Point2,
14474 tol: Tolerances,
14475) -> OgeomResult<Point2> {
14476 let foot = ogeom_algo::project_on_surface_from(surface, target, (end.x, end.y), tol)?;
14477 let raw = Point2::new(foot.parameters.0, foot.parameters.1);
14478 let turn = core::f64::consts::TAU;
14479 let mut shifts = period_shifts(surface, tol);
14480 for du in [-turn, 0.0, turn] {
14481 for dv in [-turn, 0.0, turn] {
14482 shifts.push((du, dv));
14483 }
14484 }
14485 Ok(shifts
14486 .into_iter()
14487 .map(|(du, dv)| Point2::new(raw.x + du, raw.y + dv))
14488 .filter(|c| {
14489 surface
14490 .point_at(c.x, c.y, tol)
14491 .is_ok_and(|q| q.distance(foot.point) <= tol.confusion() * 1e2)
14492 })
14493 .min_by(|a, b| {
14494 a.distance(end)
14495 .partial_cmp(&b.distance(end))
14496 .unwrap_or(core::cmp::Ordering::Equal)
14497 })
14498 .unwrap_or(raw))
14499}
14500
14501fn on_collapsed_row(surface: &SurfaceGeometry, at: Point2, radius: f64, tol: Tolerances) -> bool {
14506 let ((u0, u1), (v0, v1)) = surface.domain();
14507 let Ok((du, dv)) = surface.d1_at(at.x, at.y, tol) else {
14508 return true;
14509 };
14510 du.magnitude() * (u1 - u0).abs() <= radius || dv.magnitude() * (v1 - v0).abs() <= radius
14511}
14512
14513fn pcurve_onto_ends(
14526 pcurve: &PlanarCurve,
14527 range: (f64, f64),
14528 surface: &SurfaceGeometry,
14529 targets: [Option<Point2>; 2],
14530 tol: Tolerances,
14531) -> OgeomResult<PlanarCurve> {
14532 const SAMPLES: usize = 32;
14533 let ends = [
14534 pcurve.point_at(range.0, tol)?,
14535 pcurve.point_at(range.1, tol)?,
14536 ];
14537 let cost = |at: Point2, by: ogeom_math::Vector2| -> Option<f64> {
14539 let (du, dv) = surface.d1_at(at.x, at.y, tol).ok()?;
14540 Some((du * by.x + dv * by.y).magnitude())
14541 };
14542 let mut deltas = [ogeom_math::Vector2::new(0.0, 0.0); 2];
14543 let mut at_end = [None; 2];
14544 for (k, target) in targets.iter().enumerate() {
14545 let Some(target) = target else {
14546 continue;
14547 };
14548 deltas[k] = *target - ends[k];
14549 at_end[k] = cost(ends[k], deltas[k]);
14550 }
14551 let share = |k: usize, p: Point2, linear: f64| -> f64 {
14555 let (Some(end), Some(here)) = (at_end[k], cost(p, deltas[k])) else {
14556 return linear;
14557 };
14558 if here > end {
14559 linear * end / here
14560 } else {
14561 linear
14562 }
14563 };
14564 let (lo, hi) = (range.0.min(range.1), range.0.max(range.1));
14568 let stations = ogeom_algo::traced::stations_2d(pcurve, (lo, hi), SAMPLES);
14569 let fitted = ogeom_algo::traced::fit_traced_2d(
14570 |t| {
14571 let along = if hi > lo {
14572 (t - range.0) / (range.1 - range.0)
14573 } else {
14574 0.0
14575 };
14576 let p = pcurve.point_at(t, tol)?;
14577 Ok(p + deltas[0] * share(0, p, 1.0 - along) + deltas[1] * share(1, p, along))
14578 },
14579 &stations,
14580 3,
14581 tol.confusion() * 10.0,
14582 tol,
14583 )?;
14584 Ok(PlanarCurve::BSpline(fitted.curve))
14585}
14586
14587fn assemble_result(
14593 model: &mut Model,
14594 fused: &GeneralFused,
14595 kept: &[(usize, bool)],
14596 keep_aside: [bool; 2],
14597 a: &Shape,
14598 b: &Shape,
14599 tol: Tolerances,
14600) -> OgeomResult<Built> {
14601 ogeom_core::progress::stage("boolean: assemble");
14602 let local = !fused.a.aside.is_empty() || !fused.b.aside.is_empty();
14603 let mut history = History::new();
14604 let source_face = |piece: &FacePiece| -> Shape {
14605 if piece.from_a {
14606 fused.a.faces[piece.face].face.clone()
14607 } else {
14608 fused.b.faces[piece.face].face.clone()
14609 }
14610 };
14611
14612 if kept.is_empty() {
14613 if (keep_aside[0] && !fused.a.aside.is_empty())
14614 || (keep_aside[1] && !fused.b.aside.is_empty())
14615 {
14616 ogeom_bail!(NotDone, "no piece is kept beside the faces set aside");
14617 }
14618 let empty = model.add_compound(&[])?;
14620 for piece in &fused.pieces {
14621 history.delete(&source_face(piece));
14622 }
14623 for (face, _) in fused.a.aside.iter().chain(&fused.b.aside) {
14624 history.delete(face);
14625 }
14626 history.modify(a, empty.clone());
14627 history.modify(b, empty.clone());
14628 return Ok(Built::new(empty, history));
14629 }
14630
14631 let Rebuilt {
14632 faces,
14633 settled,
14634 weld: floor,
14635 holes,
14636 kept: built_from,
14637 } = rebuilt_pieces(model, fused, kept, &mut history, tol)?;
14638 let sewn = sew_around_within(model, &faces, &settled, SEW_ACROSS.get(), tol)?;
14639 history = history.followed_by(&sewn.history);
14642 let mut after = if local {
14645 sewn.history.clone()
14646 } else {
14647 History::new()
14648 };
14649 let mut sewn = sewn;
14650 let dropped = without_membranes(model, &mut sewn.shells, floor, tol)?;
14651 history = history.followed_by(&dropped);
14652 let held: ogeom_core::FastSet<ogeom_topo::TShapeId> = fused
14653 .a
14654 .shared
14655 .values()
14656 .chain(fused.b.shared.values())
14657 .map(Shape::node)
14658 .collect();
14659 let joined = seam_join::join_across_seams(model, &mut sewn.shells, &held, tol)?;
14660 history = history.followed_by(&joined).without_repeated_images();
14661 let mut closed = None;
14662 let mut made = None;
14663 if local {
14664 after = after.followed_by(&dropped).followed_by(&joined);
14665 let (shells, shut, new) = with_aside(
14666 model,
14667 fused,
14668 &built_from,
14669 &faces,
14670 &holes,
14671 &after,
14672 &sewn.shells,
14673 keep_aside,
14674 &mut history,
14675 )?;
14676 sewn.shells = shells;
14677 closed = Some(shut);
14678 made = Some(new);
14679 }
14680 if sewn.shells.is_empty() {
14681 let empty = model.add_compound(&[])?;
14683 history.modify(a, empty.clone());
14684 history.modify(b, empty.clone());
14685 return Ok(Built::new(empty, history));
14686 }
14687 for (i, shell) in sewn.shells.iter().enumerate() {
14688 let shut = match &closed {
14689 Some(closed) => closed[i],
14690 None => is_shell_closed(model, shell)?,
14691 };
14692 if !shut {
14693 if *ARRANGE_DEBUG {
14696 use ogeom_geom::Curve3d as _;
14697 for edge in ogeom_topo::explore_unique(model, shell, ShapeType::Edge)? {
14698 let users = ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))?
14699 .iter()
14700 .filter(|f| {
14701 ogeom_topo::explore_unique(model, f, ShapeType::Edge)
14702 .map(|es| es.iter().any(|e2| e2.node() == edge.node()))
14703 .unwrap_or(false)
14704 })
14705 .count();
14706 let mut occurrences = 0_usize;
14707 for f in ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))? {
14708 for wire in model.children_of(&f)? {
14709 for e2 in model.children_of(&wire)? {
14710 if e2.node() == edge.node() {
14711 occurrences += 1;
14712 }
14713 }
14714 }
14715 }
14716 if occurrences % 2 == 1
14717 && let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge())
14718 && let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) =
14719 data.curve3d()
14720 && let Some(g) = model.geometry().curve(*curve)
14721 {
14722 let a = g.point_at(range.0, tol)?;
14723 let b = g.point_at(range.1, tol)?;
14724 eprintln!(
14725 " open edge uses={occurrences} faces={users} {:?} range={range:?}: {a:?} -> {b:?}",
14726 core::mem::discriminant(g)
14727 );
14728 for other in ogeom_topo::explore_unique(model, shell, ShapeType::Edge)? {
14731 if other.node() == edge.node() {
14732 continue;
14733 }
14734 let Some(od) = model.node(&other).and_then(|n| n.data().as_edge())
14735 else {
14736 continue;
14737 };
14738 let Some(ogeom_topo::EdgeRepr::Curve3d {
14739 curve: oc,
14740 range: orange,
14741 ..
14742 }) = od.curve3d()
14743 else {
14744 continue;
14745 };
14746 let Some(og) = model.geometry().curve(*oc) else {
14747 continue;
14748 };
14749 let oa = og.point_at(orange.0, tol)?;
14750 let ob = og.point_at(orange.1, tol)?;
14751 let same = (oa.distance(a) <= 1e-3 && ob.distance(b) <= 1e-3)
14752 || (oa.distance(b) <= 1e-3 && ob.distance(a) <= 1e-3);
14753 if same {
14754 let om = og.point_at(f64::midpoint(orange.0, orange.1), tol)?;
14755 let m = g.point_at(f64::midpoint(range.0, range.1), tol)?;
14756 eprintln!(
14757 " twin {:?} range={orange:?} mid gap {:.2e} tol {:.2e} vs {:.2e}",
14758 core::mem::discriminant(og),
14759 om.distance(m),
14760 od.tolerance.get(),
14761 data.tolerance.get()
14762 );
14763 }
14764 }
14765 for f in ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))?
14766 {
14767 let uses_it = ogeom_topo::explore_unique(model, &f, ShapeType::Edge)
14768 .map(|es| es.iter().any(|e2| e2.node() == edge.node()))
14769 .unwrap_or(false);
14770 if !uses_it {
14771 continue;
14772 }
14773 if let Some(ogeom_topo::NodeData::Face(fd)) =
14774 model.node(&f).map(|n| n.data())
14775 && let Some(sg) = model.geometry().surface(fd.surface)
14776 {
14777 let bound = shape_bounds(model, &f, tol)?;
14778 eprintln!(
14779 " used by face on {:?} bound {:?}",
14780 core::mem::discriminant(sg),
14781 bound
14782 );
14783 }
14784 }
14785 }
14786 }
14787 }
14788 if *ARRANGE_DEBUG {
14789 use ogeom_geom::Curve3d as _;
14790 for f in ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))? {
14791 let Some(ogeom_topo::NodeData::Face(fd)) = model.node(&f).map(|n| n.data())
14792 else {
14793 continue;
14794 };
14795 let Some(sg) = model.geometry().surface(fd.surface) else {
14796 continue;
14797 };
14798 let edges = ogeom_topo::explore_unique(model, &f, ShapeType::Edge)?;
14799 eprintln!(
14800 " face {:?} with {} edges",
14801 core::mem::discriminant(sg),
14802 edges.len()
14803 );
14804 if matches!(sg, SurfaceGeometry::Sphere(_)) {
14805 for e in &edges {
14806 if let Some(ed) = model.node(e).and_then(|n| n.data().as_edge())
14807 && let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) =
14808 ed.curve3d()
14809 && let Some(g) = model.geometry().curve(*curve)
14810 {
14811 let a = g.point_at(range.0, tol)?;
14812 let b = g.point_at(range.1, tol)?;
14813 eprintln!(
14814 " sphere edge {:?} ({:.4},{:.4},{:.4}) -> ({:.4},{:.4},{:.4})",
14815 core::mem::discriminant(g),
14816 a.x,
14817 a.y,
14818 a.z,
14819 b.x,
14820 b.y,
14821 b.z
14822 );
14823 }
14824 }
14825 }
14826 }
14827 }
14828 ogeom_bail!(
14829 NotDone,
14830 "the kept pieces did not close into a shell; the configuration \
14831 is beyond what the boolean currently resolves"
14832 );
14833 }
14834 }
14835
14836 let mut solids = Vec::new();
14841 for (outer, voids) in nest_shells(model, &sewn.shells, tol)? {
14842 let mut group = vec![sewn.shells[outer].clone()];
14843 group.extend(voids.iter().map(|&j| sewn.shells[j].clone()));
14844 solids.push(model.add_solid(&group)?);
14845 }
14846 let result = if solids.len() == 1 {
14847 solids.remove(0)
14848 } else {
14849 model.add_compound(&solids)?
14850 };
14851 let built_faces = match &made {
14859 Some(made) => made.clone(),
14860 None => {
14861 let given: ogeom_core::FastSet<ogeom_topo::TShapeId> = [a, b]
14862 .into_iter()
14863 .map(|operand| explore_unique(model, operand, ShapeType::Face))
14864 .collect::<OgeomResult<Vec<_>>>()?
14865 .into_iter()
14866 .flatten()
14867 .map(|face| face.node())
14868 .collect();
14869 explore_unique(model, &result, ShapeType::Face)?
14870 .into_iter()
14871 .filter(|f| !given.contains(&f.node()))
14872 .collect()
14873 }
14874 };
14875 let given_wires: ogeom_core::FastSet<ogeom_topo::TShapeId> = fused
14879 .a
14880 .faces
14881 .iter()
14882 .chain(&fused.b.faces)
14883 .map(|gathered| model.ordered_children_of(&gathered.face))
14884 .collect::<OgeomResult<Vec<_>>>()?
14885 .into_iter()
14886 .flatten()
14887 .map(|wire| wire.node())
14888 .collect();
14889 let taken = |wire: &Shape| given_wires.contains(&wire.node());
14890 for face in &built_faces {
14891 if !ogeom_algo::chart_breaks(model, face, &taken, tol)?.is_empty() {
14892 ogeom_bail!(
14893 NotDone,
14894 "a face the boolean made does not close in its surface's chart; the \
14895 configuration is beyond what the boolean currently resolves"
14896 );
14897 }
14898 }
14899 match &made {
14902 Some(made) => keep_face_boxes(model, made)?,
14903 None => keep_face_boxes(model, &explore_unique(model, &result, ShapeType::Face)?)?,
14904 }
14905 history.modify(a, result.clone());
14906 history.modify(b, result.clone());
14907 Ok(Built::new(result, history))
14908}
14909
14910fn keep_face_boxes(model: &Model, faces: &[Shape]) -> OgeomResult<()> {
14914 for face in faces {
14915 if model.face_bounds(face).is_some() {
14916 continue;
14917 }
14918 let ruled = model
14919 .node(face)
14920 .and_then(|n| n.data().as_face())
14921 .and_then(|d| model.geometry().surface(d.surface))
14922 .is_some_and(|s| {
14923 matches!(
14924 s,
14925 SurfaceGeometry::Plane(_)
14926 | SurfaceGeometry::Cylinder(_)
14927 | SurfaceGeometry::Cone(_)
14928 )
14929 });
14930 if ruled {
14931 ogeom_algo::face_bounds(model, face)?;
14932 }
14933 }
14934 Ok(())
14935}
14936
14937fn assemble_sheet(
14942 model: &mut Model,
14943 fused: &GeneralFused,
14944 kept: &[usize],
14945 sheet: &Shape,
14946 tol: Tolerances,
14947) -> OgeomResult<Built> {
14948 ogeom_core::progress::stage("boolean: assemble");
14949 let mut history = History::new();
14950 let whole = model.kind_of(sheet)? != ShapeType::Face;
14951 if kept.is_empty() {
14952 let empty = model.add_compound(&[])?;
14953 for piece in &fused.pieces {
14954 history.delete(&fused.a.faces[piece.face].face);
14955 }
14956 if whole {
14957 history.modify(sheet, empty.clone());
14958 }
14959 return Ok(Built::new(empty, history));
14960 }
14961 let kept: Vec<(usize, bool)> = kept.iter().map(|&i| (i, false)).collect();
14962 let Rebuilt { faces, settled, .. } = rebuilt_pieces(model, fused, &kept, &mut history, tol)?;
14963 let mut sewn = sew_around(model, &faces, &settled, tol)?;
14964 history = history.followed_by(&sewn.history);
14965 let joined = seam_join::join_across_seams(
14966 model,
14967 &mut sewn.shells,
14968 &ogeom_core::FastSet::default(),
14969 tol,
14970 )?;
14971 history = history.followed_by(&joined).without_repeated_images();
14972 let result = if sewn.shells.len() == 1 {
14973 sewn.shells.remove(0)
14974 } else {
14975 model.add_compound(&sewn.shells)?
14976 };
14977 if whole {
14978 history.modify(sheet, result.clone());
14979 }
14980 Ok(Built::new(result, history))
14981}
14982
14983fn rebuilt_pieces(
14988 model: &mut Model,
14989 fused: &GeneralFused,
14990 kept: &[(usize, bool)],
14991 history: &mut History,
14992 tol: Tolerances,
14993) -> OgeomResult<Rebuilt> {
14994 let source_face = |piece: &FacePiece| -> Shape {
14995 if piece.from_a {
14996 fused.a.faces[piece.face].face.clone()
14997 } else {
14998 fused.b.faces[piece.face].face.clone()
14999 }
15000 };
15001 let weld = fused
15002 .contacts
15003 .iter()
15004 .map(|c| c.tolerance)
15005 .chain(fused.sections.iter().map(|s| s.tolerance))
15006 .fold(0.0_f64, |acc, t| acc.max(honest(t, tol) * 3.0));
15007 let floor = weld.max(tol.confusion() * 1e2);
15008 let junction_reach = fused
15009 .junctions
15010 .iter()
15011 .fold(0.0_f64, |acc, j| acc.max(j.reach));
15012 let mut junction_bins = bins::Bins::new(junction_reach + floor);
15013 for (index, j) in fused.junctions.iter().enumerate() {
15014 junction_bins.insert(j.at, index);
15015 }
15016 let loosest = fused
15020 .a
15021 .faces
15022 .iter()
15023 .chain(fused.b.faces.iter())
15024 .flat_map(|f| f.edges.iter().map(|e| e.tolerance))
15025 .chain(fused.contacts.iter().map(|c| c.tolerance))
15026 .chain(fused.sections.iter().map(|s| s.tolerance))
15027 .fold(junction_reach, f64::max);
15028 let mut rebuild = Rebuild {
15029 model,
15030 surfaces_a: vec![None; fused.a.faces.len()],
15031 surfaces_b: vec![None; fused.b.faces.len()],
15032 vertices: Vec::new(),
15033 vertex_bins: bins::Bins::new(floor + loosest),
15034 near: Vec::new(),
15035 widest: 0.0,
15036 weld,
15037 junctions: fused.junctions.iter().map(|j| (*j, None)).collect(),
15038 junction_bins,
15039 junction_reach,
15040 onto_vertex: ogeom_core::FastSet::default(),
15041 whole_edges: ogeom_core::FastMap::default(),
15042 kept: ogeom_core::FastMap::default(),
15043 strained: false,
15044 };
15045 for (at, vertex) in fused
15046 .a
15047 .shared_vertices
15048 .iter()
15049 .chain(&fused.b.shared_vertices)
15050 {
15051 rebuild.remember(*at, vertex);
15052 rebuild.kept.insert(vertex.node(), *at);
15053 }
15054 let clean = |piece: &FacePiece, model: &Model| -> OgeomResult<bool> {
15060 if !piece.whole {
15061 return Ok(false);
15062 }
15063 let face = &source_face(piece);
15064 for vertex in explore_unique(model, face, ShapeType::Vertex)? {
15065 let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) else {
15066 continue;
15067 };
15068 let at = vertex.transform(model.datums())?.apply(data.point);
15069 if fused
15070 .junctions
15071 .iter()
15072 .any(|j| j.at.distance(at) <= j.reach + floor)
15073 {
15074 return Ok(false);
15075 }
15076 }
15077 Ok(true)
15078 };
15079 let mut pieces_built: Vec<(usize, bool, Shape)> = Vec::with_capacity(kept.len());
15082 for &(index, flip) in kept {
15083 if let Some(built) = build_piece(&mut rebuild, fused, &fused.pieces[index], tol)? {
15084 pieces_built.push((index, flip, built));
15085 }
15086 }
15087 let kept: Vec<(usize, bool)> = pieces_built.iter().map(|&(i, f, _)| (i, f)).collect();
15088 let kept = kept.as_slice();
15089 let mut copies = Vec::with_capacity(kept.len());
15092 for &(index, flip) in kept {
15093 copies.push(!flip && clean(&fused.pieces[index], rebuild.model)?);
15094 }
15095 let mut faces = Vec::new();
15096 let mut kept_sources: ogeom_core::FastSet<Shape> = ogeom_core::FastSet::default();
15097 let mut built_of: ogeom_core::FastMap<usize, usize> = ogeom_core::FastMap::default();
15098 for (slot, (index, flip, built)) in pieces_built.into_iter().enumerate() {
15099 built_of.insert(index, slot);
15100 let piece = &fused.pieces[index];
15101 let mut built = built;
15102 if flip {
15103 built = built.reversed();
15104 }
15105 if copies[slot] {
15106 history.copy(&source_face(piece), built.clone());
15107 } else {
15108 history.modify(&source_face(piece), built.clone());
15109 }
15110 kept_sources.insert(source_face(piece));
15111 faces.push(built);
15112 }
15113 if rebuild.strained {
15114 ogeom_bail!(
15115 NotDone,
15116 "a strand ends further from a vertex shared with a face set aside \
15117 than the vertex reaches"
15118 );
15119 }
15120 let holes = holes_put_back(rebuild.model, fused, kept, tol)?;
15121 let mut stood_for: ogeom_core::FastSet<Shape> = ogeom_core::FastSet::default();
15124 for &(index_a, index_b) in &fused.coincident {
15125 let (dropped, standing) = match (built_of.get(&index_a), built_of.get(&index_b)) {
15126 (Some(&slot), None) => (index_b, slot),
15127 (None, Some(&slot)) => (index_a, slot),
15128 _ => continue,
15129 };
15130 let source = source_face(&fused.pieces[dropped]);
15131 if !history
15132 .modified(&source)
15133 .iter()
15134 .any(|f| f.is_same(&faces[standing]))
15135 {
15136 history.modify(&source, faces[standing].clone());
15137 }
15138 stood_for.insert(source);
15139 }
15140 for piece in &fused.pieces {
15141 let source = source_face(piece);
15142 if !kept_sources.contains(&source) && !stood_for.contains(&source) {
15143 history.delete(&source);
15144 }
15145 }
15146
15147 let class = |slot: usize| -> u8 {
15154 match (copies[slot], fused.pieces[kept[slot].0].from_a) {
15155 (true, true) => 1,
15156 (true, false) => 2,
15157 (false, true) => 4,
15158 (false, false) => 8,
15159 }
15160 };
15161 let mut held: ogeom_core::FastMap<ogeom_topo::TShapeId, u8> = ogeom_core::FastMap::default();
15162 let mut corners = Vec::with_capacity(faces.len());
15163 for (slot, face) in faces.iter().enumerate() {
15164 let vertices = explore_unique(rebuild.model, face, ShapeType::Vertex)?;
15165 for vertex in &vertices {
15166 *held.entry(vertex.node()).or_default() |= class(slot);
15167 }
15168 corners.push(vertices);
15169 }
15170 let settled = corners
15171 .iter()
15172 .enumerate()
15173 .map(|(slot, vertices)| {
15174 let own = if fused.pieces[kept[slot].0].from_a {
15175 1 | 4
15176 } else {
15177 2 | 8
15178 };
15179 copies[slot]
15180 && vertices
15181 .iter()
15182 .all(|v| held.get(&v.node()).is_some_and(|by| by & !own == 0))
15183 })
15184 .collect();
15185
15186 Ok(Rebuilt {
15187 faces,
15188 settled,
15189 weld: floor,
15190 holes,
15191 kept: kept.to_vec(),
15192 })
15193}
15194
15195#[allow(
15201 clippy::too_many_arguments,
15202 reason = "the assembly's own state, read once"
15203)]
15204fn with_aside(
15205 model: &mut Model,
15206 fused: &GeneralFused,
15207 kept: &[(usize, bool)],
15208 faces: &[Shape],
15209 holes: &[Vec<usize>],
15210 after: &History,
15211 shells: &[Shape],
15212 keep_aside: [bool; 2],
15213 history: &mut History,
15214) -> OgeomResult<(Vec<Shape>, Vec<bool>, Vec<Shape>)> {
15215 if kept.len() != faces.len() {
15216 ogeom_bail!(
15217 Construction,
15218 "{} kept pieces for {} rebuilt faces; each face is read with its own piece",
15219 kept.len(),
15220 faces.len()
15221 );
15222 }
15223 let mut remade: ogeom_core::FastMap<ogeom_topo::TShapeId, Remade> =
15226 ogeom_core::FastMap::default();
15227 let mut put_back = History::new();
15228 let mut rank: ogeom_core::FastMap<ogeom_topo::TShapeId, (bool, usize, usize)> =
15231 ogeom_core::FastMap::default();
15232 for (slot, built) in faces.iter().enumerate() {
15233 let piece = &fused.pieces[kept[slot].0];
15234 let own = if piece.from_a { &fused.a } else { &fused.b };
15235 let source = &own.faces[piece.face].face;
15236 let place = (
15237 !piece.from_a,
15238 own.order
15239 .get(&ogeom_topo::SameKey(source.clone()))
15240 .copied()
15241 .unwrap_or(usize::MAX),
15242 slot,
15243 );
15244 rank.insert(built.node(), place);
15245 for image in after.modified(built) {
15246 rank.insert(image.node(), place);
15247 }
15248 }
15249 for (slot, (built, holes)) in faces.iter().zip(holes).enumerate() {
15250 if holes.is_empty() {
15251 continue;
15252 }
15253 let image = match after.modified(built) {
15254 [] if !after.is_deleted(built) => built.clone(),
15255 [image] => image.clone(),
15256 _ => ogeom_bail!(NotDone, "a face taking back its holes was split or dropped"),
15257 };
15258 let piece = &fused.pieces[kept[slot].0];
15259 let own = if piece.from_a { &fused.a } else { &fused.b };
15260 let source = &own.faces[piece.face];
15261 let side = usize::from(!piece.from_a);
15262 let wires: Vec<Shape> = holes
15263 .iter()
15264 .map(|&h| source.holes_aside[h].wire.clone())
15265 .collect();
15266 let joins: Vec<(usize, u32)> = holes
15267 .iter()
15268 .flat_map(|&h| source.holes_aside[h].beside.iter().map(move |&j| (side, j)))
15269 .collect();
15270 let walk = model.node(&image).map_or(0, |n| n.children().len());
15271 let made = with_holes(model, &image, &wires)?;
15272 ogeom_algo::face_bounds(model, &image)?;
15276 let found = model.kept_face_box(&image, |_| {
15277 Err(ogeom_core::ogeom_err!(
15278 NotDone,
15279 "the face's box was not kept"
15280 ))
15281 });
15282 if let Ok(found) = found {
15283 model.kept_face_box(&made, |_| Ok(found))?;
15284 }
15285 let copied = after.copy_of(built).is_some() || after.modified(built).is_empty();
15286 if copied {
15287 put_back.copy(&image, made.clone());
15288 } else {
15289 put_back.modify(&image, made.clone());
15290 }
15291 remade.insert(image.node(), (made, walk, joins));
15292 }
15293 let mut all = Vec::new();
15296 for shell in shells {
15297 for face in model.children_of(shell)? {
15298 let place = rank
15299 .get(&face.node())
15300 .copied()
15301 .unwrap_or((true, usize::MAX, usize::MAX));
15302 all.push(match remade.remove(&face.node()) {
15303 Some((made, walk, joins)) => (
15304 place,
15305 aside::Member {
15306 face: made,
15307 aside: None,
15308 walk,
15309 joins,
15310 },
15311 ),
15312 None => (
15313 place,
15314 aside::Member {
15315 face,
15316 aside: None,
15317 walk: usize::MAX,
15318 joins: Vec::new(),
15319 },
15320 ),
15321 });
15322 }
15323 }
15324 *history = core::mem::take(history).followed_by(&put_back);
15325 for (g, keep, from_b) in [
15328 (&fused.a, keep_aside[0], false),
15329 (&fused.b, keep_aside[1], true),
15330 ] {
15331 for (face, place) in &g.aside {
15332 if keep {
15333 history.copy(face, face.clone());
15334 all.push((
15335 (from_b, *place, 0),
15336 aside::Member {
15337 face: face.clone(),
15338 aside: Some((usize::from(from_b), *place)),
15339 walk: 0,
15340 joins: Vec::new(),
15341 },
15342 ));
15343 } else {
15344 history.delete(face);
15345 }
15346 }
15347 }
15348 all.sort_by_key(|(place, _)| *place);
15351 let all: Vec<aside::Member> = all.into_iter().map(|(_, member)| member).collect();
15352 if all.is_empty() {
15353 return Ok((Vec::new(), Vec::new(), Vec::new()));
15354 }
15355 let made = all
15356 .iter()
15357 .filter(|member| member.aside.is_none())
15358 .map(|member| member.face.clone())
15359 .collect();
15360 let (shells, closed) = aside::shells_of(model, &all, [&fused.a.beside, &fused.b.beside])?;
15361 Ok((shells, closed, made))
15362}
15363
15364type Remade = (Shape, usize, Vec<(usize, u32)>);
15367
15368struct Rebuilt {
15370 faces: Vec<Shape>,
15372 settled: Vec<bool>,
15374 weld: f64,
15376 holes: Vec<Vec<usize>>,
15379 kept: Vec<(usize, bool)>,
15382}
15383
15384fn holes_put_back(
15393 model: &Model,
15394 fused: &GeneralFused,
15395 kept: &[(usize, bool)],
15396 tol: Tolerances,
15397) -> OgeomResult<Vec<Vec<usize>>> {
15398 let mut holes = vec![Vec::new(); kept.len()];
15399 let mut by_face: std::collections::BTreeMap<(bool, usize), Vec<usize>> =
15400 std::collections::BTreeMap::new();
15401 for (slot, &(index, _)) in kept.iter().enumerate() {
15402 let piece = &fused.pieces[index];
15403 let own = if piece.from_a { &fused.a } else { &fused.b };
15404 if !own.faces[piece.face].holes_aside.is_empty() {
15405 by_face
15406 .entry((piece.from_a, piece.face))
15407 .or_default()
15408 .push(slot);
15409 }
15410 }
15411 for ((from_a, fi), slots) in by_face {
15412 let face = if from_a {
15413 &fused.a.faces[fi]
15414 } else {
15415 &fused.b.faces[fi]
15416 };
15417 let outside = slots
15418 .iter()
15419 .any(|&slot| fused.pieces[kept[slot].0].state == PieceState::Out);
15420 if let [slot] = slots.as_slice()
15421 && outside
15422 {
15423 holes[*slot] = (0..face.holes_aside.len()).collect();
15424 continue;
15425 }
15426 for (h, hole) in face.holes_aside.iter().enumerate() {
15427 let Some(at) = hole_point(model, face, &hole.wire, tol)? else {
15428 ogeom_bail!(NotDone, "a hole set aside has no point in its face's chart");
15429 };
15430 let mut within = slots
15431 .iter()
15432 .filter(|&&slot| inside_rings(&fused.pieces[kept[slot].0].outlines, at));
15433 match (within.next(), within.next()) {
15434 (Some(&slot), None) if fused.pieces[kept[slot].0].state == PieceState::Out => {
15435 holes[slot].push(h);
15436 }
15437 (None, _) if !outside => {}
15438 _ => ogeom_bail!(
15439 NotDone,
15440 "a hole set aside lies in no one piece standing outside the other solid"
15441 ),
15442 }
15443 }
15444 }
15445 Ok(holes)
15446}
15447
15448fn hole_point(
15450 model: &Model,
15451 face: &GFace,
15452 hole: &Shape,
15453 tol: Tolerances,
15454) -> OgeomResult<Option<Point2>> {
15455 let Some(surface) = face.own_surface else {
15456 return Ok(None);
15457 };
15458 let Some(edge) = model.children_of(hole)?.into_iter().next() else {
15459 return Ok(None);
15460 };
15461 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
15462 return Ok(None);
15463 };
15464 let (pc, range) = match data.pcurve_for(surface, edge.location()) {
15465 Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
15466 Some(EdgeRepr::Seam { forward, range, .. }) => (*forward, *range),
15467 _ => return Ok(None),
15468 };
15469 let Some(pcurve) = model.geometry().pcurve(pc) else {
15470 return Ok(None);
15471 };
15472 Ok(Some(pcurve.point_at(f64::midpoint(range.0, range.1), tol)?))
15473}
15474
15475fn with_holes(model: &mut Model, image: &Shape, holes: &[Shape]) -> OgeomResult<Shape> {
15478 if !image.location().is_identity() {
15479 ogeom_bail!(
15480 NotDone,
15481 "a face taking back its holes stands under a placement"
15482 );
15483 }
15484 let Some(NodeData::Face(data)) = model.node(image).map(|n| n.data().clone()) else {
15485 ogeom_bail!(Dangling, "face is not in this model");
15486 };
15487 let mut data = *data;
15488 data.triangulation = None;
15489 if data.tolerance.get() > ogeom_core::Tolerance::MIN.get() {
15493 for hole in holes {
15494 for edge in model.children_of(hole)? {
15495 if model
15496 .node(&edge)
15497 .and_then(|n| n.data().as_edge())
15498 .is_none_or(|e| e.tolerance.get() < data.tolerance.get())
15499 {
15500 ogeom_bail!(NotDone, "a face is looser than a hole it takes back");
15501 }
15502 }
15503 }
15504 }
15505 let mut wires = model.children_of(&Shape::of(image.node()))?;
15506 wires.extend(holes.iter().cloned());
15507 Ok(model.add_face(data, &wires)?.oriented(image.orientation()))
15508}
15509
15510fn without_membranes(
15523 model: &mut Model,
15524 shells: &mut Vec<Shape>,
15525 weld: f64,
15526 tol: Tolerances,
15527) -> OgeomResult<History> {
15528 use ogeom_geom::Transformable as _;
15529 let mut dropped = History::new();
15530 let within = MEMBRANE_WITHIN.get();
15531 let bounding = |model: &Model, face: &Shape| -> OgeomResult<Vec<ogeom_topo::TShapeId>> {
15532 let mut edges: Vec<ogeom_topo::TShapeId> = explore_unique(model, face, ShapeType::Edge)?
15533 .iter()
15534 .filter(|e| {
15535 model
15536 .node(e)
15537 .and_then(|n| n.data().as_edge())
15538 .is_some_and(|d| !d.degenerate)
15539 })
15540 .map(Shape::node)
15541 .collect();
15542 edges.sort_unstable();
15543 Ok(edges)
15544 };
15545 let membrane = |model: &Model, first: &Shape, second: &Shape| -> OgeomResult<bool> {
15547 let (at, n) = ogeom_algo::face_normal(model, first, tol)?;
15548 let (_, m) = ogeom_algo::face_normal(model, second, tol)?;
15549 if n.dot(m) > -0.99 * n.magnitude() * m.magnitude() {
15550 return Ok(false);
15551 }
15552 let mesh =
15557 ogeom_mesh::triangulate_face(model, first, ogeom_mesh::Deflection::default(), tol)?;
15558 let mut probes: Vec<Point> = mesh
15559 .triangles
15560 .iter()
15561 .map(|t| {
15562 let [p, q, r] = t.map(|i| mesh.positions[i as usize]);
15563 Point::from_vector((p.to_vector() + q.to_vector() + r.to_vector()) / 3.0)
15564 })
15565 .collect();
15566 if probes.is_empty() {
15567 probes.push(at);
15568 }
15569 let Some(NodeData::Face(data)) = model.node(second).map(|n| n.data()) else {
15570 return Ok(false);
15571 };
15572 let Some(surface) = model.geometry().surface(data.surface) else {
15573 return Ok(false);
15574 };
15575 let placed = surface.transformed(&second.transform(model.datums())?, tol)?;
15576 let owned = explore_unique(model, first, ShapeType::Vertex)?
15579 .iter()
15580 .filter_map(|v| model.node(v).and_then(|n| n.data().as_vertex()))
15581 .fold(0.0_f64, |acc, d| acc.max(d.tolerance.get()));
15582 let reach = (weld + owned).min(within);
15583 for probe in probes {
15584 let foot = ogeom_algo::project_on_surface(&placed, probe, 16, tol)?;
15585 if foot.point.distance(probe) > reach {
15586 return Ok(false);
15587 }
15588 }
15589 Ok(true)
15590 };
15591 let mut emptied = Vec::new();
15592 for (index, shell) in shells.iter_mut().enumerate() {
15593 let faces = ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))?;
15594 let edges = faces
15595 .iter()
15596 .map(|f| bounding(model, f))
15597 .collect::<OgeomResult<Vec<_>>>()?;
15598 let mut alike: ogeom_core::FastMap<&[ogeom_topo::TShapeId], Vec<usize>> =
15601 ogeom_core::FastMap::default();
15602 for (i, bounded_by) in edges.iter().enumerate() {
15603 if !bounded_by.is_empty() {
15604 alike.entry(bounded_by.as_slice()).or_default().push(i);
15605 }
15606 }
15607 let mut groups: Vec<Vec<usize>> = alike.into_values().filter(|g| g.len() > 1).collect();
15608 groups.sort_unstable();
15609 let mut gone = vec![false; faces.len()];
15610 for group in &groups {
15611 for (k, &i) in group.iter().enumerate() {
15612 for &j in &group[k + 1..] {
15613 if gone[i] || gone[j] {
15614 continue;
15615 }
15616 if membrane(model, &faces[i], &faces[j])? {
15617 gone[i] = true;
15618 gone[j] = true;
15619 }
15620 }
15621 }
15622 }
15623 if !gone.contains(&true) {
15624 continue;
15625 }
15626 let mut kept = Vec::new();
15627 for (face, gone) in faces.iter().zip(&gone) {
15628 if *gone {
15629 dropped.delete(face);
15630 } else {
15631 kept.push(face.clone());
15632 }
15633 }
15634 if kept.is_empty() {
15635 emptied.push(index);
15636 } else {
15637 *shell = model.add_shell(&kept)?;
15638 }
15639 }
15640 for index in emptied.into_iter().rev() {
15641 shells.remove(index);
15642 }
15643 Ok(dropped)
15644}
15645
15646fn nest_shells(
15658 model: &Model,
15659 shells: &[Shape],
15660 tol: Tolerances,
15661) -> OgeomResult<Vec<(usize, Vec<usize>)>> {
15662 if shells.len() == 1 {
15663 return Ok(vec![(0, Vec::new())]);
15664 }
15665 let mut bounds = Vec::with_capacity(shells.len());
15666 for shell in shells {
15667 bounds.push(shape_bounds(model, shell, tol)?);
15668 }
15669 let reach = tol.confusion() * 1e2;
15673 let mut boundaries: Vec<Option<ogeom_algo::SolidBoundary>> =
15674 (0..shells.len()).map(|_| None).collect();
15675 let mut encloses = vec![vec![false; shells.len()]; shells.len()];
15676 for i in 0..shells.len() {
15677 for j in 0..shells.len() {
15678 if i == j || !bounds[i].expanded(reach).contains_box(&bounds[j]) {
15679 continue;
15680 }
15681 if boundaries[i].is_none() {
15682 boundaries[i] = Some(ogeom_algo::SolidBoundary::of(
15683 model,
15684 &shells[i],
15685 tol.confusion() * 1e4,
15686 tol,
15687 )?);
15688 }
15689 let Some(boundary) = boundaries[i].as_ref() else {
15690 continue;
15691 };
15692 let mut verdict = None;
15693 for vertex in explore_unique(model, &shells[j], ShapeType::Vertex)? {
15694 let Some(data) = model.node(&vertex).and_then(|n| n.data().as_vertex()) else {
15695 continue;
15696 };
15697 let at = vertex.transform(model.datums())?.apply(data.point);
15698 match boundary.holds(model, at, tol)? {
15699 Containment::In => verdict = Some(true),
15700 Containment::Out => verdict = Some(false),
15701 Containment::On => continue,
15702 }
15703 break;
15704 }
15705 encloses[i][j] = verdict.unwrap_or(true);
15706 }
15707 }
15708 let depth: Vec<usize> = (0..shells.len())
15709 .map(|j| (0..shells.len()).filter(|&i| encloses[i][j]).count())
15710 .collect();
15711 let mut out: Vec<(usize, Vec<usize>)> = (0..shells.len())
15712 .filter(|&i| depth[i].is_multiple_of(2))
15713 .map(|i| (i, Vec::new()))
15714 .collect();
15715 for j in (0..shells.len()).filter(|&j| !depth[j].is_multiple_of(2)) {
15716 let parent = (0..shells.len()).find(|&i| encloses[i][j] && depth[i] + 1 == depth[j]);
15717 if let Some(parent) = parent
15718 && let Some(slot) = out.iter_mut().find(|(i, _)| *i == parent)
15719 {
15720 slot.1.push(j);
15721 }
15722 }
15723 Ok(out)
15724}
15725
15726pub fn make_volume(model: &mut Model, faces: &[Shape], tol: Tolerances) -> OgeomResult<Built> {
15736 let sewn = ogeom_algo::sew(model, faces, tol)?;
15737 for shell in &sewn.shells {
15738 if !ogeom_algo::is_shell_closed(model, shell)? {
15739 ogeom_bail!(
15740 Construction,
15741 "the faces do not close into shells; an open soup encloses no volume"
15742 );
15743 }
15744 }
15745 let mut solids = Vec::new();
15746 for (outer, voids) in nest_shells(model, &sewn.shells, tol)? {
15747 let mut group = vec![sewn.shells[outer].clone()];
15748 group.extend(voids.iter().map(|&j| sewn.shells[j].reversed()));
15751 solids.push(model.add_solid(&group)?);
15752 }
15753 let mut history = History::new();
15754 let result = if solids.len() == 1 {
15755 solids.remove(0)
15756 } else {
15757 model.add_compound(&solids)?
15758 };
15759 for face in faces {
15760 history.modify(face, result.clone());
15761 }
15762 Ok(Built::new(result, history))
15763}
15764
15765#[derive(Debug)]
15769pub struct Cells {
15770 pub a_not_b: Built,
15772 pub b_not_a: Built,
15774 pub common: Built,
15776}
15777
15778pub fn cells(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Cells> {
15784 let (a, b) = (
15785 &resolved_half_space(model, a, b, tol)?,
15786 &resolved_half_space(model, b, a, tol)?,
15787 );
15788 let (a, b) = (
15789 &baked_if_scaled(model, a, tol)?,
15790 &baked_if_scaled(model, b, tol)?,
15791 );
15792 let fused = general_fuse_classified(model, a, b, None, tol)?;
15796 let keep = |choose: &dyn Fn(&FacePiece) -> Option<bool>| -> Vec<(usize, bool)> {
15797 fused
15798 .pieces
15799 .iter()
15800 .enumerate()
15801 .filter_map(|(i, p)| choose(p).map(|flip| (i, flip)))
15802 .collect()
15803 };
15804 let cut_from = |first: bool| {
15805 move |p: &FacePiece| match (p.from_a == first, p.state) {
15806 (true, PieceState::Out | PieceState::OnOpposed) => Some(false),
15807 (false, PieceState::In) => Some(true),
15808 _ => None,
15809 }
15810 };
15811 let a_only = keep(&cut_from(true));
15812 let b_only = keep(&cut_from(false));
15813 let both = keep(&|p: &FacePiece| {
15814 (p.state == PieceState::In || (p.state == PieceState::OnAligned && !p.covered))
15815 .then_some(false)
15816 });
15817 Ok(Cells {
15818 a_not_b: assemble_result(model, &fused, &a_only, [true, false], a, b, tol)?,
15819 b_not_a: assemble_result(model, &fused, &b_only, [false, true], a, b, tol)?,
15820 common: assemble_result(model, &fused, &both, [false, false], a, b, tol)?,
15821 })
15822}
15823
15824fn fuzzed(fuzz: f64, tol: Tolerances) -> OgeomResult<Tolerances> {
15828 if !fuzz.is_finite() || fuzz <= 0.0 {
15829 ogeom_bail!(Construction, "a fuzz of {fuzz} is not a distance");
15830 }
15831 if fuzz <= tol.confusion() {
15832 return Ok(tol);
15833 }
15834 Tolerances::with_scale(ogeom_core::tolerance::CONFUSION / fuzz)
15835}
15836
15837pub fn fuse_fuzzy(
15851 model: &mut Model,
15852 a: &Shape,
15853 b: &Shape,
15854 fuzz: f64,
15855 tol: Tolerances,
15856) -> OgeomResult<Built> {
15857 at_a_fuzz(model, a, b, fuzz, tol, fuse, |va, vb| (va.max(vb), va + vb))
15858}
15859
15860pub fn cut_fuzzy(
15866 model: &mut Model,
15867 a: &Shape,
15868 b: &Shape,
15869 fuzz: f64,
15870 tol: Tolerances,
15871) -> OgeomResult<Built> {
15872 at_a_fuzz(model, a, b, fuzz, tol, cut, |va, vb| {
15873 ((va - vb).max(0.0), va)
15874 })
15875}
15876
15877pub fn common_fuzzy(
15883 model: &mut Model,
15884 a: &Shape,
15885 b: &Shape,
15886 fuzz: f64,
15887 tol: Tolerances,
15888) -> OgeomResult<Built> {
15889 at_a_fuzz(model, a, b, fuzz, tol, common, |va, vb| (0.0, va.min(vb)))
15890}
15891
15892fn at_a_fuzz(
15897 model: &mut Model,
15898 a: &Shape,
15899 b: &Shape,
15900 fuzz: f64,
15901 tol: Tolerances,
15902 operation: fn(&mut Model, &Shape, &Shape, Tolerances) -> OgeomResult<Built>,
15903 span: fn(f64, f64) -> (f64, f64),
15904) -> OgeomResult<Built> {
15905 let loosened = fuzzed(fuzz, tol)?;
15906 if loosened.confusion() <= tol.confusion() {
15907 return operation(model, a, b, tol);
15908 }
15909 let measure = |model: &Model, shape: &Shape| -> Option<(f64, f64)> {
15910 let found =
15911 ogeom_algo::volume_properties(model, shape, ogeom_mesh::Deflection::default(), tol)
15912 .ok()?;
15913 Some((found.mass, found.deflection))
15914 };
15915 let (Some((va, da)), Some((vb, db))) = (measure(model, a), measure(model, b)) else {
15918 return operation(model, a, b, loosened);
15919 };
15920 let area = box_area(model, a, tol) + box_area(model, b, tol);
15921 let exact = operation(model, a, b, tol);
15922 let held = match &exact {
15923 Ok(built) => measure(model, &built.shape).map(|(v, _)| (v, v)),
15924 Err(_) => Some(span(va, vb)),
15925 };
15926 let Some((low, high)) = held else {
15927 return exact;
15928 };
15929 let mut first_error = None;
15930 let mut f = fuzz;
15931 while f > tol.confusion() {
15932 let at = fuzzed(f, tol)?;
15933 match operation(model, a, b, at) {
15934 Ok(built) => {
15935 let slack = (f + da.max(db)) * area + (va + vb) * 1e-9;
15937 if let Some((v, dv)) = measure(model, &built.shape)
15938 && v >= low - slack - dv * area
15939 && v <= high + slack + dv * area
15940 {
15941 return Ok(built);
15942 }
15943 }
15944 Err(e) => {
15945 first_error.get_or_insert(e);
15946 }
15947 }
15948 f /= 10.0;
15949 }
15950 match (exact, first_error) {
15951 (Ok(built), _) => Ok(built),
15952 (Err(_), Some(e)) => Err(e),
15953 (Err(e), None) => Err(e),
15954 }
15955}
15956
15957fn box_area(model: &Model, shape: &Shape, tol: Tolerances) -> f64 {
15960 let Ok(bound) = ogeom_algo::tight_bounds(model, shape, tol) else {
15961 return 0.0;
15962 };
15963 let (Some(lo), Some(hi)) = (bound.low(), bound.high()) else {
15964 return 0.0;
15965 };
15966 let (x, y, z) = (hi.x - lo.x, hi.y - lo.y, hi.z - lo.z);
15967 2.0 * (x * y + y * z + z * x)
15968}
15969
15970pub fn make_periodic(
15977 model: &mut Model,
15978 shape: &Shape,
15979 step: ogeom_math::Vector,
15980 count: usize,
15981 tol: Tolerances,
15982) -> OgeomResult<Built> {
15983 if count == 0 {
15984 ogeom_bail!(Construction, "a pattern of zero copies is nothing");
15985 }
15986 if step.magnitude() <= tol.confusion() {
15987 ogeom_bail!(Construction, "a zero period stacks every copy on the first");
15988 }
15989 let mut parts: Vec<Shape> = Vec::new();
15992 for kind in [ShapeType::Face, ShapeType::Edge] {
15993 parts.extend(ogeom_topo::explore_unique(model, shape, kind)?);
15994 }
15995 let mut history = History::new();
15996 let mut pieces: Vec<Shape> = vec![shape.clone()];
16000 for i in 1..count {
16001 #[allow(clippy::cast_precision_loss, reason = "pattern counts are small")]
16002 let offset = step * i as f64;
16003 let moved =
16004 ogeom_algo::transformed(model, shape, ogeom_math::Transform::translation(offset))?
16005 .shape;
16006 let placement = moved.location().then(&shape.location().inverted());
16009 let mut images: Vec<(usize, Vec<Shape>)> = parts
16010 .iter()
16011 .enumerate()
16012 .map(|(k, part)| (k, vec![part.moved(&placement)]))
16013 .collect();
16014 let mut current = moved;
16015 let mut apart: Vec<Shape> = Vec::new();
16016 for piece in pieces {
16017 let joined = fuse(model, &piece, ¤t, tol)?;
16018 if model.kind_of(&joined.shape)? != ShapeType::Solid {
16019 apart.push(piece);
16020 continue;
16021 }
16022 history = history.then(&joined.history);
16023 for (_, traced) in &mut images {
16024 *traced = traced
16025 .iter()
16026 .flat_map(|image| joined.history.trace(image).to_vec())
16027 .collect();
16028 }
16029 current = joined.shape;
16030 }
16031 for (k, traced) in images {
16032 for image in traced {
16033 history.generate(&parts[k], image);
16034 }
16035 }
16036 apart.push(current);
16037 pieces = apart;
16038 }
16039 let result = if let [one] = pieces.as_slice() {
16040 one.clone()
16041 } else {
16042 ogeom_algo::build::make_compound(model, &pieces)?.shape
16043 };
16044 history.generate(shape, result.clone());
16045 Ok(Built::new(result, history))
16046}
16047
16048fn baked_if_scaled(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Shape> {
16060 let mut restate = false;
16061 for face in ogeom_topo::explore(model, shape, Filter::OfType(ShapeType::Face))? {
16062 if face.location().is_identity() {
16063 continue;
16064 }
16065 let placement = face.transform(model.datums())?;
16066 if (placement.scale_factor().abs() - 1.0).abs() > 1e-9 || !placement.preserves_handedness()
16071 {
16072 restate = true;
16073 break;
16074 }
16075 }
16076 if !restate {
16077 return Ok(shape.clone());
16078 }
16079 Ok(ogeom_algo::baked_shape(model, shape, tol)?.shape)
16080}
16081
16082fn is_half_space(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<bool> {
16085 Ok(half_space::half_space_face(model, shape, tol)?.is_some())
16086}
16087
16088fn resolved_half_space(
16098 model: &mut Model,
16099 shape: &Shape,
16100 other: &Shape,
16101 tol: Tolerances,
16102) -> OgeomResult<Shape> {
16103 let Some(face) = half_space::half_space_face(model, shape, tol)? else {
16104 return Ok(shape.clone());
16105 };
16106 let planar = model
16107 .node(&face)
16108 .and_then(|n| n.data().as_face())
16109 .and_then(|d| model.geometry().surface(d.surface))
16110 .is_some_and(|s| matches!(s, SurfaceGeometry::Plane(_)));
16111 if !planar {
16112 return half_space::resolved(model, &face, other, tol);
16113 }
16114 let (at, outward) = ogeom_algo::face_normal(model, &face, tol)?;
16116 let bound = ogeom_algo::shape_bounds(model, other, tol)?;
16117 let Some(centre) = bound.centre() else {
16118 ogeom_bail!(
16119 Construction,
16120 "the other argument has no bound to fill against"
16121 );
16122 };
16123 let reach = bound.diagonal().max(tol.confusion() * 1e3) * 2.0;
16124 let into = ogeom_math::Direction::new(-outward, tol)?;
16125 let foot = centre - outward * outward.dot(centre - at);
16126 let seed = if into.vector().x.abs() < 0.9 {
16127 ogeom_math::Vector::new(1.0, 0.0, 0.0)
16128 } else {
16129 ogeom_math::Vector::new(0.0, 1.0, 0.0)
16130 };
16131 let frame_x = ogeom_math::Direction::from_cross(into.vector(), seed, tol)?;
16132 let oriented = ogeom_math::Frame::new(foot, into, frame_x, tol)?;
16133 let corner =
16134 foot - oriented.x().vector() * (reach / 2.0) - oriented.y().vector() * (reach / 2.0);
16135 let placed = ogeom_math::Frame::new(corner, into, frame_x, tol)?;
16136 let depth = reach + (centre - at).dot(into.vector()).max(0.0);
16140 Ok(ogeom_algo::make_box(model, placed, (reach, reach, depth), tol)?.shape)
16141}
16142
16143fn half_space_plane(
16146 model: &Model,
16147 shape: &Shape,
16148 tol: Tolerances,
16149) -> OgeomResult<Option<ogeom_math::Plane>> {
16150 let Some(face) = half_space::half_space_face(model, shape, tol)? else {
16151 return Ok(None);
16152 };
16153 let Some(surface) = model
16154 .node(&face)
16155 .and_then(|n| n.data().as_face())
16156 .and_then(|d| model.geometry().surface(d.surface))
16157 .cloned()
16158 else {
16159 return Ok(None);
16160 };
16161 use ogeom_geom::Transformable as _;
16162 match surface.transformed(&face.transform(model.datums())?, tol)? {
16163 SurfaceGeometry::Plane(p) => Ok(Some(p.plane())),
16164 _ => Ok(None),
16165 }
16166}
16167
16168pub fn fuse(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
16177 if is_sheet(model, a)? || is_sheet(model, b)? {
16178 ogeom_bail!(
16179 Construction,
16180 "a union takes solids; common and cut trim a sheet, and split_sheet \
16181 splits one"
16182 );
16183 }
16184 let built = noting_held(model, |model| {
16185 ball_chart::with_poles_clear(model, a, b, tol, |model, a, b| {
16186 settled(model, tol, |model, tol| fuse_once(model, a, b, tol))
16187 })
16188 })?;
16189 sub_shapes::with_sub_shapes(model, [a, b], built, tol)
16190}
16191
16192fn noting_held(
16197 model: &mut Model,
16198 run: impl FnOnce(&mut Model) -> OgeomResult<Built>,
16199) -> OgeomResult<Built> {
16200 let since = model.node_count();
16201 let built = run(model)?;
16202 model.note_held(&built.shape, since)?;
16203 Ok(built)
16204}
16205
16206fn fuse_once(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
16207 if is_half_space(model, a, tol)? || is_half_space(model, b, tol)? {
16208 ogeom_bail!(
16209 Construction,
16210 "the union with a half space is unbounded; a half space serves cut, \
16211 common and section"
16212 );
16213 }
16214 let (a, b) = (
16215 &baked_if_scaled(model, a, tol)?,
16216 &baked_if_scaled(model, b, tol)?,
16217 );
16218 let built = fused_and_assembled(model, a, b, [true, true], tol, |p| {
16222 (p.state == PieceState::Out || (p.state == PieceState::OnAligned && !p.covered))
16223 .then_some(false)
16224 });
16225 let built = match built {
16227 Ok(built) if holds_nothing(model, &built.shape)? => {
16228 Err(ogeom_core::ogeom_err!(NotDone, "the union came out empty"))
16229 }
16230 other => other,
16231 };
16232 or_nested(model, built, a, b, true, tol)
16235}
16236
16237fn nested(model: &Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Option<bool>> {
16247 sampled_relation(model, a, b, Relation::Nested, tol)
16248}
16249
16250fn apart(model: &Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<bool> {
16253 let (box_a, box_b) = (
16254 ogeom_algo::shape_bounds(model, a, tol)?,
16255 ogeom_algo::shape_bounds(model, b, tol)?,
16256 );
16257 if !box_a.expanded(tol.confusion()).intersects(&box_b) {
16258 return Ok(true);
16259 }
16260 Ok(sampled_relation(model, a, b, Relation::Apart, tol)? == Some(true))
16261}
16262
16263#[derive(Clone, Copy, PartialEq, Eq)]
16265enum Relation {
16266 Nested,
16268 Apart,
16271 Covered,
16274}
16275
16276fn sampled_relation(
16278 model: &Model,
16279 a: &Shape,
16280 b: &Shape,
16281 relation: Relation,
16282 tol: Tolerances,
16283) -> OgeomResult<Option<bool>> {
16284 let samples = |shape: &Shape, fine: f64| -> OgeomResult<Vec<Point>> {
16291 let mut out = Vec::new();
16292 for face in explore_unique(model, shape, ShapeType::Face)? {
16293 let Some(NodeData::Face(data)) = model.node(&face).map(|n| n.data()) else {
16294 continue;
16295 };
16296 let Some(surface) = model.geometry().surface(data.surface) else {
16297 ogeom_bail!(Dangling, "surface is not in this model");
16298 };
16299 let placement = face.transform(model.datums())?;
16300 let mesh =
16301 ogeom_mesh::triangulate_face(model, &face, ogeom_mesh::Deflection::default(), tol)?;
16302 out.extend(mesh.positions.iter().copied());
16303 if mesh.parameters.len() != mesh.positions.len() {
16304 continue;
16305 }
16306 let trim = ogeom_algo::classify::FaceTrim::of(
16307 model,
16308 &face,
16309 ogeom_mesh::Deflection {
16310 chord: fine,
16311 ..ogeom_mesh::Deflection::default()
16312 },
16313 tol,
16314 )?;
16315 for t in &mesh.triangles {
16316 let [p, q, r] = t.map(|i| mesh.parameters[i as usize]);
16317 let (u, v) = ((p.0 + q.0 + r.0) / 3.0, (p.1 + q.1 + r.1) / 3.0);
16318 if trim.holds(model, (u, v), 0.0, tol)? == Containment::In {
16319 out.push(placement.apply(surface.point_at(u, v, tol)?));
16320 }
16321 }
16322 }
16323 Ok(out)
16324 };
16325 let all_read = |points: &[Point],
16329 boundary: &ogeom_algo::SolidBoundary,
16330 allowed: &dyn Fn(Containment) -> bool|
16331 -> OgeomResult<bool> {
16332 let mut unread = 0_usize;
16333 for p in points {
16334 ogeom_core::progress::checkpoint()?;
16335 match boundary.holds(model, *p, tol) {
16336 Ok(says) if allowed(says) => {}
16337 Ok(_) => return Ok(false),
16338 Err(ogeom_core::OgeomError::NotDone(_)) => unread += 1,
16339 Err(e) => return Err(e),
16340 }
16341 }
16342 Ok(unread * 10 <= points.len())
16343 };
16344 let mut loosest = tol.confusion();
16348 for shape in [a, b] {
16349 for kind in [ShapeType::Edge, ShapeType::Vertex] {
16350 for sub in explore_unique(model, shape, kind)? {
16351 let stated = match model.node(&sub).map(|n| n.data()) {
16352 Some(NodeData::Edge(d)) => d.tolerance.get(),
16353 Some(NodeData::Vertex(d)) => d.tolerance.get(),
16354 _ => continue,
16355 };
16356 loosest = loosest.max(stated);
16357 }
16358 }
16359 }
16360 let band = (tol.confusion() * 1e4).max(loosest);
16366 let tol = if loosest > tol.confusion() {
16367 fuzzed(loosest, tol)?
16368 } else {
16369 tol
16370 };
16371 let (of_a, of_b) = (
16372 ogeom_algo::SolidBoundary::of(model, a, band, tol)?,
16373 ogeom_algo::SolidBoundary::of(model, b, band, tol)?,
16374 );
16375 let (on_a, on_b) = (samples(a, band)?, samples(b, band)?);
16376 if relation == Relation::Apart {
16377 let clear = all_read(&on_a, &of_b, &|c| c != Containment::In)?
16378 && all_read(&on_b, &of_a, &|c| c != Containment::In)?;
16379 return Ok(Some(clear));
16380 }
16381 if relation == Relation::Covered {
16382 return Ok(Some(all_read(&on_a, &of_b, &|c| c != Containment::Out)?));
16383 }
16384 let within = |inner: &[Point],
16385 outer_boundary: &ogeom_algo::SolidBoundary,
16386 outer: &[Point],
16387 inner_boundary: &ogeom_algo::SolidBoundary|
16388 -> OgeomResult<bool> {
16389 Ok(all_read(inner, outer_boundary, &|c| c != Containment::Out)?
16390 && all_read(outer, inner_boundary, &|c| c != Containment::In)?)
16391 };
16392 if within(&on_b, &of_a, &on_a, &of_b)? && !pokes_out(model, b, a, &of_a, tol)? {
16393 return Ok(Some(true));
16394 }
16395 if within(&on_a, &of_b, &on_b, &of_a)? && !pokes_out(model, a, b, &of_b, tol)? {
16396 return Ok(Some(false));
16397 }
16398 Ok(None)
16399}
16400
16401fn pokes_out(
16411 model: &Model,
16412 inner: &Shape,
16413 outer: &Shape,
16414 outer_boundary: &ogeom_algo::SolidBoundary,
16415 tol: Tolerances,
16416) -> OgeomResult<bool> {
16417 use ogeom_geom::Surface as _;
16418 use ogeom_geom::Transformable as _;
16419 let deflection = ogeom_mesh::Deflection::default();
16422 let reach = deflection.chord * 4.0;
16423 const STARTS: usize = 6;
16426 let placed = |face: &Shape| -> OgeomResult<SurfaceGeometry> {
16427 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
16428 ogeom_bail!(Dangling, "face is not in this model");
16429 };
16430 let Some(surface) = model.geometry().surface(data.surface) else {
16431 ogeom_bail!(Dangling, "surface is not in this model");
16432 };
16433 surface.transformed(&face.transform(model.datums())?, tol)
16434 };
16435 let mut walls = Vec::new();
16438 for face in explore_unique(model, outer, ShapeType::Face)? {
16439 let outward = if face.orientation() == ogeom_topo::Orientation::Reversed {
16440 -1.0
16441 } else {
16442 1.0
16443 };
16444 let bound = ogeom_algo::shape_bounds(model, &face, tol)?.expanded(reach);
16445 walls.push((placed(&face)?, outward, bound));
16446 }
16447 let outside = |p: Point, wall: &(SurfaceGeometry, f64, ogeom_math::Aabb)| -> Option<f64> {
16449 let found = ogeom_algo::project_on_surface(&wall.0, p, 8, tol).ok()?;
16450 let (u, v) = found.parameters;
16451 let n = wall.0.normal_at(u, v, tol).ok()?;
16452 Some((p - found.point).dot(n.vector()) * wall.1)
16453 };
16454 for face in explore_unique(model, inner, ShapeType::Face)? {
16455 ogeom_core::progress::checkpoint()?;
16456 let surface = placed(&face)?;
16457 let mesh = ogeom_mesh::triangulate_face(model, &face, deflection, tol)?;
16458 if mesh.parameters.len() != mesh.positions.len() || mesh.parameters.is_empty() {
16459 continue;
16460 }
16461 let (mut lo, mut hi) = (mesh.parameters[0], mesh.parameters[0]);
16462 for &(u, v) in &mesh.parameters {
16463 lo = (lo.0.min(u), lo.1.min(v));
16464 hi = (hi.0.max(u), hi.1.max(v));
16465 }
16466 let span = (hi.0 - lo.0).max(hi.1 - lo.1);
16467 if span <= 0.0 {
16468 continue;
16469 }
16470 let lift = |at: (f64, f64)| surface.point_at(at.0, at.1, tol);
16471 let mut starts: Vec<(f64, f64)> = mesh.parameters.clone();
16472 for t in &mesh.triangles {
16473 let [p, q, r] = t.map(|i| mesh.parameters[i as usize]);
16474 starts.push(((p.0 + q.0 + r.0) / 3.0, (p.1 + q.1 + r.1) / 3.0));
16475 }
16476 for wall in &walls {
16477 let mut near: Vec<((f64, f64), f64)> = Vec::new();
16478 for &at in &starts {
16479 let p = lift(at)?;
16480 if !wall.2.contains(p) {
16481 continue;
16482 }
16483 if let Some(d) = outside(p, wall)
16484 && d >= -reach
16485 {
16486 near.push((at, d));
16487 }
16488 }
16489 near.sort_by(|x, y| y.1.total_cmp(&x.1));
16490 near.truncate(STARTS);
16491 for (start, d) in near {
16492 let (mut at, mut best, mut step) = (start, d, span * 0.05);
16496 let mut rounds = 0;
16497 while step > span * 1e-7 && rounds < 400 {
16498 rounds += 1;
16499 let mut climbed = false;
16500 for (du, dv) in [(step, 0.0), (-step, 0.0), (0.0, step), (0.0, -step)] {
16501 let next = ((at.0 + du).clamp(lo.0, hi.0), (at.1 + dv).clamp(lo.1, hi.1));
16502 if let Ok(q) = lift(next)
16503 && let Some(g) = outside(q, wall)
16504 && g > best
16505 {
16506 (at, best) = (next, g);
16507 climbed = true;
16508 }
16509 }
16510 if !climbed {
16511 step *= 0.5;
16512 }
16513 }
16514 if best <= tol.confusion() {
16515 continue;
16516 }
16517 let q = lift(at)?;
16518 if ogeom_algo::classify_on_face(model, &face, q, deflection, tol)?
16519 != Containment::Out
16520 && outer_boundary
16521 .holds(model, q, tol)
16522 .is_ok_and(|c| c == Containment::Out)
16523 {
16524 return Ok(true);
16525 }
16526 }
16527 }
16528 }
16529 Ok(false)
16530}
16531
16532fn or_nested(
16537 model: &mut Model,
16538 built: OgeomResult<Built>,
16539 a: &Shape,
16540 b: &Shape,
16541 outer: bool,
16542 tol: Tolerances,
16543) -> OgeomResult<Built> {
16544 let refusal = match built {
16545 Ok(built) => return Ok(built),
16546 Err(e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_))) => {
16547 return Err(e);
16548 }
16549 Err(e) => e,
16550 };
16551 let Some(b_in_a) = nested(model, a, b, tol)? else {
16552 return Err(refusal);
16553 };
16554 let (kept, gone) = if b_in_a == outer { (a, b) } else { (b, a) };
16557 let mut history = ogeom_algo::History::new();
16558 for face in explore_unique(model, gone, ShapeType::Face)? {
16559 history.delete(&face);
16560 }
16561 Ok(Built::new(kept.clone(), history))
16562}
16563
16564pub fn common(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
16583 match (is_sheet(model, a)?, is_sheet(model, b)?) {
16584 (true, true) => ogeom_bail!(
16585 Construction,
16586 "two sheets share no volume to keep; split_sheet splits one by the other"
16587 ),
16588 (true, false) => trimmed_sheet(model, a, b, true, tol),
16589 (false, true) => trimmed_sheet(model, b, a, true, tol),
16590 (false, false) => {
16591 let built = noting_held(model, |model| {
16592 ball_chart::with_poles_clear(model, a, b, tol, |model, a, b| {
16593 settled(model, tol, |model, tol| common_once(model, a, b, tol))
16594 })
16595 })?;
16596 sub_shapes::with_sub_shapes(model, [a, b], built, tol)
16597 }
16598 }
16599}
16600
16601fn common_once(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
16602 let (a, b) = (
16603 &resolved_half_space(model, a, b, tol)?,
16604 &resolved_half_space(model, b, a, tol)?,
16605 );
16606 let (a, b) = (
16607 &baked_if_scaled(model, a, tol)?,
16608 &baked_if_scaled(model, b, tol)?,
16609 );
16610 let built = fused_and_assembled(model, a, b, [false, false], tol, |p| {
16613 (p.state == PieceState::In || (p.state == PieceState::OnAligned && !p.covered))
16614 .then_some(false)
16615 });
16616 let built = match built {
16618 Ok(built) if holds_nothing(model, &built.shape)? && !apart(model, a, b, tol)? => {
16619 Err(ogeom_core::ogeom_err!(
16620 NotDone,
16621 "the intersection came out empty though the solids overlap"
16622 ))
16623 }
16624 other => other,
16625 };
16626 or_nested(model, built, a, b, false, tol)
16628}
16629
16630fn fused_and_assembled(
16638 model: &mut Model,
16639 a: &Shape,
16640 b: &Shape,
16641 keep_aside: [bool; 2],
16642 tol: Tolerances,
16643 keep: impl Fn(&FacePiece) -> Option<bool>,
16644) -> OgeomResult<Built> {
16645 let attempt = |model: &mut Model, aside: Option<&[aside::Aside; 2]>| {
16646 let fused = general_fuse_classified(model, a, b, aside, tol)?;
16647 let kept: Vec<(usize, bool)> = fused
16648 .pieces
16649 .iter()
16650 .enumerate()
16651 .filter_map(|(i, p)| keep(p).map(|flip| (i, flip)))
16652 .collect();
16653 assemble_result(model, &fused, &kept, keep_aside, a, b, tol)
16654 };
16655 if let Ok(Some(aside)) = aside::set_aside(model, a, b, tol) {
16656 let mark = model.note_widened();
16661 match attempt(model, Some(&aside)) {
16662 Ok(built) => {
16663 model.keep_widened(mark);
16664 return Ok(built);
16665 }
16666 Err(e) => {
16667 model.undo_widened(mark);
16668 if matches!(e, ogeom_core::OgeomError::Cancelled) {
16669 return Err(e);
16670 }
16671 }
16672 }
16673 }
16674 attempt(model, None)
16675}
16676
16677fn holds_nothing(model: &Model, shape: &Shape) -> OgeomResult<bool> {
16680 let mut stack = vec![shape.clone()];
16682 while let Some(at) = stack.pop() {
16683 let Some(node) = model.node(&at) else {
16684 ogeom_bail!(Dangling, "shape refers to a node not in this model");
16685 };
16686 match node.kind() {
16687 ShapeType::Face => return Ok(false),
16688 ShapeType::Compound | ShapeType::CompSolid | ShapeType::Solid | ShapeType::Shell => {
16689 stack.extend(node.children().iter().cloned());
16690 }
16691 _ => {}
16692 }
16693 }
16694 Ok(true)
16695}
16696
16697pub fn cut(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
16713 if is_sheet(model, b)? {
16714 ogeom_bail!(
16715 Construction,
16716 "a sheet bounds no volume to cut away; split_sheet splits by a sheet"
16717 );
16718 }
16719 if is_sheet(model, a)? {
16720 return trimmed_sheet(model, a, b, false, tol);
16721 }
16722 let built = noting_held(model, |model| {
16723 ball_chart::with_poles_clear(model, a, b, tol, |model, a, b| {
16724 settled(model, tol, |model, tol| cut_once(model, a, b, tol))
16725 })
16726 })?;
16727 sub_shapes::with_sub_shapes(model, [a, b], built, tol)
16728}
16729
16730pub fn cut_seated(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
16754 if is_sheet(model, a)? || is_sheet(model, b)? {
16755 return cut(model, a, b, tol);
16756 }
16757 let built = noting_held(model, |model| {
16758 ball_chart::with_poles_clear(model, a, b, tol, |model, a, b| {
16759 match seated::seated_cut(model, a, b, false, tol) {
16760 Ok(Some(built)) => return Ok(built),
16761 Err(
16762 e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_)),
16763 ) => {
16764 return Err(e);
16765 }
16766 Ok(None) | Err(_) => {}
16767 }
16768 let rest = |model: &mut Model, a: &Shape, b: &Shape| {
16769 settled(model, tol, |model, tol| cut_once(model, a, b, tol))
16770 };
16771 match seated::rings_first(model, a, b, false, tol, rest) {
16772 Ok(Some(built)) => return Ok(built),
16773 Err(
16774 e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_)),
16775 ) => {
16776 return Err(e);
16777 }
16778 Ok(None) | Err(_) => {}
16779 }
16780 settled(model, tol, |model, tol| cut_once(model, a, b, tol))
16781 })
16782 })?;
16783 sub_shapes::with_sub_shapes(model, [a, b], built, tol)
16784}
16785
16786pub fn fuse_seated(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
16806 if is_sheet(model, a)? || is_sheet(model, b)? {
16807 return fuse(model, a, b, tol);
16808 }
16809 let built = noting_held(model, |model| {
16810 ball_chart::with_poles_clear(model, a, b, tol, |model, a, b| {
16811 match seated::seated_cut(model, a, b, true, tol) {
16812 Ok(Some(built)) => return Ok(built),
16813 Err(
16814 e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_)),
16815 ) => {
16816 return Err(e);
16817 }
16818 Ok(None) | Err(_) => {}
16819 }
16820 let rest = |model: &mut Model, a: &Shape, b: &Shape| {
16821 settled(model, tol, |model, tol| fuse_once(model, a, b, tol))
16822 };
16823 match seated::rings_first(model, a, b, true, tol, rest) {
16824 Ok(Some(built)) => return Ok(built),
16825 Err(
16826 e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_)),
16827 ) => {
16828 return Err(e);
16829 }
16830 Ok(None) | Err(_) => {}
16831 }
16832 settled(model, tol, |model, tol| fuse_once(model, a, b, tol))
16833 })
16834 })?;
16835 sub_shapes::with_sub_shapes(model, [a, b], built, tol)
16836}
16837
16838fn trimmed_sheet(
16841 model: &mut Model,
16842 sheet: &Shape,
16843 tool: &Shape,
16844 inside: bool,
16845 tol: Tolerances,
16846) -> OgeomResult<Built> {
16847 if !is_sheet(model, sheet)? {
16848 ogeom_bail!(
16849 Construction,
16850 "the sheet to trim is a face, a shell or a compound of those"
16851 );
16852 }
16853 trim_sheet_edges(model, sheet, tol)?;
16854 let built = noting_held(model, |model| {
16855 settled(model, tol, |model, tol| {
16856 let tool = &resolved_half_space(model, tool, sheet, tol)?;
16857 let (sheet, tool) = (
16858 &baked_if_scaled(model, sheet, tol)?,
16859 &baked_if_scaled(model, tool, tol)?,
16860 );
16861 let fused = general_fuse_as(model, sheet, tool, Operands::SheetBySolid, None, tol)?;
16862 let mut kept = Vec::new();
16863 for (index, piece) in fused.pieces.iter().enumerate() {
16864 match piece.state {
16865 PieceState::In if inside => kept.push(index),
16866 PieceState::Out if !inside => kept.push(index),
16867 PieceState::In | PieceState::Out => {}
16868 PieceState::OnAligned | PieceState::OnOpposed => ogeom_bail!(
16869 NotDone,
16870 "a piece of the sheet lies on the tool's boundary, which \
16871 keeps it on neither side"
16872 ),
16873 PieceState::Unread => ogeom_bail!(
16874 Construction,
16875 "a piece of the sheet was not classified against the tool"
16876 ),
16877 }
16878 }
16879 assemble_sheet(model, &fused, &kept, sheet, tol)
16880 })
16881 })?;
16882 sub_shapes::with_sub_shapes(model, [sheet, tool], built, tol)
16883}
16884
16885pub fn split_sheet(
16907 model: &mut Model,
16908 sheet: &Shape,
16909 by: &Shape,
16910 tol: Tolerances,
16911) -> OgeomResult<Built> {
16912 for shape in [sheet, by] {
16913 if !is_sheet(model, shape)? {
16914 ogeom_bail!(
16915 Construction,
16916 "split_sheet splits a sheet by a sheet, each a face, a shell or \
16917 a compound of those; common and cut trim a sheet by a solid"
16918 );
16919 }
16920 trim_sheet_edges(model, shape, tol)?;
16921 }
16922 let built = noting_held(model, |model| {
16923 settled(model, tol, |model, tol| {
16924 let (sheet, by) = (
16925 &baked_if_scaled(model, sheet, tol)?,
16926 &baked_if_scaled(model, by, tol)?,
16927 );
16928 let fused = general_fuse_as(model, sheet, by, Operands::SheetBySheet, None, tol)?;
16929 let kept: Vec<usize> = (0..fused.pieces.len()).collect();
16930 assemble_sheet(model, &fused, &kept, sheet, tol)
16931 })
16932 })?;
16933 sub_shapes::with_sub_shapes(model, [sheet, by], built, tol)
16934}
16935
16936fn cut_once(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
16937 let (a, b) = (
16938 &resolved_half_space(model, a, b, tol)?,
16939 &resolved_half_space(model, b, a, tol)?,
16940 );
16941 let (a, b) = (
16942 &baked_if_scaled(model, a, tol)?,
16943 &baked_if_scaled(model, b, tol)?,
16944 );
16945 let built = fused_and_assembled(model, a, b, [true, false], tol, |p| {
16952 match (p.from_a, p.state) {
16953 (true, PieceState::Out | PieceState::OnOpposed) => Some(false),
16954 (false, PieceState::In) => Some(true),
16955 _ => None,
16956 }
16957 });
16958 let refusal = match built {
16959 Ok(built)
16962 if !holds_nothing(model, &built.shape)?
16963 || sampled_relation(model, a, b, Relation::Covered, tol)? == Some(true) =>
16964 {
16965 return Ok(built);
16966 }
16967 Ok(_) => ogeom_core::ogeom_err!(
16968 NotDone,
16969 "the cut left nothing of a solid that does not lie within the tool"
16970 ),
16971 Err(e @ (ogeom_core::OgeomError::Cancelled | ogeom_core::OgeomError::Dangling(_))) => {
16972 return Err(e);
16973 }
16974 Err(e) => e,
16975 };
16976 if nested(model, b, a, tol)? != Some(true) {
16979 return Err(refusal);
16980 }
16981 let empty = model.add_compound(&[])?;
16982 let mut history = ogeom_algo::History::new();
16983 for face in explore_unique(model, a, ShapeType::Face)? {
16984 history.delete(&face);
16985 }
16986 history.modify(a, empty.clone());
16987 Ok(Built::new(empty, history))
16988}
16989
16990fn past_a_pole(
17000 section: &SectionRec,
17001 pcurve: &PlanarCurve,
17002 face: &GFace,
17003 tol: Tolerances,
17004) -> OgeomResult<()> {
17005 const SAMPLES: usize = 256;
17006 let SurfaceGeometry::Sphere(sphere) = &face.surface else {
17007 return Ok(());
17008 };
17009 let radius = sphere.sphere().radius();
17010 let reach = section.tolerance.max(tol.confusion());
17011 let range = section.curve.domain();
17012 let worst = |measure: &dyn Fn(f64) -> OgeomResult<f64>| -> OgeomResult<f64> {
17013 #[allow(clippy::cast_precision_loss)]
17014 let step = |k: usize| range.0 + (range.1 - range.0) * k as f64 / SAMPLES as f64;
17015 let mut top = (f64::NEG_INFINITY, 0);
17016 for k in 0..=SAMPLES {
17017 let m = measure(step(k))?;
17018 if m > top.0 {
17019 top = (m, k);
17020 }
17021 }
17022 let (mut lo, mut hi) = (
17023 step(top.1.saturating_sub(1)),
17024 step((top.1 + 1).min(SAMPLES)),
17025 );
17026 let golden = (5.0_f64.sqrt() - 1.0) / 2.0;
17027 for _ in 0..100 {
17028 let (a, b) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
17029 if measure(a)? > measure(b)? {
17030 hi = b;
17031 } else {
17032 lo = a;
17033 }
17034 }
17035 Ok(measure(f64::midpoint(lo, hi))?.max(top.0))
17036 };
17037 let past =
17038 worst(&|t| Ok((pcurve.point_at(t, tol)?.y.abs() - core::f64::consts::FRAC_PI_2) * radius))?;
17039 if past > reach {
17040 ogeom_bail!(
17041 NotDone,
17042 "a section's image on a sphere runs {past} past the chart's pole, beyond \
17043 its tolerance of {reach}; a section through a sphere chart's pole is not \
17044 resolved; see docs/CAPABILITIES.md, bool.booleans"
17045 );
17046 }
17047 let (start, end) = (
17048 section.curve.point_at(range.0, tol)?,
17049 section.curve.point_at(range.1, tol)?,
17050 );
17051 for pole in &face.poles {
17052 if start.distance(pole.point) <= reach || end.distance(pole.point) <= reach {
17053 continue;
17054 }
17055 let near = -worst(&|t| Ok(-section.curve.point_at(t, tol)?.distance(pole.point)))?;
17056 if near <= reach {
17057 ogeom_bail!(
17058 NotDone,
17059 "a section passes within {near} of a sphere chart's pole between its \
17060 ends, inside its tolerance of {reach}; a section through a sphere \
17061 chart's pole is not resolved; see docs/CAPABILITIES.md, bool.booleans"
17062 );
17063 }
17064 }
17065 Ok(())
17066}
17067
17068pub fn section(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
17079 for (sheet, other) in [(a, b), (b, a)] {
17082 if model.kind_of(sheet)? != ShapeType::Solid
17083 && let Some(plane) = half_space_plane(model, other, tol)?
17084 {
17085 return section_face(model, sheet, &plane, tol);
17086 }
17087 }
17088 let (a, b) = (
17089 &resolved_half_space(model, a, b, tol)?,
17090 &resolved_half_space(model, b, a, tol)?,
17091 );
17092 let (a, b) = (
17093 &baked_if_scaled(model, a, tol)?,
17094 &baked_if_scaled(model, b, tol)?,
17095 );
17096 let fused = general_fuse(model, a, b, None, tol)?;
17097 let mut wanted: Vec<(usize, (f64, f64))> = Vec::new();
17100 for piece in &fused.pieces {
17101 for ring in &piece.rings {
17102 for traversal in ring {
17103 if let Tag::Section { section, range } = &traversal.tag {
17104 let near = |a: (f64, f64), b: (f64, f64)| {
17105 (a.0 - b.0).abs() <= tol.parametric()
17106 && (a.1 - b.1).abs() <= tol.parametric()
17107 };
17108 if !wanted.iter().any(|(s, r)| s == section && near(*r, *range)) {
17109 wanted.push((*section, *range));
17110 }
17111 }
17112 }
17113 }
17114 }
17115 let mut history = History::new();
17116 let mut edges = Vec::new();
17117 let mut vertices: Vec<(Point, Shape)> = Vec::new();
17121 for (si, range) in wanted {
17122 let s = &fused.sections[si];
17123 let domain = s.curve.domain();
17124 let (f0, f1) = folded_range(range, domain, s.closed);
17125 let from = s.curve.point_at(at_param(f0, domain, s.closed), tol)?;
17126 let to = s.curve.point_at(at_param(f1, domain, s.closed), tol)?;
17127 let v0 = section_face::vertex(model, &mut vertices, from, tol)?;
17128 let v1 = section_face::vertex(model, &mut vertices, to, tol)?;
17129 edges.push(make_edge_between(model, s.curve.clone(), (f0, f1), &v0, &v1, tol)?.shape);
17130 }
17131 for contact in &fused.tangents {
17135 for (lo, hi) in contact_intervals(&fused, contact, tol)? {
17136 let from = contact.curve.point_at(lo, tol)?;
17137 let to = contact.curve.point_at(hi, tol)?;
17138 let v0 = section_face::vertex(model, &mut vertices, from, tol)?;
17139 let v1 = section_face::vertex(model, &mut vertices, to, tol)?;
17140 edges.push(
17141 make_edge_between(model, contact.curve.clone(), (lo, hi), &v0, &v1, tol)?.shape,
17142 );
17143 }
17144 }
17145 let result = model.add_compound(&edges)?;
17146 history.modify(a, result.clone());
17147 history.modify(b, result.clone());
17148 Ok(Built::new(result, history))
17149}
17150
17151#[cfg(test)]
17152#[allow(clippy::unwrap_used)]
17153mod tests {
17154 use super::*;
17155 use ogeom_algo::{check, make_box, make_cylinder, volume_properties};
17156 use ogeom_math::{Direction, Frame};
17157 use ogeom_mesh::Deflection;
17158
17159 const T: Tolerances = Tolerances::millimetres();
17160 const PI: f64 = core::f64::consts::PI;
17161
17162 #[test]
17170 fn a_coaxial_chart_map_lands_on_the_same_point_across_handedness() {
17171 let frames = |slide: f64| -> Vec<Frame> {
17172 let mut out = Vec::new();
17173 for up in [Direction::Z, -Direction::Z] {
17174 let x = Direction::new(ogeom_math::Vector::new(0.6, 0.8, 0.0), T).unwrap();
17175 let frame = Frame::new(Point::new(0.0, 0.0, slide), up, x, T).unwrap();
17176 out.extend([frame, frame.mirrored()]);
17177 }
17178 out
17179 };
17180 let cylinder = |f: Frame| -> SurfaceGeometry {
17181 ogeom_geom::CylinderSurface::new(
17182 ogeom_math::Cylinder::new(f, 2.0, T).unwrap(),
17183 (-5.0, 5.0),
17184 )
17185 .unwrap()
17186 .into()
17187 };
17188 let torus = |f: Frame| -> SurfaceGeometry {
17189 ogeom_geom::TorusSurface::new(ogeom_math::Torus::new(f, 2.0, 0.5, T).unwrap()).into()
17190 };
17191 let cone = |f: Frame| -> SurfaceGeometry {
17195 let up = f.z().vector().z;
17196 let height = f.origin().z;
17197 ogeom_geom::ConeSurface::new(
17198 ogeom_math::Cone::new(f, 0.25_f64.mul_add(height, 2.0), up * 0.25_f64.atan(), T)
17199 .unwrap(),
17200 (-5.0, 5.0),
17201 )
17202 .unwrap()
17203 .into()
17204 };
17205 let kinds: [(&dyn Fn(Frame) -> SurfaceGeometry, f64); 3] =
17206 [(&cylinder, 1.5), (&cone, 1.5), (&torus, 0.0)];
17207 for (make, slide) in kinds {
17208 for target_frame in [Frame::WORLD, Frame::WORLD.mirrored()] {
17209 let target = make(target_frame);
17210 for owner_frame in frames(slide) {
17211 let owner = make(owner_frame);
17212 let map = coaxial_chart_map(&owner, &target, 0.0, T).unwrap();
17213 for (u, v) in [(0.3, 0.2), (2.0, -1.0), (5.0, 1.1)] {
17214 let at = owner.point_at(u, v, T).unwrap();
17215 let uv = map.apply(Point2::new(u, v));
17216 let tau = core::f64::consts::TAU;
17217 let lifted = target
17218 .point_at(uv.x.rem_euclid(tau), uv.y, T)
17219 .or_else(|_| {
17220 target.point_at(uv.x.rem_euclid(tau), uv.y.rem_euclid(tau), T)
17221 })
17222 .unwrap();
17223 assert!(
17225 at.distance(lifted) < 1e-12,
17226 "{:?} owner on {:?} target, ({u}, {v}): {at:?} against {lifted:?}",
17227 owner_frame.handedness(),
17228 target_frame.handedness()
17229 );
17230 }
17231 }
17232 }
17233 }
17234 }
17235
17236 #[test]
17243 fn a_thin_piece_reads_the_other_solid_s_wedge_at_its_edges() {
17244 let mut model = Model::new();
17245 let profile = [
17246 (0.0, -1.0),
17247 (1.0, -1.0),
17248 (1.0, 0.0),
17249 (2.0, 0.0),
17250 (2.0, 1.0),
17251 (0.0, 1.0),
17252 ]
17253 .map(|(x, z)| Point::new(x, 0.0, z));
17254 let wire = ogeom_algo::make_polygon(&mut model, &profile, true, T)
17255 .unwrap()
17256 .shape;
17257 let plane = ogeom_geom::PlaneSurface::over(
17258 ogeom_math::Plane::new(
17259 Frame::new(Point::ORIGIN, Direction::Y, Direction::X, T).unwrap(),
17260 ),
17261 (-5.0, 5.0),
17262 (-5.0, 5.0),
17263 )
17264 .unwrap();
17265 let section = ogeom_algo::make_face(&mut model, plane.into(), &[wire], T)
17266 .unwrap()
17267 .shape;
17268 let step = ogeom_algo::make_prism(
17269 &mut model,
17270 §ion,
17271 ogeom_math::Vector::new(0.0, 1.0, 0.0),
17272 T,
17273 )
17274 .unwrap()
17275 .shape;
17276 let floor = make_box(
17277 &mut model,
17278 Frame::new(Point::new(-1.0, -1.0, -3.0), Direction::Z, Direction::X, T).unwrap(),
17279 (4.0, 3.0, 3.0),
17280 T,
17281 )
17282 .unwrap()
17283 .shape;
17284 let theirs = gather(&model, &step, false, None, T).unwrap();
17285 let ours = gather(&model, &floor, false, None, T).unwrap();
17286 let top = ours
17287 .faces
17288 .iter()
17289 .find(|f| {
17290 matches!(f.surface, SurfaceGeometry::Plane(_))
17291 && f.edges
17292 .iter()
17293 .all(|e| e.curve.point_at(e.crange.0, T).unwrap().z.abs() < 1e-9)
17294 })
17295 .unwrap();
17296 let contact = |x: f64, beyond: f64| -> (ContactRec, (f64, f64), bool) {
17300 let e = theirs
17301 .faces
17302 .iter()
17303 .flat_map(|f| f.edges.iter())
17304 .find(|e| {
17305 [e.crange.0, e.crange.1].iter().all(|&t| {
17306 let p = e.curve.point_at(t, T).unwrap();
17307 (p.x - x).abs() < 1e-9 && p.z.abs() < 1e-9
17308 })
17309 })
17310 .unwrap();
17311 let ends = [e.crange.0, e.crange.1].map(|t| {
17312 chart_point_within(top, e.curve.point_at(t, T).unwrap(), 1e-6, T).unwrap()
17313 });
17314 let line = ogeom_geom::Line2d::segment(ends[0], ends[1], T).unwrap();
17315 let prange = ogeom_geom::Curve2d::domain(&line);
17316 let rec = ContactRec {
17317 curve: (*e.curve).clone(),
17318 crange: e.crange,
17319 pcurve: line.into(),
17320 prange,
17321 node: e.node,
17322 tolerance: e.tolerance,
17323 target_from_a: true,
17324 target_face: 0,
17325 bound: ogeom_math::Aabb::EMPTY,
17326 crossed: false,
17327 };
17328 let t = f64::midpoint(e.crange.0, e.crange.1);
17331 let along = e.curve.d1_at(t, T).unwrap();
17332 let uv = ends[0];
17333 let normal = top.surface.normal_at(uv.x, uv.y, T).unwrap().vector();
17334 let left = normal.cross(along);
17335 (rec, e.crange, left.x * beyond < 0.0)
17336 };
17337 let thin = vec![
17338 Point2::new(0.0, 0.0),
17339 Point2::new(1.0, 0.0),
17340 Point2::new(1.0, 1e-6),
17341 Point2::new(0.0, 1e-6),
17342 ];
17343 let read = |strands: &[(f64, f64)]| -> Thin {
17344 let mut contacts = Vec::new();
17345 let mut ring = Vec::new();
17346 for (k, &(x, beyond)) in strands.iter().enumerate() {
17347 let (rec, range, reversed) = contact(x, beyond);
17348 contacts.push(rec);
17349 ring.push(Traversal {
17350 tag: Tag::Contact { contact: k, range },
17351 reversed,
17352 nodes: None,
17353 });
17354 }
17355 thin_by_strands(
17356 top,
17357 &[ring],
17358 std::slice::from_ref(&thin),
17359 &contacts,
17360 &theirs,
17361 T,
17362 )
17363 .unwrap()
17364 };
17365 assert!(matches!(read(&[(1.0, -1.0)]), Thin::Reads(PieceState::In)));
17366 assert!(matches!(read(&[(2.0, 1.0)]), Thin::Reads(PieceState::Out)));
17367 assert!(matches!(read(&[(1.0, -1.0), (2.0, 1.0)]), Thin::Disagree));
17368 assert!(matches!(read(&[(2.0, -1.0)]), Thin::Undecided));
17371 }
17372
17373 #[test]
17381 fn a_period_jump_on_a_wall_smaller_than_its_radii_is_folded_back() {
17382 let wall = |restate: f64| -> (Model, Vec<(EdgeKey, Point2)>) {
17383 let mut model = Model::new();
17384 let x = Direction::new(ogeom_math::Vector::new(-1.0, -1.0, 0.0), T).unwrap();
17385 let whole = make_cylinder(
17386 &mut model,
17387 Frame::new(Point::ORIGIN, Direction::Z, x, T).unwrap(),
17388 0.002,
17389 0.01,
17390 T,
17391 )
17392 .unwrap()
17393 .shape;
17394 let quadrant = make_box(
17395 &mut model,
17396 Frame::new(Point::new(0.0, 0.0, -1.0), Direction::Z, Direction::X, T).unwrap(),
17397 (1.0, 1.0, 2.0),
17398 T,
17399 )
17400 .unwrap()
17401 .shape;
17402 let part = common(&mut model, &whole, &quadrant, T).unwrap().shape;
17403 let face = explore_unique(&model, &part, ShapeType::Face).unwrap()[0].clone();
17404 let surface = model.node(&face).unwrap().data().as_face().unwrap().surface;
17405 let mut restated = false;
17406 for edge in explore_unique(&model, &face, ShapeType::Edge).unwrap() {
17407 model
17408 .widen(&edge, ogeom_core::Tolerance::new(0.003).unwrap())
17409 .unwrap();
17410 let Some(EdgeRepr::PCurve { curve, range, .. }) = model
17411 .node(&edge)
17412 .unwrap()
17413 .data()
17414 .as_edge()
17415 .unwrap()
17416 .pcurve_for(surface, edge.location())
17417 .cloned()
17418 else {
17419 continue;
17420 };
17421 let stated = model.geometry().pcurve(curve).unwrap().clone();
17422 let (a, b) = (
17423 stated.point_at(range.0, T).unwrap(),
17424 stated.point_at(range.1, T).unwrap(),
17425 );
17426 if restate == 0.0 || restated || (a.x - b.x).abs() > 1e-9 {
17427 continue;
17428 }
17429 let moved = stated
17430 .transformed(
17431 &ogeom_math::Transform2::translation(ogeom_math::Vector2::new(
17432 restate, 0.0,
17433 )),
17434 T,
17435 )
17436 .unwrap();
17437 let moved = model.geometry_mut().add_pcurve(moved);
17438 if let Some(node) = model.node_mut(&edge)
17439 && let NodeData::Edge(data) = node.data_mut()
17440 {
17441 for repr in &mut data.representations {
17442 if let EdgeRepr::PCurve {
17443 curve, surface: s, ..
17444 } = repr
17445 && *s == surface
17446 {
17447 *curve = moved;
17448 }
17449 }
17450 }
17451 restated = true;
17452 }
17453 assert!(restate == 0.0 || restated);
17454 let gathered = gather(&model, &part, false, None, T).unwrap();
17455 let starts = gathered
17456 .faces
17457 .iter()
17458 .find(|f| matches!(f.surface, SurfaceGeometry::Cylinder(_)))
17459 .unwrap()
17460 .edges
17461 .iter()
17462 .map(|e| (e.node, e.pcurve.point_at(e.prange.0, T).unwrap()))
17463 .collect();
17464 (model, starts)
17465 };
17466 let (_, want) = wall(0.0);
17467 for by in [core::f64::consts::TAU, -core::f64::consts::TAU] {
17468 let (_, got) = wall(by);
17469 assert_eq!(got.len(), want.len());
17470 for ((_, g), (_, w)) in got.iter().zip(&want) {
17471 assert!(
17472 g.distance(*w) < 1e-9,
17473 "moved {by}: {got:?} against {want:?}"
17474 );
17475 }
17476 }
17477 }
17478
17479 #[test]
17486 fn a_graze_s_crossings_are_one_junction_however_tightly_fitted() {
17487 let radius = 23.6;
17488 let curve = Curve::Line(
17489 ogeom_geom::LineCurve::segment(
17490 Point::new(-10.0, 0.0, 0.0),
17491 Point::new(10.0, 0.0, 0.0),
17492 T,
17493 )
17494 .unwrap(),
17495 );
17496 let crange = ogeom_geom::Curve3d::domain(&curve);
17497 let off_drill = |_: (bool, usize), p: Point| -> OgeomResult<f64> {
17498 Ok(p.x.hypot(p.z - radius) - radius + 4.6e-6)
17499 };
17500 let half = 0.0148;
17501 for honesty in [2.6e-4, 1e-3] {
17502 let paves: Vec<Pave> = [-half, half]
17503 .into_iter()
17504 .map(|x| Pave {
17505 t: x + 10.0,
17506 honesty,
17507 across: Some((false, 0)),
17508 spread: 0.0,
17509 })
17510 .collect();
17511 let clusters =
17512 cluster_paves(&curve, crange, 1e-7, &paves, Some(&off_drill), &[], T).unwrap();
17513 assert_eq!(clusters.len(), 1, "honesty {honesty}: {clusters:?}");
17514 assert!(clusters[0].at.x.abs() < half, "{clusters:?}");
17515 }
17516 }
17517
17518 fn box_with_an_edge_drawn_off_its_top(model: &mut Model, shift: f64, loose: f64) -> Shape {
17523 let solid = make_box(model, Frame::WORLD, (20.0, 20.0, 10.0), T)
17524 .unwrap()
17525 .shape;
17526 let top = explore_unique(model, &solid, ShapeType::Face)
17527 .unwrap()
17528 .into_iter()
17529 .find(|f| {
17530 let b = shape_bounds(model, f, T).unwrap();
17531 b.low().unwrap().z > 9.9
17532 })
17533 .unwrap();
17534 let top_surface = model.node(&top).unwrap().data().as_face().unwrap().surface;
17535 let edge = explore_unique(model, &top, ShapeType::Edge)
17536 .unwrap()
17537 .into_iter()
17538 .find(|e| {
17539 let b = shape_bounds(model, e, T).unwrap();
17540 b.high().unwrap().y.abs() < 1e-6
17541 })
17542 .unwrap();
17543 let (old, range) = match model
17544 .node(&edge)
17545 .unwrap()
17546 .data()
17547 .as_edge()
17548 .unwrap()
17549 .pcurve_on(top_surface)
17550 {
17551 Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
17552 _ => unreachable!("a box's edge has a plain pcurve on its top"),
17553 };
17554 let pcurve = model.geometry().pcurve(old).unwrap().clone();
17555 let (from, to) = (
17556 pcurve.point_at(range.0, T).unwrap(),
17557 pcurve.point_at(range.1, T).unwrap(),
17558 );
17559 let along = (to - from) / (to - from).magnitude();
17561 let mut across = ogeom_math::Vector2::new(-along.y, along.x);
17562 let middle = Point2::new(f64::midpoint(from.x, to.x), f64::midpoint(from.y, to.y));
17563 let lift = |p: Point2| {
17564 let SurfaceGeometry::Plane(plane) = model.geometry().surface(top_surface).unwrap()
17565 else {
17566 unreachable!("a box's top is a plane")
17567 };
17568 plane.point_at(p.x, p.y, T).unwrap()
17569 };
17570 if lift(middle + across).y < 0.0 {
17571 across = -across;
17572 }
17573 let line =
17574 ogeom_geom::Line2d::segment(from + across * shift, to + across * shift, T).unwrap();
17575 let domain = (0.0, from.distance(to));
17576 let id = model.geometry_mut().add_pcurve(PlanarCurve::Line(line));
17577 let Some(NodeData::Edge(data)) = model.node_mut(&edge).map(|n| n.data_mut()) else {
17578 unreachable!("an edge")
17579 };
17580 for repr in &mut data.representations {
17581 if let EdgeRepr::PCurve {
17582 curve,
17583 surface,
17584 range,
17585 ..
17586 } = repr
17587 && *surface == top_surface
17588 {
17589 *curve = id;
17590 *range = domain;
17591 }
17592 }
17593 data.tolerance = data.tolerance.widen_to(loose);
17594 solid
17595 }
17596
17597 #[test]
17602 fn a_face_reads_an_edge_as_far_as_it_stands_off_that_face() {
17603 let mut model = Model::new();
17604 let solid = box_with_an_edge_drawn_off_its_top(&mut model, 0.004, 0.005);
17605 let gathered = gather(&model, &solid, false, None, T).unwrap();
17606 let mut read = 0;
17607 for face in &gathered.faces {
17608 for e in &face.edges {
17609 if e.tolerance < 1e-3 {
17610 continue;
17611 }
17612 read += 1;
17613 let normal = face.surface.normal_at(0.0, 0.0, T).unwrap().vector();
17614 let (drawn, range, off) = e.drawn();
17615 let middle = drawn.point_at(f64::midpoint(range.0, range.1), T).unwrap();
17616 if normal.z.abs() > 0.5 {
17617 assert!((e.standoff - 0.004).abs() < 1e-6, "top: {}", e.standoff);
17618 assert!(e.image.is_some() && off == 0.0);
17619 assert!((middle.y - 0.004).abs() < 1e-6, "top drawn at {middle:?}");
17620 } else {
17621 assert!(e.standoff < 1e-9, "front: {}", e.standoff);
17622 assert!(e.image.is_none());
17623 assert!(middle.y.abs() < 1e-9, "front drawn at {middle:?}");
17624 }
17625 }
17626 }
17627 assert_eq!(read, 2);
17628
17629 let mut model = Model::new();
17630 let solid = box_with_an_edge_drawn_off_its_top(&mut model, 1e-4, 0.005);
17631 let gathered = gather(&model, &solid, false, None, T).unwrap();
17632 for e in gathered.faces.iter().flat_map(|f| f.edges.iter()) {
17633 if e.tolerance >= 1e-3 {
17634 assert_eq!(e.standoff, e.tolerance);
17635 assert!(e.image.is_none());
17636 }
17637 }
17638 }
17639
17640 #[test]
17645 fn a_loose_edge_s_crossings_join_across_their_spread_and_no_further() {
17646 let curve = Curve::Line(
17647 ogeom_geom::LineCurve::segment(Point::new(0.0, 0.0, 0.0), Point::new(2.0, 0.0, 0.0), T)
17648 .unwrap(),
17649 );
17650 let crange = ogeom_geom::Curve3d::domain(&curve);
17651 let pave = |t: f64, spread: f64| Pave {
17652 t,
17653 honesty: 0.0,
17654 across: None,
17655 spread,
17656 };
17657 let two = [pave(1.0, 0.03), pave(1.05, 0.03)];
17658 let clusters = cluster_paves(&curve, crange, 0.0096, &two, None, &[], T).unwrap();
17659 assert_eq!(clusters.len(), 1, "{clusters:?}");
17660 assert!((clusters[0].span - 0.05).abs() < 1e-9, "{clusters:?}");
17661 let tight = [pave(1.0, 0.0), pave(1.05, 0.0)];
17662 assert_eq!(
17663 cluster_paves(&curve, crange, 0.0096, &tight, None, &[], T)
17664 .unwrap()
17665 .len(),
17666 2
17667 );
17668 let run: Vec<Pave> = (0..8)
17669 .map(|k| pave(0.5 + 0.05 * f64::from(k), 0.03))
17670 .collect();
17671 let clusters = cluster_paves(&curve, crange, 0.0096, &run, None, &[], T).unwrap();
17672 assert!(clusters.len() > 1, "{clusters:?}");
17673 for c in &clusters {
17674 assert!(c.span <= 0.06 + 1e-9, "{clusters:?}");
17675 }
17676 }
17677
17678 #[test]
17686 fn a_crossing_where_the_edge_touches_the_other_surface_spreads_across_its_valley() {
17687 let cylinder: SurfaceGeometry = ogeom_geom::CylinderSurface::new(
17688 ogeom_math::Cylinder::new(Frame::WORLD, 2.0, T).unwrap(),
17689 (-5.0, 5.0),
17690 )
17691 .unwrap()
17692 .into();
17693 let line = |x: f64| {
17694 Curve::Line(
17695 ogeom_geom::LineCurve::segment(
17696 Point::new(x, -3.0, 0.0),
17697 Point::new(x, 3.0, 0.0),
17698 T,
17699 )
17700 .unwrap(),
17701 )
17702 };
17703 let touching = line(2.0);
17704 let spread_at = |curve: &Curve, y: f64, honesty: f64| {
17705 let t = y + 3.0;
17706 let at = curve.point_at(t, T).unwrap();
17707 touch_spread((curve, t), &cylinder, at, honesty, T)
17708 };
17709 let at_touch = spread_at(&touching, 0.0, 1e-6);
17710 assert!((at_touch - 2e-3).abs() < 1e-9, "{at_touch}");
17711 let d = 1e-3;
17712 let beside = spread_at(&touching, d, 1e-6);
17713 let want = d + d.mul_add(d, 4e-6).sqrt();
17714 assert!((beside - want).abs() < 1e-6, "{beside} against {want}");
17715 assert_eq!(spread_at(&touching, 0.0, 0.0), 0.0);
17716 assert_eq!(spread_at(&line(1.0), 3.0_f64.sqrt(), 1e-6), 0.0);
17717 }
17718
17719 #[test]
17724 fn a_crossing_spreads_along_a_loose_edge_by_its_lean() {
17725 let mut model = Model::new();
17726 let solid = make_box(&mut model, Frame::WORLD, (20.0, 20.0, 10.0), T)
17727 .unwrap()
17728 .shape;
17729 let mut gathered = gather(&model, &solid, false, None, T).unwrap();
17730 let e = gathered
17731 .faces
17732 .iter_mut()
17733 .flat_map(|f| f.edges.iter_mut())
17734 .find(|e| matches!(&*e.curve, Curve::Line(_)))
17735 .unwrap();
17736 let t = f64::midpoint(e.crange.0, e.crange.1);
17737 let at = e.curve.point_at(t, T).unwrap();
17738 let along = e.curve.d1_at(t, T).unwrap();
17739 let along = along / along.magnitude();
17740 let square = if along.z.abs() < 0.9 {
17741 along.cross(ogeom_math::Vector::Z)
17742 } else {
17743 along.cross(ogeom_math::Vector::X)
17744 };
17745 let square = square / square.magnitude();
17746 let plane_leaning = |lean: f64| -> SurfaceGeometry {
17747 let normal = square * lean.cos() + along * lean.sin();
17748 let frame = Frame::new(
17749 at,
17750 Direction::new(normal, T).unwrap(),
17751 Direction::new(normal.cross(square).cross(normal), T)
17752 .or_else(|_| Direction::new(along, T))
17753 .unwrap(),
17754 T,
17755 )
17756 .unwrap();
17757 ogeom_geom::PlaneSurface::new(ogeom_math::Plane::new(frame)).into()
17758 };
17759 assert_eq!(lean_spread(e, t, &plane_leaning(0.3), at, T), 0.0);
17760 e.tolerance = 0.01;
17761 for lean in [0.3_f64, 1.2] {
17762 let spread = lean_spread(e, t, &plane_leaning(lean), at, T);
17763 assert!(
17764 (spread - 0.01 / lean.sin()).abs() < 1e-9,
17765 "{lean}: {spread}"
17766 );
17767 }
17768 let spread = lean_spread(e, t, &plane_leaning(0.01), at, T);
17769 assert!((spread - 0.1).abs() < 1e-12, "{spread}");
17770 let apex = ogeom_geom::ConeSurface::new(
17771 ogeom_math::Cone::new(
17772 Frame::new(
17773 at,
17774 Direction::new(square, T).unwrap(),
17775 Direction::new(along, T).unwrap(),
17776 T,
17777 )
17778 .unwrap(),
17779 0.0,
17780 0.5,
17781 T,
17782 )
17783 .unwrap(),
17784 (0.0, 5.0),
17785 )
17786 .unwrap();
17787 let spread = lean_spread(e, t, &SurfaceGeometry::Cone(apex), at, T);
17788 assert!(spread.is_finite() && spread >= 0.0, "{spread}");
17789 }
17790
17791 #[test]
17794 fn a_point_beside_a_face_lies_within_it_only_to_the_margin() {
17795 let mut model = Model::new();
17796 let solid = make_box(&mut model, Frame::WORLD, (20.0, 20.0, 10.0), T)
17797 .unwrap()
17798 .shape;
17799 let gathered = gather(&model, &solid, false, None, T).unwrap();
17800 let top = gathered
17801 .faces
17802 .iter()
17803 .find(|f| f.bound.low().unwrap().z > 9.9)
17804 .unwrap();
17805 assert!(within_face(top, Point::new(10.0, 10.0, 10.0), 1e-7, T).unwrap());
17806 let beside = Point::new(10.0, -0.05, 10.0);
17807 assert!(!within_face(top, beside, 1e-4, T).unwrap());
17808 assert!(within_face(top, beside, 0.06, T).unwrap());
17809 }
17810
17811 #[test]
17815 fn a_circle_is_sampled_to_the_strand_sag_in_closed_form() {
17816 for radius in [0.01, 1.0, 40.0] {
17817 let circle = ogeom_math::Circle2::new(ogeom_math::Frame2::WORLD, radius, T).unwrap();
17818 let pcurve = PlanarCurve::Circle(ogeom_geom::Circle2d::new(circle));
17819 for (lo, hi) in [(0.0, 2.0 * PI), (1.0, 2.5)] {
17820 let count = circle_sag_count(&pcurve, lo, hi, 8).unwrap();
17821 let step = (hi - lo) / f64::from(u32::try_from(count).unwrap());
17822 assert!(
17823 count == 512
17824 || radius * (1.0 - (step / 2.0).cos()) <= STRAND_SAG * (1.0 + 1e-9)
17825 );
17826 let (measured, _) = sag_count(&|t| pcurve.point_at(t, T), lo, hi, 8).unwrap();
17827 assert!(count <= measured, "{count} against {measured}");
17828 }
17829 }
17830 }
17831
17832 #[test]
17837 fn an_outline_closes_across_the_openings_its_ends_leave() {
17838 let top = PI / 2.0;
17839 for short in [4e-4, 2.6e-8] {
17840 let mut lines = vec![
17841 vec![Point2::new(0.0, 0.0), Point2::new(0.06, 0.0)],
17842 vec![Point2::new(0.06, 0.0), Point2::new(0.06, top)],
17843 vec![Point2::new(0.06, top - short), Point2::new(0.0, 0.0)],
17844 ];
17845 bridge_outline_gaps(&mut lines);
17846 let lines: Vec<&[Point2]> = lines.iter().map(Vec::as_slice).collect();
17847 let level = Point2::new(-5.298, top - short / 2.0);
17850 assert!(!arrange::inside_many(&lines, level), "{short}");
17851 assert!(
17852 arrange::inside_many(&lines, Point2::new(0.05, 0.5)),
17853 "{short}"
17854 );
17855 }
17856 }
17857
17858 fn frame_at(origin: Point) -> Frame {
17859 Frame::new(origin, Direction::Z, Direction::X, T).unwrap()
17860 }
17861
17862 fn boxes(model: &mut Model) -> (Shape, Shape) {
17863 let a = make_box(model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
17865 let b = make_box(
17866 model,
17867 frame_at(Point::new(1.0, 1.0, 1.0)),
17868 (2.0, 2.0, 2.0),
17869 T,
17870 )
17871 .unwrap();
17872 (a.shape, b.shape)
17873 }
17874
17875 fn volume(model: &Model, shape: &Shape) -> f64 {
17876 let fine = Deflection {
17877 chord: 1e-3,
17878 ..Deflection::default()
17879 };
17880 volume_properties(model, shape, fine, T).unwrap().mass
17881 }
17882
17883 fn assert_valid(model: &Model, shape: &Shape) {
17884 let diagnosis = check(model, shape, T).unwrap();
17885 assert!(
17886 diagnosis.is_valid(),
17887 "the result fails validity: {:?}",
17888 diagnosis.problems
17889 );
17890 }
17891
17892 #[test]
17893 fn fuse_of_overlapping_boxes_has_the_inclusion_exclusion_volume() {
17894 let mut model = Model::new();
17895 let (a, b) = boxes(&mut model);
17896 let fused = fuse(&mut model, &a, &b, T).unwrap();
17897 assert_valid(&model, &fused.shape);
17898 assert!((volume(&model, &fused.shape) - 15.0).abs() < 1e-9);
17899 assert_eq!(
17900 fused.history.modified(&a),
17901 std::slice::from_ref(&fused.shape)
17902 );
17903 }
17904
17905 #[test]
17906 fn common_of_overlapping_boxes_is_the_overlap_cube() {
17907 let mut model = Model::new();
17908 let (a, b) = boxes(&mut model);
17909 let result = common(&mut model, &a, &b, T).unwrap();
17910 assert_valid(&model, &result.shape);
17911 assert!((volume(&model, &result.shape) - 1.0).abs() < 1e-9);
17912 }
17913
17914 #[test]
17915 fn cut_removes_the_overlap_from_the_first_argument() {
17916 let mut model = Model::new();
17917 let (a, b) = boxes(&mut model);
17918 let result = cut(&mut model, &a, &b, T).unwrap();
17919 assert_valid(&model, &result.shape);
17920 assert!((volume(&model, &result.shape) - 7.0).abs() < 1e-9);
17921 }
17922
17923 #[test]
17924 fn section_of_overlapping_boxes_is_the_six_segment_seam() {
17925 let mut model = Model::new();
17926 let (a, b) = boxes(&mut model);
17927 let result = section(&mut model, &a, &b, T).unwrap();
17928 let edges = explore(&model, &result.shape, Filter::OfType(ShapeType::Edge)).unwrap();
17929 assert_eq!(edges.len(), 6);
17930 let vertices =
17932 ogeom_topo::explore_unique(&model, &result.shape, ShapeType::Vertex).unwrap();
17933 assert_eq!(vertices.len(), 6);
17934 }
17935
17936 #[test]
17937 fn fuse_of_disjoint_boxes_is_a_compound_of_both() {
17938 let mut model = Model::new();
17939 let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
17940 let b = make_box(
17941 &mut model,
17942 frame_at(Point::new(5.0, 0.0, 0.0)),
17943 (2.0, 2.0, 2.0),
17944 T,
17945 )
17946 .unwrap();
17947 let fused = fuse(&mut model, &a.shape, &b.shape, T).unwrap();
17948 assert_eq!(model.kind_of(&fused.shape).unwrap(), ShapeType::Compound);
17949 assert!((volume(&model, &fused.shape) - 16.0).abs() < 1e-9);
17950 }
17951
17952 #[test]
17953 fn cutting_a_through_post_leaves_a_slab_with_a_hole() {
17954 let mut model = Model::new();
17955 let slab = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
17956 let post = make_box(
17957 &mut model,
17958 frame_at(Point::new(1.5, 1.5, -1.0)),
17959 (1.0, 1.0, 3.0),
17960 T,
17961 )
17962 .unwrap();
17963 let result = cut(&mut model, &slab.shape, &post.shape, T).unwrap();
17964 assert_valid(&model, &result.shape);
17965 assert!((volume(&model, &result.shape) - 15.0).abs() < 1e-9);
17966 }
17967
17968 #[test]
17969 fn common_with_a_contained_box_is_that_box() {
17970 let mut model = Model::new();
17971 let outer = make_box(&mut model, Frame::WORLD, (6.0, 6.0, 6.0), T).unwrap();
17972 let inner = make_box(
17973 &mut model,
17974 frame_at(Point::new(2.0, 2.0, 2.0)),
17975 (2.0, 2.0, 2.0),
17976 T,
17977 )
17978 .unwrap();
17979 let result = common(&mut model, &outer.shape, &inner.shape, T).unwrap();
17980 assert_valid(&model, &result.shape);
17981 assert!((volume(&model, &result.shape) - 8.0).abs() < 1e-9);
17982 }
17983
17984 #[test]
17985 fn cutting_everything_away_leaves_an_empty_compound() {
17986 let mut model = Model::new();
17987 let outer = make_box(&mut model, Frame::WORLD, (6.0, 6.0, 6.0), T).unwrap();
17988 let inner = make_box(
17989 &mut model,
17990 frame_at(Point::new(2.0, 2.0, 2.0)),
17991 (2.0, 2.0, 2.0),
17992 T,
17993 )
17994 .unwrap();
17995 let result = cut(&mut model, &inner.shape, &outer.shape, T).unwrap();
17996 assert_eq!(model.kind_of(&result.shape).unwrap(), ShapeType::Compound);
17997 assert!(
17998 explore(&model, &result.shape, Filter::OfType(ShapeType::Face))
17999 .unwrap()
18000 .is_empty()
18001 );
18002 }
18003
18004 #[test]
18005 fn drilling_a_box_leaves_a_cylindrical_hole() {
18006 let mut model = Model::new();
18012 let block = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
18013 let drill = make_cylinder(
18014 &mut model,
18015 frame_at(Point::new(2.0, 2.0, -1.0)),
18016 0.5,
18017 3.0,
18018 T,
18019 )
18020 .unwrap();
18021 let result = cut(&mut model, &block.shape, &drill.shape, T).unwrap();
18022 assert_valid(&model, &result.shape);
18023 let exact = 16.0 - PI * 0.25;
18024 let got = volume(&model, &result.shape);
18025 assert!(
18026 (got - exact).abs() / exact < 2e-3,
18027 "volume {got} against {exact}"
18028 );
18029 }
18030
18031 #[test]
18037 fn a_rod_through_a_long_drill_s_wall_is_cut_on_both_sides() {
18038 let (big, small, offset) = (56.0, 5.0, 46.0);
18039 let mut model = Model::new();
18040 let rod = make_cylinder(
18041 &mut model,
18042 Frame::new(
18043 Point::new(5.0, -1000.0, offset),
18044 Direction::Y,
18045 Direction::X,
18046 T,
18047 )
18048 .unwrap(),
18049 small,
18050 2000.0,
18051 T,
18052 )
18053 .unwrap();
18054 let drill = make_cylinder(
18055 &mut model,
18056 Frame::new(Point::new(-900.0, 0.0, 0.0), Direction::X, Direction::Z, T).unwrap(),
18057 big,
18058 1800.0,
18059 T,
18060 )
18061 .unwrap();
18062 let segment = |y: f64| -> f64 {
18065 let d = (big * big - y * y).max(0.0).sqrt() - offset;
18066 if d <= -small {
18067 0.0
18068 } else if d >= small {
18069 PI * small * small
18070 } else {
18071 small * small * (-d / small).acos() + d * (small * small - d * d).sqrt()
18072 }
18073 };
18074 let reach = (big * big - (offset - small).powi(2)).sqrt();
18075 let steps = 20_000_u32;
18076 let h = 2.0 * reach / f64::from(steps);
18077 let held = (0..=steps)
18078 .map(|i| {
18079 let w = if i == 0 || i == steps {
18080 1.0
18081 } else if i % 2 == 1 {
18082 4.0
18083 } else {
18084 2.0
18085 };
18086 w * segment(-reach + h * f64::from(i))
18087 })
18088 .sum::<f64>()
18089 * h
18090 / 3.0;
18091 let whole = PI * small * small * 2000.0;
18092 let kept = cut(&mut model, &rod.shape, &drill.shape, T).unwrap();
18093 assert_valid(&model, &kept.shape);
18094 let inside = common(&mut model, &rod.shape, &drill.shape, T).unwrap();
18095 assert_valid(&model, &inside.shape);
18096 let got = volume(&model, &inside.shape);
18097 assert!(
18098 (got - held).abs() / held < 2e-3,
18099 "common {got} against {held}"
18100 );
18101 let got = volume(&model, &kept.shape);
18102 let exact = whole - held;
18103 assert!(
18104 (got - exact).abs() / exact < 2e-3,
18105 "cut {got} against {exact}"
18106 );
18107 }
18108
18109 #[test]
18110 fn common_of_a_box_and_a_cylinder_is_the_post_inside_it() {
18111 let mut model = Model::new();
18112 let block = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
18113 let post = make_cylinder(
18114 &mut model,
18115 frame_at(Point::new(2.0, 2.0, -1.0)),
18116 0.5,
18117 3.0,
18118 T,
18119 )
18120 .unwrap();
18121 let result = common(&mut model, &block.shape, &post.shape, T).unwrap();
18122 assert_valid(&model, &result.shape);
18123 let exact = PI * 0.25;
18124 let got = volume(&model, &result.shape);
18125 assert!(
18126 (got - exact).abs() / exact < 2e-3,
18127 "volume {got} against {exact}"
18128 );
18129 }
18130
18131 #[test]
18132 fn fusing_a_post_onto_a_slab_adds_what_stands_proud() {
18133 let mut model = Model::new();
18134 let slab = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
18135 let post = make_cylinder(
18136 &mut model,
18137 frame_at(Point::new(2.0, 2.0, -1.0)),
18138 0.5,
18139 3.0,
18140 T,
18141 )
18142 .unwrap();
18143 let result = fuse(&mut model, &slab.shape, &post.shape, T).unwrap();
18144 assert_valid(&model, &result.shape);
18145 let exact = PI.mul_add(0.25 * 3.0, 16.0) - PI * 0.25;
18146 let got = volume(&model, &result.shape);
18147 assert!(
18148 (got - exact).abs() / exact < 2e-3,
18149 "volume {got} against {exact}"
18150 );
18151 }
18152
18153 #[test]
18154 fn crossed_cylinders_run_through_the_marched_sections() {
18155 let mut model = Model::new();
18164 let upright = make_cylinder(&mut model, Frame::WORLD, 1.0, 4.0, T).unwrap();
18165 let across_frame =
18166 Frame::new(Point::new(-2.0, 0.0, 2.0), Direction::X, Direction::Y, T).unwrap();
18167 let across = make_cylinder(&mut model, across_frame, 0.6, 4.0, T).unwrap();
18168
18169 let both = fuse(&mut model, &upright.shape, &across.shape, T).unwrap();
18170 assert_valid(&model, &both.shape);
18171 let shared = common(&mut model, &upright.shape, &across.shape, T).unwrap();
18172 assert_valid(&model, &shared.shape);
18173 let pierced = cut(&mut model, &upright.shape, &across.shape, T).unwrap();
18174 assert_valid(&model, &pierced.shape);
18175
18176 let va = volume(&model, &upright.shape);
18177 let vb = volume(&model, &across.shape);
18178 let vf = volume(&model, &both.shape);
18179 let vc = volume(&model, &shared.shape);
18180 let vx = volume(&model, &pierced.shape);
18181
18182 assert!(vc > 0.0 && vc < vb, "the overlap is real and partial: {vc}");
18183 assert!(
18184 (vf - (va + vb - vc)).abs() / vf < 2e-3,
18185 "fuse {vf} against A + B - common {}",
18186 va + vb - vc
18187 );
18188 assert!(
18189 (vx - (va - vc)).abs() / vx < 2e-3,
18190 "cut {vx} against A - common {}",
18191 va - vc
18192 );
18193 }
18194
18195 #[test]
18196 fn stacked_boxes_fuse_into_one_solid_and_the_contact_vanishes() {
18197 let mut model = Model::new();
18200 let lower = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 1.0), T).unwrap();
18201 let upper = make_box(
18202 &mut model,
18203 frame_at(Point::new(0.0, 0.0, 1.0)),
18204 (2.0, 2.0, 1.0),
18205 T,
18206 )
18207 .unwrap();
18208 let fused = fuse(&mut model, &lower.shape, &upper.shape, T).unwrap();
18209 assert_valid(&model, &fused.shape);
18210 assert_eq!(model.kind_of(&fused.shape).unwrap(), ShapeType::Solid);
18211 assert!((volume(&model, &fused.shape) - 8.0).abs() < 1e-9);
18212 }
18213
18214 #[test]
18215 fn a_small_box_on_a_big_one_fuses_with_a_partial_contact() {
18216 let mut model = Model::new();
18220 let big = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
18221 let small = make_box(
18222 &mut model,
18223 frame_at(Point::new(1.0, 1.0, 1.0)),
18224 (2.0, 2.0, 1.0),
18225 T,
18226 )
18227 .unwrap();
18228 let fused = fuse(&mut model, &big.shape, &small.shape, T).unwrap();
18229 assert_valid(&model, &fused.shape);
18230 assert!((volume(&model, &fused.shape) - 20.0).abs() < 1e-9);
18231
18232 let mut model = Model::new();
18235 let big = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
18236 let small = make_box(
18237 &mut model,
18238 frame_at(Point::new(1.0, 1.0, 1.0)),
18239 (2.0, 2.0, 1.0),
18240 T,
18241 )
18242 .unwrap();
18243 let result = cut(&mut model, &big.shape, &small.shape, T).unwrap();
18244 assert_valid(&model, &result.shape);
18245 assert!((volume(&model, &result.shape) - 16.0).abs() < 1e-9);
18246 }
18247
18248 #[test]
18249 fn flush_walls_fuse_cut_and_meet_with_aligned_contact() {
18250 let mut model = Model::new();
18254 let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
18255 let b = make_box(
18256 &mut model,
18257 frame_at(Point::new(1.0, 0.0, 0.0)),
18258 (2.0, 2.0, 2.0),
18259 T,
18260 )
18261 .unwrap();
18262 let fused = fuse(&mut model, &a.shape, &b.shape, T).unwrap();
18263 assert_valid(&model, &fused.shape);
18264 assert!((volume(&model, &fused.shape) - 12.0).abs() < 1e-9);
18265
18266 let mut model = Model::new();
18267 let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
18268 let b = make_box(
18269 &mut model,
18270 frame_at(Point::new(1.0, 0.0, 0.0)),
18271 (2.0, 2.0, 2.0),
18272 T,
18273 )
18274 .unwrap();
18275 let shared = common(&mut model, &a.shape, &b.shape, T).unwrap();
18276 assert_valid(&model, &shared.shape);
18277 assert!((volume(&model, &shared.shape) - 4.0).abs() < 1e-9);
18278
18279 let mut model = Model::new();
18280 let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
18281 let b = make_box(
18282 &mut model,
18283 frame_at(Point::new(1.0, 0.0, 0.0)),
18284 (2.0, 2.0, 2.0),
18285 T,
18286 )
18287 .unwrap();
18288 let cut_result = cut(&mut model, &a.shape, &b.shape, T).unwrap();
18289 assert_valid(&model, &cut_result.shape);
18290 assert!((volume(&model, &cut_result.shape) - 4.0).abs() < 1e-9);
18291 }
18292
18293 #[test]
18294 fn every_source_face_is_accounted_for_in_the_history() {
18295 let mut model = Model::new();
18296 let (a, b) = boxes(&mut model);
18297 let result = cut(&mut model, &a, &b, T).unwrap();
18298 for face in explore(&model, &a, Filter::OfType(ShapeType::Face)).unwrap() {
18299 assert!(
18300 result.history.is_affected(&face),
18301 "a face of the first argument vanished from the history"
18302 );
18303 }
18304 }
18305
18306 fn drilled_plate(model: &mut Model, rows: u32) -> Shape {
18309 let size = 10.0 * f64::from(rows) + 10.0;
18310 let block = make_box(model, Frame::WORLD, (size, size, 10.0), T)
18311 .unwrap()
18312 .shape;
18313 let mut pins = Vec::new();
18314 for i in 0..rows {
18315 for j in 0..rows {
18316 let at = Point::new(10.0 + 10.0 * f64::from(i), 10.0 + 10.0 * f64::from(j), -5.0);
18317 pins.push(
18318 make_cylinder(model, frame_at(at), 1.5, 20.0, T)
18319 .unwrap()
18320 .shape,
18321 );
18322 }
18323 }
18324 let pins = model.add_compound(&pins).unwrap();
18325 cut(model, &block, &pins, T).unwrap().shape
18326 }
18327
18328 fn with_every_face(
18330 model: &mut Model,
18331 a: &Shape,
18332 b: &Shape,
18333 keep_aside: [bool; 2],
18334 keep: impl Fn(&FacePiece) -> Option<bool>,
18335 ) -> Built {
18336 let fused = general_fuse_classified(model, a, b, None, T).unwrap();
18337 let kept: Vec<(usize, bool)> = fused
18338 .pieces
18339 .iter()
18340 .enumerate()
18341 .filter_map(|(i, p)| keep(p).map(|flip| (i, flip)))
18342 .collect();
18343 assemble_result(model, &fused, &kept, keep_aside, a, b, T).unwrap()
18344 }
18345
18346 fn cut_keeps(p: &FacePiece) -> Option<bool> {
18347 match (p.from_a, p.state) {
18348 (true, PieceState::Out | PieceState::OnOpposed) => Some(false),
18349 (false, PieceState::In) => Some(true),
18350 _ => None,
18351 }
18352 }
18353
18354 fn clear_of(model: &Model, face: &Shape, tool: &Shape) -> bool {
18357 let face = ogeom_algo::face_bounds(model, face).unwrap().expanded(1.0);
18358 explore_unique(model, tool, ShapeType::Solid)
18359 .unwrap()
18360 .iter()
18361 .all(|lump| !face.intersects(&ogeom_algo::tight_bounds(model, lump, T).unwrap()))
18362 }
18363
18364 fn same_as_with_every_face(model: &Model, a: &Shape, b: &Shape, local: &Built, whole: &Built) {
18368 assert_valid(model, &local.shape);
18369 assert_valid(model, &whole.shape);
18370 let (v_local, v_whole) = (volume(model, &local.shape), volume(model, &whole.shape));
18371 assert!(
18372 (v_local - v_whole).abs() <= 1e-9 * v_whole.abs(),
18373 "{v_local} against {v_whole}"
18374 );
18375 let count = |shape: &Shape| explore_unique(model, shape, ShapeType::Face).unwrap().len();
18376 assert_eq!(count(&local.shape), count(&whole.shape));
18377 let mut passed = 0;
18378 for face in explore_unique(model, a, ShapeType::Face).unwrap() {
18379 if clear_of(model, &face, b) {
18380 let copy = local.history.copy_of(&face);
18381 assert!(
18382 copy.is_some_and(|c| c.is_same(&face)),
18383 "a face clear of the tool was not passed through"
18384 );
18385 passed += 1;
18386 }
18387 }
18388 assert!(passed > 0);
18389 }
18390
18391 #[test]
18397 fn a_one_lump_tool_spanning_a_plate_leaves_the_holes_it_misses_as_they_were() {
18398 let mut model = Model::new();
18399 let plate = drilled_plate(&mut model, 3);
18400 let along_y = make_box(
18401 &mut model,
18402 frame_at(Point::new(-1.0, -1.0, 8.0)),
18403 (5.0, 42.0, 3.0),
18404 T,
18405 )
18406 .unwrap()
18407 .shape;
18408 let along_x = make_box(
18409 &mut model,
18410 frame_at(Point::new(-1.0, -1.0, 9.0)),
18411 (42.0, 5.0, 2.0),
18412 T,
18413 )
18414 .unwrap()
18415 .shape;
18416 let corner = fuse(&mut model, &along_y, &along_x, T).unwrap().shape;
18417 assert_eq!(
18418 explore_unique(&model, &corner, ShapeType::Solid)
18419 .unwrap()
18420 .len(),
18421 1
18422 );
18423 let local = cut(&mut model, &plate, &corner, T).unwrap();
18424 let whole = with_every_face(&mut model, &plate, &corner, [true, false], cut_keeps);
18425 assert_valid(&model, &local.shape);
18426 let (v_local, v_whole) = (volume(&model, &local.shape), volume(&model, &whole.shape));
18427 assert!(
18428 (v_local - v_whole).abs() <= 1e-9 * v_whole,
18429 "{v_local} against {v_whole}"
18430 );
18431 let walls: Vec<Shape> = explore_unique(&model, &plate, ShapeType::Face)
18432 .unwrap()
18433 .into_iter()
18434 .filter(|f| {
18435 let data = model.node(f).unwrap().data().as_face().unwrap();
18436 matches!(
18437 model.geometry().surface(data.surface),
18438 Some(SurfaceGeometry::Cylinder(_))
18439 )
18440 })
18441 .collect();
18442 assert_eq!(walls.len(), 9);
18443 for wall in &walls {
18444 let copy = local.history.copy_of(wall);
18445 assert!(
18446 copy.is_some_and(|c| c.is_same(wall)),
18447 "a hole wall clear of the tool was not passed through"
18448 );
18449 }
18450 }
18451
18452 #[test]
18453 fn a_hole_drilled_at_a_plates_corner_leaves_the_rest_as_it_was() {
18454 let mut model = Model::new();
18455 let plate = drilled_plate(&mut model, 3);
18456 let corner = make_cylinder(
18457 &mut model,
18458 frame_at(Point::new(5.0, 5.0, -5.0)),
18459 1.5,
18460 20.0,
18461 T,
18462 )
18463 .unwrap()
18464 .shape;
18465 assert!(
18466 aside::set_aside(&model, &plate, &corner, T)
18467 .unwrap()
18468 .is_some()
18469 );
18470 let local = cut(&mut model, &plate, &corner, T).unwrap();
18471 let whole = with_every_face(&mut model, &plate, &corner, [true, false], cut_keeps);
18472 same_as_with_every_face(&model, &plate, &corner, &local, &whole);
18473 let holed = 40.0 * 40.0 * 10.0 - 10.0 * PI * 1.5 * 1.5 * 10.0;
18474 assert!((volume(&model, &local.shape) - holed).abs() < 1e-6 * holed);
18475 }
18476
18477 #[test]
18480 fn holes_at_opposite_corners_leave_the_faces_between_them_as_they_were() {
18481 let mut model = Model::new();
18482 let plate = drilled_plate(&mut model, 3);
18483 let mut pins = Vec::new();
18484 for at in [5.0, 35.0] {
18485 pins.push(
18486 make_cylinder(&mut model, frame_at(Point::new(at, at, -5.0)), 1.5, 20.0, T)
18487 .unwrap()
18488 .shape,
18489 );
18490 }
18491 let pins = model.add_compound(&pins).unwrap();
18492 let local = cut(&mut model, &plate, &pins, T).unwrap();
18493 let whole = with_every_face(&mut model, &plate, &pins, [true, false], cut_keeps);
18494 same_as_with_every_face(&model, &plate, &pins, &local, &whole);
18495 let holed = 40.0 * 40.0 * 10.0 - 11.0 * PI * 1.5 * 1.5 * 10.0;
18496 assert!((volume(&model, &local.shape) - holed).abs() < 1e-6 * holed);
18497 }
18498
18499 #[test]
18504 fn a_frame_fused_from_bars_cuts_the_rim_off_a_box() {
18505 let mut model = Model::new();
18506 let block = make_box(&mut model, Frame::WORLD, (10.0, 10.0, 10.0), T)
18507 .unwrap()
18508 .shape;
18509 let bars = [
18510 ((0.0, 0.0), (10.0, 1.0)),
18511 ((0.0, 9.0), (10.0, 1.0)),
18512 ((0.0, 0.0), (1.0, 10.0)),
18513 ((9.0, 0.0), (1.0, 10.0)),
18514 ];
18515 let mut frame: Option<Shape> = None;
18516 for ((x, y), (dx, dy)) in bars {
18517 let bar = make_box(
18518 &mut model,
18519 frame_at(Point::new(x, y, 9.0)),
18520 (dx, dy, 1.0),
18521 T,
18522 )
18523 .unwrap()
18524 .shape;
18525 frame = Some(match frame {
18526 None => bar,
18527 Some(held) => fuse(&mut model, &held, &bar, T).unwrap().shape,
18528 });
18529 }
18530 let frame = frame.unwrap();
18531 let rimmed = cut(&mut model, &block, &frame, T).unwrap();
18532 assert_valid(&model, &rimmed.shape);
18533 let want = 1000.0 - (100.0 - 64.0);
18534 let got = volume(&model, &rimmed.shape);
18535 assert!((got - want).abs() < 1e-9 * want, "{got} against {want}");
18536 }
18537
18538 #[test]
18539 fn a_notch_through_a_plates_edge_meets_the_faces_left_as_they_were() {
18540 let mut model = Model::new();
18544 let plate = drilled_plate(&mut model, 3);
18545 let notch = make_box(
18546 &mut model,
18547 frame_at(Point::new(-1.0, -1.0, -1.0)),
18548 (5.0, 4.0, 12.0),
18549 T,
18550 )
18551 .unwrap()
18552 .shape;
18553 let local = cut(&mut model, &plate, ¬ch, T).unwrap();
18554 let whole = with_every_face(&mut model, &plate, ¬ch, [true, false], cut_keeps);
18555 same_as_with_every_face(&model, &plate, ¬ch, &local, &whole);
18556 }
18557
18558 #[test]
18559 fn a_boss_fused_and_kept_in_common_beside_faces_set_aside() {
18560 let mut model = Model::new();
18561 let plate = drilled_plate(&mut model, 3);
18562 let boss = make_cylinder(
18563 &mut model,
18564 frame_at(Point::new(5.0, 5.0, 5.0)),
18565 3.0,
18566 10.0,
18567 T,
18568 )
18569 .unwrap()
18570 .shape;
18571 let local = fuse(&mut model, &plate, &boss, T).unwrap();
18572 let whole = with_every_face(&mut model, &plate, &boss, [true, true], |p| {
18573 (p.state == PieceState::Out || (p.state == PieceState::OnAligned && !p.covered))
18574 .then_some(false)
18575 });
18576 same_as_with_every_face(&model, &plate, &boss, &local, &whole);
18577
18578 let local = common(&mut model, &plate, &boss, T).unwrap();
18579 let whole = with_every_face(&mut model, &plate, &boss, [false, false], |p| {
18580 (p.state == PieceState::In || (p.state == PieceState::OnAligned && !p.covered))
18581 .then_some(false)
18582 });
18583 assert_valid(&model, &local.shape);
18584 let (v_local, v_whole) = (volume(&model, &local.shape), volume(&model, &whole.shape));
18585 assert!(
18586 (v_local - v_whole).abs() <= 1e-9 * v_whole,
18587 "{v_local} against {v_whole}"
18588 );
18589 for face in explore_unique(&model, &plate, ShapeType::Face).unwrap() {
18590 if clear_of(&model, &face, &boss) {
18591 assert!(local.history.is_deleted(&face));
18592 }
18593 }
18594 }
18595
18596 #[test]
18601 #[ignore = "heavy"]
18602 fn a_corner_hole_in_a_large_plate_is_the_solid_every_face_makes() {
18603 let mut model = Model::new();
18604 let plate = drilled_plate(&mut model, 24);
18605 assert_eq!(
18606 explore_unique(&model, &plate, ShapeType::Face)
18607 .unwrap()
18608 .len(),
18609 582
18610 );
18611 let corner = make_cylinder(
18612 &mut model,
18613 frame_at(Point::new(5.0, 5.0, -5.0)),
18614 1.5,
18615 20.0,
18616 T,
18617 )
18618 .unwrap()
18619 .shape;
18620 let local = cut(&mut model, &plate, &corner, T).unwrap();
18621 let whole = with_every_face(&mut model, &plate, &corner, [true, false], cut_keeps);
18622 same_as_with_every_face(&model, &plate, &corner, &local, &whole);
18623 let copied = explore_unique(&model, &plate, ShapeType::Face)
18626 .unwrap()
18627 .iter()
18628 .filter(|face| local.history.copy_of(face).is_some())
18629 .count();
18630 assert_eq!(copied, 580);
18631 }
18632
18633 fn face_with_role(model: &Model, solid: &Shape, role: ogeom_core::Role) -> Shape {
18635 explore_unique(model, solid, ShapeType::Face)
18636 .unwrap()
18637 .into_iter()
18638 .find(|f| {
18639 matches!(
18640 model.provenance_of(f),
18641 Some(
18642 ogeom_core::Provenance::Primitive { role: r, .. }
18643 | ogeom_core::Provenance::Derived { role: r, .. }
18644 ) if *r == role
18645 )
18646 })
18647 .unwrap()
18648 }
18649
18650 fn traced_into(model: &Model, result: &Shape, history: &History, source: &Shape) -> Vec<Shape> {
18652 explore_unique(model, result, ShapeType::Face)
18653 .unwrap()
18654 .into_iter()
18655 .filter(|r| history.trace(source).iter().any(|t| t.is_same(r)))
18656 .collect()
18657 }
18658
18659 #[test]
18664 fn a_coincident_piece_dropped_for_the_others_traces_to_it() {
18665 use ogeom_algo::primitive::roles::{FACE_MAX_Z, FACE_MIN_Z};
18666 let mut model = Model::new();
18667 let cube = make_box(&mut model, Frame::WORLD, (100.0, 100.0, 100.0), T)
18668 .unwrap()
18669 .shape;
18670 let cylinder = make_cylinder(&mut model, Frame::WORLD, 50.0, 100.0, T)
18671 .unwrap()
18672 .shape;
18673 for keep_in in [true, false] {
18674 let built = if keep_in {
18675 common(&mut model, &cube, &cylinder, T).unwrap()
18676 } else {
18677 fuse(&mut model, &cube, &cylinder, T).unwrap()
18678 };
18679 let expected = if keep_in {
18680 PI * 50.0 * 50.0 * 100.0 / 4.0
18681 } else {
18682 1e6 + PI * 50.0 * 50.0 * 100.0 * 3.0 / 4.0
18683 };
18684 assert_valid(&model, &built.shape);
18685 let v = volume(&model, &built.shape);
18686 assert!(
18687 (v - expected).abs() <= 1e-3 * expected,
18688 "{v} against {expected}"
18689 );
18690 for role in [FACE_MIN_Z, FACE_MAX_Z] {
18691 let (side, cap) = (
18692 face_with_role(&model, &cube, role),
18693 face_with_role(&model, &cylinder, role),
18694 );
18695 assert!(!built.history.is_deleted(&cap));
18696 let from_cap = traced_into(&model, &built.shape, &built.history, &cap);
18697 let from_side = traced_into(&model, &built.shape, &built.history, &side);
18698 if keep_in {
18699 assert_eq!(from_cap.len(), 1, "{role:?}: the cap");
18702 assert_eq!(from_side.len(), 1, "{role:?}: the cube's face");
18703 }
18704 let shared = from_cap
18705 .iter()
18706 .filter(|f| from_side.iter().any(|g| g.is_same(f)))
18707 .count();
18708 assert_eq!(shared, 1, "common {keep_in}");
18709 }
18710 }
18711 }
18712
18713 #[test]
18719 fn a_tool_face_on_a_floor_a_cut_left_traces_to_that_floor() {
18720 use ogeom_algo::primitive::roles::{FACE_MAX_Z, FACE_MIN_X, FACE_MIN_Z};
18721 let mut model = Model::new();
18722 let block = make_box(&mut model, Frame::WORLD, (100.0, 100.0, 50.0), T)
18723 .unwrap()
18724 .shape;
18725 let step = make_box(
18726 &mut model,
18727 frame_at(Point::new(50.0, -10.0, 30.0)),
18728 (60.0, 120.0, 20.0),
18729 T,
18730 )
18731 .unwrap()
18732 .shape;
18733 let slot = make_box(
18734 &mut model,
18735 frame_at(Point::new(-10.0, 40.0, 30.0)),
18736 (120.0, 20.0, 30.0),
18737 T,
18738 )
18739 .unwrap()
18740 .shape;
18741 let (step_floor, slot_floor) = (
18742 face_with_role(&model, &step, FACE_MIN_Z),
18743 face_with_role(&model, &slot, FACE_MIN_Z),
18744 );
18745 let stepped = cut(&mut model, &block, &step, T).unwrap();
18746 let slotted = cut(&mut model, &stepped.shape, &slot, T).unwrap();
18747 assert_valid(&model, &slotted.shape);
18748 let history = stepped.history.then(&slotted.history);
18749 let v = volume(&model, &slotted.shape);
18750 assert!((v - 380_000.0).abs() <= 1e-6 * 380_000.0, "{v}");
18751 let from_slot = traced_into(&model, &slotted.shape, &history, &slot_floor);
18752 let from_step = traced_into(&model, &slotted.shape, &history, &step_floor);
18753 assert_eq!(from_slot.len(), 2);
18754 let shared = from_slot
18755 .iter()
18756 .filter(|f| from_step.iter().any(|g| g.is_same(f)))
18757 .count();
18758 assert_eq!(shared, 1);
18759 for role in [FACE_MAX_Z, FACE_MIN_X] {
18762 assert!(history.is_deleted(&face_with_role(&model, &slot, role)));
18763 }
18764 }
18765
18766 #[test]
18769 fn a_cut_then_a_move_traces_its_inputs_to_the_moved_result() {
18770 let mut model = Model::new();
18771 let (a, b) = boxes(&mut model);
18772 let cut_out = cut(&mut model, &a, &b, T).unwrap();
18773 let moved = ogeom_algo::transformed(
18774 &mut model,
18775 &cut_out.shape,
18776 ogeom_math::Transform::translation(ogeom_math::Vector::new(100.0, 0.0, 0.0)),
18777 )
18778 .unwrap();
18779 let history = cut_out.history.then(&moved.history);
18780 let result = explore_unique(&model, &moved.shape, ShapeType::Face).unwrap();
18781 let mut traced = 0;
18782 for face in explore_unique(&model, &a, ShapeType::Face)
18783 .unwrap()
18784 .into_iter()
18785 .chain(explore_unique(&model, &b, ShapeType::Face).unwrap())
18786 {
18787 if history.is_deleted(&face) {
18788 continue;
18789 }
18790 let images = history.trace(&face);
18791 assert!(!images.is_empty());
18792 for image in images {
18793 assert!(result.iter().any(|r| r.is_same(image)));
18794 }
18795 traced += 1;
18796 }
18797 assert_eq!(traced, 9);
18798 }
18799
18800 #[test]
18805 fn a_cut_then_a_shear_traces_its_inputs_to_the_sheared_result() {
18806 let mut model = Model::new();
18807 let (a, b) = boxes(&mut model);
18808 let cut_out = cut(&mut model, &a, &b, T).unwrap();
18809 let shear = ogeom_math::GeneralTransform::new(
18810 ogeom_math::Matrix3 {
18811 rows: [[1.0, 0.5, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
18812 },
18813 ogeom_math::Vector::ZERO,
18814 );
18815 let sheared =
18816 ogeom_algo::general_transformed_shape(&mut model, &cut_out.shape, &shear, T).unwrap();
18817 let history = cut_out.history.then(&sheared.history);
18818 for (kind, expected) in [
18819 (ShapeType::Face, 9),
18820 (ShapeType::Edge, 15),
18821 (ShapeType::Vertex, 8),
18822 ] {
18823 let between = explore_unique(&model, &cut_out.shape, kind).unwrap();
18824 let result = explore_unique(&model, &sheared.shape, kind).unwrap();
18825 let mut traced = 0;
18826 for part in explore_unique(&model, &a, kind)
18827 .unwrap()
18828 .into_iter()
18829 .chain(explore_unique(&model, &b, kind).unwrap())
18830 {
18831 let carried = cut_out.history.trace(&part);
18832 if carried.is_empty()
18833 || !carried.iter().all(|c| between.iter().any(|r| r.is_same(c)))
18834 {
18835 continue;
18836 }
18837 let images = history.trace(&part);
18838 assert_eq!(images.len(), carried.len(), "{kind:?}");
18839 assert!(history.is_affected(&part), "{kind:?}");
18840 for image in images {
18841 assert!(result.iter().any(|r| r.is_same(image)), "{kind:?}");
18842 }
18843 traced += 1;
18844 }
18845 assert_eq!(traced, expected, "{kind:?}");
18846 }
18847 }
18848
18849 fn sub_shapes_traced(
18854 model: &Model,
18855 argument: &Shape,
18856 result: &Shape,
18857 history: &History,
18858 ) -> [(usize, usize, usize); 2] {
18859 let mut counts = [(0, 0, 0); 2];
18860 for (slot, kind) in [ShapeType::Edge, ShapeType::Vertex].into_iter().enumerate() {
18861 let held = explore_unique(model, result, kind).unwrap();
18862 for part in explore_unique(model, argument, kind).unwrap() {
18863 if history.is_deleted(&part) {
18864 counts[slot].0 += 1;
18865 continue;
18866 }
18867 let images = history.trace(&part);
18868 assert!(!images.is_empty(), "{kind:?}");
18869 for image in images {
18870 assert!(
18871 held.iter().any(|h| h.is_same(image)),
18872 "a {kind:?} traces to a shape the result does not hold"
18873 );
18874 }
18875 if history.copy_of(&part).is_some() || !history.is_affected(&part) {
18876 counts[slot].1 += 1;
18877 } else {
18878 counts[slot].2 += 1;
18879 }
18880 }
18881 }
18882 counts
18883 }
18884
18885 fn vertex_point(model: &Model, vertex: &Shape) -> Point {
18886 let data = model.node(vertex).unwrap().data().as_vertex().unwrap();
18887 vertex.transform(model.datums()).unwrap().apply(data.point)
18888 }
18889
18890 #[test]
18897 fn a_drill_whose_seam_grazes_a_face_keeps_it_a_seam() {
18898 let chord = Deflection::with_chord(0.01).unwrap();
18899 let volume_at = |rise: f64| {
18900 let mut model = Model::new();
18901 let block = make_box(
18902 &mut model,
18903 frame_at(Point::new(-20.0, 0.0, 0.0)),
18904 (40.0, 30.0, 10.0),
18905 T,
18906 )
18907 .unwrap()
18908 .shape;
18909 let up = ogeom_math::Direction::Y;
18910 let seam = ogeom_math::Direction::Z;
18911 let drill = make_cylinder(
18912 &mut model,
18913 Frame::new(Point::new(0.0, -5.0, 7.0 + rise), up, seam, T).unwrap(),
18914 3.0,
18915 40.0,
18916 T,
18917 )
18918 .unwrap()
18919 .shape;
18920 let out = cut(&mut model, &block, &drill, T).unwrap();
18921 let d = check(&model, &out.shape, T).unwrap();
18922 assert!(d.is_valid(), "rise {rise}: {d}");
18923 volume_properties(&model, &out.shape, chord, T)
18924 .unwrap()
18925 .mass
18926 };
18927 let tangent = volume_at(0.0);
18928 for rise in [1e-9, 1e-8, 1e-7, 2e-7] {
18929 let v = volume_at(rise);
18930 assert!(
18931 (v - tangent).abs() < 1e-6 * tangent,
18932 "rise {rise}: {v} against {tangent}"
18933 );
18934 }
18935 }
18936
18937 #[test]
18945 fn a_drill_tangent_to_a_wall_along_a_rounds_edge_cuts_the_sliver_behind_it() {
18946 let mut model = Model::new();
18947 let slab = make_box(&mut model, Frame::WORLD, (40.0, 40.0, 10.0), T)
18948 .unwrap()
18949 .shape;
18950 let notch = make_box(
18951 &mut model,
18952 frame_at(Point::new(20.0, 20.0, -1.0)),
18953 (21.0, 21.0, 12.0),
18954 T,
18955 )
18956 .unwrap()
18957 .shape;
18958 let notched = cut(&mut model, &slab, ¬ch, T).unwrap().shape;
18959 let square = make_box(
18963 &mut model,
18964 frame_at(Point::new(20.0, 20.0, 0.0)),
18965 (2.0, 2.0, 10.0),
18966 T,
18967 )
18968 .unwrap()
18969 .shape;
18970 let bore = make_cylinder(
18971 &mut model,
18972 frame_at(Point::new(22.0, 22.0, -1.0)),
18973 2.0,
18974 12.0,
18975 T,
18976 )
18977 .unwrap()
18978 .shape;
18979 let round = cut(&mut model, &square, &bore, T).unwrap().shape;
18980 let part = fuse(&mut model, ¬ched, &round, T).unwrap().shape;
18981 let chord = Deflection::with_chord(0.01).unwrap();
18982 let before = volume_properties(&model, &part, chord, T).unwrap().mass;
18983 for seam in [
18984 Direction::X,
18985 Direction::new(ogeom_math::Vector::new(-1.0, 0.0, 0.0), T).unwrap(),
18986 ] {
18987 let drill = make_cylinder(
18988 &mut model,
18989 Frame::new(Point::new(22.0, 50.0, -1.0), Direction::Z, seam, T).unwrap(),
18990 30.0,
18991 12.0,
18992 T,
18993 )
18994 .unwrap()
18995 .shape;
18996 let out = cut(&mut model, &part, &drill, T).unwrap();
18997 check(&model, &out.shape, T).unwrap();
18998 let seg = |u: f64, r: f64| 0.5 * (u * (r * r - u * u).sqrt() + r * r * (u / r).asin());
19000 let block = (seg(-2.0, 30.0) - seg(-22.0, 30.0)) - 10.0 * 20.0;
19003 let corner = (4.0 - (seg(0.0, 2.0) - seg(-2.0, 2.0)))
19006 - (60.0 - (seg(0.0, 30.0) - seg(-2.0, 30.0)));
19007 let expected = before - 10.0 * (block + corner);
19008 let after = volume_properties(&model, &out.shape, chord, T)
19009 .unwrap()
19010 .mass;
19011 assert!(
19012 (after - expected).abs() < 1e-6 * expected,
19013 "seam {seam:?}: {after} against {expected}"
19014 );
19015 }
19016 }
19017
19018 #[test]
19023 fn a_cut_records_every_edge_and_vertex_of_both_boxes() {
19024 let mut model = Model::new();
19025 let (a, b) = boxes(&mut model);
19026 let out = cut(&mut model, &a, &b, T).unwrap();
19027 let h = &out.history;
19028 assert_eq!(
19030 sub_shapes_traced(&model, &a, &out.shape, h),
19031 [(0, 9, 3), (1, 7, 0)]
19032 );
19033 assert_eq!(
19034 sub_shapes_traced(&model, &b, &out.shape, h),
19035 [(9, 0, 3), (7, 0, 1)]
19036 );
19037 for vertex in explore_unique(&model, &a, ShapeType::Vertex).unwrap() {
19038 let at = vertex_point(&model, &vertex);
19039 assert_eq!(
19040 h.is_deleted(&vertex),
19041 at.distance(Point::new(2.0, 2.0, 2.0)) < 1e-9
19042 );
19043 }
19044 for edge in explore_unique(&model, &a, ShapeType::Edge).unwrap() {
19046 if h.copy_of(&edge).is_some() || !h.is_affected(&edge) {
19047 continue;
19048 }
19049 let [image] = h.modified(&edge) else {
19050 panic!("a shortened edge is one edge");
19051 };
19052 let ends: Vec<Point> = explore_unique(&model, image, ShapeType::Vertex)
19053 .unwrap()
19054 .iter()
19055 .map(|v| vertex_point(&model, v))
19056 .collect();
19057 assert_eq!(ends.len(), 2);
19058 assert!((ends[0].distance(ends[1]) - 1.0).abs() < 1e-9);
19059 }
19060 }
19061
19062 #[test]
19066 fn a_drill_records_every_edge_and_vertex_of_plate_and_drill() {
19067 let mut model = Model::new();
19068 let plate = make_box(&mut model, Frame::WORLD, (20.0, 20.0, 10.0), T)
19069 .unwrap()
19070 .shape;
19071 let drill = make_cylinder(
19072 &mut model,
19073 frame_at(Point::new(10.0, 10.0, -1.0)),
19074 3.0,
19075 12.0,
19076 T,
19077 )
19078 .unwrap()
19079 .shape;
19080 let out = cut(&mut model, &plate, &drill, T).unwrap();
19081 let h = &out.history;
19082 assert_eq!(
19083 sub_shapes_traced(&model, &plate, &out.shape, h),
19084 [(0, 12, 0), (0, 8, 0)]
19085 );
19086 assert_eq!(
19087 sub_shapes_traced(&model, &drill, &out.shape, h),
19088 [(2, 0, 1), (2, 0, 0)]
19089 );
19090 }
19091
19092 #[test]
19095 fn a_cut_then_a_move_traces_every_edge_and_vertex() {
19096 let mut model = Model::new();
19097 let (a, b) = boxes(&mut model);
19098 let out = cut(&mut model, &a, &b, T).unwrap();
19099 let moved = ogeom_algo::transformed(
19100 &mut model,
19101 &out.shape,
19102 ogeom_math::Transform::translation(ogeom_math::Vector::new(5.0, 0.0, 0.0)),
19103 )
19104 .unwrap();
19105 let composed = out.history.then(&moved.history);
19106 let carried = |counts: [(usize, usize, usize); 2]| counts.map(|(d, c, m)| (d, c + m));
19109 for argument in [&a, &b] {
19110 let composed_counts = sub_shapes_traced(&model, argument, &moved.shape, &composed);
19111 assert!(composed_counts.iter().all(|&(_, copied, _)| copied == 0));
19112 assert_eq!(
19113 carried(composed_counts),
19114 carried(sub_shapes_traced(
19115 &model,
19116 argument,
19117 &out.shape,
19118 &out.history
19119 )),
19120 );
19121 for kind in [ShapeType::Edge, ShapeType::Vertex] {
19122 for part in explore_unique(&model, argument, kind).unwrap() {
19123 assert_eq!(composed.trace(&part).len(), out.history.trace(&part).len());
19124 }
19125 }
19126 }
19127 }
19128}