1use std::f64::consts::TAU;
65
66use axiolid_brep::{ExactBRep, ExactBRepBuilder};
67use axiolid_contracts::{GeomError, GeomResult, Operation};
68use axiolid_core::{Frame2, Interval, Point2, Point3, Scalar, Tolerance, Transform3, Vec2, Vec3};
69use axiolid_curve::{Circle2, Circle3, Curve2, Curve3, Line2, Line3};
70use axiolid_guarantees::Sign;
71use axiolid_overlay::{ArcRing, ArcVertex};
72use axiolid_profile::{Profile, RectangleProfile};
73use axiolid_surface::{Cylinder, Plane, Surface};
74use axiolid_topology::{
75 Edge, EdgeUse, Face, FaceBound, FaceId, Loop, Orientation, Shell, Solid, Vertex,
76};
77
78use crate::extrude_exact::extrude_profile_exact;
79use crate::revolve::{along_axis, side_of_axis};
80use crate::BACKEND_ID;
81
82fn unsupported(input: &'static str) -> GeomError {
83 GeomError::UnsupportedInput {
84 backend: BACKEND_ID,
85 operation: Operation::Sweep,
86 input,
87 }
88}
89
90pub fn revolve_profile_exact(
113 profile: &Profile,
114 axis_origin: Point3,
115 axis_direction: Vec3,
116 angle: Scalar,
117 tolerance: Tolerance,
118) -> GeomResult<ExactBRep> {
119 let frame = axis_frame(axis_origin, axis_direction)?;
120 if !angle.is_finite() || angle == 0.0 {
121 return Err(GeomError::InvalidInput(format!(
122 "revolution angle must be finite and non-zero, got {angle}"
123 )));
124 }
125 if angle.abs() > TAU + tolerance.linear() {
127 return Err(unsupported("exact revolution beyond a full turn"));
128 }
129 if (angle.abs() - TAU).abs() > tolerance.linear() {
132 return revolve_partial(profile, &frame, angle, tolerance);
133 }
134
135 match profile {
136 Profile::Rectangle(rectangle) if frame.placement.is_none() => {
138 revolve_rectangle(rectangle, &frame, tolerance)
139 }
140 Profile::Circle(_) => revolve_via_contour(profile, &frame, None, tolerance),
143 Profile::Rectangle(_) => revolve_via_contour(profile, &frame, None, tolerance),
144 Profile::Ellipse(_) => Err(unsupported("ellipse exact revolution")),
145 Profile::Section(_)
150 | Profile::Contour(_)
151 | Profile::Derived { .. }
152 | Profile::Composite(_)
153 | Profile::CenterLine(_) => revolve_via_contour(profile, &frame, None, tolerance),
154 _ => Err(unsupported("unknown profile exact revolution")),
155 }
156}
157
158#[derive(Debug, Clone)]
160struct AxisFrame {
161 origin: Point2,
163 given: Vec3,
166 unit: Vec2,
168 placement: Option<Transform3>,
171}
172
173fn axis_frame(axis_origin: Point3, axis_direction: Vec3) -> GeomResult<AxisFrame> {
181 if !(axis_origin.is_finite() && axis_direction.is_finite()) || axis_direction == Vec3::ZERO {
182 return Err(GeomError::InvalidInput(
183 "revolution axis must be a finite non-zero direction through a finite origin"
184 .to_owned(),
185 ));
186 }
187 if axis_direction.z != 0.0 || axis_origin.z != 0.0 {
190 return Err(unsupported(
191 "exact revolution about an axis out of the profile plane",
192 ));
193 }
194 let origin = Point2::new(axis_origin.x, axis_origin.y);
195 let length = axis_direction.x.hypot(axis_direction.y);
196 let unit = Vec2::new(axis_direction.x / length, axis_direction.y / length);
197 let placement = (axis_direction.x != 0.0).then(|| {
198 let normal = Vec2::new(unit.y, -unit.x);
199 Transform3::from_cols(
200 Vec3::new(normal.x, normal.y, 0.0),
201 Vec3::new(unit.x, unit.y, 0.0),
202 Vec3::Z,
203 Vec3::new(origin.x, origin.y, 0.0),
204 )
205 });
206 Ok(AxisFrame {
207 origin,
208 given: axis_direction,
209 unit,
210 placement,
211 })
212}
213
214fn into_axis_frame(ring: &ArcRing, frame: &AxisFrame) -> GeomResult<ArcRing> {
226 let normal = Vec2::new(frame.unit.y, -frame.unit.x);
227 let count = ring.vertices.len();
228 let mut points = Vec::with_capacity(count);
229 let mut on_axis = Vec::with_capacity(count);
230 for vertex in &ring.vertices {
231 let p = vertex.point;
232 if !(p.x.is_finite() && p.y.is_finite()) {
233 return Err(GeomError::InvalidInput(
234 "revolved section must have finite vertices".to_owned(),
235 ));
236 }
237 let offset = p - frame.origin;
238 let side = side_of_axis(frame.origin, frame.given, p);
239 let across = offset.x * normal.x + offset.y * normal.y;
240 let along = offset.x * frame.unit.x + offset.y * frame.unit.y;
241 let consistent = match side {
243 Sign::Zero => true,
244 Sign::Positive => across < 0.0,
245 Sign::Negative => across > 0.0,
246 _ => false,
247 };
248 if !consistent {
249 return Err(unsupported(
250 "exact revolution of a section with a vertex closer to the axis than the rounding of the axis frame",
251 ));
252 }
253 on_axis.push(side == Sign::Zero);
254 points.push(Point2::new(
255 if side == Sign::Zero { 0.0 } else { across },
256 along,
257 ));
258 }
259 for index in 0..count {
260 let next = (index + 1) % count;
261 if on_axis[index] == on_axis[next] || ring.vertices[index].bulge != 0.0 {
262 continue;
263 }
264 let (on, off) = if on_axis[index] {
265 (index, next)
266 } else {
267 (next, index)
268 };
269 let from = ring.vertices[index].point;
270 let to = ring.vertices[next].point;
271 if along_axis(frame.given, from, to) == Sign::Zero {
272 points[on].y = points[off].y;
273 } else if points[on].y == points[off].y {
274 return Err(unsupported(
275 "exact revolution of an edge ending on the axis within the tolerance of perpendicular to it but not exactly",
276 ));
277 }
278 }
279 Ok(ArcRing::new(
280 ring.vertices
281 .iter()
282 .zip(points)
283 .map(|(vertex, point)| ArcVertex {
284 point,
285 bulge: vertex.bulge,
286 })
287 .collect(),
288 ))
289}
290
291fn place_in_axis_frame(solid: ExactBRep, frame: &AxisFrame) -> GeomResult<ExactBRep> {
293 match &frame.placement {
294 None => Ok(solid),
295 Some(placement) => {
296 solid
297 .transformed(placement)
298 .map_err(|error| GeomError::BackendContractViolation {
299 backend: BACKEND_ID,
300 detail: format!("placing a revolution in its axis frame failed: {error}"),
301 })
302 }
303 }
304}
305
306fn revolve_rectangle(
308 rectangle: &RectangleProfile,
309 frame: &AxisFrame,
310 tolerance: Tolerance,
311) -> GeomResult<ExactBRep> {
312 let axis_origin = Point3::new(frame.origin.x, frame.origin.y, 0.0);
313 if rectangle.thickness.is_some()
314 || rectangle.outer_radius.is_some()
315 || rectangle.inner_radius.is_some()
316 {
317 return revolve_via_contour(&Profile::Rectangle(*rectangle), frame, None, tolerance);
321 }
322 if !rectangle.x.is_finite()
323 || !rectangle.y.is_finite()
324 || rectangle.x <= 0.0
325 || rectangle.y <= 0.0
326 {
327 return Err(GeomError::InvalidInput(format!(
328 "exact rectangle profile must have positive finite extents, got {} x {}",
329 rectangle.x, rectangle.y
330 )));
331 }
332
333 let (half_x, half_y) = (rectangle.x / 2.0, rectangle.y / 2.0);
334 let left = -half_x - axis_origin.x;
338 let right = half_x - axis_origin.x;
339
340 if left == 0.0 || right == 0.0 {
347 return revolve_via_contour(&Profile::Rectangle(*rectangle), frame, None, tolerance);
348 }
349 if left * right < 0.0 {
350 return Err(unsupported(
351 "exact revolution of a section crossing the axis",
352 ));
353 }
354 if left.abs() <= tolerance.linear() || right.abs() <= tolerance.linear() {
355 return Err(unsupported(
356 "exact revolution of a section within tolerance of the axis but not on it",
357 ));
358 }
359 let inner = left.abs().min(right.abs());
360 let outer = left.abs().max(right.abs());
361
362 let bottom = axis_origin.y - half_y;
364 let top = axis_origin.y + half_y;
365 let frame_at = |y: Scalar| crate::revolve_contour::frame_at(axis_origin.x, y);
367 let frame2 = Frame2 {
368 origin: Vec2::ZERO,
369 x: Vec2::X,
370 y: Vec2::Y,
371 };
372
373 let mut builder = ExactBRepBuilder::default();
374 builder
376 .topology_mut()
377 .try_reserve(4, 6, 6, 4, 1, 1)
378 .map_err(|_| GeomError::BudgetExceeded {
379 resource: "exact revolution topology",
380 })?;
381
382 let circle_edge = |builder: &mut ExactBRepBuilder, y: Scalar, radius: Scalar| {
384 let position = Point3::new(axis_origin.x + radius, y, 0.0);
385 let vertex = builder.topology_mut().add_vertex(Vertex { position });
386 let curve = builder.add_curve3(Curve3::Circle(Circle3 {
387 frame: frame_at(y),
388 radius,
389 }));
390 let edge = builder.topology_mut().add_edge(Edge {
391 start: vertex,
392 end: vertex,
393 curve: Some(curve),
394 });
395 builder.set_edge_interval(edge, Interval::new(0.0, TAU));
396 (edge, vertex, position)
397 };
398
399 let (bottom_outer, bottom_outer_vertex, bottom_outer_point) =
400 circle_edge(&mut builder, bottom, outer);
401 let (bottom_inner, bottom_inner_vertex, bottom_inner_point) =
402 circle_edge(&mut builder, bottom, inner);
403 let (top_outer, top_outer_vertex, _) = circle_edge(&mut builder, top, outer);
404 let (top_inner, top_inner_vertex, _) = circle_edge(&mut builder, top, inner);
405
406 let seam_edge = |builder: &mut ExactBRepBuilder, start, end, from: Point3| {
408 let curve = builder.add_curve3(Curve3::Line(Line3 {
409 origin: from,
410 direction: Vec3::new(0.0, top - bottom, 0.0),
411 }));
412 let edge = builder.topology_mut().add_edge(Edge {
413 start,
414 end,
415 curve: Some(curve),
416 });
417 builder.set_edge_interval(edge, Interval::UNIT);
418 edge
419 };
420 let outer_seam = seam_edge(
421 &mut builder,
422 bottom_outer_vertex,
423 top_outer_vertex,
424 bottom_outer_point,
425 );
426 let inner_seam = seam_edge(
427 &mut builder,
428 bottom_inner_vertex,
429 top_inner_vertex,
430 bottom_inner_point,
431 );
432
433 let circle_loop = |builder: &mut ExactBRepBuilder, edge, radius, orientation| {
436 let pcurve = builder.add_curve2(Curve2::Circle(Circle2 {
437 frame: frame2,
438 radius,
439 }));
440 let loop_id = builder.topology_mut().add_loop(Loop {
441 edges: vec![EdgeUse {
442 edge,
443 orientation,
444 pcurve: Some(pcurve),
445 }],
446 });
447 let interval = match orientation {
450 Orientation::Forward => Interval::new(0.0, TAU),
451 Orientation::Reversed => Interval::new(TAU, 0.0),
452 };
453 builder.set_pcurve_interval(loop_id, 0, interval);
454 loop_id
455 };
456
457 let bottom_outer_loop = circle_loop(&mut builder, bottom_outer, outer, Orientation::Forward);
458 let bottom_inner_loop = circle_loop(&mut builder, bottom_inner, inner, Orientation::Reversed);
459 let top_outer_loop = circle_loop(&mut builder, top_outer, outer, Orientation::Forward);
460 let top_inner_loop = circle_loop(&mut builder, top_inner, inner, Orientation::Reversed);
461
462 let wall_loop = |builder: &mut ExactBRepBuilder, lower, upper, seam, height: Scalar| {
468 let lower_pcurve = builder.add_curve2(Curve2::Line(Line2 {
469 origin: Vec2::ZERO,
470 direction: Vec2::X,
471 }));
472 let upper_pcurve = builder.add_curve2(Curve2::Line(Line2 {
473 origin: Vec2::new(0.0, height),
474 direction: Vec2::X,
475 }));
476 let up_pcurve = builder.add_curve2(Curve2::Line(Line2 {
477 origin: Vec2::new(TAU, 0.0),
478 direction: Vec2::new(0.0, height),
479 }));
480 let down_pcurve = builder.add_curve2(Curve2::Line(Line2 {
481 origin: Vec2::ZERO,
482 direction: Vec2::new(0.0, height),
483 }));
484 let loop_id = builder.topology_mut().add_loop(Loop {
485 edges: vec![
486 EdgeUse {
487 edge: lower,
488 orientation: Orientation::Forward,
489 pcurve: Some(lower_pcurve),
490 },
491 EdgeUse {
492 edge: seam,
493 orientation: Orientation::Forward,
494 pcurve: Some(up_pcurve),
495 },
496 EdgeUse {
497 edge: upper,
498 orientation: Orientation::Reversed,
499 pcurve: Some(upper_pcurve),
500 },
501 EdgeUse {
502 edge: seam,
503 orientation: Orientation::Reversed,
504 pcurve: Some(down_pcurve),
505 },
506 ],
507 });
508 builder.set_pcurve_interval(loop_id, 0, Interval::new(0.0, TAU));
509 builder.set_pcurve_interval(loop_id, 1, Interval::UNIT);
510 builder.set_pcurve_interval(loop_id, 2, Interval::new(TAU, 0.0));
511 builder.set_pcurve_interval(loop_id, 3, Interval::new(1.0, 0.0));
512 loop_id
513 };
514
515 let height = top - bottom;
516 let outer_loop = wall_loop(&mut builder, bottom_outer, top_outer, outer_seam, height);
517 let inner_loop = wall_loop(&mut builder, bottom_inner, top_inner, inner_seam, height);
518
519 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
520 frame: frame_at(bottom),
521 }));
522 let top_surface = builder.add_surface(Surface::Plane(Plane {
523 frame: frame_at(top),
524 }));
525 let outer_surface = builder.add_surface(Surface::Cylinder(Cylinder {
526 frame: frame_at(bottom),
527 radius: outer,
528 }));
529 let inner_surface = builder.add_surface(Surface::Cylinder(Cylinder {
530 frame: frame_at(bottom),
531 radius: inner,
532 }));
533
534 let annulus = |builder: &mut ExactBRepBuilder, surface, outer_loop, inner_loop, orientation| {
536 builder.topology_mut().add_face(Face {
537 surface: Some(surface),
538 bounds: vec![
539 FaceBound {
540 loop_id: outer_loop,
541 orientation: Orientation::Forward,
542 outer: true,
543 },
544 FaceBound {
545 loop_id: inner_loop,
546 orientation: Orientation::Forward,
547 outer: false,
548 },
549 ],
550 orientation,
551 })
552 };
553
554 let bottom_face = annulus(
555 &mut builder,
556 bottom_surface,
557 bottom_outer_loop,
558 bottom_inner_loop,
559 Orientation::Reversed,
560 );
561 let top_face = annulus(
562 &mut builder,
563 top_surface,
564 top_outer_loop,
565 top_inner_loop,
566 Orientation::Forward,
567 );
568
569 let wall_face = |builder: &mut ExactBRepBuilder, surface, loop_id, orientation| {
570 builder.topology_mut().add_face(Face {
571 surface: Some(surface),
572 bounds: vec![FaceBound {
573 loop_id,
574 orientation: Orientation::Forward,
575 outer: true,
576 }],
577 orientation,
578 })
579 };
580 let outer_face = wall_face(
583 &mut builder,
584 outer_surface,
585 outer_loop,
586 Orientation::Forward,
587 );
588 let inner_face = wall_face(
589 &mut builder,
590 inner_surface,
591 inner_loop,
592 Orientation::Reversed,
593 );
594
595 finish_solid(builder, vec![bottom_face, top_face, outer_face, inner_face])
596}
597
598fn finish_solid(mut builder: ExactBRepBuilder, faces: Vec<FaceId>) -> GeomResult<ExactBRep> {
604 let shell = builder.topology_mut().add_shell(Shell {
605 faces: faces
606 .into_iter()
607 .map(|face| (face, Orientation::Forward))
608 .collect(),
609 closed: true,
610 });
611 builder.topology_mut().add_solid(Solid {
612 outer: shell,
613 voids: Vec::new(),
614 });
615 let exact = builder
616 .finish()
617 .map_err(|error| GeomError::BackendContractViolation {
618 backend: BACKEND_ID,
619 detail: format!("exact revolution assembly failed: {error}"),
620 })?;
621 let health = axiolid_topology::audit_brep(exact.topology());
622 if !health.is_closed_manifold() {
623 return Err(GeomError::BackendContractViolation {
624 backend: BACKEND_ID,
625 detail: format!("exact revolution is not a closed manifold: {health:?}"),
626 });
627 }
628 Ok(exact)
629}
630
631pub fn fixed_reference_sweep_exact(
643 profile: &Profile,
644 path: &[Point3],
645 reference_direction: Vec3,
646 tolerance: Tolerance,
647) -> GeomResult<ExactBRep> {
648 if path.len() < 2 {
649 return Err(GeomError::InvalidInput(
650 "a sweep path needs at least two points".to_owned(),
651 ));
652 }
653 if !path.iter().all(|p| p.is_finite()) {
654 return Err(GeomError::InvalidInput(
655 "sweep path points must be finite".to_owned(),
656 ));
657 }
658 if !reference_direction.is_finite() || reference_direction.length() <= 0.0 {
659 return Err(GeomError::InvalidInput(
660 "sweep reference direction must be finite and non-zero".to_owned(),
661 ));
662 }
663
664 let start = path[0];
667 let end = path[path.len() - 1];
668 let span = end - start;
669 let length = span.length();
670 if length <= tolerance.linear() {
671 return Err(GeomError::Degenerate(
672 "sweep path start and end coincide, so the path has no direction".to_owned(),
673 ));
674 }
675 let direction = span / length;
676 for point in &path[1..path.len() - 1] {
677 let offset = *point - start;
678 let perpendicular = offset - direction * direction.dot(offset);
679 if perpendicular.length() > tolerance.linear() {
680 return Err(unsupported("exact sweep along a curved directrix"));
681 }
682 }
683
684 extrude_profile_exact(profile, direction, length, tolerance)
685}
686
687fn revolve_partial(
690 profile: &Profile,
691 frame: &AxisFrame,
692 angle: Scalar,
693 tolerance: Tolerance,
694) -> GeomResult<ExactBRep> {
695 match profile {
696 Profile::Ellipse(_) => return Err(unsupported("ellipse exact revolution")),
697 Profile::Rectangle(_)
698 | Profile::Circle(_)
699 | Profile::Section(_)
700 | Profile::Contour(_)
701 | Profile::Derived { .. }
702 | Profile::Composite(_)
703 | Profile::CenterLine(_) => {}
704 _ => return Err(unsupported("unknown profile exact revolution")),
705 }
706 revolve_via_contour(profile, frame, Some(angle), tolerance)
707}
708
709fn revolve_via_contour(
718 profile: &Profile,
719 frame: &AxisFrame,
720 angle: Option<Scalar>,
721 tolerance: Tolerance,
722) -> GeomResult<ExactBRep> {
723 let placed = frame.placement.is_some();
724 let (axis_origin, angle) = if placed {
725 (Point3::ZERO, angle)
726 } else {
727 (
730 Point3::new(frame.origin.x, frame.origin.y, 0.0),
731 angle.map(|angle| if frame.given.y < 0.0 { -angle } else { angle }),
732 )
733 };
734 let section = |ring: &ArcRing| -> GeomResult<ArcRing> {
735 if placed {
736 into_axis_frame(ring, frame)
737 } else {
738 Ok(ring.clone())
739 }
740 };
741 let magnitude = |rings: &[&ArcRing]| -> Scalar {
744 if !placed {
745 return 0.0;
746 }
747 rings
748 .iter()
749 .flat_map(|ring| ring.vertices.iter())
750 .fold(frame.origin.x.abs().max(frame.origin.y.abs()), |m, v| {
751 m.max(v.point.x.abs()).max(v.point.y.abs())
752 })
753 };
754
755 if let Profile::Composite(members) = profile {
758 let regions = crate::profile_lower::composite_regions(members, tolerance)?;
759 let mut pieces = Vec::with_capacity(regions.len());
760 for region in ®ions {
761 let holes = region
762 .holes
763 .iter()
764 .map(§ion)
765 .collect::<GeomResult<Vec<_>>>()?;
766 let rings: Vec<&ArcRing> = core::iter::once(®ion.outer)
767 .chain(region.holes.iter())
768 .collect();
769 pieces.push(revolve_region(
770 §ion(®ion.outer)?,
771 &holes,
772 axis_origin,
773 magnitude(&rings),
774 angle,
775 tolerance,
776 )?);
777 }
778 return place_in_axis_frame(crate::assemble::merge_solids(pieces)?, frame);
779 }
780
781 let contour = crate::extrude_exact::profile_to_contour(profile, tolerance)?;
782 let outer = crate::contour_lower::contour_to_arc_ring(&contour.outer, tolerance)?;
783 let holes = contour
784 .holes
785 .iter()
786 .map(|hole| crate::contour_lower::contour_to_arc_ring(hole, tolerance))
787 .collect::<GeomResult<Vec<_>>>()?;
788 let rings: Vec<&ArcRing> = core::iter::once(&outer).chain(holes.iter()).collect();
789 let scale = magnitude(&rings);
790 let holes = holes.iter().map(§ion).collect::<GeomResult<Vec<_>>>()?;
791 let solid = revolve_region(
792 §ion(&outer)?,
793 &holes,
794 axis_origin,
795 scale,
796 angle,
797 tolerance,
798 )?;
799 place_in_axis_frame(solid, frame)
800}
801
802fn revolve_region(
805 outer: &ArcRing,
806 holes: &[ArcRing],
807 axis_origin: Point3,
808 magnitude: Scalar,
809 angle: Option<Scalar>,
810 tolerance: Tolerance,
811) -> GeomResult<ExactBRep> {
812 if let Some(angle) = angle {
813 return crate::revolve_partial::revolve_section_partial(
814 outer,
815 holes,
816 axis_origin,
817 angle,
818 tolerance,
819 );
820 }
821 let solid =
822 crate::revolve_contour::revolve_ring_scaled(outer, axis_origin, magnitude, tolerance)?;
823 if holes.is_empty() {
824 return Ok(solid);
825 }
826 if solid.topology().solids().len() != 1 || !solid.topology().solids()[0].voids.is_empty() {
827 return Err(unsupported(
828 "exact revolution of a section with holes and several contacts with the axis",
829 ));
830 }
831 let mut builder = ExactBRepBuilder::default();
836 let outer = builder.append(&solid, false);
837 let mut voids = Vec::with_capacity(holes.len());
838 for hole in holes {
839 let cavity =
840 crate::revolve_contour::revolve_ring_scaled(hole, axis_origin, magnitude, tolerance)?;
841 voids.extend(builder.append(&cavity, true));
842 }
843 let [outer] = outer[..] else {
844 return Err(GeomError::Degenerate(
845 "a revolved section must have one outer shell".to_owned(),
846 ));
847 };
848 builder.topology_mut().add_solid(Solid { outer, voids });
849 builder.finish().map_err(|error| {
850 GeomError::Degenerate(format!(
851 "revolved section with holes did not assemble: {error}"
852 ))
853 })
854}