1use std::f64::consts::TAU;
4
5use axiolid_brep::{EdgeName, ExactBRep, ExactBRepBuilder, FaceName, SweptFace};
6use axiolid_contracts::{GeomError, GeomResult, Operation};
7use axiolid_core::{Frame2, Frame3, Interval, Point2, Point3, Scalar, Tolerance, Vec2, Vec3};
8use axiolid_curve::{Circle2, Circle3, Curve2, Curve3, Ellipse2, Ellipse3, Line2, Line3};
9use axiolid_profile::{CircleProfile, ContourProfile, EllipseProfile, Profile, RectangleProfile};
10use axiolid_surface::{Cylinder, EllipticalCylinder, Plane, Surface};
11use axiolid_topology::{
12 audit_brep, Edge, EdgeId, EdgeUse, Face, FaceBound, FaceId, Loop, LoopId, Orientation, Shell,
13 Solid, Vertex, VertexId,
14};
15
16use crate::center_line_exact::center_line_contour;
17use crate::contour_lower::{contour_to_arc_ring, orient_arc_ring};
18use crate::extrude_arc::extrude_arc_rings;
19use crate::profile_lower::lower_derived;
20use crate::section_lower::{rectangle_contour, section_contour};
21use crate::BACKEND_ID;
22
23#[derive(Debug)]
24pub(crate) struct RingTopology {
25 bottom_edges: Vec<EdgeId>,
26 top_edges: Vec<EdgeId>,
27 vertical_edges: Vec<EdgeId>,
28 bottom_points: Vec<Point3>,
29}
30
31pub fn extrude_profile_exact(
38 profile: &Profile,
39 direction: Vec3,
40 depth: Scalar,
41 tolerance: Tolerance,
42) -> GeomResult<ExactBRep> {
43 let offset = extrusion_offset(direction, depth, tolerance)?;
44 match profile {
45 Profile::Rectangle(rectangle) => extrude_rectangle(rectangle, offset, tolerance),
46 Profile::Circle(circle) => extrude_circle(circle, offset),
47 Profile::Ellipse(ellipse) => extrude_ellipse(ellipse, offset),
48 Profile::Section(section) => {
49 let contour = section_contour(section)?;
50 extrude_contour(&contour, offset, tolerance)
51 }
52 Profile::Contour(contour) => extrude_contour(contour, offset, tolerance),
53 Profile::Derived { basis, transform } => {
54 let lowered = lower_derived(basis, transform, tolerance)?;
58 extrude_profile_exact(&lowered, direction, depth, tolerance)
59 }
60 Profile::Composite(members) => extrude_composite(members, offset, tolerance),
61 Profile::CenterLine(center_line) => {
62 let contour = center_line_contour(center_line, tolerance)?;
65 extrude_contour(&contour, offset, tolerance)
66 }
67 _ => Err(unsupported("unknown profile extrusion")),
68 }
69}
70
71fn normalize_direction(direction: Vec3) -> Option<Vec3> {
72 if !direction.is_finite() {
73 return None;
74 }
75
76 let scale = direction.abs().max_element();
79 if scale == 0.0 {
80 return None;
81 }
82 let scaled = direction / scale;
83 Some(scaled / scaled.length())
84}
85
86const MAGNITUDE_BITS: u64 = 0x7fff_ffff_ffff_ffff;
88
89fn loses_nonzero_component(input: Vec3, output: Vec3) -> bool {
90 (input.x.to_bits() & MAGNITUDE_BITS != 0 && output.x.to_bits() & MAGNITUDE_BITS == 0)
91 || (input.y.to_bits() & MAGNITUDE_BITS != 0 && output.y.to_bits() & MAGNITUDE_BITS == 0)
92 || (input.z.to_bits() & MAGNITUDE_BITS != 0 && output.z.to_bits() & MAGNITUDE_BITS == 0)
93}
94
95fn extrusion_offset(direction: Vec3, depth: Scalar, tolerance: Tolerance) -> GeomResult<Vec3> {
96 if !depth.is_finite() || depth <= 0.0 {
97 return Err(GeomError::InvalidInput(format!(
98 "exact extrusion depth must be positive and finite, got {depth}"
99 )));
100 }
101 let normalized = normalize_direction(direction).ok_or_else(|| {
102 GeomError::InvalidInput("exact extrusion direction must be finite and non-zero".to_owned())
103 })?;
104 if loses_nonzero_component(direction, normalized) {
105 return Err(GeomError::Degenerate(
106 "exact extrusion direction cannot be normalized without losing a nonzero component"
107 .to_owned(),
108 ));
109 }
110 let offset = normalized * depth;
111 if !offset.is_finite() || loses_nonzero_component(normalized, offset) {
112 return Err(GeomError::Degenerate(
113 "exact extrusion direction could not be scaled without range loss".to_owned(),
114 ));
115 }
116 if offset.z <= tolerance.linear() {
117 return Err(unsupported("non-forward planar extrusion"));
118 }
119 Ok(offset)
120}
121
122fn extrude_rectangle(
123 rectangle: &RectangleProfile,
124 offset: Vec3,
125 tolerance: Tolerance,
126) -> GeomResult<ExactBRep> {
127 if rectangle.outer_radius.is_some() || rectangle.inner_radius.is_some() {
128 return extrude_contour(&rectangle_contour(rectangle)?, offset, tolerance);
132 }
133 if !rectangle.x.is_finite()
134 || !rectangle.y.is_finite()
135 || rectangle.x <= 0.0
136 || rectangle.y <= 0.0
137 {
138 return Err(GeomError::InvalidInput(format!(
139 "exact rectangle profile must have positive finite extents, got {} x {}",
140 rectangle.x, rectangle.y
141 )));
142 }
143
144 let (hx, hy) = (rectangle.x / 2.0, rectangle.y / 2.0);
145 let mut rings = vec![vec![
146 Point2::new(-hx, -hy),
147 Point2::new(hx, -hy),
148 Point2::new(hx, hy),
149 Point2::new(-hx, hy),
150 ]];
151 if let Some(thickness) = rectangle.thickness {
152 if !thickness.is_finite()
153 || thickness <= 0.0
154 || 2.0 * thickness >= rectangle.x
155 || 2.0 * thickness >= rectangle.y
156 {
157 return Err(GeomError::InvalidInput(format!(
158 "exact hollow rectangle wall thickness {thickness} does not fit inside {} x {}",
159 rectangle.x, rectangle.y
160 )));
161 }
162 let (ix, iy) = (hx - thickness, hy - thickness);
163 if tolerance.eq(ix, 0.0) || tolerance.eq(iy, 0.0) {
164 return Err(GeomError::Degenerate(
165 "exact hollow rectangle has a collapsed inner ring".to_owned(),
166 ));
167 }
168 rings.push(vec![
170 Point2::new(-ix, -iy),
171 Point2::new(-ix, iy),
172 Point2::new(ix, iy),
173 Point2::new(ix, -iy),
174 ]);
175 }
176
177 extrude_polygon_rings(&rings, offset)
178}
179
180pub(crate) fn extrude_polygon_rings(rings: &[Vec<Point2>], offset: Vec3) -> GeomResult<ExactBRep> {
187 extrude_polygon_rings_named(rings, offset, &mut |_| None)
188}
189
190pub(crate) fn extrude_polygon_rings_named(
197 rings: &[Vec<Point2>],
198 offset: Vec3,
199 name_side: &mut dyn FnMut((Point2, Point2)) -> Option<FaceName>,
200) -> GeomResult<ExactBRep> {
201 let ring_count = rings.len();
202 let edge_count = ring_count * 12;
203 let pcurve_count = ring_count * 24;
204 let face_count = 2 + ring_count * 4;
205 let mut builder = ExactBRepBuilder::default();
206 reserve(
207 &mut builder,
208 ring_count * 8,
209 edge_count,
210 2 * ring_count + 4 * ring_count,
211 face_count,
212 edge_count,
213 pcurve_count,
214 face_count,
215 )?;
216
217 let ring_topology = rings
218 .iter()
219 .map(|ring| add_polygon_ring(&mut builder, ring.as_slice(), offset))
220 .collect::<GeomResult<Vec<_>>>()?;
221
222 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
223 frame: identity_frame3(Vec3::ZERO),
224 }));
225 let top_surface = builder.add_surface(Surface::Plane(Plane {
226 frame: identity_frame3(offset),
227 }));
228
229 let mut bottom_bounds = Vec::with_capacity(ring_count);
230 let mut top_bounds = Vec::with_capacity(ring_count);
231 for (ring_index, ring) in ring_topology.iter().enumerate() {
232 let bottom_loop = add_cap_loop(&mut builder, &ring.bottom_edges, &ring.bottom_points);
233 let top_loop = add_cap_loop(&mut builder, &ring.top_edges, &ring.bottom_points);
234 bottom_bounds.push(FaceBound {
235 loop_id: bottom_loop,
236 orientation: Orientation::Forward,
237 outer: ring_index == 0,
238 });
239 top_bounds.push(FaceBound {
240 loop_id: top_loop,
241 orientation: Orientation::Forward,
242 outer: ring_index == 0,
243 });
244 }
245
246 let bottom_face = builder.topology_mut().add_face(Face {
247 surface: Some(bottom_surface),
248 bounds: bottom_bounds,
249 orientation: Orientation::Reversed,
250 });
251 let top_face = builder.topology_mut().add_face(Face {
252 surface: Some(top_surface),
253 bounds: top_bounds,
254 orientation: Orientation::Forward,
255 });
256
257 let mut faces = vec![bottom_face, top_face];
258 for (ring_index, ring) in ring_topology.iter().enumerate() {
259 let source = &rings[ring_index];
260 for edge_index in 0..ring.bottom_edges.len() {
261 let face = add_planar_side(&mut builder, ring, edge_index, offset)?;
262 let start = source[edge_index];
265 let end = source[(edge_index + 1) % source.len()];
266 if let Some(name) = name_side((start, end)) {
267 builder.set_face_name(face, name);
268 }
269 faces.push(face);
270 }
271 }
272 finish_closed(builder, faces)
273}
274
275fn add_polygon_ring(
276 builder: &mut ExactBRepBuilder,
277 ring: &[Point2],
278 offset: Vec3,
279) -> GeomResult<RingTopology> {
280 add_polygon_ring_with_blends(builder, ring, offset, &[])
281}
282
283pub(crate) fn add_polygon_ring_with_blends(
291 builder: &mut ExactBRepBuilder,
292 ring: &[Point2],
293 offset: Vec3,
294 blends: &[Option<(BlendCorner, Scalar)>],
295) -> GeomResult<RingTopology> {
296 let bottom_points: Vec<_> = ring
297 .iter()
298 .map(|point| Point3::new(point.x, point.y, 0.0))
299 .collect();
300 let top_points: Vec<_> = bottom_points.iter().map(|point| *point + offset).collect();
301 let bottom_vertices: Vec<_> = bottom_points
302 .iter()
303 .map(|&position| builder.topology_mut().add_vertex(Vertex { position }))
304 .collect();
305 let top_vertices: Vec<_> = top_points
306 .iter()
307 .map(|&position| builder.topology_mut().add_vertex(Vertex { position }))
308 .collect();
309
310 let mut bottom_edges = Vec::with_capacity(ring.len());
311 let mut top_edges = Vec::with_capacity(ring.len());
312 let mut vertical_edges = Vec::with_capacity(ring.len());
313 for index in 0..ring.len() {
314 let next = (index + 1) % ring.len();
315 if let Some(Some((blend, radius))) = blends.get(index) {
317 let (bottom_edge, top_edge) = add_blend_arc_edges(
318 builder,
319 blend,
320 *radius,
321 (bottom_vertices[index], bottom_vertices[next]),
322 (top_vertices[index], top_vertices[next]),
323 offset,
324 );
325 bottom_edges.push(bottom_edge);
326 top_edges.push(top_edge);
327 } else {
328 bottom_edges.push(add_line_edge(
329 builder,
330 bottom_vertices[index],
331 bottom_vertices[next],
332 bottom_points[index],
333 bottom_points[next] - bottom_points[index],
334 ));
335 top_edges.push(add_line_edge(
336 builder,
337 top_vertices[index],
338 top_vertices[next],
339 top_points[index],
340 top_points[next] - top_points[index],
341 ));
342 }
343 vertical_edges.push(add_line_edge(
344 builder,
345 bottom_vertices[index],
346 top_vertices[index],
347 bottom_points[index],
348 offset,
349 ));
350 }
351 Ok(RingTopology {
352 bottom_edges,
353 top_edges,
354 vertical_edges,
355 bottom_points,
356 })
357}
358
359pub(crate) fn add_line_edge(
360 builder: &mut ExactBRepBuilder,
361 start: VertexId,
362 end: VertexId,
363 origin: Point3,
364 direction: Vec3,
365) -> EdgeId {
366 let curve = builder.add_curve3(Curve3::Line(Line3 { origin, direction }));
367 let edge = builder.topology_mut().add_edge(Edge {
368 start,
369 end,
370 curve: Some(curve),
371 });
372 builder.set_edge_interval(edge, Interval::UNIT);
373 edge
374}
375
376fn add_cap_loop(builder: &mut ExactBRepBuilder, edges: &[EdgeId], points: &[Point3]) -> LoopId {
377 add_cap_loop_with_blends(builder, edges, points, &[])
378}
379
380fn add_cap_loop_with_blends(
389 builder: &mut ExactBRepBuilder,
390 edges: &[EdgeId],
391 points: &[Point3],
392 blends: &[Option<(BlendCorner, Scalar)>],
393) -> LoopId {
394 let mut uses = Vec::with_capacity(edges.len());
395 let mut intervals = Vec::with_capacity(edges.len());
396 for (index, &edge) in edges.iter().enumerate() {
397 let next = (index + 1) % edges.len();
398 let (pcurve, interval) = match blends.get(index) {
399 Some(Some((blend, radius))) => {
400 let start = blend.start - blend.centre;
401 let length = start.length();
402 let x = if length == 0.0 {
403 Vec2::X
404 } else {
405 start / length
406 };
407 let curve = Curve2::Circle(Circle2 {
408 frame: Frame2 {
409 origin: Vec2::new(blend.centre.x, blend.centre.y),
410 x,
411 y: Vec2::new(-x.y, x.x),
412 },
413 radius: *radius,
414 });
415 (curve, Interval::new(0.0, blend.sweep))
416 }
417 _ => {
418 let origin = points[index].truncate();
419 let direction = (points[next] - points[index]).truncate();
420 (Curve2::Line(Line2 { origin, direction }), Interval::UNIT)
421 }
422 };
423 let pcurve = builder.add_curve2(pcurve);
424 uses.push(EdgeUse {
425 edge,
426 orientation: Orientation::Forward,
427 pcurve: Some(pcurve),
428 });
429 intervals.push(interval);
430 }
431 add_loop(builder, uses, intervals)
432}
433
434fn add_planar_side(
435 builder: &mut ExactBRepBuilder,
436 ring: &RingTopology,
437 index: usize,
438 offset: Vec3,
439) -> GeomResult<FaceId> {
440 let next = (index + 1) % ring.bottom_edges.len();
441 let origin = ring.bottom_points[index];
442 let edge_vector = ring.bottom_points[next] - origin;
443 let x = edge_vector.normalize();
444 let z = edge_vector.cross(offset).normalize();
445 let y = z.cross(x);
446 if !(x.is_finite() && y.is_finite() && z.is_finite()) {
447 return Err(GeomError::Degenerate(
448 "exact rectangle extrusion produced a degenerate side plane".to_owned(),
449 ));
450 }
451 let surface = builder.add_surface(Surface::Plane(Plane {
452 frame: Frame3 { origin, x, y, z },
453 }));
454 let edge_length = edge_vector.length();
455 let offset_uv = Vec2::new(offset.dot(x), offset.dot(y));
456 let pcurves = [
457 Curve2::Line(Line2 {
458 origin: Vec2::ZERO,
459 direction: Vec2::new(edge_length, 0.0),
460 }),
461 Curve2::Line(Line2 {
462 origin: Vec2::new(edge_length, 0.0),
463 direction: offset_uv,
464 }),
465 Curve2::Line(Line2 {
466 origin: offset_uv,
467 direction: Vec2::new(edge_length, 0.0),
468 }),
469 Curve2::Line(Line2 {
470 origin: Vec2::ZERO,
471 direction: offset_uv,
472 }),
473 ];
474 let edge_uses = [
475 (ring.bottom_edges[index], Orientation::Forward),
476 (ring.vertical_edges[next], Orientation::Forward),
477 (ring.top_edges[index], Orientation::Reversed),
478 (ring.vertical_edges[index], Orientation::Reversed),
479 ];
480 let mut uses = Vec::with_capacity(4);
481 let mut intervals = Vec::with_capacity(4);
482 for ((edge, orientation), pcurve) in edge_uses.into_iter().zip(pcurves) {
483 let pcurve = builder.add_curve2(pcurve);
484 uses.push(EdgeUse {
485 edge,
486 orientation,
487 pcurve: Some(pcurve),
488 });
489 intervals.push(match orientation {
490 Orientation::Forward => Interval::UNIT,
491 Orientation::Reversed => Interval::new(1.0, 0.0),
492 });
493 }
494 let loop_id = add_loop(builder, uses, intervals);
495 Ok(builder.topology_mut().add_face(Face {
496 surface: Some(surface),
497 bounds: vec![FaceBound {
498 loop_id,
499 orientation: Orientation::Forward,
500 outer: true,
501 }],
502 orientation: Orientation::Forward,
503 }))
504}
505
506fn extrude_circle(circle: &CircleProfile, offset: Vec3) -> GeomResult<ExactBRep> {
507 if circle.thickness.is_some() {
508 return Err(unsupported("annular circle extrusion"));
509 }
510 if !circle.radius.is_finite() || circle.radius <= 0.0 {
511 return Err(GeomError::InvalidInput(format!(
512 "exact circle profile radius must be positive and finite, got {}",
513 circle.radius
514 )));
515 }
516 if offset.x != 0.0 || offset.y != 0.0 {
517 return Err(unsupported("oblique circle extrusion"));
518 }
519
520 let depth = offset.z;
521 let frame_bottom = identity_frame3(Vec3::ZERO);
522 let frame_top = identity_frame3(offset);
523 let frame2 = Frame2 {
524 origin: Vec2::ZERO,
525 x: Vec2::X,
526 y: Vec2::Y,
527 };
528 let bottom_point = Vec3::new(circle.radius, 0.0, 0.0);
529 let top_point = bottom_point + offset;
530
531 let mut builder = ExactBRepBuilder::default();
532 reserve(&mut builder, 2, 3, 3, 3, 3, 8, 3)?;
533 let bottom_vertex = builder.topology_mut().add_vertex(Vertex {
534 position: bottom_point,
535 });
536 let top_vertex = builder.topology_mut().add_vertex(Vertex {
537 position: top_point,
538 });
539
540 let bottom_curve = builder.add_curve3(Curve3::Circle(Circle3 {
541 frame: frame_bottom,
542 radius: circle.radius,
543 }));
544 let bottom_edge = builder.topology_mut().add_edge(Edge {
545 start: bottom_vertex,
546 end: bottom_vertex,
547 curve: Some(bottom_curve),
548 });
549 builder.set_edge_interval(bottom_edge, Interval::new(0.0, TAU));
550
551 let top_curve = builder.add_curve3(Curve3::Circle(Circle3 {
552 frame: frame_top,
553 radius: circle.radius,
554 }));
555 let top_edge = builder.topology_mut().add_edge(Edge {
556 start: top_vertex,
557 end: top_vertex,
558 curve: Some(top_curve),
559 });
560 builder.set_edge_interval(top_edge, Interval::new(0.0, TAU));
561
562 let seam_curve = builder.add_curve3(Curve3::Line(Line3 {
563 origin: bottom_point,
564 direction: offset,
565 }));
566 let seam_edge = builder.topology_mut().add_edge(Edge {
567 start: bottom_vertex,
568 end: top_vertex,
569 curve: Some(seam_curve),
570 });
571 builder.set_edge_interval(seam_edge, Interval::UNIT);
572
573 let bottom_loop = add_circle_cap_loop(&mut builder, bottom_edge, frame2, circle.radius);
574 let top_loop = add_circle_cap_loop(&mut builder, top_edge, frame2, circle.radius);
575
576 let side_curves = [
577 Curve2::Line(Line2 {
578 origin: Vec2::ZERO,
579 direction: Vec2::X,
580 }),
581 Curve2::Line(Line2 {
582 origin: Vec2::new(TAU, 0.0),
583 direction: Vec2::new(0.0, depth),
584 }),
585 Curve2::Line(Line2 {
586 origin: Vec2::new(0.0, depth),
587 direction: Vec2::X,
588 }),
589 Curve2::Line(Line2 {
590 origin: Vec2::ZERO,
591 direction: Vec2::new(0.0, depth),
592 }),
593 ];
594 let side_edges = [
595 (bottom_edge, Orientation::Forward, Interval::new(0.0, TAU)),
596 (seam_edge, Orientation::Forward, Interval::UNIT),
597 (top_edge, Orientation::Reversed, Interval::new(TAU, 0.0)),
598 (seam_edge, Orientation::Reversed, Interval::new(1.0, 0.0)),
599 ];
600 let mut side_uses = Vec::with_capacity(4);
601 let mut side_intervals = Vec::with_capacity(4);
602 for ((edge, orientation, interval), curve) in side_edges.into_iter().zip(side_curves) {
603 let pcurve = builder.add_curve2(curve);
604 side_uses.push(EdgeUse {
605 edge,
606 orientation,
607 pcurve: Some(pcurve),
608 });
609 side_intervals.push(interval);
610 }
611 let side_loop = add_loop(&mut builder, side_uses, side_intervals);
612
613 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
614 frame: frame_bottom,
615 }));
616 let top_surface = builder.add_surface(Surface::Plane(Plane { frame: frame_top }));
617 let side_surface = builder.add_surface(Surface::Cylinder(Cylinder {
618 frame: frame_bottom,
619 radius: circle.radius,
620 }));
621 let bottom_face = add_single_bound_face(
622 &mut builder,
623 bottom_surface,
624 bottom_loop,
625 Orientation::Reversed,
626 );
627 let top_face = add_single_bound_face(&mut builder, top_surface, top_loop, Orientation::Forward);
628 let side_face =
629 add_single_bound_face(&mut builder, side_surface, side_loop, Orientation::Forward);
630 finish_closed(builder, vec![bottom_face, top_face, side_face])
631}
632
633fn add_circle_cap_loop(
634 builder: &mut ExactBRepBuilder,
635 edge: EdgeId,
636 frame: Frame2,
637 radius: Scalar,
638) -> LoopId {
639 let pcurve = builder.add_curve2(Curve2::Circle(Circle2 { frame, radius }));
640 add_loop(
641 builder,
642 vec![EdgeUse {
643 edge,
644 orientation: Orientation::Forward,
645 pcurve: Some(pcurve),
646 }],
647 vec![Interval::new(0.0, TAU)],
648 )
649}
650
651pub(crate) fn add_single_bound_face(
652 builder: &mut ExactBRepBuilder,
653 surface: axiolid_brep::SurfaceId,
654 loop_id: LoopId,
655 orientation: Orientation,
656) -> FaceId {
657 builder.topology_mut().add_face(Face {
658 surface: Some(surface),
659 bounds: vec![FaceBound {
660 loop_id,
661 orientation: Orientation::Forward,
662 outer: true,
663 }],
664 orientation,
665 })
666}
667
668pub(crate) fn add_loop(
669 builder: &mut ExactBRepBuilder,
670 uses: Vec<EdgeUse<axiolid_brep::Curve2Id>>,
671 intervals: Vec<Interval>,
672) -> LoopId {
673 debug_assert_eq!(uses.len(), intervals.len());
674 let loop_id = builder.topology_mut().add_loop(Loop { edges: uses });
675 for (use_index, interval) in intervals.into_iter().enumerate() {
676 builder.set_pcurve_interval(loop_id, use_index, interval);
677 }
678 loop_id
679}
680
681pub(crate) fn finish_closed(
682 mut builder: ExactBRepBuilder,
683 faces: Vec<FaceId>,
684) -> GeomResult<ExactBRep> {
685 let shell = builder.topology_mut().add_shell(Shell {
686 faces: faces
687 .into_iter()
688 .map(|face| (face, Orientation::Forward))
689 .collect(),
690 closed: true,
691 });
692 builder.topology_mut().add_solid(Solid {
693 outer: shell,
694 voids: Vec::new(),
695 });
696 let exact = builder
697 .finish()
698 .map_err(|error| GeomError::BackendContractViolation {
699 backend: BACKEND_ID,
700 detail: format!("exact extrusion assembly failed: {error}"),
701 })?;
702 let health = audit_brep(exact.topology());
703 if !health.is_closed_manifold() {
704 return Err(GeomError::BackendContractViolation {
705 backend: BACKEND_ID,
706 detail: format!("exact extrusion is not a closed manifold: {health:?}"),
707 });
708 }
709 Ok(exact)
710}
711
712#[allow(clippy::too_many_arguments)]
713pub(crate) fn reserve(
714 builder: &mut ExactBRepBuilder,
715 vertices: usize,
716 edges: usize,
717 loops: usize,
718 faces: usize,
719 curves3: usize,
720 curves2: usize,
721 surfaces: usize,
722) -> GeomResult<()> {
723 builder
724 .topology_mut()
725 .try_reserve(vertices, edges, loops, faces, 1, 1)
726 .map_err(|_| GeomError::BudgetExceeded { resource: "memory" })?;
727 builder
728 .try_reserve(curves3, curves2, surfaces, edges, curves2)
729 .map_err(|_| GeomError::BudgetExceeded { resource: "memory" })
730}
731
732pub(crate) fn identity_frame3(origin: Point3) -> Frame3 {
733 Frame3 {
734 origin,
735 x: Vec3::X,
736 y: Vec3::Y,
737 z: Vec3::Z,
738 }
739}
740
741fn unsupported(input: &'static str) -> GeomError {
742 GeomError::UnsupportedInput {
743 backend: BACKEND_ID,
744 operation: Operation::Sweep,
745 input,
746 }
747}
748
749pub(crate) use crate::feature::BlendCorner;
757
758pub(crate) fn extrude_with_cylindrical_blend(
759 ring: &[Point2],
760 offset: Vec3,
761 blend_index: usize,
762 blend: &BlendCorner,
763 radius: Scalar,
764) -> GeomResult<ExactBRep> {
765 let mut blends: Vec<Option<(BlendCorner, Scalar)>> = (0..ring.len()).map(|_| None).collect();
766 blends[blend_index] = Some((
767 BlendCorner {
768 centre: blend.centre,
769 start: blend.start,
770 end: blend.end,
771 sweep: blend.sweep,
772 },
773 radius,
774 ));
775 extrude_with_cylindrical_blends(ring, offset, &blends)
776}
777
778pub(crate) fn extrude_with_cylindrical_blends(
784 ring: &[Point2],
785 offset: Vec3,
786 blends: &[Option<(BlendCorner, Scalar)>],
787) -> GeomResult<ExactBRep> {
788 let mut builder = ExactBRepBuilder::default();
789 let n = ring.len();
790 reserve(
791 &mut builder,
792 n * 2,
793 n * 3,
794 2 + n,
795 2 + n,
796 n * 3,
797 n * 6,
798 2 + n,
799 )?;
800
801 let topology = add_polygon_ring_with_blends(&mut builder, ring, offset, blends)?;
803
804 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
805 frame: identity_frame3(Vec3::ZERO),
806 }));
807 let top_surface = builder.add_surface(Surface::Plane(Plane {
808 frame: identity_frame3(offset),
809 }));
810 let bottom_loop = add_cap_loop_with_blends(
811 &mut builder,
812 &topology.bottom_edges,
813 &topology.bottom_points,
814 blends,
815 );
816 let top_loop = add_cap_loop_with_blends(
817 &mut builder,
818 &topology.top_edges,
819 &topology.bottom_points,
820 blends,
821 );
822
823 let bottom_face = builder.topology_mut().add_face(Face {
824 surface: Some(bottom_surface),
825 bounds: vec![FaceBound {
826 loop_id: bottom_loop,
827 orientation: Orientation::Forward,
828 outer: true,
829 }],
830 orientation: Orientation::Reversed,
831 });
832 let top_face = builder.topology_mut().add_face(Face {
833 surface: Some(top_surface),
834 bounds: vec![FaceBound {
835 loop_id: top_loop,
836 orientation: Orientation::Forward,
837 outer: true,
838 }],
839 orientation: Orientation::Forward,
840 });
841 builder.set_face_name(bottom_face, FaceName::swept(SweptFace::StartCap));
842 builder.set_face_name(top_face, FaceName::swept(SweptFace::EndCap));
843
844 let mut faces = vec![bottom_face, top_face];
845 let side_count = topology.bottom_edges.len();
846 for edge_index in 0..side_count {
847 let ordinal = u32::try_from(edge_index).map_err(|_| {
851 GeomError::Degenerate("profile edge count exceeds u32 capacity".to_owned())
852 })?;
853 if let Some((blend, radius)) = blends.get(edge_index).and_then(Option::as_ref) {
854 let face =
855 add_cylindrical_blend(&mut builder, &topology, edge_index, offset, blend, *radius)?;
856 let previous = (edge_index + side_count - 1) % side_count;
861 let previous_ordinal = u32::try_from(previous).map_err(|_| {
862 GeomError::Degenerate("profile edge count exceeds u32 capacity".to_owned())
863 })?;
864 builder.set_face_name(
865 face,
866 FaceName::blend(EdgeName::between(
867 FaceName::swept(SweptFace::Side(previous_ordinal)),
868 FaceName::swept(SweptFace::Side(ordinal)),
869 )),
870 );
871 faces.push(face);
872 } else {
873 let face = add_planar_side(&mut builder, &topology, edge_index, offset)?;
874 builder.set_face_name(face, FaceName::swept(SweptFace::Side(ordinal)));
875 faces.push(face);
876 }
877 }
878 finish_closed(builder, faces)
879}
880
881fn add_cylindrical_blend(
891 builder: &mut ExactBRepBuilder,
892 ring: &RingTopology,
893 index: usize,
894 offset: Vec3,
895 blend: &BlendCorner,
896 radius: Scalar,
897) -> GeomResult<FaceId> {
898 let next = (index + 1) % ring.bottom_edges.len();
899 let centre = Point3::new(blend.centre.x, blend.centre.y, 0.0);
900 let start = ring.bottom_points[index];
903 let x = (start - centre).normalize();
904 let z = offset.normalize();
905 let y = z.cross(x);
906 if !(x.is_finite() && y.is_finite() && z.is_finite()) {
907 return Err(GeomError::Degenerate(
908 "fillet blend produced a degenerate cylinder frame".to_owned(),
909 ));
910 }
911 let surface = builder.add_surface(Surface::Cylinder(Cylinder {
912 frame: Frame3 {
913 origin: centre,
914 x,
915 y,
916 z,
917 },
918 radius,
919 }));
920
921 let height = offset.length();
922 let pcurves = [
923 Curve2::Line(Line2 {
925 origin: Vec2::ZERO,
926 direction: Vec2::new(blend.sweep, 0.0),
927 }),
928 Curve2::Line(Line2 {
929 origin: Vec2::new(blend.sweep, 0.0),
930 direction: Vec2::new(0.0, height),
931 }),
932 Curve2::Line(Line2 {
933 origin: Vec2::new(0.0, height),
934 direction: Vec2::new(blend.sweep, 0.0),
935 }),
936 Curve2::Line(Line2 {
937 origin: Vec2::ZERO,
938 direction: Vec2::new(0.0, height),
939 }),
940 ];
941 let edge_uses = [
942 (ring.bottom_edges[index], Orientation::Forward),
943 (ring.vertical_edges[next], Orientation::Forward),
944 (ring.top_edges[index], Orientation::Reversed),
945 (ring.vertical_edges[index], Orientation::Reversed),
946 ];
947
948 let mut uses = Vec::with_capacity(4);
949 let mut intervals = Vec::with_capacity(4);
950 for ((edge, orientation), pcurve) in edge_uses.into_iter().zip(pcurves) {
951 let pcurve = builder.add_curve2(pcurve);
952 uses.push(EdgeUse {
953 edge,
954 orientation,
955 pcurve: Some(pcurve),
956 });
957 intervals.push(match orientation {
958 Orientation::Forward => Interval::UNIT,
959 Orientation::Reversed => Interval::new(1.0, 0.0),
960 });
961 }
962 let loop_id = add_loop(builder, uses, intervals);
963 Ok(builder.topology_mut().add_face(Face {
964 surface: Some(surface),
965 bounds: vec![FaceBound {
966 loop_id,
967 orientation: Orientation::Forward,
968 outer: true,
969 }],
970 orientation: Orientation::Forward,
971 }))
972}
973
974fn add_blend_arc_edges(
980 builder: &mut ExactBRepBuilder,
981 blend: &BlendCorner,
982 radius: Scalar,
983 bottom: (VertexId, VertexId),
984 top: (VertexId, VertexId),
985 offset: Vec3,
986) -> (EdgeId, EdgeId) {
987 let centre = Point3::new(blend.centre.x, blend.centre.y, 0.0);
988 let start = Point3::new(blend.start.x, blend.start.y, 0.0);
989 let x = (start - centre).normalize_or_zero();
990 let z = offset.normalize_or_zero();
991 let y = z.cross(x);
992
993 let mut arc_edge = |origin: Point3, ends: (VertexId, VertexId)| {
994 let curve = builder.add_curve3(Curve3::Circle(Circle3 {
995 frame: Frame3 { origin, x, y, z },
996 radius,
997 }));
998 let edge = builder.topology_mut().add_edge(Edge {
999 start: ends.0,
1000 end: ends.1,
1001 curve: Some(curve),
1002 });
1003 builder.set_edge_interval(edge, Interval::new(0.0, blend.sweep));
1006 edge
1007 };
1008
1009 let bottom_edge = arc_edge(centre, bottom);
1010 let top_edge = arc_edge(centre + offset, top);
1011 (bottom_edge, top_edge)
1012}
1013
1014fn extrude_contour(
1025 contour: &ContourProfile,
1026 offset: Vec3,
1027 tolerance: Tolerance,
1028) -> GeomResult<ExactBRep> {
1029 let outer = orient_arc_ring(&contour_to_arc_ring(&contour.outer, tolerance)?, true)?;
1034 let mut rings = Vec::with_capacity(1 + contour.holes.len());
1035 rings.push(outer);
1036 for hole in &contour.holes {
1037 rings.push(orient_arc_ring(
1038 &contour_to_arc_ring(hole, tolerance)?,
1039 false,
1040 )?);
1041 }
1042
1043 if rings
1047 .iter()
1048 .all(|ring| ring.vertices.iter().all(|vertex| vertex.bulge == 0.0))
1049 {
1050 let polygons: Vec<Vec<Point2>> = rings
1051 .iter()
1052 .map(|ring| ring.vertices.iter().map(|vertex| vertex.point).collect())
1053 .collect();
1054 return extrude_polygon_rings(&polygons, offset);
1055 }
1056 extrude_arc_rings(&rings, offset)
1057}
1058
1059fn extrude_composite(
1063 members: &[Profile],
1064 offset: Vec3,
1065 tolerance: Tolerance,
1066) -> GeomResult<ExactBRep> {
1067 let regions = crate::profile_lower::composite_regions(members, tolerance)?;
1068 let mut pieces = Vec::with_capacity(regions.len());
1069 for region in regions {
1070 let mut rings = Vec::with_capacity(1 + region.holes.len());
1071 rings.push(orient_arc_ring(®ion.outer, true)?);
1072 for hole in ®ion.holes {
1073 rings.push(orient_arc_ring(hole, false)?);
1074 }
1075 if rings
1077 .iter()
1078 .all(|ring| ring.vertices.iter().all(|vertex| vertex.bulge == 0.0))
1079 {
1080 let polygons: Vec<Vec<Point2>> = rings
1081 .iter()
1082 .map(|ring| ring.vertices.iter().map(|vertex| vertex.point).collect())
1083 .collect();
1084 pieces.push(extrude_polygon_rings(&polygons, offset)?);
1085 } else {
1086 pieces.push(extrude_arc_rings(&rings, offset)?);
1087 }
1088 }
1089 crate::assemble::merge_solids(pieces)
1090}
1091
1092fn extrude_ellipse(ellipse: &EllipseProfile, offset: Vec3) -> GeomResult<ExactBRep> {
1100 let (a, b) = (ellipse.semi_axis_x, ellipse.semi_axis_y);
1101 if !a.is_finite() || !b.is_finite() || a <= 0.0 || b <= 0.0 {
1102 return Err(GeomError::InvalidInput(format!(
1103 "exact ellipse profile semi-axes must be positive and finite, got {a} x {b}"
1104 )));
1105 }
1106 if offset.x != 0.0 || offset.y != 0.0 {
1107 return Err(unsupported("oblique ellipse extrusion"));
1108 }
1109
1110 let frame_bottom = identity_frame3(Vec3::ZERO);
1111 let frame_top = identity_frame3(offset);
1112 let frame2 = Frame2 {
1113 origin: Vec2::ZERO,
1114 x: Vec2::X,
1115 y: Vec2::Y,
1116 };
1117 let bottom_point = Vec3::new(a, 0.0, 0.0);
1119 let top_point = bottom_point + offset;
1120
1121 let mut builder = ExactBRepBuilder::default();
1122 reserve(&mut builder, 2, 3, 3, 3, 3, 8, 3)?;
1123 let bottom_vertex = builder.topology_mut().add_vertex(Vertex {
1124 position: bottom_point,
1125 });
1126 let top_vertex = builder.topology_mut().add_vertex(Vertex {
1127 position: top_point,
1128 });
1129
1130 let bottom_curve = builder.add_curve3(Curve3::Ellipse(Ellipse3 {
1131 frame: frame_bottom,
1132 semi_axis_x: a,
1133 semi_axis_y: b,
1134 }));
1135 let bottom_edge = builder.topology_mut().add_edge(Edge {
1136 start: bottom_vertex,
1137 end: bottom_vertex,
1138 curve: Some(bottom_curve),
1139 });
1140 builder.set_edge_interval(bottom_edge, Interval::new(0.0, TAU));
1141
1142 let top_curve = builder.add_curve3(Curve3::Ellipse(Ellipse3 {
1143 frame: frame_top,
1144 semi_axis_x: a,
1145 semi_axis_y: b,
1146 }));
1147 let top_edge = builder.topology_mut().add_edge(Edge {
1148 start: top_vertex,
1149 end: top_vertex,
1150 curve: Some(top_curve),
1151 });
1152 builder.set_edge_interval(top_edge, Interval::new(0.0, TAU));
1153
1154 let seam_curve = builder.add_curve3(Curve3::Line(Line3 {
1155 origin: bottom_point,
1156 direction: offset,
1157 }));
1158 let seam_edge = builder.topology_mut().add_edge(Edge {
1159 start: bottom_vertex,
1160 end: top_vertex,
1161 curve: Some(seam_curve),
1162 });
1163 builder.set_edge_interval(seam_edge, Interval::UNIT);
1164
1165 let bottom_loop = add_ellipse_cap_loop(&mut builder, bottom_edge, frame2, a, b);
1166 let top_loop = add_ellipse_cap_loop(&mut builder, top_edge, frame2, a, b);
1167
1168 let depth = offset.z;
1172 let side_curves = [
1173 Curve2::Line(Line2 {
1174 origin: Vec2::ZERO,
1175 direction: Vec2::X,
1176 }),
1177 Curve2::Line(Line2 {
1178 origin: Vec2::new(TAU, 0.0),
1179 direction: Vec2::new(0.0, depth),
1180 }),
1181 Curve2::Line(Line2 {
1182 origin: Vec2::new(0.0, depth),
1183 direction: Vec2::X,
1184 }),
1185 Curve2::Line(Line2 {
1186 origin: Vec2::ZERO,
1187 direction: Vec2::new(0.0, depth),
1188 }),
1189 ];
1190 let side_edges = [
1191 (bottom_edge, Orientation::Forward, Interval::new(0.0, TAU)),
1192 (seam_edge, Orientation::Forward, Interval::UNIT),
1193 (top_edge, Orientation::Reversed, Interval::new(TAU, 0.0)),
1194 (seam_edge, Orientation::Reversed, Interval::new(1.0, 0.0)),
1195 ];
1196 let mut side_uses = Vec::with_capacity(4);
1197 let mut side_intervals = Vec::with_capacity(4);
1198 for ((edge, orientation, interval), curve) in side_edges.into_iter().zip(side_curves) {
1199 let pcurve = builder.add_curve2(curve);
1200 side_uses.push(EdgeUse {
1201 edge,
1202 orientation,
1203 pcurve: Some(pcurve),
1204 });
1205 side_intervals.push(interval);
1206 }
1207 let side_loop = add_loop(&mut builder, side_uses, side_intervals);
1208
1209 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
1210 frame: frame_bottom,
1211 }));
1212 let top_surface = builder.add_surface(Surface::Plane(Plane { frame: frame_top }));
1213 let side_surface = builder.add_surface(Surface::EllipticalCylinder(EllipticalCylinder {
1214 frame: frame_bottom,
1215 semi_axis_x: a,
1216 semi_axis_y: b,
1217 }));
1218 let bottom_face = add_single_bound_face(
1219 &mut builder,
1220 bottom_surface,
1221 bottom_loop,
1222 Orientation::Reversed,
1223 );
1224 let top_face = add_single_bound_face(&mut builder, top_surface, top_loop, Orientation::Forward);
1225 let side_face =
1226 add_single_bound_face(&mut builder, side_surface, side_loop, Orientation::Forward);
1227 finish_closed(builder, vec![bottom_face, top_face, side_face])
1228}
1229
1230fn add_ellipse_cap_loop(
1232 builder: &mut ExactBRepBuilder,
1233 edge: EdgeId,
1234 frame: Frame2,
1235 semi_axis_x: Scalar,
1236 semi_axis_y: Scalar,
1237) -> LoopId {
1238 let pcurve = builder.add_curve2(Curve2::Ellipse(Ellipse2 {
1239 frame,
1240 semi_axis_x,
1241 semi_axis_y,
1242 }));
1243 add_loop(
1244 builder,
1245 vec![EdgeUse {
1246 edge,
1247 orientation: Orientation::Forward,
1248 pcurve: Some(pcurve),
1249 }],
1250 vec![Interval::new(0.0, TAU)],
1251 )
1252}
1253
1254pub fn profile_to_contour(profile: &Profile, tolerance: Tolerance) -> GeomResult<ContourProfile> {
1261 match profile {
1262 Profile::Contour(contour) => Ok(contour.clone()),
1263 Profile::Rectangle(rectangle) => rectangle_contour(rectangle),
1264 Profile::Circle(circle) => crate::section_lower::circle_contour(circle),
1265 Profile::Section(section) => section_contour(section),
1266 Profile::CenterLine(center_line) => center_line_contour(center_line, tolerance),
1267 Profile::Derived { basis, transform } => {
1268 let lowered = lower_derived(basis, transform, tolerance)?;
1269 profile_to_contour(&lowered, tolerance)
1270 }
1271 Profile::Composite(_) => Err(unsupported(
1278 "composite profile does not lower to an exact contour",
1279 )),
1280 _ => Err(unsupported("profile does not lower to a contour")),
1281 }
1282}