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 {
250 curve: Curve2::Circle(Circle2 {
251 frame: Frame2 {
252 origin: apply(transform, circle.frame.origin),
253 x: transform.matrix2 * circle.frame.x,
254 y: transform.matrix2 * circle.frame.y,
255 },
256 radius: circle.radius * scale,
257 }),
258 domain: segment.domain,
259 same_sense,
260 })
261 }
262 _ => Err(unsupported(
263 "derived contour over a segment kind that cannot be transformed exactly",
264 )),
265 }
266}
267
268fn polygon_contour(points: &[Point2]) -> Contour {
270 let count = points.len();
271 let segments = (0..count)
272 .map(|index| {
273 let from = points[index];
274 let to = points[(index + 1) % count];
275 ProfileSegment {
276 curve: Curve2::Line(Line2 {
277 origin: from,
278 direction: to - from,
279 }),
280 domain: Interval::UNIT,
281 same_sense: true,
282 }
283 })
284 .collect();
285 Contour::new(segments)
286}
287
288fn signed_area(points: &[Point2]) -> Scalar {
290 let count = points.len();
291 (0..count)
292 .map(|index| {
293 let a = points[index];
294 let b = points[(index + 1) % count];
295 a.perp_dot(b)
296 })
297 .sum::<Scalar>()
298 / 2.0
299}
300
301fn orient_counter_clockwise(points: &mut [Point2]) {
306 if signed_area(points) < 0.0 {
307 points.reverse();
308 }
309}
310
311pub fn lower_composite(
322 members: &[Profile],
323 tolerance: Tolerance,
324) -> GeomResult<(Vec<Point2>, Vec<Vec<Point2>>)> {
325 if members.is_empty() {
326 return Err(GeomError::InvalidInput(
327 "a composite profile needs at least one member".to_owned(),
328 ));
329 }
330
331 let mut rings = Vec::with_capacity(members.len());
332 for member in members {
333 rings.push(Ring {
334 points: member_ring(member, tolerance)?,
335 });
336 }
337
338 let polygons = union_soup(&rings, tolerance).map_err(|error| {
339 GeomError::InvalidInput(format!("composite member union failed: {error:?}"))
340 })?;
341
342 match polygons.len() {
343 0 => Err(GeomError::Degenerate(
344 "composite profile members union to nothing".to_owned(),
345 )),
346 1 => {
347 let polygon = &polygons[0];
348 Ok((
349 polygon.outer.points.clone(),
350 polygon.holes.iter().map(|h| h.points.clone()).collect(),
351 ))
352 }
353 _ => Err(unsupported("composite profile whose members are disjoint")),
356 }
357}
358
359#[derive(Debug, Clone, PartialEq)]
361pub struct CompositeRegion {
362 pub outer: ArcRing,
364 pub holes: Vec<ArcRing>,
366}
367
368pub fn composite_regions(
381 members: &[Profile],
382 tolerance: Tolerance,
383) -> GeomResult<Vec<CompositeRegion>> {
384 if members.is_empty() {
385 return Err(GeomError::InvalidInput(
386 "a composite profile needs at least one member".to_owned(),
387 ));
388 }
389 let mut rings = Vec::new();
390 let mut layout = Vec::with_capacity(members.len());
392 for member in members {
393 if matches!(member, Profile::Composite(_)) {
394 return Err(unsupported("composite profile nested in a composite"));
395 }
396 let contour = crate::extrude_exact::profile_to_contour(member, tolerance)?;
397 let outer = rings.len();
398 rings.push(crate::contour_lower::contour_to_arc_ring(
399 &contour.outer,
400 tolerance,
401 )?);
402 let mut holes = Vec::with_capacity(contour.holes.len());
403 for hole in &contour.holes {
404 holes.push(rings.len());
405 rings.push(crate::contour_lower::contour_to_arc_ring(hole, tolerance)?);
406 }
407 layout.push((outer, holes));
408 }
409 weld_ring_vertices(&mut rings, tolerance);
410 let arrangement = ArcArrangement::new(&rings, tolerance).map_err(|error| {
411 GeomError::InvalidInput(format!("composite member rings are invalid: {error:?}"))
412 })?;
413 let regions = arrangement
414 .regions(|inside| {
415 layout
416 .iter()
417 .any(|(outer, holes)| inside[*outer] && !holes.iter().any(|hole| inside[*hole]))
418 })
419 .map_err(|error| {
420 GeomError::InvalidInput(format!("composite member union failed: {error:?}"))
421 })?;
422 if regions.is_empty() {
423 return Err(GeomError::Degenerate(
424 "composite profile members union to nothing".to_owned(),
425 ));
426 }
427 Ok(regions
428 .iter()
429 .map(|region| CompositeRegion {
430 outer: without_zero_pieces(arrangement.ring(®ion.outer)),
431 holes: region
432 .holes
433 .iter()
434 .map(|hole| without_zero_pieces(arrangement.ring(hole)))
435 .collect(),
436 })
437 .collect())
438}
439
440fn without_zero_pieces(mut ring: ArcRing) -> ArcRing {
448 let mut index = 0;
449 while ring.vertices.len() > 1 && index < ring.vertices.len() {
450 let next = (index + 1) % ring.vertices.len();
451 if ring.vertices[index].point == ring.vertices[next].point {
452 ring.vertices.remove(index);
453 } else {
454 index += 1;
455 }
456 }
457 ring
458}
459
460fn weld_ring_vertices(rings: &mut [ArcRing], tolerance: Tolerance) {
469 let limit = tolerance.linear();
470 let mut seen: Vec<Point2> = Vec::new();
471 for ring in rings.iter_mut() {
472 for vertex in &mut ring.vertices {
473 match seen
474 .iter()
475 .find(|point| (**point - vertex.point).length() <= limit)
476 {
477 Some(point) => vertex.point = *point,
478 None => seen.push(vertex.point),
479 }
480 }
481 }
482}
483
484fn member_ring(member: &Profile, tolerance: Tolerance) -> GeomResult<Vec<Point2>> {
490 let lowered;
491 let resolved = match member {
492 Profile::Derived { basis, transform } => {
493 lowered = lower_derived(basis, transform, tolerance)?;
494 &lowered
495 }
496 other => other,
497 };
498
499 match resolved {
500 Profile::Rectangle(rectangle) => {
501 if rectangle.thickness.is_some()
502 || rectangle.outer_radius.is_some()
503 || rectangle.inner_radius.is_some()
504 {
505 return Err(unsupported(
506 "composite member with a hollow or rounded rectangle",
507 ));
508 }
509 let (half_x, half_y) = (rectangle.x / 2.0, rectangle.y / 2.0);
510 Ok(vec![
511 Point2::new(-half_x, -half_y),
512 Point2::new(half_x, -half_y),
513 Point2::new(half_x, half_y),
514 Point2::new(-half_x, half_y),
515 ])
516 }
517 Profile::Contour(contour) => {
518 if !contour.holes.is_empty() {
519 return Err(unsupported("composite member carrying its own holes"));
520 }
521 let mut points: Vec<Point2> = Vec::with_capacity(contour.outer.segments.len());
522 for segment in &contour.outer.segments {
523 match &segment.curve {
524 Curve2::Line(line) => {
525 let t = if segment.same_sense {
526 segment.domain.start
527 } else {
528 segment.domain.end
529 };
530 points.push(line.origin + line.direction * t);
531 }
532 _ => return Err(unsupported("composite member with a curved segment")),
533 }
534 }
535 orient_counter_clockwise(&mut points);
536 Ok(points)
537 }
538 _ => Err(unsupported(
539 "composite member of an unsupported profile kind",
540 )),
541 }
542}