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