1use axiolid_contracts::{GeomError, GeomResult, Operation};
24use axiolid_core::{Interval, Point2, Scalar, Tolerance, Vec2};
25use axiolid_curve::{Circle2, Curve2, Line2};
26use axiolid_profile::{CenterLineProfile, Contour, ContourProfile, ProfileSegment};
27
28use crate::BACKEND_ID;
29
30fn unsupported(input: &'static str) -> GeomError {
31 GeomError::UnsupportedInput {
32 backend: BACKEND_ID,
33 operation: Operation::Sweep,
34 input,
35 }
36}
37
38#[derive(Debug, Clone, Copy)]
40enum Piece {
41 Line {
42 from: Point2,
43 to: Point2,
44 },
45 Arc {
46 centre: Point2,
47 x: Vec2,
48 y: Vec2,
49 radius: Scalar,
50 start: Scalar,
51 end: Scalar,
52 sweep: Scalar,
54 },
55}
56
57impl Piece {
58 fn start_point(&self) -> Point2 {
59 match self {
60 Piece::Line { from, .. } => *from,
61 Piece::Arc {
62 centre,
63 x,
64 y,
65 radius,
66 start,
67 ..
68 } => conic(*centre, *x, *y, *radius, *start),
69 }
70 }
71
72 fn end_point(&self) -> Point2 {
73 match self {
74 Piece::Line { to, .. } => *to,
75 Piece::Arc {
76 centre,
77 x,
78 y,
79 radius,
80 end,
81 ..
82 } => conic(*centre, *x, *y, *radius, *end),
83 }
84 }
85
86 fn start_tangent(&self) -> Vec2 {
88 match self {
89 Piece::Line { from, to } => (*to - *from).normalize_or_zero(),
90 Piece::Arc {
91 x, y, start, end, ..
92 } => arc_tangent(*x, *y, *start, *end > *start),
93 }
94 }
95
96 fn end_tangent(&self) -> Vec2 {
98 match self {
99 Piece::Line { from, to } => (*to - *from).normalize_or_zero(),
100 Piece::Arc {
101 x, y, start, end, ..
102 } => arc_tangent(*x, *y, *end, *end > *start),
103 }
104 }
105}
106
107fn conic(centre: Point2, x: Vec2, y: Vec2, radius: Scalar, t: Scalar) -> Point2 {
108 let (s, c) = t.sin_cos();
109 centre + x * (radius * c) + y * (radius * s)
110}
111
112fn arc_tangent(x: Vec2, y: Vec2, t: Scalar, increasing: bool) -> Vec2 {
113 let (s, c) = t.sin_cos();
114 let d = x * -s + y * c;
115 let d = if increasing { d } else { -d };
116 d.normalize_or_zero()
117}
118fn read_piece(segment: &ProfileSegment) -> GeomResult<Piece> {
120 let (from, to) = if segment.same_sense {
121 (segment.domain.start, segment.domain.end)
122 } else {
123 (segment.domain.end, segment.domain.start)
124 };
125 match &segment.curve {
126 Curve2::Line(line) => Ok(Piece::Line {
127 from: line.origin + line.direction * from,
128 to: line.origin + line.direction * to,
129 }),
130 Curve2::Circle(circle) => {
131 let handedness = circle.frame.x.perp_dot(circle.frame.y);
132 if handedness == 0.0 {
133 return Err(GeomError::Degenerate(
134 "centre-line arc has a degenerate frame".to_owned(),
135 ));
136 }
137 Ok(Piece::Arc {
138 centre: circle.frame.origin,
139 x: circle.frame.x,
140 y: circle.frame.y,
141 radius: circle.radius,
142 start: from,
143 end: to,
144 sweep: (to - from) * handedness.signum(),
148 })
149 }
150 Curve2::Ellipse(_) => Err(unsupported(
151 "centre-line elliptical segment has no elliptical offset",
152 )),
153 _ => Err(unsupported(
154 "centre-line segment of a kind with no exact offset",
155 )),
156 }
157}
158
159fn offset_piece(piece: &Piece, distance: Scalar) -> GeomResult<ProfileSegment> {
163 match piece {
164 Piece::Line { from, to } => {
165 let along = (*to - *from).normalize_or_zero();
166 if along == Vec2::ZERO {
167 return Err(GeomError::Degenerate(
168 "centre line has a zero-length segment".to_owned(),
169 ));
170 }
171 let normal = Vec2::new(-along.y, along.x);
172 let start = *from + normal * distance;
173 let end = *to + normal * distance;
174 Ok(ProfileSegment {
175 curve: Curve2::Line(Line2 {
176 origin: start,
177 direction: end - start,
178 }),
179 domain: Interval::UNIT,
180 same_sense: true,
181 })
182 }
183 Piece::Arc {
184 centre,
185 x,
186 y,
187 radius,
188 start,
189 end,
190 sweep,
191 } => {
192 let offset_radius = radius - distance * sweep.signum();
197 if offset_radius <= 0.0 {
198 return Err(GeomError::Degenerate(format!(
199 "centre-line half-width {} collapses an arc of radius {radius}",
200 distance.abs()
201 )));
202 }
203 Ok(ProfileSegment {
204 curve: Curve2::Circle(Circle2 {
205 frame: axiolid_core::Frame2 {
206 origin: *centre,
207 x: *x,
208 y: *y,
209 },
210 radius: offset_radius,
211 }),
212 domain: Interval::new(*start, *end),
213 same_sense: true,
214 })
215 }
216 }
217}
218pub fn center_line_contour(
225 profile: &CenterLineProfile,
226 tolerance: Tolerance,
227) -> GeomResult<ContourProfile> {
228 if profile.half_width <= 0.0 || profile.half_width.is_nan() {
230 return Err(GeomError::Degenerate(format!(
231 "centre line half-width must be positive, got {}",
232 profile.half_width
233 )));
234 }
235 if profile.path.segments.is_empty() {
236 return Err(GeomError::Degenerate(
237 "centre line path has no segments".to_owned(),
238 ));
239 }
240
241 let pieces = profile
242 .path
243 .segments
244 .iter()
245 .map(read_piece)
246 .collect::<GeomResult<Vec<_>>>()?;
247
248 let eps = tolerance.linear();
252 for pair in pieces.windows(2) {
253 let gap = (pair[1].start_point() - pair[0].end_point()).length();
254 if gap > eps {
255 return Err(GeomError::InvalidInput(format!(
256 "centre line path is disconnected by {gap}"
257 )));
258 }
259 }
260 let first = pieces.first().expect("at least one segment");
261 let last = pieces.last().expect("at least one segment");
262 if (last.end_point() - first.start_point()).length() <= eps {
263 return Err(unsupported(
264 "closed centre-line path denotes an annulus, not a single ring",
265 ));
266 }
267
268 for pair in pieces.windows(2) {
272 let incoming = pair[0].end_tangent();
273 let outgoing = pair[1].start_tangent();
274 if incoming.dot(outgoing) <= -1.0 + 1e-12 {
275 return Err(GeomError::Degenerate(
276 "centre line reverses on itself".to_owned(),
277 ));
278 }
279 }
280
281 let half = profile.half_width;
282 let mut segments = Vec::with_capacity(2 * pieces.len() + 2);
283
284 for piece in &pieces {
289 segments.push(offset_piece(piece, -half)?);
290 }
291 let end_centre = last.end_point();
293 let end_normal = left_normal(last.end_tangent());
294 segments.push(straight(
295 end_centre - end_normal * half,
296 end_centre + end_normal * half,
297 ));
298 for piece in pieces.iter().rev() {
300 let mut offset = offset_piece(piece, half)?;
301 offset.same_sense = !offset.same_sense;
302 segments.push(offset);
303 }
304 let start_centre = first.start_point();
306 let start_normal = left_normal(first.start_tangent());
307 segments.push(straight(
308 start_centre + start_normal * half,
309 start_centre - start_normal * half,
310 ));
311
312 Ok(ContourProfile {
313 outer: Contour::new(segments),
314 holes: Vec::new(),
315 })
316}
317
318fn left_normal(tangent: Vec2) -> Vec2 {
319 Vec2::new(-tangent.y, tangent.x)
320}
321
322fn straight(from: Point2, to: Point2) -> ProfileSegment {
323 ProfileSegment {
324 curve: Curve2::Line(Line2 {
325 origin: from,
326 direction: to - from,
327 }),
328 domain: Interval::UNIT,
329 same_sense: true,
330 }
331}