1use axiolid_contracts::{GeomError, GeomResult};
14use axiolid_core::{Point2, Scalar, Tolerance};
15use axiolid_profile::{CircleProfile, EllipseProfile, Profile, RectangleProfile};
16
17#[derive(Debug, Clone, Default, PartialEq)]
20pub struct Rings {
21 pub outer: Vec<Point2>,
23 pub holes: Vec<Vec<Point2>>,
25}
26
27pub fn profile_rings(
33 profile: &Profile,
34 chord_error: Scalar,
35 tolerance: Tolerance,
36) -> GeomResult<Rings> {
37 match profile {
38 Profile::Rectangle(r) => rectangle_rings(r, chord_error, tolerance),
39 Profile::Circle(c) => circle_rings(c, chord_error),
40 Profile::Ellipse(e) => ellipse_rings(e, chord_error),
44 Profile::Contour(c) => {
45 let outer = contour_points(&c.outer, chord_error, tolerance)?;
46 let mut holes = Vec::with_capacity(c.holes.len());
47 for hole in &c.holes {
48 holes.push(contour_points(hole, chord_error, tolerance)?);
49 }
50 Ok(orient_rings(outer, holes))
51 }
52 Profile::Derived { basis, transform } => {
53 let mut rings = profile_rings(basis, chord_error, tolerance)?;
54 apply2(&mut rings.outer, transform);
55 for hole in &mut rings.holes {
56 apply2(hole, transform);
57 }
58 if transform.matrix2.determinant() < 0.0 {
62 rings.outer.reverse();
63 for hole in &mut rings.holes {
64 hole.reverse();
65 }
66 }
67 Ok(rings)
68 }
69 Profile::CenterLine(cl) => {
70 crate::center_line::center_line_rings(cl, chord_error, tolerance, contour_points)
71 }
72 other => Err(GeomError::Unsupported {
73 backend: crate::BACKEND_ID,
74 operation: axiolid_contracts::Operation::ProfileTriangulation,
75 })
76 .inspect_err(|_| {
77 let _ = other;
78 }),
79 }
80}
81
82fn rectangle_rings(
84 r: &RectangleProfile,
85 _chord_error: Scalar,
86 tolerance: Tolerance,
87) -> GeomResult<Rings> {
88 if !(r.x.is_finite() && r.y.is_finite()) || r.x <= 0.0 || r.y <= 0.0 {
89 return Err(GeomError::InvalidInput(format!(
90 "rectangle profile must have positive finite extents, got {} x {}",
91 r.x, r.y
92 )));
93 }
94 let (hx, hy) = (r.x / 2.0, r.y / 2.0);
95 let outer = vec![
96 Point2::new(-hx, -hy),
97 Point2::new(hx, -hy),
98 Point2::new(hx, hy),
99 Point2::new(-hx, hy),
100 ];
101 let mut holes = Vec::new();
102 if let Some(t) = r.thickness {
103 if t <= 0.0 || 2.0 * t >= r.x || 2.0 * t >= r.y {
104 return Err(GeomError::InvalidInput(format!(
105 "hollow rectangle wall thickness {t} does not fit inside {} x {}",
106 r.x, r.y
107 )));
108 }
109 let (ix, iy) = (hx - t, hy - t);
110 if !tolerance.eq(ix, 0.0) && !tolerance.eq(iy, 0.0) {
111 holes.push(vec![
113 Point2::new(-ix, -iy),
114 Point2::new(-ix, iy),
115 Point2::new(ix, iy),
116 Point2::new(ix, -iy),
117 ]);
118 }
119 }
120 Ok(Rings { outer, holes })
121}
122
123fn circle_rings(c: &CircleProfile, chord_error: Scalar) -> GeomResult<Rings> {
125 if !c.radius.is_finite() || c.radius <= 0.0 {
126 return Err(GeomError::InvalidInput(format!(
127 "circle profile radius must be positive and finite, got {}",
128 c.radius
129 )));
130 }
131 let outer = flatten_circle(c.radius, chord_error)?;
134 let mut holes = Vec::new();
135 if let Some(t) = c.thickness {
136 if t <= 0.0 || t >= c.radius {
137 return Err(GeomError::InvalidInput(format!(
138 "annulus wall thickness {t} does not fit inside radius {}",
139 c.radius
140 )));
141 }
142 let inner = c.radius - t;
143 let mut ring = flatten_circle(inner, chord_error)?;
144 ring.reverse(); holes.push(ring);
146 }
147 Ok(Rings { outer, holes })
148}
149
150fn ellipse_rings(e: &EllipseProfile, chord_error: Scalar) -> GeomResult<Rings> {
152 if !e.semi_axis_x.is_finite() || e.semi_axis_x <= 0.0 {
153 return Err(GeomError::InvalidInput(format!(
154 "ellipse semi-axis x must be positive and finite, got {}",
155 e.semi_axis_x
156 )));
157 }
158 if !e.semi_axis_y.is_finite() || e.semi_axis_y <= 0.0 {
159 return Err(GeomError::InvalidInput(format!(
160 "ellipse semi-axis y must be positive and finite, got {}",
161 e.semi_axis_y
162 )));
163 }
164 use axiolid_core::{Frame2, Interval, Vec2};
165 use axiolid_curve::{Curve2, Ellipse2};
166
167 let curve = Curve2::Ellipse(Ellipse2 {
168 frame: Frame2 {
169 origin: Point2::new(0.0, 0.0),
170 x: Vec2::new(1.0, 0.0),
171 y: Vec2::new(0.0, 1.0),
172 },
173 semi_axis_x: e.semi_axis_x,
174 semi_axis_y: e.semi_axis_y,
175 });
176 let mut ring = axiolid_reference::curve::flatten2(
177 &curve,
178 Interval {
179 start: 0.0,
180 end: core::f64::consts::TAU,
181 },
182 chord_error,
183 MAX_SUBDIVISION_DEPTH,
184 )?;
185 ring.pop();
187 Ok(Rings {
188 outer: ring,
189 holes: Vec::new(),
190 })
191}
192
193fn flatten_circle(radius: Scalar, chord_error: Scalar) -> GeomResult<Vec<Point2>> {
199 use axiolid_core::{Frame2, Interval, Vec2};
200 use axiolid_curve::{Circle2, Curve2};
201
202 let curve = Curve2::Circle(Circle2 {
203 frame: Frame2 {
204 origin: Point2::new(0.0, 0.0),
205 x: Vec2::new(1.0, 0.0),
206 y: Vec2::new(0.0, 1.0),
207 },
208 radius,
209 });
210 let mut ring = axiolid_reference::curve::flatten2(
211 &curve,
212 Interval {
213 start: 0.0,
214 end: core::f64::consts::TAU,
215 },
216 chord_error,
217 MAX_SUBDIVISION_DEPTH,
218 )?;
219 ring.pop();
220 Ok(ring)
221}
222
223pub fn triangulate(rings: &Rings) -> GeomResult<(Vec<Point2>, Vec<[u32; 3]>)> {
228 if rings.outer.len() < 3 {
229 return Err(GeomError::InvalidInput(format!(
230 "profile outer ring needs at least 3 vertices, got {}",
231 rings.outer.len()
232 )));
233 }
234 let mut verts: Vec<[Scalar; 2]> = rings.outer.iter().map(|p| [p.x, p.y]).collect();
235 let mut hole_starts = Vec::with_capacity(rings.holes.len());
236 for hole in &rings.holes {
237 if hole.len() < 3 {
238 return Err(GeomError::InvalidInput(format!(
239 "profile hole needs at least 3 vertices, got {}",
240 hole.len()
241 )));
242 }
243 hole_starts.push(verts.len());
244 verts.extend(hole.iter().map(|p| [p.x, p.y]));
245 }
246
247 let mut earcutter = earcut::Earcut::new();
248 let mut flat: Vec<usize> = Vec::new();
249 earcutter.earcut(verts.iter().copied(), &hole_starts, &mut flat);
250
251 if flat.is_empty() || flat.len() % 3 != 0 {
252 return Err(GeomError::Degenerate(format!(
253 "triangulation produced {} indices for a {}-vertex profile",
254 flat.len(),
255 verts.len()
256 )));
257 }
258 let tris = flat
259 .chunks_exact(3)
260 .map(|c| [c[0] as u32, c[1] as u32, c[2] as u32])
261 .collect();
262 let points = verts.into_iter().map(|v| Point2::new(v[0], v[1])).collect();
263 Ok((points, tris))
264}
265
266fn apply2(ring: &mut [Point2], t: &axiolid_core::Transform2) {
268 for p in ring.iter_mut() {
269 *p = t.transform_point2(*p);
270 }
271}
272
273fn contour_points(
279 contour: &axiolid_profile::Contour,
280 chord_error: Scalar,
281 tolerance: Tolerance,
282) -> GeomResult<Vec<Point2>> {
283 let mut out: Vec<Point2> = Vec::new();
284 for segment in &contour.segments {
285 let mut pts = segment_points(segment, chord_error)?;
286 if !segment.same_sense {
287 pts.reverse();
288 }
289 for p in pts {
290 if out
291 .last()
292 .is_none_or(|last| !near2(*last, p, tolerance.linear()))
293 {
294 out.push(p);
295 }
296 }
297 }
298 while out.len() > 1 && near2(out[0], *out.last().expect("non-empty"), tolerance.linear()) {
300 out.pop();
301 }
302 if out.len() < 3 {
303 return Err(GeomError::Degenerate(format!(
304 "contour flattened to {} points, need at least 3",
305 out.len()
306 )));
307 }
308 Ok(out)
309}
310
311fn near2(a: Point2, b: Point2, linear: Scalar) -> bool {
313 (a.x - b.x).abs() <= linear && (a.y - b.y).abs() <= linear
314}
315
316const MAX_SUBDIVISION_DEPTH: u32 = 24;
328
329fn segment_points(
330 segment: &axiolid_profile::ProfileSegment,
331 chord_error: Scalar,
332) -> GeomResult<Vec<Point2>> {
333 axiolid_reference::curve::flatten2(
334 &segment.curve,
335 segment.domain,
336 chord_error,
337 MAX_SUBDIVISION_DEPTH,
338 )
339}
340
341use axiolid_reference::signed_area2;
347
348fn orient_rings(mut outer: Vec<Point2>, mut holes: Vec<Vec<Point2>>) -> Rings {
349 if signed_area2(&outer) < 0.0 {
350 outer.reverse();
351 }
352 for hole in &mut holes {
353 if signed_area2(hole) > 0.0 {
354 hole.reverse();
355 }
356 }
357 Rings { outer, holes }
358}