1use std::f64::consts::TAU;
36
37use axiolid_brep::{ExactBRep, ExactBRepBuilder};
38use axiolid_contracts::{GeomError, GeomResult, Operation};
39use axiolid_core::{Frame2, Interval, Point3, Scalar, Tolerance, Vec2, Vec3};
40use axiolid_curve::{Circle2, Circle3, Curve2, Curve3, Line2, Line3};
41use axiolid_overlay::ArcRing;
42use axiolid_profile::{Profile, RectangleProfile};
43use axiolid_surface::{Cylinder, Plane, Surface};
44use axiolid_topology::{
45 Edge, EdgeUse, Face, FaceBound, FaceId, Loop, Orientation, Shell, Solid, Vertex,
46};
47
48use crate::extrude_exact::extrude_profile_exact;
49use crate::BACKEND_ID;
50
51fn unsupported(input: &'static str) -> GeomError {
52 GeomError::UnsupportedInput {
53 backend: BACKEND_ID,
54 operation: Operation::Sweep,
55 input,
56 }
57}
58
59pub fn revolve_profile_exact(
77 profile: &Profile,
78 axis_origin: Point3,
79 axis_direction: Vec3,
80 angle: Scalar,
81 tolerance: Tolerance,
82) -> GeomResult<ExactBRep> {
83 if !angle.is_finite() || angle == 0.0 {
84 return Err(GeomError::InvalidInput(format!(
85 "revolution angle must be finite and non-zero, got {angle}"
86 )));
87 }
88 if angle.abs() > TAU + tolerance.linear() {
90 return Err(unsupported("exact revolution beyond a full turn"));
91 }
92 if (angle.abs() - TAU).abs() > tolerance.linear() {
95 return revolve_partial(profile, axis_origin, axis_direction, angle, tolerance);
96 }
97
98 match profile {
99 Profile::Rectangle(rectangle) => {
100 revolve_rectangle(rectangle, axis_origin, axis_direction, tolerance)
101 }
102 Profile::Circle(_) => {
105 revolve_via_contour(profile, axis_origin, axis_direction, None, tolerance)
106 }
107 Profile::Ellipse(_) => Err(unsupported("ellipse exact revolution")),
108 Profile::Section(_)
113 | Profile::Contour(_)
114 | Profile::Derived { .. }
115 | Profile::Composite(_)
116 | Profile::CenterLine(_) => {
117 revolve_via_contour(profile, axis_origin, axis_direction, None, tolerance)
118 }
119 _ => Err(unsupported("unknown profile exact revolution")),
120 }
121}
122
123fn revolve_rectangle(
124 rectangle: &RectangleProfile,
125 axis_origin: Point3,
126 axis_direction: Vec3,
127 tolerance: Tolerance,
128) -> GeomResult<ExactBRep> {
129 if rectangle.thickness.is_some()
130 || rectangle.outer_radius.is_some()
131 || rectangle.inner_radius.is_some()
132 {
133 return revolve_via_contour(
137 &Profile::Rectangle(*rectangle),
138 axis_origin,
139 axis_direction,
140 None,
141 tolerance,
142 );
143 }
144 if !rectangle.x.is_finite()
145 || !rectangle.y.is_finite()
146 || rectangle.x <= 0.0
147 || rectangle.y <= 0.0
148 {
149 return Err(GeomError::InvalidInput(format!(
150 "exact rectangle profile must have positive finite extents, got {} x {}",
151 rectangle.x, rectangle.y
152 )));
153 }
154
155 let length = axis_direction.length();
156 if !length.is_finite() || length <= 0.0 {
157 return Err(GeomError::InvalidInput(
158 "revolution axis must be a finite non-zero direction".to_owned(),
159 ));
160 }
161 let axis = axis_direction / length;
162
163 if (axis.dot(Vec3::Y).abs() - 1.0).abs() > tolerance.linear() {
168 return Err(unsupported(
169 "exact revolution about an axis that is not the profile's local y",
170 ));
171 }
172 if axis_origin.z.abs() > tolerance.linear() {
173 return Err(unsupported(
174 "exact revolution about an axis off the profile plane",
175 ));
176 }
177
178 let (half_x, half_y) = (rectangle.x / 2.0, rectangle.y / 2.0);
179 let left = -half_x - axis_origin.x;
183 let right = half_x - axis_origin.x;
184
185 if left * right < 0.0 || left.abs() <= tolerance.linear() || right.abs() <= tolerance.linear() {
189 return Err(unsupported(
190 "exact revolution of a profile touching or crossing the axis",
191 ));
192 }
193 let inner = left.abs().min(right.abs());
194 let outer = left.abs().max(right.abs());
195
196 let bottom = axis_origin.y - half_y;
198 let top = axis_origin.y + half_y;
199 let frame_at = |y: Scalar| crate::revolve_contour::frame_at(axis_origin.x, y);
201 let frame2 = Frame2 {
202 origin: Vec2::ZERO,
203 x: Vec2::X,
204 y: Vec2::Y,
205 };
206
207 let mut builder = ExactBRepBuilder::default();
208 builder
210 .topology_mut()
211 .try_reserve(4, 6, 6, 4, 1, 1)
212 .map_err(|_| GeomError::BudgetExceeded {
213 resource: "exact revolution topology",
214 })?;
215
216 let circle_edge = |builder: &mut ExactBRepBuilder, y: Scalar, radius: Scalar| {
218 let position = Point3::new(axis_origin.x + radius, y, 0.0);
219 let vertex = builder.topology_mut().add_vertex(Vertex { position });
220 let curve = builder.add_curve3(Curve3::Circle(Circle3 {
221 frame: frame_at(y),
222 radius,
223 }));
224 let edge = builder.topology_mut().add_edge(Edge {
225 start: vertex,
226 end: vertex,
227 curve: Some(curve),
228 });
229 builder.set_edge_interval(edge, Interval::new(0.0, TAU));
230 (edge, vertex, position)
231 };
232
233 let (bottom_outer, bottom_outer_vertex, bottom_outer_point) =
234 circle_edge(&mut builder, bottom, outer);
235 let (bottom_inner, bottom_inner_vertex, bottom_inner_point) =
236 circle_edge(&mut builder, bottom, inner);
237 let (top_outer, top_outer_vertex, _) = circle_edge(&mut builder, top, outer);
238 let (top_inner, top_inner_vertex, _) = circle_edge(&mut builder, top, inner);
239
240 let seam_edge = |builder: &mut ExactBRepBuilder, start, end, from: Point3| {
242 let curve = builder.add_curve3(Curve3::Line(Line3 {
243 origin: from,
244 direction: Vec3::new(0.0, top - bottom, 0.0),
245 }));
246 let edge = builder.topology_mut().add_edge(Edge {
247 start,
248 end,
249 curve: Some(curve),
250 });
251 builder.set_edge_interval(edge, Interval::UNIT);
252 edge
253 };
254 let outer_seam = seam_edge(
255 &mut builder,
256 bottom_outer_vertex,
257 top_outer_vertex,
258 bottom_outer_point,
259 );
260 let inner_seam = seam_edge(
261 &mut builder,
262 bottom_inner_vertex,
263 top_inner_vertex,
264 bottom_inner_point,
265 );
266
267 let circle_loop = |builder: &mut ExactBRepBuilder, edge, radius, orientation| {
270 let pcurve = builder.add_curve2(Curve2::Circle(Circle2 {
271 frame: frame2,
272 radius,
273 }));
274 let loop_id = builder.topology_mut().add_loop(Loop {
275 edges: vec![EdgeUse {
276 edge,
277 orientation,
278 pcurve: Some(pcurve),
279 }],
280 });
281 let interval = match orientation {
284 Orientation::Forward => Interval::new(0.0, TAU),
285 Orientation::Reversed => Interval::new(TAU, 0.0),
286 };
287 builder.set_pcurve_interval(loop_id, 0, interval);
288 loop_id
289 };
290
291 let bottom_outer_loop = circle_loop(&mut builder, bottom_outer, outer, Orientation::Forward);
292 let bottom_inner_loop = circle_loop(&mut builder, bottom_inner, inner, Orientation::Reversed);
293 let top_outer_loop = circle_loop(&mut builder, top_outer, outer, Orientation::Forward);
294 let top_inner_loop = circle_loop(&mut builder, top_inner, inner, Orientation::Reversed);
295
296 let wall_loop = |builder: &mut ExactBRepBuilder, lower, upper, seam, height: Scalar| {
302 let lower_pcurve = builder.add_curve2(Curve2::Line(Line2 {
303 origin: Vec2::ZERO,
304 direction: Vec2::X,
305 }));
306 let upper_pcurve = builder.add_curve2(Curve2::Line(Line2 {
307 origin: Vec2::new(0.0, height),
308 direction: Vec2::X,
309 }));
310 let up_pcurve = builder.add_curve2(Curve2::Line(Line2 {
311 origin: Vec2::new(TAU, 0.0),
312 direction: Vec2::new(0.0, height),
313 }));
314 let down_pcurve = builder.add_curve2(Curve2::Line(Line2 {
315 origin: Vec2::ZERO,
316 direction: Vec2::new(0.0, height),
317 }));
318 let loop_id = builder.topology_mut().add_loop(Loop {
319 edges: vec![
320 EdgeUse {
321 edge: lower,
322 orientation: Orientation::Forward,
323 pcurve: Some(lower_pcurve),
324 },
325 EdgeUse {
326 edge: seam,
327 orientation: Orientation::Forward,
328 pcurve: Some(up_pcurve),
329 },
330 EdgeUse {
331 edge: upper,
332 orientation: Orientation::Reversed,
333 pcurve: Some(upper_pcurve),
334 },
335 EdgeUse {
336 edge: seam,
337 orientation: Orientation::Reversed,
338 pcurve: Some(down_pcurve),
339 },
340 ],
341 });
342 builder.set_pcurve_interval(loop_id, 0, Interval::new(0.0, TAU));
343 builder.set_pcurve_interval(loop_id, 1, Interval::UNIT);
344 builder.set_pcurve_interval(loop_id, 2, Interval::new(TAU, 0.0));
345 builder.set_pcurve_interval(loop_id, 3, Interval::new(1.0, 0.0));
346 loop_id
347 };
348
349 let height = top - bottom;
350 let outer_loop = wall_loop(&mut builder, bottom_outer, top_outer, outer_seam, height);
351 let inner_loop = wall_loop(&mut builder, bottom_inner, top_inner, inner_seam, height);
352
353 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
354 frame: frame_at(bottom),
355 }));
356 let top_surface = builder.add_surface(Surface::Plane(Plane {
357 frame: frame_at(top),
358 }));
359 let outer_surface = builder.add_surface(Surface::Cylinder(Cylinder {
360 frame: frame_at(bottom),
361 radius: outer,
362 }));
363 let inner_surface = builder.add_surface(Surface::Cylinder(Cylinder {
364 frame: frame_at(bottom),
365 radius: inner,
366 }));
367
368 let annulus = |builder: &mut ExactBRepBuilder, surface, outer_loop, inner_loop, orientation| {
370 builder.topology_mut().add_face(Face {
371 surface: Some(surface),
372 bounds: vec![
373 FaceBound {
374 loop_id: outer_loop,
375 orientation: Orientation::Forward,
376 outer: true,
377 },
378 FaceBound {
379 loop_id: inner_loop,
380 orientation: Orientation::Forward,
381 outer: false,
382 },
383 ],
384 orientation,
385 })
386 };
387
388 let bottom_face = annulus(
389 &mut builder,
390 bottom_surface,
391 bottom_outer_loop,
392 bottom_inner_loop,
393 Orientation::Reversed,
394 );
395 let top_face = annulus(
396 &mut builder,
397 top_surface,
398 top_outer_loop,
399 top_inner_loop,
400 Orientation::Forward,
401 );
402
403 let wall_face = |builder: &mut ExactBRepBuilder, surface, loop_id, orientation| {
404 builder.topology_mut().add_face(Face {
405 surface: Some(surface),
406 bounds: vec![FaceBound {
407 loop_id,
408 orientation: Orientation::Forward,
409 outer: true,
410 }],
411 orientation,
412 })
413 };
414 let outer_face = wall_face(
417 &mut builder,
418 outer_surface,
419 outer_loop,
420 Orientation::Forward,
421 );
422 let inner_face = wall_face(
423 &mut builder,
424 inner_surface,
425 inner_loop,
426 Orientation::Reversed,
427 );
428
429 finish_solid(builder, vec![bottom_face, top_face, outer_face, inner_face])
430}
431
432fn finish_solid(mut builder: ExactBRepBuilder, faces: Vec<FaceId>) -> GeomResult<ExactBRep> {
438 let shell = builder.topology_mut().add_shell(Shell {
439 faces: faces
440 .into_iter()
441 .map(|face| (face, Orientation::Forward))
442 .collect(),
443 closed: true,
444 });
445 builder.topology_mut().add_solid(Solid {
446 outer: shell,
447 voids: Vec::new(),
448 });
449 let exact = builder
450 .finish()
451 .map_err(|error| GeomError::BackendContractViolation {
452 backend: BACKEND_ID,
453 detail: format!("exact revolution assembly failed: {error}"),
454 })?;
455 let health = axiolid_topology::audit_brep(exact.topology());
456 if !health.is_closed_manifold() {
457 return Err(GeomError::BackendContractViolation {
458 backend: BACKEND_ID,
459 detail: format!("exact revolution is not a closed manifold: {health:?}"),
460 });
461 }
462 Ok(exact)
463}
464
465pub fn fixed_reference_sweep_exact(
477 profile: &Profile,
478 path: &[Point3],
479 reference_direction: Vec3,
480 tolerance: Tolerance,
481) -> GeomResult<ExactBRep> {
482 if path.len() < 2 {
483 return Err(GeomError::InvalidInput(
484 "a sweep path needs at least two points".to_owned(),
485 ));
486 }
487 if !path.iter().all(|p| p.is_finite()) {
488 return Err(GeomError::InvalidInput(
489 "sweep path points must be finite".to_owned(),
490 ));
491 }
492 if !reference_direction.is_finite() || reference_direction.length() <= 0.0 {
493 return Err(GeomError::InvalidInput(
494 "sweep reference direction must be finite and non-zero".to_owned(),
495 ));
496 }
497
498 let start = path[0];
501 let end = path[path.len() - 1];
502 let span = end - start;
503 let length = span.length();
504 if length <= tolerance.linear() {
505 return Err(GeomError::Degenerate(
506 "sweep path start and end coincide, so the path has no direction".to_owned(),
507 ));
508 }
509 let direction = span / length;
510 for point in &path[1..path.len() - 1] {
511 let offset = *point - start;
512 let perpendicular = offset - direction * direction.dot(offset);
513 if perpendicular.length() > tolerance.linear() {
514 return Err(unsupported("exact sweep along a curved directrix"));
515 }
516 }
517
518 extrude_profile_exact(profile, direction, length, tolerance)
519}
520
521fn revolve_partial(
524 profile: &Profile,
525 axis_origin: Point3,
526 axis_direction: Vec3,
527 angle: Scalar,
528 tolerance: Tolerance,
529) -> GeomResult<ExactBRep> {
530 match profile {
531 Profile::Ellipse(_) => return Err(unsupported("ellipse exact revolution")),
532 Profile::Rectangle(_)
533 | Profile::Circle(_)
534 | Profile::Section(_)
535 | Profile::Contour(_)
536 | Profile::Derived { .. }
537 | Profile::Composite(_)
538 | Profile::CenterLine(_) => {}
539 _ => return Err(unsupported("unknown profile exact revolution")),
540 }
541 revolve_via_contour(profile, axis_origin, axis_direction, Some(angle), tolerance)
542}
543
544fn revolve_via_contour(
551 profile: &Profile,
552 axis_origin: Point3,
553 axis_direction: Vec3,
554 angle: Option<Scalar>,
555 tolerance: Tolerance,
556) -> GeomResult<ExactBRep> {
557 let axis = axis_direction.normalize_or_zero();
558 if (axis.dot(Vec3::Y).abs() - 1.0).abs() > tolerance.linear() {
559 return Err(unsupported(
560 "exact revolution about an axis that is not the profile's local y",
561 ));
562 }
563 if axis_origin.z.abs() > tolerance.linear() {
564 return Err(unsupported(
565 "exact revolution about an axis off the profile plane",
566 ));
567 }
568
569 let angle = angle.map(|angle| if axis.y < 0.0 { -angle } else { angle });
572
573 if let Profile::Composite(members) = profile {
576 let regions = crate::profile_lower::composite_regions(members, tolerance)?;
577 let mut pieces = Vec::with_capacity(regions.len());
578 for region in ®ions {
579 pieces.push(revolve_region(
580 ®ion.outer,
581 ®ion.holes,
582 axis_origin,
583 angle,
584 tolerance,
585 )?);
586 }
587 return crate::assemble::merge_solids(pieces);
588 }
589
590 let contour = crate::extrude_exact::profile_to_contour(profile, tolerance)?;
591 let outer = crate::contour_lower::contour_to_arc_ring(&contour.outer, tolerance)?;
592 let holes = contour
593 .holes
594 .iter()
595 .map(|hole| crate::contour_lower::contour_to_arc_ring(hole, tolerance))
596 .collect::<GeomResult<Vec<_>>>()?;
597 revolve_region(&outer, &holes, axis_origin, angle, tolerance)
598}
599
600fn revolve_region(
603 outer: &ArcRing,
604 holes: &[ArcRing],
605 axis_origin: Point3,
606 angle: Option<Scalar>,
607 tolerance: Tolerance,
608) -> GeomResult<ExactBRep> {
609 if let Some(angle) = angle {
610 return crate::revolve_partial::revolve_section_partial(
611 outer,
612 holes,
613 axis_origin,
614 angle,
615 tolerance,
616 );
617 }
618 let solid = crate::revolve_contour::revolve_arc_ring(outer, axis_origin, tolerance)?;
619 if holes.is_empty() {
620 return Ok(solid);
621 }
622 let mut builder = ExactBRepBuilder::default();
627 let outer = builder.append(&solid, false);
628 let mut voids = Vec::with_capacity(holes.len());
629 for hole in holes {
630 let cavity = crate::revolve_contour::revolve_arc_ring(hole, axis_origin, tolerance)?;
631 voids.extend(builder.append(&cavity, true));
632 }
633 let [outer] = outer[..] else {
634 return Err(GeomError::Degenerate(
635 "a revolved section must have one outer shell".to_owned(),
636 ));
637 };
638 builder.topology_mut().add_solid(Solid { outer, voids });
639 builder.finish().map_err(|error| {
640 GeomError::Degenerate(format!(
641 "revolved section with holes did not assemble: {error}"
642 ))
643 })
644}