axiolid_construct/profile/
paired.rs1use axiolid_contracts::{GeomError, GeomResult};
46use axiolid_core::{Frame2, Interval, Point2, Scalar, Tolerance, Transform2, Vec2};
47use axiolid_curve::{Circle2, Curve2, Ellipse2};
48use axiolid_profile::{Contour, ContourProfile, Profile, ProfileSegment};
49use axiolid_reference::curve::evaluate2;
50
51use super::{near2, snap_joints, stretch2, Rings};
52
53const MAX_VERTICES: Scalar = (1u32 << 20) as Scalar;
55
56#[non_exhaustive]
59#[derive(Debug, Clone, PartialEq)]
60pub struct PairedRings {
61 pub rings: Vec<Rings>,
64 pub deviation: Scalar,
68}
69
70struct Lowered {
72 rings: Vec<Vec<ProfileSegment>>,
74 transforms: Vec<Transform2>,
76 stretch: Scalar,
78 reversed: Vec<bool>,
80}
81
82pub fn paired_profile_rings(
91 profiles: &[&Profile],
92 chord_error: Scalar,
93 tolerance: Tolerance,
94) -> GeomResult<Option<PairedRings>> {
95 if !chord_error.is_finite() || chord_error <= 0.0 {
96 return Err(GeomError::InvalidInput(format!(
97 "paired profile chord error must be positive and finite, got {chord_error}"
98 )));
99 }
100 if profiles.is_empty() {
101 return Ok(None);
102 }
103 let mut lowered = Vec::with_capacity(profiles.len());
104 for profile in profiles {
105 let Some(mut one) = lower(profile)? else {
106 return Ok(None);
107 };
108 if !orient(&mut one)? {
109 return Ok(None);
110 }
111 lowered.push(one);
112 }
113 if lowered.iter().any(|l| l.reversed != lowered[0].reversed) {
117 return Ok(None);
118 }
119 let first = &lowered[0];
120 if lowered.iter().any(|l| {
121 l.rings.len() != first.rings.len()
122 || l.rings
123 .iter()
124 .zip(&first.rings)
125 .any(|(a, b)| a.len() != b.len())
126 }) {
127 return Ok(None);
128 }
129 let mut counts: Vec<Vec<usize>> = Vec::with_capacity(first.rings.len());
131 let mut total: Scalar = 0.0;
132 for (r, ring) in first.rings.iter().enumerate() {
133 let mut row = Vec::with_capacity(ring.len());
134 for j in 0..ring.len() {
135 let mut n: Scalar = 1.0;
136 for l in &lowered {
137 let segment = &l.rings[r][j];
138 let Some(m2) = second_bound(&segment.curve) else {
139 return Ok(None);
140 };
141 let steps = span(segment)? * (m2 * l.stretch / (8.0 * chord_error)).sqrt();
142 n = n.max(steps.ceil());
143 }
144 total += n;
145 if total.is_nan() || total > MAX_VERTICES {
146 return Ok(None);
147 }
148 row.push(n as usize);
149 }
150 counts.push(row);
151 }
152 let circles = profiles.iter().all(|p| matches!(p, Profile::Circle(_)));
154 let mut rings = Vec::with_capacity(lowered.len());
155 let mut deviation: Scalar = 0.0;
156 for l in &lowered {
157 let mut moved: Scalar = 0.0;
158 let mut chord: Scalar = 0.0;
159 let mut flat: Vec<Vec<Point2>> = Vec::with_capacity(l.rings.len());
160 for (ring, row) in l.rings.iter().zip(&counts) {
161 let mut pieces: Vec<Vec<Point2>> = Vec::with_capacity(ring.len());
162 for (segment, &n) in ring.iter().zip(row) {
163 let step = span(segment)? / n as Scalar;
164 let m2 = second_bound(&segment.curve).unwrap_or(0.0) * l.stretch;
165 chord = chord.max(step * step * m2 / 8.0);
166 let shift = if circles {
167 core::f64::consts::PI / n as Scalar
168 } else {
169 0.0
170 };
171 pieces.push(sample(segment, n, shift)?);
172 }
173 let contour = Contour::new(ring.clone());
174 snap_joints(&contour, &mut pieces, tolerance, &mut moved, true)?;
175 let mut points = Vec::new();
176 for mut piece in pieces {
177 piece.pop();
178 points.extend(piece);
179 }
180 transform(&mut points, &l.transforms);
181 flat.push(points);
182 }
183 deviation = deviation.max(chord + moved * l.stretch);
184 if flat.iter().any(|ring| !distinct(ring, tolerance)) {
185 return Ok(None);
186 }
187 let mut it = flat.into_iter();
188 let outer = it.next().unwrap_or_default();
189 rings.push(Rings {
190 outer,
191 holes: it.collect(),
192 });
193 }
194 Ok(Some(PairedRings {
195 rings,
196 deviation: deviation * (1.0 + 1e-12),
197 }))
198}
199
200fn transform(points: &mut [Point2], transforms: &[Transform2]) {
201 for p in points {
202 for t in transforms {
203 *p = t.transform_point2(*p);
204 }
205 }
206}
207
208fn distinct(ring: &[Point2], tolerance: Tolerance) -> bool {
211 ring.len() >= 3
212 && (0..ring.len()).all(|i| !near2(ring[i], ring[(i + 1) % ring.len()], tolerance.linear()))
213}
214
215fn span(segment: &ProfileSegment) -> GeomResult<Scalar> {
217 let span = (segment.domain.end - segment.domain.start).abs();
218 if span.is_finite() {
219 Ok(span)
220 } else {
221 Err(GeomError::InvalidInput(
222 "a profile segment's parameter domain must be finite".into(),
223 ))
224 }
225}
226
227fn second_bound(curve: &Curve2) -> Option<Scalar> {
231 match curve {
232 Curve2::Line(_) => Some(0.0),
233 Curve2::Circle(c) => Some(c.radius * c.frame.x.length().hypot(c.frame.y.length())),
234 Curve2::Ellipse(e) => {
235 Some((e.semi_axis_x * e.frame.x.length()).hypot(e.semi_axis_y * e.frame.y.length()))
236 }
237 _ => None,
238 }
239}
240
241fn sample(segment: &ProfileSegment, count: usize, shift: Scalar) -> GeomResult<Vec<Point2>> {
245 let (lo, hi) = (segment.domain.start + shift, segment.domain.end + shift);
246 let (from, to) = if segment.same_sense {
247 (lo, hi)
248 } else {
249 (hi, lo)
250 };
251 (0..=count)
252 .map(|m| {
253 let t = if m == 0 {
254 from
255 } else if m == count {
256 to
257 } else {
258 from + (to - from) * (m as Scalar / count as Scalar)
259 };
260 evaluate2(&segment.curve, t)
261 })
262 .collect()
263}
264
265fn orient(lowered: &mut Lowered) -> GeomResult<bool> {
269 let det: Scalar = lowered
270 .transforms
271 .iter()
272 .map(|t| t.matrix2.determinant())
273 .product();
274 for (r, ring) in lowered.rings.iter_mut().enumerate() {
275 let mut area = 0.0;
276 for segment in ring.iter() {
277 area += segment_area(segment)?;
278 }
279 area *= det;
280 if area == 0.0 || !area.is_finite() {
281 return Ok(false);
282 }
283 let reverse = (r == 0 && area < 0.0) || (r > 0 && area > 0.0);
284 if reverse {
285 ring.reverse();
286 for segment in ring.iter_mut() {
287 segment.same_sense = !segment.same_sense;
288 }
289 }
290 lowered.reversed.push(reverse);
291 }
292 Ok(true)
293}
294
295fn segment_area(segment: &ProfileSegment) -> GeomResult<Scalar> {
300 let (t0, t1) = (segment.domain.start, segment.domain.end);
301 let cross = |a: Vec2, b: Vec2| a.x * b.y - a.y * b.x;
302 let conic = |o: Point2, a: Vec2, b: Vec2| {
303 let o = Vec2::new(o.x, o.y);
304 0.5 * (cross(a, b) * (t1 - t0)
305 + cross(o, b) * (t1.sin() - t0.sin())
306 + cross(o, a) * (t1.cos() - t0.cos()))
307 };
308 let forward = match &segment.curve {
309 Curve2::Line(_) => {
310 let p = evaluate2(&segment.curve, t0)?;
311 let q = evaluate2(&segment.curve, t1)?;
312 0.5 * (p.x * q.y - q.x * p.y)
313 }
314 Curve2::Circle(c) => conic(c.frame.origin, c.frame.x * c.radius, c.frame.y * c.radius),
315 Curve2::Ellipse(e) => conic(
316 e.frame.origin,
317 e.frame.x * e.semi_axis_x,
318 e.frame.y * e.semi_axis_y,
319 ),
320 _ => return Ok(Scalar::NAN),
321 };
322 Ok(if segment.same_sense {
323 forward
324 } else {
325 -forward
326 })
327}
328
329fn lower(profile: &Profile) -> GeomResult<Option<Lowered>> {
331 let plain = |rings: Vec<Vec<ProfileSegment>>| Lowered {
332 rings,
333 transforms: Vec::new(),
334 stretch: 1.0,
335 reversed: Vec::new(),
336 };
337 let contour = |c: &ContourProfile| {
338 plain(
339 core::iter::once(&c.outer)
340 .chain(&c.holes)
341 .map(|contour| contour.segments.clone())
342 .collect(),
343 )
344 };
345 Ok(match profile {
346 Profile::Rectangle(r) => Some(contour(&crate::section_lower::rectangle_contour(r)?)),
347 Profile::Section(s) => Some(contour(&crate::section_lower::section_contour(s)?)),
348 Profile::Contour(c) => Some(contour(c)),
349 Profile::Circle(c) => {
350 if !c.radius.is_finite() || c.radius <= 0.0 {
351 return Err(GeomError::InvalidInput(format!(
352 "circle profile radius must be positive and finite, got {}",
353 c.radius
354 )));
355 }
356 let circle = |radius| {
357 full_turn(Curve2::Circle(Circle2 {
358 frame: unit_frame(),
359 radius,
360 }))
361 };
362 let mut rings = vec![vec![circle(c.radius)]];
363 if let Some(t) = c.thickness {
364 if !(t > 0.0 && t < c.radius) {
365 return Err(GeomError::InvalidInput(format!(
366 "annulus wall thickness {t} does not fit inside radius {}",
367 c.radius
368 )));
369 }
370 rings.push(vec![circle(c.radius - t)]);
371 }
372 Some(plain(rings))
373 }
374 Profile::Ellipse(e) => {
375 if !(e.semi_axis_x.is_finite() && e.semi_axis_x > 0.0)
376 || !(e.semi_axis_y.is_finite() && e.semi_axis_y > 0.0)
377 {
378 return Err(GeomError::InvalidInput(format!(
379 "ellipse semi-axes must be positive and finite, got {} and {}",
380 e.semi_axis_x, e.semi_axis_y
381 )));
382 }
383 Some(plain(vec![vec![full_turn(Curve2::Ellipse(Ellipse2 {
384 frame: unit_frame(),
385 semi_axis_x: e.semi_axis_x,
386 semi_axis_y: e.semi_axis_y,
387 }))]]))
388 }
389 Profile::Derived { basis, transform } => lower(basis)?.map(|mut inner| {
390 inner.transforms.push(*transform);
391 inner.stretch *= stretch2(transform);
392 inner
393 }),
394 _ => None,
395 })
396}
397
398fn unit_frame() -> Frame2 {
399 Frame2 {
400 origin: Point2::new(0.0, 0.0),
401 x: Vec2::new(1.0, 0.0),
402 y: Vec2::new(0.0, 1.0),
403 }
404}
405
406fn full_turn(curve: Curve2) -> ProfileSegment {
407 ProfileSegment {
408 curve,
409 domain: Interval {
410 start: 0.0,
411 end: core::f64::consts::TAU,
412 },
413 same_sense: true,
414 }
415}