axiolid_construct/
boolean_stepped.rs1use axiolid_contracts::{GeomError, GeomResult};
16use axiolid_core::{Frame2, Scalar, Tolerance, Vec2};
17use axiolid_overlay::{overlay, FillRule, OverlayInput, OverlayOperation, Polygon, Ring};
18
19use crate::boolean_exact::{unsupported, Prism};
20use crate::BACKEND_ID;
21
22#[derive(Debug, Clone, PartialEq)]
24pub struct Band {
25 pub rings: Vec<Vec<axiolid_core::Point2>>,
27 pub bottom: Scalar,
29 pub top: Scalar,
31}
32
33pub fn union_prisms_stepped(
44 subject: &Prism,
45 tool: &Prism,
46 tolerance: Tolerance,
47) -> GeomResult<Vec<Band>> {
48 if tool.bottom > subject.top + tolerance.linear()
49 || subject.bottom > tool.top + tolerance.linear()
50 {
51 return Err(unsupported(
52 "stepped union of prisms that do not meet along the axis",
53 ));
54 }
55
56 let mut cuts = vec![subject.bottom, subject.top, tool.bottom, tool.top];
61 cuts.sort_by(|a, b| a.total_cmp(b));
62 cuts.dedup_by(|a, b| tolerance.eq(*a, *b));
63
64 let mut bands = Vec::with_capacity(cuts.len().saturating_sub(1));
65 for pair in cuts.windows(2) {
66 let (bottom, top) = (pair[0], pair[1]);
67 let middle = 0.5 * (bottom + top);
70 let in_subject = middle > subject.bottom && middle < subject.top;
71 let in_tool = middle > tool.bottom && middle < tool.top;
72 let rings = match (in_subject, in_tool) {
73 (false, false) => continue,
74 (true, false) => subject.rings.clone(),
75 (false, true) => tool.rings.clone(),
76 (true, true) => section_union(subject, tool, tolerance)?,
79 };
80 bands.push(Band { rings, bottom, top });
81 }
82 if bands.is_empty() {
83 return Err(GeomError::Degenerate(
84 "stepped union has no band of positive height".to_owned(),
85 ));
86 }
87 Ok(bands)
88}
89
90fn section_union(
92 subject: &Prism,
93 tool: &Prism,
94 tolerance: Tolerance,
95) -> GeomResult<Vec<Vec<axiolid_core::Point2>>> {
96 let frame = Frame2 {
97 origin: Vec2::ZERO,
98 x: Vec2::X,
99 y: Vec2::Y,
100 };
101 let result = overlay(
102 &OverlayInput {
103 frame,
104 polygons: to_polygons(subject),
105 },
106 &OverlayInput {
107 frame,
108 polygons: to_polygons(tool),
109 },
110 OverlayOperation::Union,
111 FillRule::NonZero,
112 tolerance,
113 )
114 .map_err(|error| GeomError::BackendContractViolation {
115 backend: BACKEND_ID,
116 detail: format!("stepped union cross-section overlay failed: {error:?}"),
117 })?;
118 if result.polygons.len() != 1 {
119 return Err(unsupported(
120 "stepped union band with a disconnected cross-section",
121 ));
122 }
123 let polygon = &result.polygons[0];
124 let mut rings = Vec::with_capacity(1 + polygon.holes.len());
125 rings.push(polygon.outer.points.clone());
126 for hole in &polygon.holes {
127 rings.push(hole.points.clone());
128 }
129 Ok(rings)
130}
131
132fn to_polygons(prism: &Prism) -> Vec<Polygon> {
133 let mut rings = prism.rings.iter();
134 let outer = Ring {
135 points: rings.next().cloned().unwrap_or_default(),
136 };
137 let holes = rings.map(|r| Ring { points: r.clone() }).collect();
138 vec![Polygon { outer, holes }]
139}