1use std::f64::consts::TAU;
60
61use axiolid_brep::{EdgeName, ExactBRep, ExactBRepBuilder, FaceName, SweptFace, TransformError};
62use axiolid_contracts::{GeomError, GeomResult, Operation};
63use axiolid_core::{
64 Frame2, Frame3, Interval, Mat3, Point2, Point3, Scalar, Tolerance, Transform3, Vec2, Vec3,
65};
66use axiolid_curve::{Circle2, Circle3, Curve2, Curve3, Ellipse2, Ellipse3, Line2, Line3};
67use axiolid_profile::{CircleProfile, ContourProfile, EllipseProfile, Profile, RectangleProfile};
68use axiolid_surface::{Cylinder, EllipticalCylinder, Plane, Surface};
69use axiolid_topology::{
70 audit_brep, Edge, EdgeId, EdgeUse, Face, FaceBound, FaceId, Loop, LoopId, Orientation, Shell,
71 Solid, Vertex, VertexId,
72};
73
74use crate::center_line_exact::center_line_contour;
75use crate::contour_lower::{contour_to_arc_ring, orient_arc_ring};
76use crate::extrude_arc::{extrude_arc_rings, ObliqueWall};
77use crate::profile_lower::lower_derived;
78use crate::section_lower::{rectangle_contour, section_contour};
79use crate::BACKEND_ID;
80
81#[derive(Debug)]
82pub(crate) struct RingTopology {
83 bottom_edges: Vec<EdgeId>,
84 top_edges: Vec<EdgeId>,
85 vertical_edges: Vec<EdgeId>,
86 bottom_points: Vec<Point3>,
87}
88
89pub fn extrude_profile_exact(
103 profile: &Profile,
104 direction: Vec3,
105 depth: Scalar,
106 tolerance: Tolerance,
107) -> GeomResult<ExactBRep> {
108 let offset = extrusion_offset(direction, depth, tolerance)?;
109 if offset.z > 0.0 {
110 return extrude_forward(profile, offset, tolerance);
111 }
112 let forward = extrude_forward(profile, Vec3::new(offset.x, offset.y, -offset.z), tolerance)?;
115 mirror_in_profile_plane(&forward)
116}
117
118fn mirror_in_profile_plane(forward: &ExactBRep) -> GeomResult<ExactBRep> {
120 let mirror = Transform3::from_mat3(Mat3::from_diagonal(Vec3::new(1.0, 1.0, -1.0)));
121 forward.transformed(&mirror).map_err(|error| match error {
122 TransformError::Unsupported(what) => unsupported(what),
123 other => GeomError::InvalidInput(format!("exact extrusion mirror: {other}")),
124 })
125}
126
127fn extrude_forward(profile: &Profile, offset: Vec3, tolerance: Tolerance) -> GeomResult<ExactBRep> {
130 match profile {
131 Profile::Rectangle(rectangle) => extrude_rectangle(rectangle, offset, tolerance),
132 Profile::Circle(circle) => extrude_circle(circle, offset),
133 Profile::Ellipse(ellipse) => extrude_ellipse(ellipse, offset),
134 Profile::Section(section) => {
135 let contour = section_contour(section)?;
136 extrude_contour(&contour, offset, tolerance)
137 }
138 Profile::Contour(contour) => extrude_contour(contour, offset, tolerance),
139 Profile::Derived { basis, transform } => {
140 let lowered = lower_derived(basis, transform, tolerance)?;
144 extrude_forward(&lowered, offset, tolerance)
145 }
146 Profile::Composite(members) => extrude_composite(members, offset, tolerance),
147 Profile::CenterLine(center_line) => {
148 let contour = center_line_contour(center_line, tolerance)?;
151 extrude_contour(&contour, offset, tolerance)
152 }
153 _ => Err(unsupported("unknown profile extrusion")),
154 }
155}
156
157fn normalize_direction(direction: Vec3) -> Option<Vec3> {
158 if !direction.is_finite() {
159 return None;
160 }
161
162 let scale = direction.abs().max_element();
165 if scale == 0.0 {
166 return None;
167 }
168 let scaled = direction / scale;
169 Some(scaled / scaled.length())
170}
171
172const MAGNITUDE_BITS: u64 = 0x7fff_ffff_ffff_ffff;
174
175fn loses_nonzero_component(input: Vec3, output: Vec3) -> bool {
176 (input.x.to_bits() & MAGNITUDE_BITS != 0 && output.x.to_bits() & MAGNITUDE_BITS == 0)
177 || (input.y.to_bits() & MAGNITUDE_BITS != 0 && output.y.to_bits() & MAGNITUDE_BITS == 0)
178 || (input.z.to_bits() & MAGNITUDE_BITS != 0 && output.z.to_bits() & MAGNITUDE_BITS == 0)
179}
180
181fn extrusion_offset(direction: Vec3, depth: Scalar, tolerance: Tolerance) -> GeomResult<Vec3> {
182 if !depth.is_finite() || depth <= 0.0 {
183 return Err(GeomError::InvalidInput(format!(
184 "exact extrusion depth must be positive and finite, got {depth}"
185 )));
186 }
187 let normalized = normalize_direction(direction).ok_or_else(|| {
188 GeomError::InvalidInput("exact extrusion direction must be finite and non-zero".to_owned())
189 })?;
190 if loses_nonzero_component(direction, normalized) {
191 return Err(GeomError::Degenerate(
192 "exact extrusion direction cannot be normalized without losing a nonzero component"
193 .to_owned(),
194 ));
195 }
196 let offset = normalized * depth;
197 if !offset.is_finite() || loses_nonzero_component(normalized, offset) {
198 return Err(GeomError::Degenerate(
199 "exact extrusion direction could not be scaled without range loss".to_owned(),
200 ));
201 }
202 if offset.z.abs() <= tolerance.linear() {
205 return Err(unsupported("extrusion direction in the profile plane"));
206 }
207 Ok(offset)
208}
209
210fn extrude_rectangle(
211 rectangle: &RectangleProfile,
212 offset: Vec3,
213 tolerance: Tolerance,
214) -> GeomResult<ExactBRep> {
215 if rectangle.outer_radius.is_some() || rectangle.inner_radius.is_some() {
216 return extrude_contour(&rectangle_contour(rectangle)?, offset, tolerance);
220 }
221 if !rectangle.x.is_finite()
222 || !rectangle.y.is_finite()
223 || rectangle.x <= 0.0
224 || rectangle.y <= 0.0
225 {
226 return Err(GeomError::InvalidInput(format!(
227 "exact rectangle profile must have positive finite extents, got {} x {}",
228 rectangle.x, rectangle.y
229 )));
230 }
231
232 let (hx, hy) = (rectangle.x / 2.0, rectangle.y / 2.0);
233 let mut rings = vec![vec![
234 Point2::new(-hx, -hy),
235 Point2::new(hx, -hy),
236 Point2::new(hx, hy),
237 Point2::new(-hx, hy),
238 ]];
239 if let Some(thickness) = rectangle.thickness {
240 if !thickness.is_finite()
241 || thickness <= 0.0
242 || 2.0 * thickness >= rectangle.x
243 || 2.0 * thickness >= rectangle.y
244 {
245 return Err(GeomError::InvalidInput(format!(
246 "exact hollow rectangle wall thickness {thickness} does not fit inside {} x {}",
247 rectangle.x, rectangle.y
248 )));
249 }
250 let (ix, iy) = (hx - thickness, hy - thickness);
251 if tolerance.eq(ix, 0.0) || tolerance.eq(iy, 0.0) {
252 return Err(GeomError::Degenerate(
253 "exact hollow rectangle has a collapsed inner ring".to_owned(),
254 ));
255 }
256 rings.push(vec![
258 Point2::new(-ix, -iy),
259 Point2::new(-ix, iy),
260 Point2::new(ix, iy),
261 Point2::new(ix, -iy),
262 ]);
263 }
264
265 extrude_polygon_rings(&rings, offset)
266}
267
268pub(crate) fn extrude_polygon_rings(rings: &[Vec<Point2>], offset: Vec3) -> GeomResult<ExactBRep> {
275 extrude_polygon_rings_named(rings, offset, &mut |_| None)
276}
277
278pub(crate) fn extrude_polygon_rings_named(
285 rings: &[Vec<Point2>],
286 offset: Vec3,
287 name_side: &mut dyn FnMut((Point2, Point2)) -> Option<FaceName>,
288) -> GeomResult<ExactBRep> {
289 let ring_count = rings.len();
290 let edge_count = ring_count * 12;
291 let pcurve_count = ring_count * 24;
292 let face_count = 2 + ring_count * 4;
293 let mut builder = ExactBRepBuilder::default();
294 reserve(
295 &mut builder,
296 ring_count * 8,
297 edge_count,
298 2 * ring_count + 4 * ring_count,
299 face_count,
300 edge_count,
301 pcurve_count,
302 face_count,
303 )?;
304
305 let ring_topology = rings
306 .iter()
307 .map(|ring| add_polygon_ring(&mut builder, ring.as_slice(), offset))
308 .collect::<GeomResult<Vec<_>>>()?;
309
310 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
311 frame: identity_frame3(Vec3::ZERO),
312 }));
313 let top_surface = builder.add_surface(Surface::Plane(Plane {
314 frame: identity_frame3(offset),
315 }));
316
317 let mut bottom_bounds = Vec::with_capacity(ring_count);
318 let mut top_bounds = Vec::with_capacity(ring_count);
319 for (ring_index, ring) in ring_topology.iter().enumerate() {
320 let bottom_loop = add_cap_loop(&mut builder, &ring.bottom_edges, &ring.bottom_points);
321 let top_loop = add_cap_loop(&mut builder, &ring.top_edges, &ring.bottom_points);
322 bottom_bounds.push(FaceBound {
323 loop_id: bottom_loop,
324 orientation: Orientation::Forward,
325 outer: ring_index == 0,
326 });
327 top_bounds.push(FaceBound {
328 loop_id: top_loop,
329 orientation: Orientation::Forward,
330 outer: ring_index == 0,
331 });
332 }
333
334 let bottom_face = builder.topology_mut().add_face(Face {
335 surface: Some(bottom_surface),
336 bounds: bottom_bounds,
337 orientation: Orientation::Reversed,
338 });
339 let top_face = builder.topology_mut().add_face(Face {
340 surface: Some(top_surface),
341 bounds: top_bounds,
342 orientation: Orientation::Forward,
343 });
344
345 let mut faces = vec![bottom_face, top_face];
346 for (ring_index, ring) in ring_topology.iter().enumerate() {
347 let source = &rings[ring_index];
348 for edge_index in 0..ring.bottom_edges.len() {
349 let face = add_planar_side(&mut builder, ring, edge_index, offset)?;
350 let start = source[edge_index];
353 let end = source[(edge_index + 1) % source.len()];
354 if let Some(name) = name_side((start, end)) {
355 builder.set_face_name(face, name);
356 }
357 faces.push(face);
358 }
359 }
360 finish_closed(builder, faces)
361}
362
363fn add_polygon_ring(
364 builder: &mut ExactBRepBuilder,
365 ring: &[Point2],
366 offset: Vec3,
367) -> GeomResult<RingTopology> {
368 add_polygon_ring_with_blends(builder, ring, offset, &[])
369}
370
371pub(crate) fn add_polygon_ring_with_blends(
379 builder: &mut ExactBRepBuilder,
380 ring: &[Point2],
381 offset: Vec3,
382 blends: &[Option<(BlendCorner, Scalar)>],
383) -> GeomResult<RingTopology> {
384 let bottom_points: Vec<_> = ring
385 .iter()
386 .map(|point| Point3::new(point.x, point.y, 0.0))
387 .collect();
388 let top_points: Vec<_> = bottom_points.iter().map(|point| *point + offset).collect();
389 let bottom_vertices: Vec<_> = bottom_points
390 .iter()
391 .map(|&position| builder.topology_mut().add_vertex(Vertex { position }))
392 .collect();
393 let top_vertices: Vec<_> = top_points
394 .iter()
395 .map(|&position| builder.topology_mut().add_vertex(Vertex { position }))
396 .collect();
397
398 let mut bottom_edges = Vec::with_capacity(ring.len());
399 let mut top_edges = Vec::with_capacity(ring.len());
400 let mut vertical_edges = Vec::with_capacity(ring.len());
401 for index in 0..ring.len() {
402 let next = (index + 1) % ring.len();
403 if let Some(Some((blend, radius))) = blends.get(index) {
405 let (bottom_edge, top_edge) = add_blend_arc_edges(
406 builder,
407 blend,
408 *radius,
409 (bottom_vertices[index], bottom_vertices[next]),
410 (top_vertices[index], top_vertices[next]),
411 offset,
412 );
413 bottom_edges.push(bottom_edge);
414 top_edges.push(top_edge);
415 } else {
416 bottom_edges.push(add_line_edge(
417 builder,
418 bottom_vertices[index],
419 bottom_vertices[next],
420 bottom_points[index],
421 bottom_points[next] - bottom_points[index],
422 ));
423 top_edges.push(add_line_edge(
424 builder,
425 top_vertices[index],
426 top_vertices[next],
427 top_points[index],
428 top_points[next] - top_points[index],
429 ));
430 }
431 vertical_edges.push(add_line_edge(
432 builder,
433 bottom_vertices[index],
434 top_vertices[index],
435 bottom_points[index],
436 offset,
437 ));
438 }
439 Ok(RingTopology {
440 bottom_edges,
441 top_edges,
442 vertical_edges,
443 bottom_points,
444 })
445}
446
447pub(crate) fn add_line_edge(
448 builder: &mut ExactBRepBuilder,
449 start: VertexId,
450 end: VertexId,
451 origin: Point3,
452 direction: Vec3,
453) -> EdgeId {
454 let curve = builder.add_curve3(Curve3::Line(Line3 { origin, direction }));
455 let edge = builder.topology_mut().add_edge(Edge {
456 start,
457 end,
458 curve: Some(curve),
459 });
460 builder.set_edge_interval(edge, Interval::UNIT);
461 edge
462}
463
464fn add_cap_loop(builder: &mut ExactBRepBuilder, edges: &[EdgeId], points: &[Point3]) -> LoopId {
465 add_cap_loop_with_blends(builder, edges, points, &[])
466}
467
468fn add_cap_loop_with_blends(
477 builder: &mut ExactBRepBuilder,
478 edges: &[EdgeId],
479 points: &[Point3],
480 blends: &[Option<(BlendCorner, Scalar)>],
481) -> LoopId {
482 let mut uses = Vec::with_capacity(edges.len());
483 let mut intervals = Vec::with_capacity(edges.len());
484 for (index, &edge) in edges.iter().enumerate() {
485 let next = (index + 1) % edges.len();
486 let (pcurve, interval) = match blends.get(index) {
487 Some(Some((blend, radius))) => {
488 let start = blend.start - blend.centre;
489 let length = start.length();
490 let x = if length == 0.0 {
491 Vec2::X
492 } else {
493 start / length
494 };
495 let curve = Curve2::Circle(Circle2 {
496 frame: Frame2 {
497 origin: Vec2::new(blend.centre.x, blend.centre.y),
498 x,
499 y: Vec2::new(-x.y, x.x),
500 },
501 radius: *radius,
502 });
503 (curve, Interval::new(0.0, blend.sweep))
504 }
505 _ => {
506 let origin = points[index].truncate();
507 let direction = (points[next] - points[index]).truncate();
508 (Curve2::Line(Line2 { origin, direction }), Interval::UNIT)
509 }
510 };
511 let pcurve = builder.add_curve2(pcurve);
512 uses.push(EdgeUse {
513 edge,
514 orientation: Orientation::Forward,
515 pcurve: Some(pcurve),
516 });
517 intervals.push(interval);
518 }
519 add_loop(builder, uses, intervals)
520}
521
522fn add_planar_side(
523 builder: &mut ExactBRepBuilder,
524 ring: &RingTopology,
525 index: usize,
526 offset: Vec3,
527) -> GeomResult<FaceId> {
528 let next = (index + 1) % ring.bottom_edges.len();
529 let origin = ring.bottom_points[index];
530 let edge_vector = ring.bottom_points[next] - origin;
531 let x = edge_vector.normalize();
532 let z = edge_vector.cross(offset).normalize();
533 let y = z.cross(x);
534 if !(x.is_finite() && y.is_finite() && z.is_finite()) {
535 return Err(GeomError::Degenerate(
536 "exact rectangle extrusion produced a degenerate side plane".to_owned(),
537 ));
538 }
539 let surface = builder.add_surface(Surface::Plane(Plane {
540 frame: Frame3 { origin, x, y, z },
541 }));
542 let edge_length = edge_vector.length();
543 let offset_uv = Vec2::new(offset.dot(x), offset.dot(y));
544 let pcurves = [
545 Curve2::Line(Line2 {
546 origin: Vec2::ZERO,
547 direction: Vec2::new(edge_length, 0.0),
548 }),
549 Curve2::Line(Line2 {
550 origin: Vec2::new(edge_length, 0.0),
551 direction: offset_uv,
552 }),
553 Curve2::Line(Line2 {
554 origin: offset_uv,
555 direction: Vec2::new(edge_length, 0.0),
556 }),
557 Curve2::Line(Line2 {
558 origin: Vec2::ZERO,
559 direction: offset_uv,
560 }),
561 ];
562 let edge_uses = [
563 (ring.bottom_edges[index], Orientation::Forward),
564 (ring.vertical_edges[next], Orientation::Forward),
565 (ring.top_edges[index], Orientation::Reversed),
566 (ring.vertical_edges[index], Orientation::Reversed),
567 ];
568 let mut uses = Vec::with_capacity(4);
569 let mut intervals = Vec::with_capacity(4);
570 for ((edge, orientation), pcurve) in edge_uses.into_iter().zip(pcurves) {
571 let pcurve = builder.add_curve2(pcurve);
572 uses.push(EdgeUse {
573 edge,
574 orientation,
575 pcurve: Some(pcurve),
576 });
577 intervals.push(match orientation {
578 Orientation::Forward => Interval::UNIT,
579 Orientation::Reversed => Interval::new(1.0, 0.0),
580 });
581 }
582 let loop_id = add_loop(builder, uses, intervals);
583 Ok(builder.topology_mut().add_face(Face {
584 surface: Some(surface),
585 bounds: vec![FaceBound {
586 loop_id,
587 orientation: Orientation::Forward,
588 outer: true,
589 }],
590 orientation: Orientation::Forward,
591 }))
592}
593
594fn extrude_circle(circle: &CircleProfile, offset: Vec3) -> GeomResult<ExactBRep> {
595 if circle.thickness.is_some() {
596 return Err(unsupported("annular circle extrusion"));
597 }
598 if !circle.radius.is_finite() || circle.radius <= 0.0 {
599 return Err(GeomError::InvalidInput(format!(
600 "exact circle profile radius must be positive and finite, got {}",
601 circle.radius
602 )));
603 }
604 let oblique = if offset.x != 0.0 || offset.y != 0.0 {
608 Some(ObliqueWall::new(Point3::ZERO, circle.radius, offset)?)
609 } else {
610 None
611 };
612 let (x, y) = oblique.map_or((Vec3::X, Vec3::Y), |wall| (wall.across(), wall.along()));
613
614 let depth = offset.z;
615 let rim_frame = |origin: Point3| Frame3 {
616 origin,
617 x,
618 y,
619 z: Vec3::Z,
620 };
621 let frame_bottom = rim_frame(Vec3::ZERO);
622 let frame_top = rim_frame(offset);
623 let frame2 = Frame2 {
624 origin: Vec2::ZERO,
625 x: x.truncate(),
626 y: y.truncate(),
627 };
628 let bottom_point = x * circle.radius;
629 let top_point = bottom_point + offset;
630
631 let mut builder = ExactBRepBuilder::default();
632 reserve(&mut builder, 2, 3, 3, 3, 3, 8, 3)?;
633 let bottom_vertex = builder.topology_mut().add_vertex(Vertex {
634 position: bottom_point,
635 });
636 let top_vertex = builder.topology_mut().add_vertex(Vertex {
637 position: top_point,
638 });
639
640 let bottom_curve = builder.add_curve3(Curve3::Circle(Circle3 {
641 frame: frame_bottom,
642 radius: circle.radius,
643 }));
644 let bottom_edge = builder.topology_mut().add_edge(Edge {
645 start: bottom_vertex,
646 end: bottom_vertex,
647 curve: Some(bottom_curve),
648 });
649 builder.set_edge_interval(bottom_edge, Interval::new(0.0, TAU));
650
651 let top_curve = builder.add_curve3(Curve3::Circle(Circle3 {
652 frame: frame_top,
653 radius: circle.radius,
654 }));
655 let top_edge = builder.topology_mut().add_edge(Edge {
656 start: top_vertex,
657 end: top_vertex,
658 curve: Some(top_curve),
659 });
660 builder.set_edge_interval(top_edge, Interval::new(0.0, TAU));
661
662 let seam_curve = builder.add_curve3(Curve3::Line(Line3 {
663 origin: bottom_point,
664 direction: offset,
665 }));
666 let seam_edge = builder.topology_mut().add_edge(Edge {
667 start: bottom_vertex,
668 end: top_vertex,
669 curve: Some(seam_curve),
670 });
671 builder.set_edge_interval(seam_edge, Interval::UNIT);
672
673 let bottom_loop = add_circle_cap_loop(&mut builder, bottom_edge, frame2, circle.radius);
674 let top_loop = add_circle_cap_loop(&mut builder, top_edge, frame2, circle.radius);
675
676 let (side_curves, side_edges) = if let Some(wall) = oblique {
677 let (bottom_rim, _) = wall.rim(0.0, 0.0, TAU);
680 let (top_rim, _) = wall.rim(1.0, 0.0, TAU);
681 let (seam_end, _) = wall.ruling(TAU);
682 let (seam_start, _) = wall.ruling(0.0);
683 (
684 [bottom_rim, seam_end, top_rim, seam_start],
685 [
686 (bottom_edge, Orientation::Forward, Interval::new(0.0, TAU)),
687 (seam_edge, Orientation::Forward, Interval::UNIT),
688 (top_edge, Orientation::Reversed, Interval::new(TAU, 0.0)),
689 (seam_edge, Orientation::Reversed, Interval::new(1.0, 0.0)),
690 ],
691 )
692 } else {
693 (
694 [
695 Curve2::Line(Line2 {
696 origin: Vec2::ZERO,
697 direction: Vec2::X,
698 }),
699 Curve2::Line(Line2 {
700 origin: Vec2::new(TAU, 0.0),
701 direction: Vec2::new(0.0, depth),
702 }),
703 Curve2::Line(Line2 {
704 origin: Vec2::new(0.0, depth),
705 direction: Vec2::X,
706 }),
707 Curve2::Line(Line2 {
708 origin: Vec2::ZERO,
709 direction: Vec2::new(0.0, depth),
710 }),
711 ],
712 [
713 (bottom_edge, Orientation::Forward, Interval::new(0.0, TAU)),
714 (seam_edge, Orientation::Forward, Interval::UNIT),
715 (top_edge, Orientation::Reversed, Interval::new(TAU, 0.0)),
716 (seam_edge, Orientation::Reversed, Interval::new(1.0, 0.0)),
717 ],
718 )
719 };
720 let mut side_uses = Vec::with_capacity(4);
721 let mut side_intervals = Vec::with_capacity(4);
722 for ((edge, orientation, interval), curve) in side_edges.into_iter().zip(side_curves) {
723 let pcurve = builder.add_curve2(curve);
724 side_uses.push(EdgeUse {
725 edge,
726 orientation,
727 pcurve: Some(pcurve),
728 });
729 side_intervals.push(interval);
730 }
731 let side_loop = add_loop(&mut builder, side_uses, side_intervals);
732
733 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
736 frame: identity_frame3(Vec3::ZERO),
737 }));
738 let top_surface = builder.add_surface(Surface::Plane(Plane {
739 frame: identity_frame3(offset),
740 }));
741 let side_surface = builder.add_surface(match oblique {
742 Some(wall) => Surface::EllipticalCylinder(wall.surface),
743 None => Surface::Cylinder(Cylinder {
744 frame: frame_bottom,
745 radius: circle.radius,
746 }),
747 });
748 let bottom_face = add_single_bound_face(
749 &mut builder,
750 bottom_surface,
751 bottom_loop,
752 Orientation::Reversed,
753 );
754 let top_face = add_single_bound_face(&mut builder, top_surface, top_loop, Orientation::Forward);
755 let side_face =
756 add_single_bound_face(&mut builder, side_surface, side_loop, Orientation::Forward);
757 finish_closed(builder, vec![bottom_face, top_face, side_face])
758}
759
760fn add_circle_cap_loop(
761 builder: &mut ExactBRepBuilder,
762 edge: EdgeId,
763 frame: Frame2,
764 radius: Scalar,
765) -> LoopId {
766 let pcurve = builder.add_curve2(Curve2::Circle(Circle2 { frame, radius }));
767 add_loop(
768 builder,
769 vec![EdgeUse {
770 edge,
771 orientation: Orientation::Forward,
772 pcurve: Some(pcurve),
773 }],
774 vec![Interval::new(0.0, TAU)],
775 )
776}
777
778pub(crate) fn add_single_bound_face(
779 builder: &mut ExactBRepBuilder,
780 surface: axiolid_brep::SurfaceId,
781 loop_id: LoopId,
782 orientation: Orientation,
783) -> FaceId {
784 builder.topology_mut().add_face(Face {
785 surface: Some(surface),
786 bounds: vec![FaceBound {
787 loop_id,
788 orientation: Orientation::Forward,
789 outer: true,
790 }],
791 orientation,
792 })
793}
794
795pub(crate) fn add_loop(
796 builder: &mut ExactBRepBuilder,
797 uses: Vec<EdgeUse<axiolid_brep::Curve2Id>>,
798 intervals: Vec<Interval>,
799) -> LoopId {
800 debug_assert_eq!(uses.len(), intervals.len());
801 let loop_id = builder.topology_mut().add_loop(Loop { edges: uses });
802 for (use_index, interval) in intervals.into_iter().enumerate() {
803 builder.set_pcurve_interval(loop_id, use_index, interval);
804 }
805 loop_id
806}
807
808pub(crate) fn finish_closed(
809 mut builder: ExactBRepBuilder,
810 faces: Vec<FaceId>,
811) -> GeomResult<ExactBRep> {
812 let shell = builder.topology_mut().add_shell(Shell {
813 faces: faces
814 .into_iter()
815 .map(|face| (face, Orientation::Forward))
816 .collect(),
817 closed: true,
818 });
819 builder.topology_mut().add_solid(Solid {
820 outer: shell,
821 voids: Vec::new(),
822 });
823 let exact = builder
824 .finish()
825 .map_err(|error| GeomError::BackendContractViolation {
826 backend: BACKEND_ID,
827 detail: format!("exact extrusion assembly failed: {error}"),
828 })?;
829 let health = audit_brep(exact.topology());
830 if !health.is_closed_manifold() {
831 return Err(GeomError::BackendContractViolation {
832 backend: BACKEND_ID,
833 detail: format!("exact extrusion is not a closed manifold: {health:?}"),
834 });
835 }
836 Ok(exact)
837}
838
839#[allow(clippy::too_many_arguments)]
840pub(crate) fn reserve(
841 builder: &mut ExactBRepBuilder,
842 vertices: usize,
843 edges: usize,
844 loops: usize,
845 faces: usize,
846 curves3: usize,
847 curves2: usize,
848 surfaces: usize,
849) -> GeomResult<()> {
850 builder
851 .topology_mut()
852 .try_reserve(vertices, edges, loops, faces, 1, 1)
853 .map_err(|_| GeomError::BudgetExceeded { resource: "memory" })?;
854 builder
855 .try_reserve(curves3, curves2, surfaces, edges, curves2)
856 .map_err(|_| GeomError::BudgetExceeded { resource: "memory" })
857}
858
859pub(crate) fn identity_frame3(origin: Point3) -> Frame3 {
860 Frame3 {
861 origin,
862 x: Vec3::X,
863 y: Vec3::Y,
864 z: Vec3::Z,
865 }
866}
867
868fn unsupported(input: &'static str) -> GeomError {
869 GeomError::UnsupportedInput {
870 backend: BACKEND_ID,
871 operation: Operation::Sweep,
872 input,
873 }
874}
875
876pub(crate) use crate::feature::BlendCorner;
884
885pub(crate) fn extrude_with_cylindrical_blend(
886 ring: &[Point2],
887 offset: Vec3,
888 blend_index: usize,
889 blend: &BlendCorner,
890 radius: Scalar,
891) -> GeomResult<ExactBRep> {
892 let mut blends: Vec<Option<(BlendCorner, Scalar)>> = (0..ring.len()).map(|_| None).collect();
893 blends[blend_index] = Some((
894 BlendCorner {
895 centre: blend.centre,
896 start: blend.start,
897 end: blend.end,
898 sweep: blend.sweep,
899 },
900 radius,
901 ));
902 extrude_with_cylindrical_blends(ring, offset, &blends)
903}
904
905pub(crate) fn extrude_with_cylindrical_blends(
911 ring: &[Point2],
912 offset: Vec3,
913 blends: &[Option<(BlendCorner, Scalar)>],
914) -> GeomResult<ExactBRep> {
915 let mut builder = ExactBRepBuilder::default();
916 let n = ring.len();
917 reserve(
918 &mut builder,
919 n * 2,
920 n * 3,
921 2 + n,
922 2 + n,
923 n * 3,
924 n * 6,
925 2 + n,
926 )?;
927
928 let topology = add_polygon_ring_with_blends(&mut builder, ring, offset, blends)?;
930
931 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
932 frame: identity_frame3(Vec3::ZERO),
933 }));
934 let top_surface = builder.add_surface(Surface::Plane(Plane {
935 frame: identity_frame3(offset),
936 }));
937 let bottom_loop = add_cap_loop_with_blends(
938 &mut builder,
939 &topology.bottom_edges,
940 &topology.bottom_points,
941 blends,
942 );
943 let top_loop = add_cap_loop_with_blends(
944 &mut builder,
945 &topology.top_edges,
946 &topology.bottom_points,
947 blends,
948 );
949
950 let bottom_face = builder.topology_mut().add_face(Face {
951 surface: Some(bottom_surface),
952 bounds: vec![FaceBound {
953 loop_id: bottom_loop,
954 orientation: Orientation::Forward,
955 outer: true,
956 }],
957 orientation: Orientation::Reversed,
958 });
959 let top_face = builder.topology_mut().add_face(Face {
960 surface: Some(top_surface),
961 bounds: vec![FaceBound {
962 loop_id: top_loop,
963 orientation: Orientation::Forward,
964 outer: true,
965 }],
966 orientation: Orientation::Forward,
967 });
968 builder.set_face_name(bottom_face, FaceName::swept(SweptFace::StartCap));
969 builder.set_face_name(top_face, FaceName::swept(SweptFace::EndCap));
970
971 let mut faces = vec![bottom_face, top_face];
972 let side_count = topology.bottom_edges.len();
973 for edge_index in 0..side_count {
974 let ordinal = u32::try_from(edge_index).map_err(|_| {
978 GeomError::Degenerate("profile edge count exceeds u32 capacity".to_owned())
979 })?;
980 if let Some((blend, radius)) = blends.get(edge_index).and_then(Option::as_ref) {
981 let face =
982 add_cylindrical_blend(&mut builder, &topology, edge_index, offset, blend, *radius)?;
983 let previous = (edge_index + side_count - 1) % side_count;
988 let previous_ordinal = u32::try_from(previous).map_err(|_| {
989 GeomError::Degenerate("profile edge count exceeds u32 capacity".to_owned())
990 })?;
991 builder.set_face_name(
992 face,
993 FaceName::blend(EdgeName::between(
994 FaceName::swept(SweptFace::Side(previous_ordinal)),
995 FaceName::swept(SweptFace::Side(ordinal)),
996 )),
997 );
998 faces.push(face);
999 } else {
1000 let face = add_planar_side(&mut builder, &topology, edge_index, offset)?;
1001 builder.set_face_name(face, FaceName::swept(SweptFace::Side(ordinal)));
1002 faces.push(face);
1003 }
1004 }
1005 finish_closed(builder, faces)
1006}
1007
1008fn add_cylindrical_blend(
1018 builder: &mut ExactBRepBuilder,
1019 ring: &RingTopology,
1020 index: usize,
1021 offset: Vec3,
1022 blend: &BlendCorner,
1023 radius: Scalar,
1024) -> GeomResult<FaceId> {
1025 let next = (index + 1) % ring.bottom_edges.len();
1026 let centre = Point3::new(blend.centre.x, blend.centre.y, 0.0);
1027 let start = ring.bottom_points[index];
1030 let x = (start - centre).normalize();
1031 let z = offset.normalize();
1032 let y = z.cross(x);
1033 if !(x.is_finite() && y.is_finite() && z.is_finite()) {
1034 return Err(GeomError::Degenerate(
1035 "fillet blend produced a degenerate cylinder frame".to_owned(),
1036 ));
1037 }
1038 let surface = builder.add_surface(Surface::Cylinder(Cylinder {
1039 frame: Frame3 {
1040 origin: centre,
1041 x,
1042 y,
1043 z,
1044 },
1045 radius,
1046 }));
1047
1048 let height = offset.length();
1049 let pcurves = [
1050 Curve2::Line(Line2 {
1052 origin: Vec2::ZERO,
1053 direction: Vec2::new(blend.sweep, 0.0),
1054 }),
1055 Curve2::Line(Line2 {
1056 origin: Vec2::new(blend.sweep, 0.0),
1057 direction: Vec2::new(0.0, height),
1058 }),
1059 Curve2::Line(Line2 {
1060 origin: Vec2::new(0.0, height),
1061 direction: Vec2::new(blend.sweep, 0.0),
1062 }),
1063 Curve2::Line(Line2 {
1064 origin: Vec2::ZERO,
1065 direction: Vec2::new(0.0, height),
1066 }),
1067 ];
1068 let edge_uses = [
1069 (ring.bottom_edges[index], Orientation::Forward),
1070 (ring.vertical_edges[next], Orientation::Forward),
1071 (ring.top_edges[index], Orientation::Reversed),
1072 (ring.vertical_edges[index], Orientation::Reversed),
1073 ];
1074
1075 let mut uses = Vec::with_capacity(4);
1076 let mut intervals = Vec::with_capacity(4);
1077 for ((edge, orientation), pcurve) in edge_uses.into_iter().zip(pcurves) {
1078 let pcurve = builder.add_curve2(pcurve);
1079 uses.push(EdgeUse {
1080 edge,
1081 orientation,
1082 pcurve: Some(pcurve),
1083 });
1084 intervals.push(match orientation {
1085 Orientation::Forward => Interval::UNIT,
1086 Orientation::Reversed => Interval::new(1.0, 0.0),
1087 });
1088 }
1089 let loop_id = add_loop(builder, uses, intervals);
1090 Ok(builder.topology_mut().add_face(Face {
1091 surface: Some(surface),
1092 bounds: vec![FaceBound {
1093 loop_id,
1094 orientation: Orientation::Forward,
1095 outer: true,
1096 }],
1097 orientation: Orientation::Forward,
1098 }))
1099}
1100
1101fn add_blend_arc_edges(
1107 builder: &mut ExactBRepBuilder,
1108 blend: &BlendCorner,
1109 radius: Scalar,
1110 bottom: (VertexId, VertexId),
1111 top: (VertexId, VertexId),
1112 offset: Vec3,
1113) -> (EdgeId, EdgeId) {
1114 let centre = Point3::new(blend.centre.x, blend.centre.y, 0.0);
1115 let start = Point3::new(blend.start.x, blend.start.y, 0.0);
1116 let x = (start - centre).normalize_or_zero();
1117 let z = offset.normalize_or_zero();
1118 let y = z.cross(x);
1119
1120 let mut arc_edge = |origin: Point3, ends: (VertexId, VertexId)| {
1121 let curve = builder.add_curve3(Curve3::Circle(Circle3 {
1122 frame: Frame3 { origin, x, y, z },
1123 radius,
1124 }));
1125 let edge = builder.topology_mut().add_edge(Edge {
1126 start: ends.0,
1127 end: ends.1,
1128 curve: Some(curve),
1129 });
1130 builder.set_edge_interval(edge, Interval::new(0.0, blend.sweep));
1133 edge
1134 };
1135
1136 let bottom_edge = arc_edge(centre, bottom);
1137 let top_edge = arc_edge(centre + offset, top);
1138 (bottom_edge, top_edge)
1139}
1140
1141fn extrude_contour(
1152 contour: &ContourProfile,
1153 offset: Vec3,
1154 tolerance: Tolerance,
1155) -> GeomResult<ExactBRep> {
1156 let mut lowered = Vec::with_capacity(1 + contour.holes.len());
1161 lowered.push(contour_to_arc_ring(&contour.outer, tolerance)?);
1162 for hole in &contour.holes {
1163 lowered.push(contour_to_arc_ring(hole, tolerance)?);
1164 }
1165 for ring in lowered.iter().filter(|ring| is_flat(ring)) {
1168 orient_arc_ring(ring, true)?;
1169 }
1170 refuse_invalid_region(&lowered[0], &lowered[1..])?;
1173 let mut rings = Vec::with_capacity(lowered.len());
1174 for (index, ring) in lowered.iter().enumerate() {
1175 rings.push(orient_arc_ring(ring, index == 0)?);
1176 }
1177
1178 if rings
1182 .iter()
1183 .all(|ring| ring.vertices.iter().all(|vertex| vertex.bulge == 0.0))
1184 {
1185 let polygons: Vec<Vec<Point2>> = rings
1186 .iter()
1187 .map(|ring| ring.vertices.iter().map(|vertex| vertex.point).collect())
1188 .collect();
1189 return extrude_polygon_rings(&polygons, offset);
1190 }
1191 extrude_arc_rings(&rings, offset)
1192}
1193
1194fn is_flat(ring: &axiolid_overlay::ArcRing) -> bool {
1196 if ring.vertices.iter().any(|vertex| vertex.bulge != 0.0) {
1197 return false;
1198 }
1199 let points: Vec<Point2> = ring.vertices.iter().map(|vertex| vertex.point).collect();
1200 let Some(second) = points.iter().find(|point| **point != points[0]) else {
1201 return true;
1202 };
1203 let direction = *second - points[0];
1204 points
1205 .iter()
1206 .all(|point| direction.x * (point.y - points[0].y) == direction.y * (point.x - points[0].x))
1207}
1208
1209fn refuse_invalid_region(
1212 outer: &axiolid_overlay::ArcRing,
1213 holes: &[axiolid_overlay::ArcRing],
1214) -> GeomResult<()> {
1215 let polygon = axiolid_overlay::ArcPolygon {
1216 outer: outer.clone(),
1217 holes: holes.to_vec(),
1218 };
1219 let check = axiolid_overlay::check_arc_polygon(&polygon, Tolerance::ZERO);
1220 refusal_of(&check.defects)
1221}
1222
1223fn refusal_of(defects: &[axiolid_overlay::ArcPolygonDefect]) -> GeomResult<()> {
1229 use axiolid_overlay::ArcPolygonDefect as Defect;
1230 let definite: Vec<&Defect> = defects
1231 .iter()
1232 .filter(|defect| {
1233 matches!(
1234 defect,
1235 Defect::SelfContact { .. }
1236 | Defect::BoundaryOverlap { .. }
1237 | Defect::HoleOutsideOuter { .. }
1238 | Defect::HolesOverlap { .. }
1239 | Defect::DisconnectedInterior { .. }
1240 )
1241 })
1242 .collect();
1243 if !definite.is_empty() {
1244 return Err(GeomError::InvalidInput(format!(
1245 "exact extrusion profile is not a valid region: {definite:?}"
1246 )));
1247 }
1248 if defects
1249 .iter()
1250 .any(|defect| !matches!(defect, Defect::Ring { .. }))
1251 {
1252 return Err(unsupported(
1253 "exact extrusion profile with a contact undecided at the tolerance",
1254 ));
1255 }
1256 Ok(())
1257}
1258
1259fn extrude_composite(
1263 members: &[Profile],
1264 offset: Vec3,
1265 tolerance: Tolerance,
1266) -> GeomResult<ExactBRep> {
1267 for member in members {
1270 if let Ok(contour) = profile_to_contour(member, tolerance) {
1271 let mut lowered = vec![contour_to_arc_ring(&contour.outer, tolerance)?];
1272 for hole in &contour.holes {
1273 lowered.push(contour_to_arc_ring(hole, tolerance)?);
1274 }
1275 refuse_invalid_region(&lowered[0], &lowered[1..])?;
1276 }
1277 }
1278 let regions = crate::profile_lower::composite_regions(members, tolerance)?;
1279 let mut pieces = Vec::with_capacity(regions.len());
1280 for region in regions {
1281 refuse_invalid_region(®ion.outer, ®ion.holes)?;
1283 let mut rings = Vec::with_capacity(1 + region.holes.len());
1284 rings.push(orient_arc_ring(®ion.outer, true)?);
1285 for hole in ®ion.holes {
1286 rings.push(orient_arc_ring(hole, false)?);
1287 }
1288 if rings
1290 .iter()
1291 .all(|ring| ring.vertices.iter().all(|vertex| vertex.bulge == 0.0))
1292 {
1293 let polygons: Vec<Vec<Point2>> = rings
1294 .iter()
1295 .map(|ring| ring.vertices.iter().map(|vertex| vertex.point).collect())
1296 .collect();
1297 pieces.push(extrude_polygon_rings(&polygons, offset)?);
1298 } else {
1299 pieces.push(extrude_arc_rings(&rings, offset)?);
1300 }
1301 }
1302 crate::assemble::merge_solids(pieces)
1303}
1304
1305fn extrude_ellipse(ellipse: &EllipseProfile, offset: Vec3) -> GeomResult<ExactBRep> {
1313 let (a, b) = (ellipse.semi_axis_x, ellipse.semi_axis_y);
1314 if !a.is_finite() || !b.is_finite() || a <= 0.0 || b <= 0.0 {
1315 return Err(GeomError::InvalidInput(format!(
1316 "exact ellipse profile semi-axes must be positive and finite, got {a} x {b}"
1317 )));
1318 }
1319 if offset.x != 0.0 || offset.y != 0.0 {
1320 return Err(unsupported("oblique ellipse extrusion"));
1321 }
1322
1323 let frame_bottom = identity_frame3(Vec3::ZERO);
1324 let frame_top = identity_frame3(offset);
1325 let frame2 = Frame2 {
1326 origin: Vec2::ZERO,
1327 x: Vec2::X,
1328 y: Vec2::Y,
1329 };
1330 let bottom_point = Vec3::new(a, 0.0, 0.0);
1332 let top_point = bottom_point + offset;
1333
1334 let mut builder = ExactBRepBuilder::default();
1335 reserve(&mut builder, 2, 3, 3, 3, 3, 8, 3)?;
1336 let bottom_vertex = builder.topology_mut().add_vertex(Vertex {
1337 position: bottom_point,
1338 });
1339 let top_vertex = builder.topology_mut().add_vertex(Vertex {
1340 position: top_point,
1341 });
1342
1343 let bottom_curve = builder.add_curve3(Curve3::Ellipse(Ellipse3 {
1344 frame: frame_bottom,
1345 semi_axis_x: a,
1346 semi_axis_y: b,
1347 }));
1348 let bottom_edge = builder.topology_mut().add_edge(Edge {
1349 start: bottom_vertex,
1350 end: bottom_vertex,
1351 curve: Some(bottom_curve),
1352 });
1353 builder.set_edge_interval(bottom_edge, Interval::new(0.0, TAU));
1354
1355 let top_curve = builder.add_curve3(Curve3::Ellipse(Ellipse3 {
1356 frame: frame_top,
1357 semi_axis_x: a,
1358 semi_axis_y: b,
1359 }));
1360 let top_edge = builder.topology_mut().add_edge(Edge {
1361 start: top_vertex,
1362 end: top_vertex,
1363 curve: Some(top_curve),
1364 });
1365 builder.set_edge_interval(top_edge, Interval::new(0.0, TAU));
1366
1367 let seam_curve = builder.add_curve3(Curve3::Line(Line3 {
1368 origin: bottom_point,
1369 direction: offset,
1370 }));
1371 let seam_edge = builder.topology_mut().add_edge(Edge {
1372 start: bottom_vertex,
1373 end: top_vertex,
1374 curve: Some(seam_curve),
1375 });
1376 builder.set_edge_interval(seam_edge, Interval::UNIT);
1377
1378 let bottom_loop = add_ellipse_cap_loop(&mut builder, bottom_edge, frame2, a, b);
1379 let top_loop = add_ellipse_cap_loop(&mut builder, top_edge, frame2, a, b);
1380
1381 let depth = offset.z;
1385 let side_curves = [
1386 Curve2::Line(Line2 {
1387 origin: Vec2::ZERO,
1388 direction: Vec2::X,
1389 }),
1390 Curve2::Line(Line2 {
1391 origin: Vec2::new(TAU, 0.0),
1392 direction: Vec2::new(0.0, depth),
1393 }),
1394 Curve2::Line(Line2 {
1395 origin: Vec2::new(0.0, depth),
1396 direction: Vec2::X,
1397 }),
1398 Curve2::Line(Line2 {
1399 origin: Vec2::ZERO,
1400 direction: Vec2::new(0.0, depth),
1401 }),
1402 ];
1403 let side_edges = [
1404 (bottom_edge, Orientation::Forward, Interval::new(0.0, TAU)),
1405 (seam_edge, Orientation::Forward, Interval::UNIT),
1406 (top_edge, Orientation::Reversed, Interval::new(TAU, 0.0)),
1407 (seam_edge, Orientation::Reversed, Interval::new(1.0, 0.0)),
1408 ];
1409 let mut side_uses = Vec::with_capacity(4);
1410 let mut side_intervals = Vec::with_capacity(4);
1411 for ((edge, orientation, interval), curve) in side_edges.into_iter().zip(side_curves) {
1412 let pcurve = builder.add_curve2(curve);
1413 side_uses.push(EdgeUse {
1414 edge,
1415 orientation,
1416 pcurve: Some(pcurve),
1417 });
1418 side_intervals.push(interval);
1419 }
1420 let side_loop = add_loop(&mut builder, side_uses, side_intervals);
1421
1422 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
1423 frame: frame_bottom,
1424 }));
1425 let top_surface = builder.add_surface(Surface::Plane(Plane { frame: frame_top }));
1426 let side_surface = builder.add_surface(Surface::EllipticalCylinder(EllipticalCylinder {
1427 frame: frame_bottom,
1428 semi_axis_x: a,
1429 semi_axis_y: b,
1430 }));
1431 let bottom_face = add_single_bound_face(
1432 &mut builder,
1433 bottom_surface,
1434 bottom_loop,
1435 Orientation::Reversed,
1436 );
1437 let top_face = add_single_bound_face(&mut builder, top_surface, top_loop, Orientation::Forward);
1438 let side_face =
1439 add_single_bound_face(&mut builder, side_surface, side_loop, Orientation::Forward);
1440 finish_closed(builder, vec![bottom_face, top_face, side_face])
1441}
1442
1443fn add_ellipse_cap_loop(
1445 builder: &mut ExactBRepBuilder,
1446 edge: EdgeId,
1447 frame: Frame2,
1448 semi_axis_x: Scalar,
1449 semi_axis_y: Scalar,
1450) -> LoopId {
1451 let pcurve = builder.add_curve2(Curve2::Ellipse(Ellipse2 {
1452 frame,
1453 semi_axis_x,
1454 semi_axis_y,
1455 }));
1456 add_loop(
1457 builder,
1458 vec![EdgeUse {
1459 edge,
1460 orientation: Orientation::Forward,
1461 pcurve: Some(pcurve),
1462 }],
1463 vec![Interval::new(0.0, TAU)],
1464 )
1465}
1466
1467pub fn profile_to_contour(profile: &Profile, tolerance: Tolerance) -> GeomResult<ContourProfile> {
1474 match profile {
1475 Profile::Contour(contour) => Ok(contour.clone()),
1476 Profile::Rectangle(rectangle) => rectangle_contour(rectangle),
1477 Profile::Circle(circle) => crate::section_lower::circle_contour(circle),
1478 Profile::Section(section) => section_contour(section),
1479 Profile::CenterLine(center_line) => center_line_contour(center_line, tolerance),
1480 Profile::Derived { basis, transform } => {
1481 let lowered = lower_derived(basis, transform, tolerance)?;
1482 profile_to_contour(&lowered, tolerance)
1483 }
1484 Profile::Composite(_) => Err(unsupported(
1491 "composite profile does not lower to an exact contour",
1492 )),
1493 _ => Err(unsupported("profile does not lower to a contour")),
1494 }
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499 use axiolid_overlay::{ArcPolygonDefect, OverlayError};
1500
1501 use super::*;
1502
1503 #[test]
1504 fn a_region_that_touches_itself_is_refused() {
1505 let ring = |points: &[(f64, f64)]| {
1506 axiolid_overlay::ArcRing::from_points(
1507 &points
1508 .iter()
1509 .map(|&(x, y)| Point2::new(x, y))
1510 .collect::<Vec<_>>(),
1511 )
1512 };
1513 let pinched = ring(&[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (2.0, 0.0), (0.0, 4.0)]);
1514 assert!(matches!(
1515 refuse_invalid_region(&pinched, &[]),
1516 Err(GeomError::InvalidInput(_))
1517 ));
1518 let square = ring(&[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)]);
1519 assert!(refuse_invalid_region(&square, &[]).is_ok());
1520 }
1521
1522 #[test]
1523 fn undecided_defects_are_unsupported_and_definite_ones_invalid() {
1524 let undecided = ArcPolygonDefect::NestingUndecided {
1525 error: OverlayError::SelfIntersection,
1526 };
1527 let definite = ArcPolygonDefect::HoleOutsideOuter { hole: 0 };
1528 let ring = ArcPolygonDefect::Ring {
1529 ring: axiolid_overlay::ArcRingRef::Outer,
1530 error: OverlayError::RepeatedVertex,
1531 };
1532 assert!(refusal_of(&[]).is_ok());
1533 assert!(refusal_of(std::slice::from_ref(&ring)).is_ok());
1534 assert!(matches!(
1535 refusal_of(std::slice::from_ref(&undecided)),
1536 Err(GeomError::UnsupportedInput { .. })
1537 ));
1538 match refusal_of(&[undecided, ring, definite]) {
1539 Err(GeomError::InvalidInput(message)) => {
1540 assert!(message.contains("HoleOutsideOuter"), "{message}");
1541 assert!(!message.contains("NestingUndecided"), "{message}");
1542 }
1543 other => panic!("expected InvalidInput, got {other:?}"),
1544 }
1545 }
1546}