1use std::f64::consts::TAU;
26
27use axiolid_brep::{ExactBRep, ExactBRepBuilder};
28use axiolid_contracts::{GeomError, GeomResult, Operation};
29use axiolid_core::{Frame2, Interval, Point3, Scalar, Tolerance, Vec2, Vec3};
30use axiolid_curve::{Circle2, Circle3, Curve2, Curve3, Line2, Line3};
31use axiolid_overlay::ArcRing;
32use axiolid_profile::{Profile, RectangleProfile};
33use axiolid_surface::{Cylinder, Plane, Surface};
34use axiolid_topology::{
35 Edge, EdgeUse, Face, FaceBound, FaceId, Loop, Orientation, Shell, Solid, Vertex,
36};
37
38use crate::extrude_exact::extrude_profile_exact;
39use crate::BACKEND_ID;
40
41fn unsupported(input: &'static str) -> GeomError {
42 GeomError::UnsupportedInput {
43 backend: BACKEND_ID,
44 operation: Operation::Sweep,
45 input,
46 }
47}
48
49pub fn revolve_profile_exact(
59 profile: &Profile,
60 axis_origin: Point3,
61 axis_direction: Vec3,
62 angle: Scalar,
63 tolerance: Tolerance,
64) -> GeomResult<ExactBRep> {
65 if !angle.is_finite() {
66 return Err(GeomError::InvalidInput(
67 "revolution angle must be finite".to_owned(),
68 ));
69 }
70 if (angle.abs() - TAU).abs() > tolerance.linear() {
73 return Err(unsupported("partial-turn exact revolution"));
74 }
75
76 match profile {
77 Profile::Rectangle(rectangle) => {
78 revolve_rectangle(rectangle, axis_origin, axis_direction, tolerance)
79 }
80 Profile::Circle(_) => revolve_via_contour(profile, axis_origin, axis_direction, tolerance),
83 Profile::Ellipse(_) => Err(unsupported("ellipse exact revolution")),
84 Profile::Section(_)
89 | Profile::Contour(_)
90 | Profile::Derived { .. }
91 | Profile::Composite(_)
92 | Profile::CenterLine(_) => {
93 revolve_via_contour(profile, axis_origin, axis_direction, tolerance)
94 }
95 _ => Err(unsupported("unknown profile exact revolution")),
96 }
97}
98
99fn revolve_rectangle(
100 rectangle: &RectangleProfile,
101 axis_origin: Point3,
102 axis_direction: Vec3,
103 tolerance: Tolerance,
104) -> GeomResult<ExactBRep> {
105 if rectangle.thickness.is_some()
106 || rectangle.outer_radius.is_some()
107 || rectangle.inner_radius.is_some()
108 {
109 return revolve_via_contour(
113 &Profile::Rectangle(*rectangle),
114 axis_origin,
115 axis_direction,
116 tolerance,
117 );
118 }
119 if !rectangle.x.is_finite()
120 || !rectangle.y.is_finite()
121 || rectangle.x <= 0.0
122 || rectangle.y <= 0.0
123 {
124 return Err(GeomError::InvalidInput(format!(
125 "exact rectangle profile must have positive finite extents, got {} x {}",
126 rectangle.x, rectangle.y
127 )));
128 }
129
130 let length = axis_direction.length();
131 if !length.is_finite() || length <= 0.0 {
132 return Err(GeomError::InvalidInput(
133 "revolution axis must be a finite non-zero direction".to_owned(),
134 ));
135 }
136 let axis = axis_direction / length;
137
138 if (axis.dot(Vec3::Y).abs() - 1.0).abs() > tolerance.linear() {
143 return Err(unsupported(
144 "exact revolution about an axis that is not the profile's local y",
145 ));
146 }
147 if axis_origin.z.abs() > tolerance.linear() {
148 return Err(unsupported(
149 "exact revolution about an axis off the profile plane",
150 ));
151 }
152
153 let (half_x, half_y) = (rectangle.x / 2.0, rectangle.y / 2.0);
154 let left = -half_x - axis_origin.x;
158 let right = half_x - axis_origin.x;
159
160 if left * right < 0.0 || left.abs() <= tolerance.linear() || right.abs() <= tolerance.linear() {
164 return Err(unsupported(
165 "exact revolution of a profile touching or crossing the axis",
166 ));
167 }
168 let inner = left.abs().min(right.abs());
169 let outer = left.abs().max(right.abs());
170
171 let bottom = axis_origin.y - half_y;
173 let top = axis_origin.y + half_y;
174 let frame_at = |y: Scalar| crate::revolve_contour::frame_at(axis_origin.x, y);
176 let frame2 = Frame2 {
177 origin: Vec2::ZERO,
178 x: Vec2::X,
179 y: Vec2::Y,
180 };
181
182 let mut builder = ExactBRepBuilder::default();
183 builder
185 .topology_mut()
186 .try_reserve(4, 6, 6, 4, 1, 1)
187 .map_err(|_| GeomError::BudgetExceeded {
188 resource: "exact revolution topology",
189 })?;
190
191 let circle_edge = |builder: &mut ExactBRepBuilder, y: Scalar, radius: Scalar| {
193 let position = Point3::new(axis_origin.x + radius, y, 0.0);
194 let vertex = builder.topology_mut().add_vertex(Vertex { position });
195 let curve = builder.add_curve3(Curve3::Circle(Circle3 {
196 frame: frame_at(y),
197 radius,
198 }));
199 let edge = builder.topology_mut().add_edge(Edge {
200 start: vertex,
201 end: vertex,
202 curve: Some(curve),
203 });
204 builder.set_edge_interval(edge, Interval::new(0.0, TAU));
205 (edge, vertex, position)
206 };
207
208 let (bottom_outer, bottom_outer_vertex, bottom_outer_point) =
209 circle_edge(&mut builder, bottom, outer);
210 let (bottom_inner, bottom_inner_vertex, bottom_inner_point) =
211 circle_edge(&mut builder, bottom, inner);
212 let (top_outer, top_outer_vertex, _) = circle_edge(&mut builder, top, outer);
213 let (top_inner, top_inner_vertex, _) = circle_edge(&mut builder, top, inner);
214
215 let seam_edge = |builder: &mut ExactBRepBuilder, start, end, from: Point3| {
217 let curve = builder.add_curve3(Curve3::Line(Line3 {
218 origin: from,
219 direction: Vec3::new(0.0, top - bottom, 0.0),
220 }));
221 let edge = builder.topology_mut().add_edge(Edge {
222 start,
223 end,
224 curve: Some(curve),
225 });
226 builder.set_edge_interval(edge, Interval::UNIT);
227 edge
228 };
229 let outer_seam = seam_edge(
230 &mut builder,
231 bottom_outer_vertex,
232 top_outer_vertex,
233 bottom_outer_point,
234 );
235 let inner_seam = seam_edge(
236 &mut builder,
237 bottom_inner_vertex,
238 top_inner_vertex,
239 bottom_inner_point,
240 );
241
242 let circle_loop = |builder: &mut ExactBRepBuilder, edge, radius, orientation| {
245 let pcurve = builder.add_curve2(Curve2::Circle(Circle2 {
246 frame: frame2,
247 radius,
248 }));
249 let loop_id = builder.topology_mut().add_loop(Loop {
250 edges: vec![EdgeUse {
251 edge,
252 orientation,
253 pcurve: Some(pcurve),
254 }],
255 });
256 let interval = match orientation {
259 Orientation::Forward => Interval::new(0.0, TAU),
260 Orientation::Reversed => Interval::new(TAU, 0.0),
261 };
262 builder.set_pcurve_interval(loop_id, 0, interval);
263 loop_id
264 };
265
266 let bottom_outer_loop = circle_loop(&mut builder, bottom_outer, outer, Orientation::Forward);
267 let bottom_inner_loop = circle_loop(&mut builder, bottom_inner, inner, Orientation::Reversed);
268 let top_outer_loop = circle_loop(&mut builder, top_outer, outer, Orientation::Forward);
269 let top_inner_loop = circle_loop(&mut builder, top_inner, inner, Orientation::Reversed);
270
271 let wall_loop = |builder: &mut ExactBRepBuilder, lower, upper, seam, height: Scalar| {
277 let lower_pcurve = builder.add_curve2(Curve2::Line(Line2 {
278 origin: Vec2::ZERO,
279 direction: Vec2::X,
280 }));
281 let upper_pcurve = builder.add_curve2(Curve2::Line(Line2 {
282 origin: Vec2::new(0.0, height),
283 direction: Vec2::X,
284 }));
285 let up_pcurve = builder.add_curve2(Curve2::Line(Line2 {
286 origin: Vec2::new(TAU, 0.0),
287 direction: Vec2::new(0.0, height),
288 }));
289 let down_pcurve = builder.add_curve2(Curve2::Line(Line2 {
290 origin: Vec2::ZERO,
291 direction: Vec2::new(0.0, height),
292 }));
293 let loop_id = builder.topology_mut().add_loop(Loop {
294 edges: vec![
295 EdgeUse {
296 edge: lower,
297 orientation: Orientation::Forward,
298 pcurve: Some(lower_pcurve),
299 },
300 EdgeUse {
301 edge: seam,
302 orientation: Orientation::Forward,
303 pcurve: Some(up_pcurve),
304 },
305 EdgeUse {
306 edge: upper,
307 orientation: Orientation::Reversed,
308 pcurve: Some(upper_pcurve),
309 },
310 EdgeUse {
311 edge: seam,
312 orientation: Orientation::Reversed,
313 pcurve: Some(down_pcurve),
314 },
315 ],
316 });
317 builder.set_pcurve_interval(loop_id, 0, Interval::new(0.0, TAU));
318 builder.set_pcurve_interval(loop_id, 1, Interval::UNIT);
319 builder.set_pcurve_interval(loop_id, 2, Interval::new(TAU, 0.0));
320 builder.set_pcurve_interval(loop_id, 3, Interval::new(1.0, 0.0));
321 loop_id
322 };
323
324 let height = top - bottom;
325 let outer_loop = wall_loop(&mut builder, bottom_outer, top_outer, outer_seam, height);
326 let inner_loop = wall_loop(&mut builder, bottom_inner, top_inner, inner_seam, height);
327
328 let bottom_surface = builder.add_surface(Surface::Plane(Plane {
329 frame: frame_at(bottom),
330 }));
331 let top_surface = builder.add_surface(Surface::Plane(Plane {
332 frame: frame_at(top),
333 }));
334 let outer_surface = builder.add_surface(Surface::Cylinder(Cylinder {
335 frame: frame_at(bottom),
336 radius: outer,
337 }));
338 let inner_surface = builder.add_surface(Surface::Cylinder(Cylinder {
339 frame: frame_at(bottom),
340 radius: inner,
341 }));
342
343 let annulus = |builder: &mut ExactBRepBuilder, surface, outer_loop, inner_loop, orientation| {
345 builder.topology_mut().add_face(Face {
346 surface: Some(surface),
347 bounds: vec![
348 FaceBound {
349 loop_id: outer_loop,
350 orientation: Orientation::Forward,
351 outer: true,
352 },
353 FaceBound {
354 loop_id: inner_loop,
355 orientation: Orientation::Forward,
356 outer: false,
357 },
358 ],
359 orientation,
360 })
361 };
362
363 let bottom_face = annulus(
364 &mut builder,
365 bottom_surface,
366 bottom_outer_loop,
367 bottom_inner_loop,
368 Orientation::Reversed,
369 );
370 let top_face = annulus(
371 &mut builder,
372 top_surface,
373 top_outer_loop,
374 top_inner_loop,
375 Orientation::Forward,
376 );
377
378 let wall_face = |builder: &mut ExactBRepBuilder, surface, loop_id, orientation| {
379 builder.topology_mut().add_face(Face {
380 surface: Some(surface),
381 bounds: vec![FaceBound {
382 loop_id,
383 orientation: Orientation::Forward,
384 outer: true,
385 }],
386 orientation,
387 })
388 };
389 let outer_face = wall_face(
392 &mut builder,
393 outer_surface,
394 outer_loop,
395 Orientation::Forward,
396 );
397 let inner_face = wall_face(
398 &mut builder,
399 inner_surface,
400 inner_loop,
401 Orientation::Reversed,
402 );
403
404 finish_solid(builder, vec![bottom_face, top_face, outer_face, inner_face])
405}
406
407fn finish_solid(mut builder: ExactBRepBuilder, faces: Vec<FaceId>) -> GeomResult<ExactBRep> {
413 let shell = builder.topology_mut().add_shell(Shell {
414 faces: faces
415 .into_iter()
416 .map(|face| (face, Orientation::Forward))
417 .collect(),
418 closed: true,
419 });
420 builder.topology_mut().add_solid(Solid {
421 outer: shell,
422 voids: Vec::new(),
423 });
424 let exact = builder
425 .finish()
426 .map_err(|error| GeomError::BackendContractViolation {
427 backend: BACKEND_ID,
428 detail: format!("exact revolution assembly failed: {error}"),
429 })?;
430 let health = axiolid_topology::audit_brep(exact.topology());
431 if !health.is_closed_manifold() {
432 return Err(GeomError::BackendContractViolation {
433 backend: BACKEND_ID,
434 detail: format!("exact revolution is not a closed manifold: {health:?}"),
435 });
436 }
437 Ok(exact)
438}
439
440pub fn fixed_reference_sweep_exact(
452 profile: &Profile,
453 path: &[Point3],
454 reference_direction: Vec3,
455 tolerance: Tolerance,
456) -> GeomResult<ExactBRep> {
457 if path.len() < 2 {
458 return Err(GeomError::InvalidInput(
459 "a sweep path needs at least two points".to_owned(),
460 ));
461 }
462 if !path.iter().all(|p| p.is_finite()) {
463 return Err(GeomError::InvalidInput(
464 "sweep path points must be finite".to_owned(),
465 ));
466 }
467 if !reference_direction.is_finite() || reference_direction.length() <= 0.0 {
468 return Err(GeomError::InvalidInput(
469 "sweep reference direction must be finite and non-zero".to_owned(),
470 ));
471 }
472
473 let start = path[0];
476 let end = path[path.len() - 1];
477 let span = end - start;
478 let length = span.length();
479 if length <= tolerance.linear() {
480 return Err(GeomError::Degenerate(
481 "sweep path start and end coincide, so the path has no direction".to_owned(),
482 ));
483 }
484 let direction = span / length;
485 for point in &path[1..path.len() - 1] {
486 let offset = *point - start;
487 let perpendicular = offset - direction * direction.dot(offset);
488 if perpendicular.length() > tolerance.linear() {
489 return Err(unsupported("exact sweep along a curved directrix"));
490 }
491 }
492
493 extrude_profile_exact(profile, direction, length, tolerance)
494}
495
496fn revolve_via_contour(
502 profile: &Profile,
503 axis_origin: Point3,
504 axis_direction: Vec3,
505 tolerance: Tolerance,
506) -> GeomResult<ExactBRep> {
507 let axis = axis_direction.normalize_or_zero();
508 if (axis.dot(Vec3::Y).abs() - 1.0).abs() > tolerance.linear() {
509 return Err(unsupported(
510 "exact revolution about an axis that is not the profile's local y",
511 ));
512 }
513 if axis_origin.z.abs() > tolerance.linear() {
514 return Err(unsupported(
515 "exact revolution about an axis off the profile plane",
516 ));
517 }
518
519 if let Profile::Composite(members) = profile {
522 let regions = crate::profile_lower::composite_regions(members, tolerance)?;
523 let mut pieces = Vec::with_capacity(regions.len());
524 for region in ®ions {
525 pieces.push(revolve_region(
526 ®ion.outer,
527 ®ion.holes,
528 axis_origin,
529 tolerance,
530 )?);
531 }
532 return crate::assemble::merge_solids(pieces);
533 }
534
535 let contour = crate::extrude_exact::profile_to_contour(profile, tolerance)?;
536 let outer = crate::contour_lower::contour_to_arc_ring(&contour.outer, tolerance)?;
537 let holes = contour
538 .holes
539 .iter()
540 .map(|hole| crate::contour_lower::contour_to_arc_ring(hole, tolerance))
541 .collect::<GeomResult<Vec<_>>>()?;
542 revolve_region(&outer, &holes, axis_origin, tolerance)
543}
544
545fn revolve_region(
548 outer: &ArcRing,
549 holes: &[ArcRing],
550 axis_origin: Point3,
551 tolerance: Tolerance,
552) -> GeomResult<ExactBRep> {
553 let solid = crate::revolve_contour::revolve_arc_ring(outer, axis_origin, tolerance)?;
554 if holes.is_empty() {
555 return Ok(solid);
556 }
557 let mut builder = ExactBRepBuilder::default();
562 let outer = builder.append(&solid, false);
563 let mut voids = Vec::with_capacity(holes.len());
564 for hole in holes {
565 let cavity = crate::revolve_contour::revolve_arc_ring(hole, axis_origin, tolerance)?;
566 voids.extend(builder.append(&cavity, true));
567 }
568 let [outer] = outer[..] else {
569 return Err(GeomError::Degenerate(
570 "a revolved section must have one outer shell".to_owned(),
571 ));
572 };
573 builder.topology_mut().add_solid(Solid { outer, voids });
574 builder.finish().map_err(|error| {
575 GeomError::Degenerate(format!(
576 "revolved section with holes did not assemble: {error}"
577 ))
578 })
579}