axiolid_construct/
swept_disk_exact.rs1use std::f64::consts::TAU;
26
27use axiolid_brep::{ExactBRep, TransformError};
28use axiolid_contracts::{GeomError, GeomResult, Operation};
29use axiolid_core::{Interval, Mat3, Point3, Scalar, Tolerance, Transform3, Vec3};
30use axiolid_curve::Circle3;
31use axiolid_profile::{CircleProfile, Profile};
32
33use crate::extrude_exact::extrude_profile_exact;
34use crate::revolve_exact::revolve_profile_exact;
35use crate::BACKEND_ID;
36
37fn unsupported(input: &'static str) -> GeomError {
38 GeomError::UnsupportedInput {
39 backend: BACKEND_ID,
40 operation: Operation::Sweep,
41 input,
42 }
43}
44
45fn disk(radius: Scalar, inner_radius: Option<Scalar>) -> GeomResult<Profile> {
47 if !radius.is_finite() || radius <= 0.0 {
48 return Err(GeomError::InvalidInput(format!(
49 "swept disk radius must be positive and finite, got {radius}"
50 )));
51 }
52 match inner_radius {
53 None | Some(0.0) => Ok(Profile::Circle(CircleProfile {
55 radius,
56 thickness: None,
57 })),
58 Some(inner) if inner.is_finite() && inner > 0.0 && inner < radius => {
59 Ok(Profile::Circle(CircleProfile {
60 radius,
61 thickness: Some(radius - inner),
62 }))
63 }
64 Some(inner) => Err(GeomError::InvalidInput(format!(
65 "swept disk inner radius must lie in [0, {radius}), got {inner}"
66 ))),
67 }
68}
69
70pub(crate) fn place(solid: &ExactBRep, transform: &Transform3) -> GeomResult<ExactBRep> {
73 solid.transformed(transform).map_err(|error| match error {
74 TransformError::Unsupported(what) => unsupported(what),
75 other => GeomError::InvalidInput(format!("swept disk placement: {other}")),
76 })
77}
78
79fn perpendicular(d: Vec3) -> Vec3 {
83 let a = d.abs();
84 let helper = if a.x <= a.y && a.x <= a.z {
85 Vec3::X
86 } else if a.y <= a.z {
87 Vec3::Y
88 } else {
89 Vec3::Z
90 };
91 (helper - d * helper.dot(d)).normalize()
92}
93
94pub fn swept_disk_along_line_exact(
103 start: Point3,
104 end: Point3,
105 radius: Scalar,
106 inner_radius: Option<Scalar>,
107 tolerance: Tolerance,
108) -> GeomResult<ExactBRep> {
109 let profile = disk(radius, inner_radius)?;
110 let along = end - start;
111 let length = along.length();
112 if !length.is_finite() || length <= tolerance.linear() {
113 return Err(GeomError::InvalidInput(format!(
114 "swept disk segment must be finite and longer than the tolerance, got {length}"
115 )));
116 }
117 let z = along / length;
118 let x = perpendicular(z);
119 let y = z.cross(x);
120 let profile = match profile {
123 hollow @ Profile::Circle(CircleProfile {
124 thickness: Some(_), ..
125 }) => Profile::Composite(vec![hollow]),
126 full => full,
127 };
128 let local = extrude_profile_exact(&profile, Vec3::Z, length, tolerance)?;
129 place(
130 &local,
131 &Transform3::from_mat3_translation(Mat3::from_cols(x, y, z), start),
132 )
133}
134
135pub fn swept_disk_along_arc_exact(
150 arc: &Circle3,
151 span: Interval,
152 radius: Scalar,
153 inner_radius: Option<Scalar>,
154 tolerance: Tolerance,
155) -> GeomResult<ExactBRep> {
156 let profile = disk(radius, inner_radius)?;
157 let bend = arc.radius;
158 if !bend.is_finite() || bend <= 0.0 {
159 return Err(GeomError::InvalidInput(format!(
160 "swept disk arc radius must be positive and finite, got {bend}"
161 )));
162 }
163 let angle = span.end - span.start;
164 if !angle.is_finite() || angle == 0.0 {
165 return Err(GeomError::InvalidInput(format!(
166 "swept disk arc span must be finite and non-zero, got {angle}"
167 )));
168 }
169 if angle.abs() > TAU + tolerance.linear() {
170 return Err(unsupported("swept disk along an arc beyond a full turn"));
171 }
172 if radius >= bend - tolerance.linear() {
173 return Err(unsupported("swept disk reaching its arc's axis"));
174 }
175 let frame = &arc.frame;
176 let axes = [frame.x, frame.y, frame.z];
177 let orthonormal = axes.iter().all(|a| a.is_finite())
178 && frame.origin.is_finite()
179 && (0..3).all(|i| {
180 (i..3).all(|j| {
181 let want = if i == j { 1.0 } else { 0.0 };
182 (axes[i].dot(axes[j]) - want).abs() <= 1e-12
183 })
184 })
185 && (frame.x.cross(frame.y) - frame.z).length() <= 1e-12;
186 if !orthonormal {
187 return Err(GeomError::InvalidInput(
188 "swept disk arc frame must be a right-handed orthonormal frame".to_owned(),
189 ));
190 }
191
192 let local = revolve_profile_exact(
196 &profile,
197 Point3::new(-bend, 0.0, 0.0),
198 Vec3::Y,
199 angle,
200 tolerance,
201 )?;
202 let (sin, cos) = span.start.sin_cos();
206 let radial = frame.x * cos + frame.y * sin;
207 let tangent = frame.z.cross(radial);
208 let rotation = Mat3::from_cols(radial, frame.z, -tangent);
209 let translation = frame.origin + radial * bend;
210 place(
211 &local,
212 &Transform3::from_mat3_translation(rotation, translation),
213 )
214}