1use axiolid_contracts::{GeomError, GeomResult, Operation};
66use axiolid_core::{Point2, Scalar, Tolerance};
67use axiolid_curve::{Circle2, Curve2, Polyline2};
68use axiolid_overlay::{ArcRing, ArcVertex};
69use axiolid_profile::{Contour, ProfileSegment};
70
71pub fn contour_to_arc_ring(contour: &Contour, tolerance: Tolerance) -> GeomResult<ArcRing> {
79 lower_contour(contour, tolerance)
80 .map(|lowered| lowered.ring)
81 .map_err(ContourFault::into_geom_error)
82}
83
84#[derive(Debug, Clone)]
87pub(crate) struct LoweredContour {
88 pub(crate) ring: ArcRing,
89 pub(crate) segment_of_edge: Vec<usize>,
92 pub(crate) max_gap: Scalar,
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub(crate) enum ContourFault {
99 TooFewSegments {
101 count: usize,
102 vertices: usize,
103 },
104 UnsupportedSegment {
105 segment: usize,
106 kind: &'static str,
107 },
108 DegenerateSegment {
109 segment: usize,
110 reason: &'static str,
111 },
112 Gap {
115 segment: usize,
116 gap: Scalar,
117 at: Point2,
118 closing: bool,
119 },
120}
121
122impl ContourFault {
123 fn into_geom_error(self) -> GeomError {
125 match self {
126 Self::TooFewSegments { count, vertices } => GeomError::InvalidInput(format!(
127 "a closed contour needs at least two ring vertices, its {count} segment(s) give {vertices}"
128 )),
129 Self::UnsupportedSegment { kind, .. } => GeomError::UnsupportedInput {
130 backend: crate::BACKEND_ID,
131 operation: Operation::Sweep,
132 input: kind,
133 },
134 Self::DegenerateSegment { reason, .. } => GeomError::Degenerate(reason.to_owned()),
135 Self::Gap {
136 gap,
137 at,
138 closing: false,
139 ..
140 } => {
141 GeomError::InvalidInput(format!("contour segments leave a gap of {gap} at {at:?}"))
142 }
143 Self::Gap {
144 gap, closing: true, ..
145 } => GeomError::InvalidInput(format!(
146 "contour does not close: {gap} from last segment end to start"
147 )),
148 }
149 }
150}
151
152pub(crate) fn lower_contour(
154 contour: &Contour,
155 tolerance: Tolerance,
156) -> Result<LoweredContour, ContourFault> {
157 if contour.segments.is_empty() {
158 return Err(ContourFault::TooFewSegments {
159 count: 0,
160 vertices: 0,
161 });
162 }
163
164 let mut vertices = Vec::with_capacity(contour.segments.len());
165 let mut segment_of_edge = Vec::with_capacity(contour.segments.len());
166 let mut previous_end: Option<(Point2, Scalar)> = None;
167 let mut first_scale = 0.0;
168 let mut max_gap: Scalar = 0.0;
169
170 for (index, segment) in contour.segments.iter().enumerate() {
171 let lowered = lower_segment(segment, index)?;
172 let start = lowered.vertices[0].point;
173 if let Some((previous, previous_scale)) = previous_end {
174 let gap = (start - previous).length();
175 if gap > joint_slack(tolerance, previous_scale, lowered.start_scale) {
176 return Err(ContourFault::Gap {
177 segment: index - 1,
178 gap,
179 at: start,
180 closing: false,
181 });
182 }
183 max_gap = max_gap.max(gap);
184 } else {
185 first_scale = lowered.start_scale;
186 }
187 segment_of_edge.extend(std::iter::repeat_n(index, lowered.vertices.len()));
188 vertices.extend(lowered.vertices);
189 previous_end = Some((lowered.end, lowered.end_scale));
190 }
191
192 if vertices.len() < 2 {
195 return Err(ContourFault::TooFewSegments {
196 count: contour.segments.len(),
197 vertices: vertices.len(),
198 });
199 }
200
201 if let (Some((last, last_scale)), Some(first)) = (previous_end, vertices.first()) {
203 let gap = (first.point - last).length();
204 if gap > joint_slack(tolerance, last_scale, first_scale) {
205 return Err(ContourFault::Gap {
206 segment: contour.segments.len() - 1,
207 gap,
208 at: first.point,
209 closing: true,
210 });
211 }
212 max_gap = max_gap.max(gap);
213 }
214
215 Ok(LoweredContour {
216 ring: ArcRing::new(vertices),
217 segment_of_edge,
218 max_gap,
219 })
220}
221
222const JOINT_ROUNDING: Scalar = 8.0 * Scalar::EPSILON;
235
236pub(crate) fn joint_slack(tolerance: Tolerance, entering: Scalar, leaving: Scalar) -> Scalar {
241 tolerance
242 .linear()
243 .max(JOINT_ROUNDING * (entering + leaving))
244}
245
246pub(crate) fn endpoint_scale(segment: &ProfileSegment, at_end: bool, point: Point2) -> Scalar {
250 let t = if at_end == segment.same_sense {
251 segment.domain.end
252 } else {
253 segment.domain.start
254 };
255 match &segment.curve {
256 Curve2::Line(line) => line.origin.length() + line.direction.length() * t.abs(),
257 Curve2::Circle(circle) => {
260 circle.frame.origin.length() + circle.radius.abs() * t.abs().max(1.0)
261 }
262 _ => point.x.abs() + point.y.abs(),
263 }
264}
265
266struct LoweredSegment {
271 vertices: Vec<ArcVertex>,
272 end: Point2,
273 start_scale: Scalar,
274 end_scale: Scalar,
275}
276
277fn lower_segment(segment: &ProfileSegment, index: usize) -> Result<LoweredSegment, ContourFault> {
279 let (from, to) = if segment.same_sense {
280 (segment.domain.start, segment.domain.end)
281 } else {
282 (segment.domain.end, segment.domain.start)
283 };
284
285 match &segment.curve {
286 Curve2::Line(line) => {
287 let start = line.origin + line.direction * from;
288 let end = line.origin + line.direction * to;
289 let origin = line.origin.length();
290 let direction = line.direction.length();
291 Ok(LoweredSegment {
292 vertices: vec![ArcVertex {
293 point: start,
294 bulge: 0.0,
295 }],
296 end,
297 start_scale: origin + direction * from.abs(),
298 end_scale: origin + direction * to.abs(),
299 })
300 }
301 Curve2::Circle(circle) => {
302 let (vertices, end) = lower_arc(circle, from, to, index)?;
303 let scale =
306 |t: Scalar| circle.frame.origin.length() + circle.radius.abs() * t.abs().max(1.0);
307 Ok(LoweredSegment {
308 vertices,
309 end,
310 start_scale: scale(from),
311 end_scale: scale(to),
312 })
313 }
314 Curve2::Polyline(polyline) => lower_polyline(polyline, from, to, index),
315 other => Err(ContourFault::UnsupportedSegment {
316 segment: index,
317 kind: unsupported_curve_name(other),
318 }),
319 }
320}
321
322fn lower_polyline(
334 polyline: &Polyline2,
335 from: Scalar,
336 to: Scalar,
337 index: usize,
338) -> Result<LoweredSegment, ContourFault> {
339 let degenerate = |reason: &'static str| ContourFault::DegenerateSegment {
340 segment: index,
341 reason,
342 };
343 let points = &polyline.points;
344 let count = points.len();
345 if count < 2 || points.iter().any(|p| !p.x.is_finite() || !p.y.is_finite()) {
346 return Err(degenerate(
347 "polyline profile segment has fewer than two finite points",
348 ));
349 }
350 if from == to {
351 return Err(degenerate(
352 "polyline profile segment has an empty parameter range",
353 ));
354 }
355 let segments = if polyline.closed { count } else { count - 1 } as Scalar;
356 let inside = |t: Scalar| (0.0..=segments).contains(&t);
357 if !inside(from) || !inside(to) {
358 return Err(degenerate(
359 "polyline profile segment's domain lies outside its points",
360 ));
361 }
362
363 let evaluate = |t: Scalar| {
364 let i = t.floor();
365 if i == t {
366 return points[i as usize % count];
367 }
368 let (a, b) = (points[i as usize], points[(i as usize + 1) % count]);
369 a + (b - a) * (t - i)
370 };
371 let start = evaluate(from);
372 let end = evaluate(to);
373
374 let mut vertices = vec![ArcVertex {
375 point: start,
376 bulge: 0.0,
377 }];
378 let mut push = |t: Scalar| {
379 let point = evaluate(t);
380 let last = vertices[vertices.len() - 1].point;
381 if point.x.to_bits() != last.x.to_bits() || point.y.to_bits() != last.y.to_bits() {
382 vertices.push(ArcVertex { point, bulge: 0.0 });
383 }
384 };
385 if from < to {
386 let mut k = from.floor() + 1.0;
387 while k < to {
388 push(k);
389 k += 1.0;
390 }
391 } else {
392 let mut k = from.ceil() - 1.0;
393 while k > to {
394 push(k);
395 k -= 1.0;
396 }
397 }
398 if vertices.len() > 1 {
401 let last = vertices[vertices.len() - 1].point;
402 if last.x.to_bits() == end.x.to_bits() && last.y.to_bits() == end.y.to_bits() {
403 vertices.pop();
404 }
405 }
406
407 Ok(LoweredSegment {
408 vertices,
409 end,
410 start_scale: start.x.abs() + start.y.abs(),
411 end_scale: end.x.abs() + end.y.abs(),
412 })
413}
414
415fn lower_arc(
433 circle: &Circle2,
434 from: Scalar,
435 to: Scalar,
436 index: usize,
437) -> Result<(Vec<ArcVertex>, Point2), ContourFault> {
438 let evaluate = |t: Scalar| {
439 let (sin, cos) = quadrant_sin_cos(t);
440 circle.frame.origin
441 + circle.frame.x * (circle.radius * cos)
442 + circle.frame.y * (circle.radius * sin)
443 };
444 let start = evaluate(from);
445 let end = evaluate(to);
446
447 let handedness = circle.frame.x.perp_dot(circle.frame.y);
448 if handedness == 0.0 {
449 return Err(ContourFault::DegenerateSegment {
450 segment: index,
451 reason: "circular profile segment has a degenerate frame",
452 });
453 }
454 let sweep = (to - from) * handedness.signum();
455 if sweep == 0.0 {
456 return Err(ContourFault::DegenerateSegment {
457 segment: index,
458 reason: "circular profile segment has an empty parameter range",
459 });
460 }
461 if sweep.abs() > core::f64::consts::TAU {
462 return Err(ContourFault::UnsupportedSegment {
463 segment: index,
464 kind: "circular profile segment sweeping more than a whole turn",
465 });
466 }
467 let pieces = (sweep.abs() / core::f64::consts::PI).floor() as usize + 1;
469 let bulge = (sweep / (4.0 * pieces as Scalar)).tan();
470 let vertices = (0..pieces)
471 .map(|k| ArcVertex {
472 point: if k == 0 {
473 start
474 } else {
475 evaluate(from + (to - from) * k as Scalar / pieces as Scalar)
476 },
477 bulge,
478 })
479 .collect();
480 Ok((vertices, end))
481}
482
483pub(crate) const QUADRANT_ROUNDING: Scalar = 2.0 * Scalar::EPSILON;
487
488pub(crate) fn quadrant_sin_cos(t: Scalar) -> (Scalar, Scalar) {
491 let (sin, cos) = t.sin_cos();
492 let slack = QUADRANT_ROUNDING * t.abs().max(1.0);
493 if cos.abs() <= slack {
494 (sin.signum(), 0.0)
495 } else if sin.abs() <= slack {
496 (0.0, cos.signum())
497 } else {
498 (sin, cos)
499 }
500}
501
502fn unsupported_curve_name(curve: &Curve2) -> &'static str {
503 match curve {
504 Curve2::Ellipse(_) => "elliptical contour segment",
505 Curve2::BSpline(_) => "B-spline contour segment",
506 Curve2::Intrinsic(_) => "intrinsic contour segment",
507 Curve2::Chain(_) => "arc-length chain contour segment",
508 _ => "contour segment of an unsupported curve kind",
509 }
510}
511
512pub fn arc_ring_signed_area(ring: &ArcRing) -> Scalar {
519 let count = ring.vertices.len();
520 let mut total = 0.0;
521 for index in 0..count {
522 let here = ring.vertices[index];
523 let next = ring.vertices[(index + 1) % count];
524 total += here.point.perp_dot(next.point);
525 if here.bulge != 0.0 {
526 let sweep = 4.0 * here.bulge.atan();
527 let chord = (next.point - here.point).length();
528 let half = (sweep.abs() / 2.0).sin();
529 if half > 0.0 {
530 let radius = chord / (2.0 * half);
531 total += radius * radius * (sweep - sweep.sin());
532 }
533 }
534 }
535 total / 2.0
536}
537
538pub fn reverse_arc_ring(ring: &ArcRing) -> ArcRing {
545 let count = ring.vertices.len();
546 let mut vertices = Vec::with_capacity(count);
547 for index in (0..count).rev() {
548 let previous = (index + count - 1) % count;
549 vertices.push(ArcVertex {
550 point: ring.vertices[index].point,
551 bulge: -ring.vertices[previous].bulge,
552 });
553 }
554 ArcRing { vertices }
555}
556
557pub fn orient_arc_ring(ring: &ArcRing, counter_clockwise: bool) -> GeomResult<ArcRing> {
562 let area = arc_ring_signed_area(ring);
563 if area == 0.0 {
564 return Err(GeomError::Degenerate(
565 "contour ring encloses no area".to_owned(),
566 ));
567 }
568 if (area > 0.0) == counter_clockwise {
569 Ok(ring.clone())
570 } else {
571 Ok(reverse_arc_ring(ring))
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use axiolid_core::{Frame2, Interval, Vec2};
579 use axiolid_curve::Line2;
580
581 fn arc(from: Scalar, to: Scalar) -> ProfileSegment {
582 ProfileSegment {
583 curve: Curve2::Circle(Circle2 {
584 frame: Frame2 {
585 origin: Point2::new(0.0, 0.0),
586 x: Vec2::X,
587 y: Vec2::Y,
588 },
589 radius: 1.0,
590 }),
591 domain: Interval::new(from, to),
592 same_sense: true,
593 }
594 }
595
596 #[test]
597 fn the_sub_edges_of_a_split_arc_map_to_their_segment() {
598 let chord = ProfileSegment {
599 curve: Curve2::Line(Line2 {
600 origin: Point2::new(0.0, -1.0),
601 direction: Point2::new(1.0, 1.0) - Point2::new(0.0, 0.0),
602 }),
603 domain: Interval::UNIT,
604 same_sense: true,
605 };
606 let contour = Contour::new(vec![arc(0.0, 1.5 * core::f64::consts::PI), chord]);
608 let lowered = lower_contour(&contour, Tolerance::METRE).expect("lowers");
609 assert_eq!(lowered.segment_of_edge, vec![0, 0, 1]);
610 assert_eq!(lowered.ring.vertices.len(), 3);
611 }
612
613 #[test]
614 fn a_half_turn_between_quadrants_lowers_with_exact_ends() {
615 use core::f64::consts::FRAC_PI_2;
616 let axis = ProfileSegment {
617 curve: Curve2::Line(Line2 {
618 origin: Point2::new(0.0, 1.0),
619 direction: Point2::new(0.0, -2.0),
620 }),
621 domain: Interval::UNIT,
622 same_sense: true,
623 };
624 let contour = Contour::new(vec![arc(-FRAC_PI_2, FRAC_PI_2), axis]);
625 let lowered = lower_contour(&contour, Tolerance::ZERO).expect("lowers");
626 let points: Vec<_> = lowered.ring.vertices.iter().map(|v| v.point).collect();
627 assert_eq!(points[0], Point2::new(0.0, -1.0));
628 assert_eq!(points[points.len() - 1], Point2::new(0.0, 1.0));
629 }
630
631 #[test]
632 fn every_form_of_a_quarter_turn_snaps() {
633 use core::f64::consts::{FRAC_PI_2, PI, TAU};
634 let quarters = [
635 (FRAC_PI_2, (1.0, 0.0)),
636 (-FRAC_PI_2, (-1.0, 0.0)),
637 (-PI / 2.0, (-1.0, 0.0)),
638 (1.5 * PI, (-1.0, 0.0)),
639 (3.0 * FRAC_PI_2, (-1.0, 0.0)),
640 (PI + FRAC_PI_2, (-1.0, 0.0)),
641 (TAU - FRAC_PI_2, (-1.0, 0.0)),
642 (2.5 * PI, (1.0, 0.0)),
643 (-1.5 * PI, (1.0, 0.0)),
644 (20.5 * PI, (1.0, 0.0)),
645 (PI, (0.0, -1.0)),
646 (TAU, (0.0, 1.0)),
647 (0.0, (0.0, 1.0)),
648 ];
649 for (t, exact) in quarters {
650 assert_eq!(quadrant_sin_cos(t), exact, "{t}");
651 }
652 }
653
654 #[test]
655 fn a_large_arc_closed_by_a_line_between_its_raw_ends_lowers_at_zero() {
656 use core::f64::consts::PI;
659 for t in [19.5, 64.5, 200.5] {
660 let (from, to) = (t * PI, (t + 1.0) * PI);
661 let (a, b) = (from.sin_cos(), to.sin_cos());
662 let close = ProfileSegment {
663 curve: Curve2::Line(Line2 {
664 origin: Point2::new(b.1, b.0),
665 direction: Point2::new(a.1, a.0) - Point2::new(b.1, b.0),
666 }),
667 domain: Interval::UNIT,
668 same_sense: true,
669 };
670 let contour = Contour::new(vec![arc(from, to), close]);
671 lower_contour(&contour, Tolerance::ZERO).unwrap_or_else(|e| panic!("{t}: {e:?}"));
672 }
673 }
674
675 #[test]
676 fn a_parameter_a_hair_off_a_quarter_turn_keeps_its_angle() {
677 use core::f64::consts::FRAC_PI_2;
678 let t = FRAC_PI_2 + 1e-12;
679 assert_eq!(quadrant_sin_cos(t), t.sin_cos());
680 assert_ne!(quadrant_sin_cos(t).1, 0.0);
681 }
682}