axiolid_construct/
contour_lower.rs1use axiolid_contracts::{GeomError, GeomResult, Operation};
16use axiolid_core::{Point2, Scalar, Tolerance};
17use axiolid_curve::{Circle2, Curve2};
18use axiolid_overlay::{ArcRing, ArcVertex};
19use axiolid_profile::{Contour, ProfileSegment};
20
21pub fn contour_to_arc_ring(contour: &Contour, tolerance: Tolerance) -> GeomResult<ArcRing> {
27 if contour.segments.len() < 2 {
28 return Err(GeomError::InvalidInput(format!(
29 "a closed contour needs at least two segments, got {}",
30 contour.segments.len()
31 )));
32 }
33
34 let mut vertices = Vec::with_capacity(contour.segments.len());
35 let mut previous_end: Option<Point2> = None;
36
37 for segment in &contour.segments {
38 let (start, end, bulge) = lower_segment(segment)?;
39 if let Some(previous) = previous_end {
40 let gap = (start - previous).length();
41 if gap > tolerance.linear() {
42 return Err(GeomError::InvalidInput(format!(
43 "contour segments leave a gap of {gap} at {start:?}"
44 )));
45 }
46 }
47 vertices.push(ArcVertex {
48 point: start,
49 bulge,
50 });
51 previous_end = Some(end);
52 }
53
54 if let (Some(last), Some(first)) = (previous_end, vertices.first()) {
56 let gap = (first.point - last).length();
57 if gap > tolerance.linear() {
58 return Err(GeomError::InvalidInput(format!(
59 "contour does not close: {gap} from last segment end to start"
60 )));
61 }
62 }
63
64 Ok(ArcRing::new(vertices))
65}
66
67fn lower_segment(segment: &ProfileSegment) -> GeomResult<(Point2, Point2, Scalar)> {
69 let (from, to) = if segment.same_sense {
70 (segment.domain.start, segment.domain.end)
71 } else {
72 (segment.domain.end, segment.domain.start)
73 };
74
75 match &segment.curve {
76 Curve2::Line(line) => {
77 let start = line.origin + line.direction * from;
78 let end = line.origin + line.direction * to;
79 Ok((start, end, 0.0))
80 }
81 Curve2::Circle(circle) => lower_arc(circle, from, to),
82 other => Err(GeomError::UnsupportedInput {
83 backend: crate::BACKEND_ID,
84 operation: Operation::Sweep,
85 input: unsupported_curve_name(other),
86 }),
87 }
88}
89
90fn lower_arc(circle: &Circle2, from: Scalar, to: Scalar) -> GeomResult<(Point2, Point2, Scalar)> {
98 let evaluate = |t: Scalar| {
99 let (sin, cos) = t.sin_cos();
100 circle.frame.origin
101 + circle.frame.x * (circle.radius * cos)
102 + circle.frame.y * (circle.radius * sin)
103 };
104 let start = evaluate(from);
105 let end = evaluate(to);
106
107 let handedness = circle.frame.x.perp_dot(circle.frame.y);
108 if handedness == 0.0 {
109 return Err(GeomError::Degenerate(
110 "circular profile segment has a degenerate frame".to_owned(),
111 ));
112 }
113 let sweep = (to - from) * handedness.signum();
114 if sweep == 0.0 {
115 return Err(GeomError::Degenerate(
116 "circular profile segment has an empty parameter range".to_owned(),
117 ));
118 }
119 if sweep.abs() >= core::f64::consts::PI {
122 return Err(GeomError::UnsupportedInput {
123 backend: crate::BACKEND_ID,
124 operation: Operation::Sweep,
125 input: "circular profile segment sweeping half a turn or more",
126 });
127 }
128 Ok((start, end, (sweep / 4.0).tan()))
129}
130
131fn unsupported_curve_name(curve: &Curve2) -> &'static str {
132 match curve {
133 Curve2::Ellipse(_) => "elliptical contour segment",
134 Curve2::Polyline(_) => "polyline contour segment",
135 Curve2::BSpline(_) => "B-spline contour segment",
136 Curve2::Intrinsic(_) => "intrinsic contour segment",
137 _ => "contour segment of an unsupported curve kind",
138 }
139}
140
141pub fn arc_ring_signed_area(ring: &ArcRing) -> Scalar {
148 let count = ring.vertices.len();
149 let mut total = 0.0;
150 for index in 0..count {
151 let here = ring.vertices[index];
152 let next = ring.vertices[(index + 1) % count];
153 total += here.point.perp_dot(next.point);
154 if here.bulge != 0.0 {
155 let sweep = 4.0 * here.bulge.atan();
156 let chord = (next.point - here.point).length();
157 let half = (sweep.abs() / 2.0).sin();
158 if half > 0.0 {
159 let radius = chord / (2.0 * half);
160 total += radius * radius * (sweep - sweep.sin());
161 }
162 }
163 }
164 total / 2.0
165}
166
167pub fn reverse_arc_ring(ring: &ArcRing) -> ArcRing {
174 let count = ring.vertices.len();
175 let mut vertices = Vec::with_capacity(count);
176 for index in (0..count).rev() {
177 let previous = (index + count - 1) % count;
178 vertices.push(ArcVertex {
179 point: ring.vertices[index].point,
180 bulge: -ring.vertices[previous].bulge,
181 });
182 }
183 ArcRing { vertices }
184}
185
186pub fn orient_arc_ring(ring: &ArcRing, counter_clockwise: bool) -> GeomResult<ArcRing> {
191 let area = arc_ring_signed_area(ring);
192 if area == 0.0 {
193 return Err(GeomError::Degenerate(
194 "contour ring encloses no area".to_owned(),
195 ));
196 }
197 if (area > 0.0) == counter_clockwise {
198 Ok(ring.clone())
199 } else {
200 Ok(reverse_arc_ring(ring))
201 }
202}