1use axiolid_contracts::{GeomError, GeomResult};
63use axiolid_core::{Interval, Scalar, Vec3};
64use axiolid_curve::{Banked3, Curve2, Curve3, Elevated3};
65
66use crate::arc_length::{plan_derivative, plan_second_derivative};
67use crate::bound::{frame_stretch2, CurveDerivativeBounds, ROUNDING};
68
69fn invalid(detail: &str) -> GeomError {
70 GeomError::InvalidInput(detail.to_owned())
71}
72
73#[must_use]
78pub fn elevated_span(curve: &Elevated3) -> Option<Scalar> {
79 let span = match curve.plan.as_ref() {
80 Curve2::Line(_) => return Some(Scalar::INFINITY),
81 Curve2::Circle(c) => core::f64::consts::TAU * c.radius,
82 Curve2::Intrinsic(i) => i.length,
83 Curve2::Chain(c) => c.length()?,
84 _ => return None,
85 };
86 (span.is_finite() && span > 0.0).then_some(span)
87}
88
89pub(crate) fn elevated_domain(curve: &Elevated3) -> Interval {
92 elevated_span(curve).map_or(
93 Interval {
94 start: 0.0,
95 end: 0.0,
96 },
97 |end| Interval { start: 0.0, end },
98 )
99}
100
101pub fn elevated_derivative(curve: &Elevated3, d: Scalar) -> GeomResult<Vec3> {
108 if !d.is_finite() {
109 return Err(invalid("plan distance must be finite"));
110 }
111 let plan = plan_derivative(&curve.plan, d)?;
112 let grade = crate::elevation::elevation_grade(&curve.elevation, d)?;
113 let out = Vec3::new(plan.x, plan.y, grade);
114 if out.is_finite() {
115 Ok(out)
116 } else {
117 Err(invalid("elevated curve derivative is not finite"))
118 }
119}
120
121pub fn elevated_second_derivative(curve: &Elevated3, d: Scalar) -> GeomResult<Vec3> {
128 if !d.is_finite() {
129 return Err(invalid("plan distance must be finite"));
130 }
131 let plan = plan_second_derivative(&curve.plan, d)?;
132 let bend = crate::elevation::elevation_second(&curve.elevation, d)?;
133 let out = Vec3::new(plan.x, plan.y, bend);
134 if out.is_finite() {
135 Ok(out)
136 } else {
137 Err(invalid("elevated curve second derivative is not finite"))
138 }
139}
140
141fn plan_bounds(plan: &Curve2, lo: Scalar, hi: Scalar) -> Option<[Scalar; 3]> {
145 let slack = |length: Scalar| 1e-9 * length.max(1.0);
146 let out = match plan {
147 Curve2::Line(line) => {
148 let length = line.direction.length();
149 if !(length.is_finite() && length > 0.0) {
150 return None;
151 }
152 [1.0, 0.0, 0.0]
153 }
154 Curve2::Circle(c) => {
155 if !(c.radius.is_finite() && c.radius > 0.0) {
156 return None;
157 }
158 let stretch = frame_stretch2(&c.frame);
159 let k = 1.0 / c.radius;
160 [stretch, stretch * k, stretch * k * k]
161 }
162 Curve2::Intrinsic(i) => {
163 if !(lo >= 0.0 && hi <= i.length + slack(i.length)) {
164 return None;
165 }
166 let stretch = frame_stretch2(&i.start);
167 let k = crate::chain::curvature_bound(&i.curvature, lo, hi)?;
168 let seamed = i
169 .curvature
170 .seams_within(i.length)
171 .iter()
172 .any(|&seam| seam > lo && seam < hi);
173 let rate = if seamed {
174 Scalar::INFINITY
175 } else {
176 crate::chain::curvature_bound(&i.curvature.derivative(), lo, hi)?
177 };
178 [stretch, stretch * k, stretch * (rate + k * k)]
179 }
180 Curve2::Chain(chain) => {
181 let (p2, p3) = crate::chain::chain_plan_bounds(chain, lo, hi)?;
182 [frame_stretch2(&chain.start), p2, p3]
183 }
184 _ => return None,
185 };
186 (out[0].is_finite() && out[1].is_finite()).then_some(out)
187}
188
189#[must_use]
194pub fn elevated_breaks(curve: &Elevated3, k: usize) -> Vec<Scalar> {
195 if k == 0 {
196 return Vec::new();
197 }
198 let mut out = crate::bound::continuity_breaks2(&curve.plan, 1);
199 out.extend(crate::elevation::elevation_seams(&curve.elevation));
200 if k >= 2 {
201 match curve.plan.as_ref() {
202 Curve2::Intrinsic(i) => out.extend(i.curvature.seams_within(i.length)),
203 Curve2::Chain(chain) => out.extend(crate::chain::chain_curvature_seams(chain)),
204 _ => {}
205 }
206 }
207 out.retain(|b| b.is_finite());
208 out.sort_by(Scalar::total_cmp);
209 out.dedup();
210 out
211}
212
213#[must_use]
219pub fn banked_breaks(curve: &Banked3, k: usize) -> Vec<Scalar> {
220 if k == 0 {
221 return Vec::new();
222 }
223 let mut out = elevated_breaks(&curve.base, k);
224 out.extend(curve.pivot.seams());
225 out.extend(crate::banked::derived_cant_seams(curve));
226 out.sort_by(Scalar::total_cmp);
227 out.dedup();
228 out
229}
230
231fn inside(breaks: &[Scalar], lo: Scalar, hi: Scalar) -> bool {
232 breaks.iter().any(|&b| b > lo && b < hi)
233}
234
235#[must_use]
243pub fn elevated_chord_bound(curve: &Elevated3, a: Scalar, b: Scalar) -> Option<Scalar> {
244 let (lo, hi) = (a.min(b), a.max(b));
245 if !(lo.is_finite() && hi.is_finite()) {
246 return None;
247 }
248 if lo == hi {
249 return Some(0.0);
250 }
251 if inside(&elevated_breaks(curve, 1), lo, hi) {
252 return None;
253 }
254 let h = hi - lo;
255 let plan = h * h * 0.125 * plan_bounds(&curve.plan, lo, hi)?[1];
256 let profile = crate::elevation::elevation_chord_bound(&curve.elevation, lo, hi)?;
257 let bound = plan.hypot(profile) * (1.0 + ROUNDING);
258 bound.is_finite().then_some(bound)
259}
260
261#[must_use]
264pub fn banked_chord_bound(curve: &Banked3, a: Scalar, b: Scalar) -> Option<Scalar> {
265 let (lo, hi) = (a.min(b), a.max(b));
266 if !(lo.is_finite() && hi.is_finite()) {
267 return None;
268 }
269 if lo == hi {
270 return Some(0.0);
271 }
272 if lo < 0.0 || inside(&banked_breaks(curve, 1), lo, hi) {
273 return None;
274 }
275 let h = hi - lo;
276 let taylor = h * h * 0.125;
277 let plan = taylor * plan_bounds(&curve.base.plan, lo, hi)?[1];
278 let profile = crate::elevation::elevation_chord_bound(&curve.base.elevation, lo, hi)?;
279 let pivot = taylor * crate::banked::pivot_bounds(curve, lo, hi)?[1];
280 let bound = plan.hypot(profile + pivot) * (1.0 + ROUNDING);
281 bound.is_finite().then_some(bound)
282}
283
284#[must_use]
289pub fn elevated_derivative_bounds(
290 curve: &Elevated3,
291 a: Scalar,
292 b: Scalar,
293) -> Option<CurveDerivativeBounds> {
294 let (lo, hi) = (a.min(b), a.max(b));
295 if !(lo.is_finite() && hi.is_finite()) || inside(&elevated_breaks(curve, 2), lo, hi) {
296 return None;
297 }
298 let plan = plan_bounds(&curve.plan, lo, hi)?;
299 let profile = crate::elevation::profile_bounds(&curve.elevation, lo, hi)?;
300 combined(plan, profile)
301}
302
303#[must_use]
306pub fn banked_derivative_bounds(
307 curve: &Banked3,
308 a: Scalar,
309 b: Scalar,
310) -> Option<CurveDerivativeBounds> {
311 let (lo, hi) = (a.min(b), a.max(b));
312 if !(lo.is_finite() && hi.is_finite()) || lo < 0.0 || inside(&banked_breaks(curve, 2), lo, hi) {
313 return None;
314 }
315 let plan = plan_bounds(&curve.base.plan, lo, hi)?;
316 let profile = crate::elevation::profile_bounds(&curve.base.elevation, lo, hi)?;
317 let pivot = crate::banked::pivot_bounds(curve, lo, hi)?;
318 combined(
319 plan,
320 [
321 profile[0] + pivot[0],
322 profile[1] + pivot[1],
323 profile[2] + pivot[2],
324 ],
325 )
326}
327
328fn combined(plan: [Scalar; 3], vertical: [Scalar; 3]) -> Option<CurveDerivativeBounds> {
329 let bound = |j: usize| plan[j].hypot(vertical[j]) * (1.0 + ROUNDING);
330 let out = CurveDerivativeBounds {
331 first: bound(0),
332 second: bound(1),
333 third: bound(2),
334 };
335 (out.first.is_finite() && out.second.is_finite() && out.third.is_finite()).then_some(out)
336}
337
338pub(crate) fn certifies_elevated(curve: &Elevated3) -> bool {
346 fn closed_form(law: &axiolid_curve::ElevationLaw) -> bool {
347 use axiolid_curve::ElevationLaw as L;
348 match law {
349 L::Polynomial { .. } | L::CircularArc { .. } => true,
350 L::Piecewise { laws, .. } => laws.iter().all(closed_form),
351 _ => false,
352 }
353 }
354 let plan = match curve.plan.as_ref() {
355 Curve2::Line(_) | Curve2::Circle(_) => true,
356 Curve2::Intrinsic(i) => {
357 crate::chain::curvature_bound(&i.curvature, 0.0, i.length.max(0.0)).is_some()
358 }
359 Curve2::Chain(chain) => crate::chain::chain_certifies(chain),
360 _ => false,
361 };
362 plan && curve.elevation.is_well_formed() && closed_form(&curve.elevation)
363}
364
365pub fn grade_corners3(curve: &Curve3, angle: Scalar) -> GeomResult<Vec<Scalar>> {
377 let (elevated, banked) = match curve {
378 Curve3::Elevated(e) => (e, None),
379 Curve3::Banked(b) => (&b.base, Some(b)),
380 _ => return Ok(Vec::new()),
381 };
382 let mut seams = crate::elevation::elevation_seams(&elevated.elevation);
383 if let Some(banked) = banked {
384 seams.extend(banked.pivot.seams());
385 seams.extend(crate::banked::derived_cant_seams(banked));
386 }
387 seams.sort_by(Scalar::total_cmp);
388 seams.dedup();
389 let mut out = Vec::new();
390 for seam in seams {
391 let mut before = crate::elevation::elevation_grade_before(&elevated.elevation, seam)?;
392 let mut after = crate::elevation::elevation_grade(&elevated.elevation, seam)?;
393 if let Some(banked) = banked {
394 let rate = |before: bool| {
395 crate::banked::pivot_jet(banked, seam, before)
396 .map(|jet| jet[1])
397 .ok_or_else(|| invalid("banked curve: pivot law has no rate at a seam"))
398 };
399 before += rate(true)?;
400 after += rate(false)?;
401 }
402 if (before.atan() - after.atan()).abs() > angle {
403 out.push(seam);
404 }
405 }
406 Ok(out)
407}