1use axiolid_contracts::{GeomError, GeomResult, Operation};
34use axiolid_core::{Frame2, Interval, Point2, Scalar, Tolerance, Transform2, Vec2};
35use axiolid_curve::{Circle2, Curve2, Line2};
36use axiolid_overlay::{union_soup, ArcArrangement, ArcRing, Ring};
37use axiolid_profile::{
38 CircleProfile, Contour, ContourProfile, Profile, ProfileSegment, RectangleProfile,
39};
40
41use crate::BACKEND_ID;
42
43fn unsupported(input: &'static str) -> GeomError {
44 GeomError::UnsupportedInput {
45 backend: BACKEND_ID,
46 operation: Operation::Sweep,
47 input,
48 }
49}
50
51fn conformal_scale(transform: &Transform2, tolerance: Tolerance) -> Option<Scalar> {
58 let x = transform.matrix2.x_axis;
59 let y = transform.matrix2.y_axis;
60 let xx = x.dot(x);
61 let yy = y.dot(y);
62 let xy = x.dot(y);
63 if xx <= 0.0 || yy <= 0.0 {
64 return None;
65 }
66 let slack = tolerance.linear() * xx.max(yy).max(1.0);
69 if xy.abs() > slack || (xx - yy).abs() > slack {
70 return None;
71 }
72 Some(xx.sqrt())
73}
74
75fn apply(transform: &Transform2, point: Point2) -> Point2 {
77 transform.transform_point2(point)
78}
79
80pub fn lower_derived(
86 basis: &Profile,
87 transform: &Transform2,
88 tolerance: Tolerance,
89) -> GeomResult<Profile> {
90 match basis {
91 Profile::Derived {
94 basis: inner,
95 transform: inner_transform,
96 } => lower_derived(inner, &(*transform * *inner_transform), tolerance),
97
98 Profile::Rectangle(rectangle) => lower_rectangle(rectangle, transform, tolerance),
99 Profile::Circle(circle) => lower_circle(circle, transform, tolerance),
100 Profile::Contour(contour) => Ok(Profile::Contour(ContourProfile {
101 outer: lower_contour(&contour.outer, transform, tolerance)?,
102 holes: contour
103 .holes
104 .iter()
105 .map(|hole| lower_contour(hole, transform, tolerance))
106 .collect::<GeomResult<Vec<_>>>()?,
107 })),
108 Profile::Composite(members) => Ok(Profile::Composite(
109 members
110 .iter()
111 .map(|member| lower_derived(member, transform, tolerance))
112 .collect::<GeomResult<Vec<_>>>()?,
113 )),
114 _ => Err(unsupported("derived profile over an unsupported basis")),
115 }
116}
117
118fn lower_rectangle(
124 rectangle: &RectangleProfile,
125 transform: &Transform2,
126 tolerance: Tolerance,
127) -> GeomResult<Profile> {
128 if rectangle.thickness.is_some()
129 || rectangle.outer_radius.is_some()
130 || rectangle.inner_radius.is_some()
131 {
132 let contour = crate::section_lower::rectangle_contour(rectangle)?;
137 return lower_derived(&Profile::Contour(contour), transform, tolerance);
138 }
139 if !rectangle.x.is_finite()
140 || !rectangle.y.is_finite()
141 || rectangle.x <= 0.0
142 || rectangle.y <= 0.0
143 {
144 return Err(GeomError::InvalidInput(format!(
145 "derived rectangle extents must be positive and finite, got {} x {}",
146 rectangle.x, rectangle.y
147 )));
148 }
149
150 let (half_x, half_y) = (rectangle.x / 2.0, rectangle.y / 2.0);
151 let corners = [
152 Point2::new(-half_x, -half_y),
153 Point2::new(half_x, -half_y),
154 Point2::new(half_x, half_y),
155 Point2::new(-half_x, half_y),
156 ];
157 let mut moved: Vec<Point2> = corners.iter().map(|p| apply(transform, *p)).collect();
158 orient_counter_clockwise(&mut moved);
159 let _ = tolerance;
160 Ok(Profile::Contour(ContourProfile {
161 outer: polygon_contour(&moved),
162 holes: Vec::new(),
163 }))
164}
165
166fn lower_circle(
168 circle: &CircleProfile,
169 transform: &Transform2,
170 tolerance: Tolerance,
171) -> GeomResult<Profile> {
172 let Some(scale) = conformal_scale(transform, tolerance) else {
173 return Err(unsupported(
177 "derived circle under a non-conformal transform is an ellipse",
178 ));
179 };
180 if transform.translation != Vec2::ZERO {
181 let contour = crate::section_lower::circle_contour(circle)?;
185 return Ok(Profile::Contour(ContourProfile {
186 outer: lower_contour(&contour.outer, transform, tolerance)?,
187 holes: contour
188 .holes
189 .iter()
190 .map(|hole| lower_contour(hole, transform, tolerance))
191 .collect::<GeomResult<Vec<_>>>()?,
192 }));
193 }
194 Ok(Profile::Circle(CircleProfile {
195 radius: circle.radius * scale,
196 thickness: circle.thickness.map(|value| value * scale),
197 }))
198}
199
200fn lower_contour(
207 contour: &Contour,
208 transform: &Transform2,
209 tolerance: Tolerance,
210) -> GeomResult<Contour> {
211 let mirrors = transform.matrix2.determinant() < 0.0;
212 let mut segments = Vec::with_capacity(contour.segments.len());
213 for segment in &contour.segments {
214 segments.push(lower_segment(segment, transform, tolerance, mirrors)?);
215 }
216 if mirrors {
217 segments.reverse();
221 }
222 Ok(Contour::new(segments))
223}
224
225fn lower_segment(
226 segment: &ProfileSegment,
227 transform: &Transform2,
228 tolerance: Tolerance,
229 mirrors: bool,
230) -> GeomResult<ProfileSegment> {
231 let same_sense = segment.same_sense != mirrors;
232 match &segment.curve {
233 Curve2::Line(line) => Ok(ProfileSegment {
234 curve: Curve2::Line(Line2 {
235 origin: apply(transform, line.origin),
236 direction: transform.matrix2 * line.direction,
239 }),
240 domain: segment.domain,
241 same_sense,
242 }),
243 Curve2::Circle(circle) => {
244 let Some(scale) = conformal_scale(transform, tolerance) else {
245 return Err(unsupported(
246 "derived contour arc under a non-conformal transform is elliptical",
247 ));
248 };
249 Ok(ProfileSegment {
252 curve: Curve2::Circle(Circle2 {
253 frame: Frame2 {
254 origin: apply(transform, circle.frame.origin),
255 x: transform.matrix2 * circle.frame.x / scale,
256 y: transform.matrix2 * circle.frame.y / scale,
257 },
258 radius: circle.radius * scale,
259 }),
260 domain: segment.domain,
261 same_sense,
262 })
263 }
264 _ => Err(unsupported(
265 "derived contour over a segment kind that cannot be transformed exactly",
266 )),
267 }
268}
269
270fn polygon_contour(points: &[Point2]) -> Contour {
272 let count = points.len();
273 let segments = (0..count)
274 .map(|index| {
275 let from = points[index];
276 let to = points[(index + 1) % count];
277 ProfileSegment {
278 curve: Curve2::Line(Line2 {
279 origin: from,
280 direction: to - from,
281 }),
282 domain: Interval::UNIT,
283 same_sense: true,
284 }
285 })
286 .collect();
287 Contour::new(segments)
288}
289
290fn signed_area(points: &[Point2]) -> Scalar {
292 let count = points.len();
293 (0..count)
294 .map(|index| {
295 let a = points[index];
296 let b = points[(index + 1) % count];
297 a.perp_dot(b)
298 })
299 .sum::<Scalar>()
300 / 2.0
301}
302
303fn orient_counter_clockwise(points: &mut [Point2]) {
308 if signed_area(points) < 0.0 {
309 points.reverse();
310 }
311}
312
313pub fn lower_composite(
324 members: &[Profile],
325 tolerance: Tolerance,
326) -> GeomResult<(Vec<Point2>, Vec<Vec<Point2>>)> {
327 if members.is_empty() {
328 return Err(GeomError::InvalidInput(
329 "a composite profile needs at least one member".to_owned(),
330 ));
331 }
332
333 let mut rings = Vec::with_capacity(members.len());
334 for member in members {
335 rings.push(Ring {
336 points: member_ring(member, tolerance)?,
337 });
338 }
339
340 let polygons = union_soup(&rings, tolerance).map_err(|error| {
341 GeomError::InvalidInput(format!("composite member union failed: {error:?}"))
342 })?;
343
344 match polygons.len() {
345 0 => Err(GeomError::Degenerate(
346 "composite profile members union to nothing".to_owned(),
347 )),
348 1 => {
349 let polygon = &polygons[0];
350 Ok((
351 polygon.outer.points.clone(),
352 polygon.holes.iter().map(|h| h.points.clone()).collect(),
353 ))
354 }
355 _ => Err(unsupported("composite profile whose members are disjoint")),
358 }
359}
360
361#[derive(Debug, Clone, PartialEq)]
363pub struct CompositeRegion {
364 pub outer: ArcRing,
366 pub holes: Vec<ArcRing>,
368}
369
370pub fn composite_regions(
383 members: &[Profile],
384 tolerance: Tolerance,
385) -> GeomResult<Vec<CompositeRegion>> {
386 if members.is_empty() {
387 return Err(GeomError::InvalidInput(
388 "a composite profile needs at least one member".to_owned(),
389 ));
390 }
391 let mut rings = Vec::new();
392 let mut layout = Vec::with_capacity(members.len());
394 for member in members {
395 if matches!(member, Profile::Composite(_)) {
396 return Err(unsupported("composite profile nested in a composite"));
397 }
398 let contour = crate::extrude_exact::profile_to_contour(member, tolerance)?;
399 let outer = rings.len();
400 rings.push(crate::contour_lower::contour_to_arc_ring(
401 &contour.outer,
402 tolerance,
403 )?);
404 let mut holes = Vec::with_capacity(contour.holes.len());
405 for hole in &contour.holes {
406 holes.push(rings.len());
407 rings.push(crate::contour_lower::contour_to_arc_ring(hole, tolerance)?);
408 }
409 layout.push((outer, holes));
410 }
411 weld_ring_vertices(&mut rings, tolerance);
412 let arrangement = ArcArrangement::new(&rings, tolerance).map_err(|error| {
413 GeomError::InvalidInput(format!("composite member rings are invalid: {error:?}"))
414 })?;
415 let regions = arrangement
416 .regions(|inside| {
417 layout
418 .iter()
419 .any(|(outer, holes)| inside[*outer] && !holes.iter().any(|hole| inside[*hole]))
420 })
421 .map_err(|error| {
422 GeomError::InvalidInput(format!("composite member union failed: {error:?}"))
423 })?;
424 if regions.is_empty() {
425 return Err(GeomError::Degenerate(
426 "composite profile members union to nothing".to_owned(),
427 ));
428 }
429 Ok(regions
430 .iter()
431 .map(|region| CompositeRegion {
432 outer: without_zero_pieces(arrangement.ring(®ion.outer)),
433 holes: region
434 .holes
435 .iter()
436 .map(|hole| without_zero_pieces(arrangement.ring(hole)))
437 .collect(),
438 })
439 .collect())
440}
441
442fn without_zero_pieces(mut ring: ArcRing) -> ArcRing {
450 let mut index = 0;
451 while ring.vertices.len() > 1 && index < ring.vertices.len() {
452 let next = (index + 1) % ring.vertices.len();
453 if ring.vertices[index].point == ring.vertices[next].point {
454 ring.vertices.remove(index);
455 } else {
456 index += 1;
457 }
458 }
459 ring
460}
461
462fn weld_ring_vertices(rings: &mut [ArcRing], tolerance: Tolerance) {
471 let limit = tolerance.linear();
472 let mut seen: Vec<Point2> = Vec::new();
473 for ring in rings.iter_mut() {
474 for vertex in &mut ring.vertices {
475 match seen
476 .iter()
477 .find(|point| (**point - vertex.point).length() <= limit)
478 {
479 Some(point) => vertex.point = *point,
480 None => seen.push(vertex.point),
481 }
482 }
483 }
484}
485
486fn member_ring(member: &Profile, tolerance: Tolerance) -> GeomResult<Vec<Point2>> {
492 let lowered;
493 let resolved = match member {
494 Profile::Derived { basis, transform } => {
495 lowered = lower_derived(basis, transform, tolerance)?;
496 &lowered
497 }
498 other => other,
499 };
500
501 match resolved {
502 Profile::Rectangle(rectangle) => {
503 if rectangle.thickness.is_some()
504 || rectangle.outer_radius.is_some()
505 || rectangle.inner_radius.is_some()
506 {
507 return Err(unsupported(
508 "composite member with a hollow or rounded rectangle",
509 ));
510 }
511 let (half_x, half_y) = (rectangle.x / 2.0, rectangle.y / 2.0);
512 Ok(vec![
513 Point2::new(-half_x, -half_y),
514 Point2::new(half_x, -half_y),
515 Point2::new(half_x, half_y),
516 Point2::new(-half_x, half_y),
517 ])
518 }
519 Profile::Contour(contour) => {
520 if !contour.holes.is_empty() {
521 return Err(unsupported("composite member carrying its own holes"));
522 }
523 let mut points: Vec<Point2> = Vec::with_capacity(contour.outer.segments.len());
524 for segment in &contour.outer.segments {
525 match &segment.curve {
526 Curve2::Line(line) => {
527 let t = if segment.same_sense {
528 segment.domain.start
529 } else {
530 segment.domain.end
531 };
532 points.push(line.origin + line.direction * t);
533 }
534 _ => return Err(unsupported("composite member with a curved segment")),
535 }
536 }
537 orient_counter_clockwise(&mut points);
538 Ok(points)
539 }
540 _ => Err(unsupported(
541 "composite member of an unsupported profile kind",
542 )),
543 }
544}