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) if r.outer_radius.is_some() || r.inner_radius.is_some() => {
41 let contour = crate::section_lower::rectangle_contour(r)?;
42 contour_rings(&contour, chord_error, tolerance)
43 }
44 Profile::Rectangle(r) => rectangle_rings(r, chord_error, tolerance),
45 Profile::Section(section) => {
48 let contour = crate::section_lower::section_contour(section)?;
49 contour_rings(&contour, chord_error, tolerance)
50 }
51 Profile::Circle(c) => circle_rings(c, chord_error),
52 Profile::Ellipse(e) => ellipse_rings(e, chord_error),
56 Profile::Contour(c) => contour_rings(c, chord_error, tolerance),
57 Profile::Derived { basis, transform } => {
58 let mut rings = profile_rings(basis, chord_error, tolerance)?;
59 apply2(&mut rings.outer, transform);
60 for hole in &mut rings.holes {
61 apply2(hole, transform);
62 }
63 if transform.matrix2.determinant() < 0.0 {
67 rings.outer.reverse();
68 for hole in &mut rings.holes {
69 hole.reverse();
70 }
71 }
72 Ok(rings)
73 }
74 Profile::CenterLine(cl) => {
75 crate::center_line::center_line_rings(cl, chord_error, tolerance, contour_points)
76 }
77 other => Err(GeomError::Unsupported {
78 backend: crate::BACKEND_ID,
79 operation: axiolid_contracts::Operation::ProfileTriangulation,
80 })
81 .inspect_err(|_| {
82 let _ = other;
83 }),
84 }
85}
86
87fn contour_rings(
89 c: &axiolid_profile::ContourProfile,
90 chord_error: Scalar,
91 tolerance: Tolerance,
92) -> GeomResult<Rings> {
93 let outer = contour_points(&c.outer, chord_error, tolerance)?;
94 let mut holes = Vec::with_capacity(c.holes.len());
95 for hole in &c.holes {
96 holes.push(contour_points(hole, chord_error, tolerance)?);
97 }
98 Ok(orient_rings(outer, holes))
99}
100
101fn rectangle_rings(
104 r: &RectangleProfile,
105 _chord_error: Scalar,
106 tolerance: Tolerance,
107) -> GeomResult<Rings> {
108 if !(r.x.is_finite() && r.y.is_finite()) || r.x <= 0.0 || r.y <= 0.0 {
109 return Err(GeomError::InvalidInput(format!(
110 "rectangle profile must have positive finite extents, got {} x {}",
111 r.x, r.y
112 )));
113 }
114 let (hx, hy) = (r.x / 2.0, r.y / 2.0);
115 let outer = vec![
116 Point2::new(-hx, -hy),
117 Point2::new(hx, -hy),
118 Point2::new(hx, hy),
119 Point2::new(-hx, hy),
120 ];
121 let mut holes = Vec::new();
122 if let Some(t) = r.thickness {
123 if t <= 0.0 || 2.0 * t >= r.x || 2.0 * t >= r.y {
124 return Err(GeomError::InvalidInput(format!(
125 "hollow rectangle wall thickness {t} does not fit inside {} x {}",
126 r.x, r.y
127 )));
128 }
129 let (ix, iy) = (hx - t, hy - t);
130 if !tolerance.eq(ix, 0.0) && !tolerance.eq(iy, 0.0) {
131 holes.push(vec![
133 Point2::new(-ix, -iy),
134 Point2::new(-ix, iy),
135 Point2::new(ix, iy),
136 Point2::new(ix, -iy),
137 ]);
138 }
139 }
140 Ok(Rings { outer, holes })
141}
142
143fn circle_rings(c: &CircleProfile, chord_error: Scalar) -> GeomResult<Rings> {
145 if !c.radius.is_finite() || c.radius <= 0.0 {
146 return Err(GeomError::InvalidInput(format!(
147 "circle profile radius must be positive and finite, got {}",
148 c.radius
149 )));
150 }
151 let outer = flatten_circle(c.radius, chord_error)?;
154 let mut holes = Vec::new();
155 if let Some(t) = c.thickness {
156 if t <= 0.0 || t >= c.radius {
157 return Err(GeomError::InvalidInput(format!(
158 "annulus wall thickness {t} does not fit inside radius {}",
159 c.radius
160 )));
161 }
162 let inner = c.radius - t;
163 let mut ring = flatten_circle(inner, chord_error)?;
164 ring.reverse(); holes.push(ring);
166 }
167 Ok(Rings { outer, holes })
168}
169
170fn ellipse_rings(e: &EllipseProfile, chord_error: Scalar) -> GeomResult<Rings> {
172 if !e.semi_axis_x.is_finite() || e.semi_axis_x <= 0.0 {
173 return Err(GeomError::InvalidInput(format!(
174 "ellipse semi-axis x must be positive and finite, got {}",
175 e.semi_axis_x
176 )));
177 }
178 if !e.semi_axis_y.is_finite() || e.semi_axis_y <= 0.0 {
179 return Err(GeomError::InvalidInput(format!(
180 "ellipse semi-axis y must be positive and finite, got {}",
181 e.semi_axis_y
182 )));
183 }
184 use axiolid_core::{Frame2, Interval, Vec2};
185 use axiolid_curve::{Curve2, Ellipse2};
186
187 let curve = Curve2::Ellipse(Ellipse2 {
188 frame: Frame2 {
189 origin: Point2::new(0.0, 0.0),
190 x: Vec2::new(1.0, 0.0),
191 y: Vec2::new(0.0, 1.0),
192 },
193 semi_axis_x: e.semi_axis_x,
194 semi_axis_y: e.semi_axis_y,
195 });
196 let mut ring = axiolid_reference::curve::flatten2(
197 &curve,
198 Interval {
199 start: 0.0,
200 end: core::f64::consts::TAU,
201 },
202 chord_error,
203 MAX_SUBDIVISION_DEPTH,
204 )?;
205 ring.pop();
207 Ok(Rings {
208 outer: ring,
209 holes: Vec::new(),
210 })
211}
212
213fn flatten_circle(radius: Scalar, chord_error: Scalar) -> GeomResult<Vec<Point2>> {
219 use axiolid_core::{Frame2, Interval, Vec2};
220 use axiolid_curve::{Circle2, Curve2};
221
222 let curve = Curve2::Circle(Circle2 {
223 frame: Frame2 {
224 origin: Point2::new(0.0, 0.0),
225 x: Vec2::new(1.0, 0.0),
226 y: Vec2::new(0.0, 1.0),
227 },
228 radius,
229 });
230 let flatten = |start: Scalar| {
231 axiolid_reference::curve::flatten2(
232 &curve,
233 Interval {
234 start,
235 end: start + core::f64::consts::TAU,
236 },
237 chord_error,
238 MAX_SUBDIVISION_DEPTH,
239 )
240 };
241 let steps = flatten(0.0)?.len().saturating_sub(1).max(1);
247 let mut ring = flatten(core::f64::consts::PI / steps as Scalar)?;
248 ring.pop();
249 Ok(ring)
250}
251
252pub fn triangulate(rings: &Rings) -> GeomResult<(Vec<Point2>, Vec<[u32; 3]>)> {
257 if rings.outer.len() < 3 {
258 return Err(GeomError::InvalidInput(format!(
259 "profile outer ring needs at least 3 vertices, got {}",
260 rings.outer.len()
261 )));
262 }
263 let mut verts: Vec<[Scalar; 2]> = rings.outer.iter().map(|p| [p.x, p.y]).collect();
264 let mut hole_starts = Vec::with_capacity(rings.holes.len());
265 for hole in &rings.holes {
266 if hole.len() < 3 {
267 return Err(GeomError::InvalidInput(format!(
268 "profile hole needs at least 3 vertices, got {}",
269 hole.len()
270 )));
271 }
272 hole_starts.push(verts.len());
273 verts.extend(hole.iter().map(|p| [p.x, p.y]));
274 }
275
276 let mut earcutter = earcut::Earcut::new();
277 let mut flat: Vec<usize> = Vec::new();
278 earcutter.earcut(verts.iter().copied(), &hole_starts, &mut flat);
279
280 if flat.is_empty() || flat.len() % 3 != 0 {
281 return Err(GeomError::Degenerate(format!(
282 "triangulation produced {} indices for a {}-vertex profile",
283 flat.len(),
284 verts.len()
285 )));
286 }
287 let tris = flat
288 .chunks_exact(3)
289 .map(|c| [c[0] as u32, c[1] as u32, c[2] as u32])
290 .collect();
291 let points = verts.into_iter().map(|v| Point2::new(v[0], v[1])).collect();
292 Ok((points, tris))
293}
294
295fn apply2(ring: &mut [Point2], t: &axiolid_core::Transform2) {
297 for p in ring.iter_mut() {
298 *p = t.transform_point2(*p);
299 }
300}
301
302fn contour_points(
308 contour: &axiolid_profile::Contour,
309 chord_error: Scalar,
310 tolerance: Tolerance,
311) -> GeomResult<Vec<Point2>> {
312 let mut out: Vec<Point2> = Vec::new();
313 for segment in &contour.segments {
314 let mut pts = segment_points(segment, chord_error)?;
315 if !segment.same_sense {
316 pts.reverse();
317 }
318 for p in pts {
319 if out
320 .last()
321 .is_none_or(|last| !near2(*last, p, tolerance.linear()))
322 {
323 out.push(p);
324 }
325 }
326 }
327 while out.len() > 1 && near2(out[0], *out.last().expect("non-empty"), tolerance.linear()) {
329 out.pop();
330 }
331 if out.len() < 3 {
332 return Err(GeomError::Degenerate(format!(
333 "contour flattened to {} points, need at least 3",
334 out.len()
335 )));
336 }
337 Ok(out)
338}
339
340fn near2(a: Point2, b: Point2, linear: Scalar) -> bool {
342 (a.x - b.x).abs() <= linear && (a.y - b.y).abs() <= linear
343}
344
345const MAX_SUBDIVISION_DEPTH: u32 = 24;
357
358fn segment_points(
359 segment: &axiolid_profile::ProfileSegment,
360 chord_error: Scalar,
361) -> GeomResult<Vec<Point2>> {
362 axiolid_reference::curve::flatten2(
363 &segment.curve,
364 segment.domain,
365 chord_error,
366 MAX_SUBDIVISION_DEPTH,
367 )
368}
369
370use axiolid_reference::signed_area2;
376
377fn orient_rings(mut outer: Vec<Point2>, mut holes: Vec<Vec<Point2>>) -> Rings {
378 if signed_area2(&outer) < 0.0 {
379 outer.reverse();
380 }
381 for hole in &mut holes {
382 if signed_area2(hole) > 0.0 {
383 hole.reverse();
384 }
385 }
386 Rings { outer, holes }
387}