axiolid_evaluate/
intrinsic_relation.rs1use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
37use axiolid_core::{Frame3, Point3, Scalar, Vec3};
38use axiolid_curve::{CurvatureLaw, Intrinsic3};
39
40use crate::frenet::{frenet_frame, frenet_point};
41
42fn unsupported() -> GeomError {
43 GeomError::Unsupported {
44 backend: BackendId::new("axiolid-evaluate"),
45 operation: Operation::CurveEvaluation,
46 }
47}
48
49fn invalid(detail: &str) -> GeomError {
50 GeomError::InvalidInput(detail.to_owned())
51}
52
53pub fn trim_intrinsic3(curve: &Intrinsic3, start: Scalar, end: Scalar) -> GeomResult<Intrinsic3> {
59 if !start.is_finite() || !end.is_finite() {
60 return Err(invalid("trim bounds must be finite"));
61 }
62 if end <= start {
63 return Err(invalid("trim end must exceed trim start"));
64 }
65 if start < 0.0 || end > curve.length {
66 return Err(invalid("trim bounds must lie within the curve"));
67 }
68 let anchor = frenet_frame(curve, start)?;
69 let curvature = curve
70 .curvature
71 .shifted(start)
72 .ok_or_else(|| invalid("curvature law cannot be re-anchored at the trim start"))?;
73 let torsion = curve
74 .torsion
75 .shifted(start)
76 .ok_or_else(|| invalid("torsion law cannot be re-anchored at the trim start"))?;
77 Ok(Intrinsic3::new(anchor, curvature, torsion, end - start))
78}
79
80fn helix_parameters(curve: &Intrinsic3) -> Option<(Scalar, Scalar)> {
85 let k = curve.curvature.constant_value()?;
86 let tau = curve.torsion.constant_value()?;
87 let denominator = k * k + tau * tau;
88 if denominator <= 0.0 || !denominator.is_finite() {
89 return None;
90 }
91 Some((k / denominator, tau / denominator))
92}
93
94pub fn offset_intrinsic3(curve: &Intrinsic3, distance: Scalar) -> GeomResult<Intrinsic3> {
100 if !distance.is_finite() {
101 return Err(invalid("offset distance must be finite"));
102 }
103 if distance == 0.0 {
104 return Ok(curve.clone());
105 }
106 if !curve.is_helical() {
107 return Err(unsupported());
110 }
111 let (a, b) = helix_parameters(curve).ok_or_else(|| invalid("degenerate helix parameters"))?;
112 let radius = a - distance;
113 let c2 = radius.hypot(b);
114 if c2 <= 0.0 || !c2.is_finite() {
115 return Err(invalid("offset distance collapses the helix onto its axis"));
117 }
118 let base = a.hypot(b);
119 if base <= 0.0 || !base.is_finite() {
120 return Err(invalid("degenerate helix parameters"));
121 }
122 let curvature = CurvatureLaw::circular(radius / (c2 * c2));
123 let torsion = CurvatureLaw::circular(b / (c2 * c2));
124
125 let start = offset_start_frame(curve, distance, a, b)?;
129 Ok(Intrinsic3::new(
131 start,
132 curvature,
133 torsion,
134 curve.length * c2 / base,
135 ))
136}
137
138fn offset_start_frame(
140 curve: &Intrinsic3,
141 distance: Scalar,
142 a: Scalar,
143 b: Scalar,
144) -> GeomResult<Frame3> {
145 let base = a.hypot(b);
146 let tangent = curve.start.x;
147 let normal = curve.start.y;
148 let binormal = curve.start.z;
149 let axis = (tangent * b + binormal * a) / base;
152 let axial_component = tangent.dot(axis);
155 let perpendicular = tangent - axis * axial_component;
156 let perpendicular_length = perpendicular.length();
157 if perpendicular_length <= 0.0 || !perpendicular_length.is_finite() {
158 return Err(invalid("helix axis is parallel to its tangent"));
159 }
160 let tangential = perpendicular / perpendicular_length;
161 let radius = a - distance;
164 let velocity = tangential * radius + axis * b;
165 let speed = velocity.length();
166 if speed <= 0.0 || !speed.is_finite() {
167 return Err(invalid("offset has no well-defined tangent"));
168 }
169 let new_tangent = velocity / speed;
170 let new_normal = if radius >= 0.0 { normal } else { -normal };
173 let new_binormal = new_tangent.cross(new_normal);
174 let binormal_length = new_binormal.length();
175 if binormal_length <= 0.0 || !binormal_length.is_finite() {
176 return Err(invalid("offset frame is degenerate"));
177 }
178 Ok(Frame3 {
179 origin: curve.start.origin + normal * distance,
180 x: new_tangent,
181 y: new_normal,
182 z: new_binormal / binormal_length,
183 })
184}
185
186pub fn join_intrinsic3(
192 first: &Intrinsic3,
193 second: &Intrinsic3,
194 position_tolerance: Scalar,
195 direction_tolerance: Scalar,
196) -> GeomResult<Intrinsic3> {
197 if !position_tolerance.is_finite() || position_tolerance < 0.0 {
198 return Err(invalid(
199 "position tolerance must be finite and non-negative",
200 ));
201 }
202 if !direction_tolerance.is_finite() || direction_tolerance < 0.0 {
203 return Err(invalid(
204 "direction tolerance must be finite and non-negative",
205 ));
206 }
207 let end_point = frenet_point(first, first.length)?;
208 let end_frame = frenet_frame(first, first.length)?;
209 if !meets(end_point, second.start.origin, position_tolerance) {
210 return Err(invalid(
211 "curves do not meet: the second does not start where the first ends",
212 ));
213 }
214 if !aligned(end_frame.x, second.start.x, direction_tolerance) {
215 return Err(invalid(
216 "curves meet but their tangents disagree, so the join would kink",
217 ));
218 }
219 let curvature = CurvatureLaw::piecewise(
220 vec![first.length],
221 vec![first.curvature.clone(), second.curvature.clone()],
222 );
223 let torsion = CurvatureLaw::piecewise(
224 vec![first.length],
225 vec![first.torsion.clone(), second.torsion.clone()],
226 );
227 Ok(Intrinsic3::new(
228 first.start,
229 curvature,
230 torsion,
231 first.length + second.length,
232 ))
233}
234
235fn meets(a: Point3, b: Point3, tolerance: Scalar) -> bool {
236 (a - b).length() <= tolerance
237}
238
239fn aligned(a: Vec3, b: Vec3, tolerance: Scalar) -> bool {
240 (a - b).length() <= tolerance
241}