1use axiolid_contracts::{GeomError, GeomResult};
32use axiolid_core::{Interval, Scalar};
33use axiolid_curve::{Curve2, Curve3};
34
35use crate::curve::{derivative2, derivative3, domain2, domain3};
36
37pub const ARC_LENGTH_TOLERANCE: Scalar = 1e-12;
41
42pub const MAX_PANELS: usize = 1 << 14;
44
45const MAX_ITERATIONS: usize = 200;
47
48pub(crate) const NODES: [Scalar; 8] = [
50 -0.960_289_856_497_536_2,
51 -0.796_666_477_413_626_7,
52 -0.525_532_409_916_328_9,
53 -0.183_434_642_495_649_8,
54 0.183_434_642_495_649_8,
55 0.525_532_409_916_328_9,
56 0.796_666_477_413_626_7,
57 0.960_289_856_497_536_2,
58];
59
60pub(crate) const WEIGHTS: [Scalar; 8] = [
62 0.101_228_536_290_376_3,
63 0.222_381_034_453_374_5,
64 0.313_706_645_877_887_3,
65 0.362_683_783_378_361_9,
66 0.362_683_783_378_361_9,
67 0.313_706_645_877_887_3,
68 0.222_381_034_453_374_5,
69 0.101_228_536_290_376_3,
70];
71
72fn invalid(detail: String) -> GeomError {
73 GeomError::InvalidInput(detail)
74}
75
76enum Family {
78 Proportional(Scalar),
80 Numeric {
84 domain: Option<(Scalar, Scalar)>,
85 breaks: Vec<Scalar>,
86 },
87}
88
89const ALL_BREAKS: usize = u16::MAX as usize + 1;
91
92fn bounded(domain: Interval) -> Option<(Scalar, Scalar)> {
93 Some((domain.start.min(domain.end), domain.start.max(domain.end)))
94}
95
96fn family2(curve: &Curve2) -> GeomResult<Family> {
97 Ok(match curve {
98 Curve2::Line(line) => {
99 let speed = line.direction.length();
100 if !(speed.is_finite() && speed > 0.0) {
101 return Err(invalid("line has no direction, so no arc length".into()));
102 }
103 Family::Proportional(speed)
104 }
105 Curve2::Intrinsic(_) | Curve2::Chain(_) => Family::Proportional(1.0),
106 Curve2::Circle(_) | Curve2::Ellipse(_) | Curve2::Sinusoid(_) => Family::Numeric {
107 domain: None,
108 breaks: Vec::new(),
109 },
110 _ => Family::Numeric {
111 domain: bounded(domain2(curve)),
112 breaks: crate::bound::continuity_breaks2(curve, ALL_BREAKS),
113 },
114 })
115}
116
117fn family3(curve: &Curve3) -> GeomResult<Family> {
118 Ok(match curve {
119 Curve3::Line(line) => {
120 let speed = line.direction.length();
121 if !(speed.is_finite() && speed > 0.0) {
122 return Err(invalid("line has no direction, so no arc length".into()));
123 }
124 Family::Proportional(speed)
125 }
126 Curve3::Intrinsic(_) => Family::Proportional(1.0),
127 Curve3::Circle(_) | Curve3::Ellipse(_) => Family::Numeric {
128 domain: None,
129 breaks: Vec::new(),
130 },
131 _ => Family::Numeric {
132 domain: bounded(domain3(curve)),
133 breaks: crate::bound::continuity_breaks3(curve, ALL_BREAKS),
134 },
135 })
136}
137
138pub fn arc_length2(curve: &Curve2, from: Scalar, to: Scalar) -> GeomResult<Scalar> {
142 let speed = |t| derivative2(curve, t).map(|d| d.length());
143 arc_length(&family2(curve)?, &speed, from, to)
144}
145
146pub fn arc_length3(curve: &Curve3, from: Scalar, to: Scalar) -> GeomResult<Scalar> {
149 let speed = |t| derivative3(curve, t).map(|d| d.length());
150 arc_length(&family3(curve)?, &speed, from, to)
151}
152
153pub fn parameter_at_arc_length2(
161 curve: &Curve2,
162 from: Scalar,
163 length: Scalar,
164) -> GeomResult<Scalar> {
165 let speed = |t| derivative2(curve, t).map(|d| d.length());
166 parameter_at(&family2(curve)?, &speed, from, length)
167}
168
169pub fn parameter_at_arc_length3(
171 curve: &Curve3,
172 from: Scalar,
173 length: Scalar,
174) -> GeomResult<Scalar> {
175 let speed = |t| derivative3(curve, t).map(|d| d.length());
176 parameter_at(&family3(curve)?, &speed, from, length)
177}
178
179fn tolerance_for(length: Scalar) -> Scalar {
180 ARC_LENGTH_TOLERANCE * length.abs().max(1.0)
181}
182
183fn check_finite(from: Scalar, other: Scalar, what: &str) -> GeomResult<()> {
184 if from.is_finite() && other.is_finite() {
185 Ok(())
186 } else {
187 Err(invalid(format!(
188 "arc length needs a finite start parameter and {what}"
189 )))
190 }
191}
192
193fn inside(domain: Option<(Scalar, Scalar)>, t: Scalar) -> bool {
194 domain.is_none_or(|(lo, hi)| {
195 let slack = 1e-12 * (hi - lo).abs().max(1.0);
196 t >= lo - slack && t <= hi + slack
197 })
198}
199
200fn arc_length<F>(family: &Family, speed: &F, from: Scalar, to: Scalar) -> GeomResult<Scalar>
201where
202 F: Fn(Scalar) -> GeomResult<Scalar>,
203{
204 check_finite(from, to, "a finite end parameter")?;
205 match family {
206 Family::Proportional(rate) => Ok(rate * (to - from)),
207 Family::Numeric { domain, breaks } => {
208 if !inside(*domain, from) || !inside(*domain, to) {
209 return Err(invalid(format!(
210 "arc length from parameter {from} to {to} leaves the curve's domain"
211 )));
212 }
213 let (lo, hi) = (from.min(to), from.max(to));
214 let magnitude = integrate(speed, breaks, lo, hi)?;
215 Ok(if to < from { -magnitude } else { magnitude })
216 }
217 }
218}
219
220fn integrate<F>(speed: &F, breaks: &[Scalar], lo: Scalar, hi: Scalar) -> GeomResult<Scalar>
222where
223 F: Fn(Scalar) -> GeomResult<Scalar>,
224{
225 if hi <= lo {
226 return Ok(0.0);
227 }
228 let mut cuts = vec![lo];
229 cuts.extend(breaks.iter().copied().filter(|&t| t > lo && t < hi));
230 cuts.push(hi);
231 let mut coarse = 0.0;
233 for w in cuts.windows(2) {
234 coarse += rule(speed, w[0], w[1])?;
235 }
236 let tolerance = tolerance_for(coarse);
237 let mut total = 0.0;
238 let mut panels = 0usize;
239 for w in cuts.windows(2) {
240 let share = tolerance * (w[1] - w[0]) / (hi - lo);
241 total += adaptive(speed, w[0], w[1], share, &mut panels)?;
242 }
243 if !total.is_finite() {
244 return Err(invalid(
245 "curve speed does not integrate to a finite arc length".into(),
246 ));
247 }
248 Ok(total)
249}
250
251fn rule<F>(speed: &F, a: Scalar, b: Scalar) -> GeomResult<Scalar>
253where
254 F: Fn(Scalar) -> GeomResult<Scalar>,
255{
256 let half = 0.5 * (b - a);
257 let mid = 0.5 * (a + b);
258 let mut sum = 0.0;
259 for (node, weight) in NODES.iter().zip(WEIGHTS.iter()) {
260 let value = speed(mid + half * node)?;
261 if !value.is_finite() {
262 return Err(invalid(format!(
263 "curve speed is not finite at parameter {}",
264 mid + half * node
265 )));
266 }
267 sum += weight * value;
268 }
269 Ok(sum * half)
270}
271
272fn adaptive<F>(
276 speed: &F,
277 a: Scalar,
278 b: Scalar,
279 tolerance: Scalar,
280 panels: &mut usize,
281) -> GeomResult<Scalar>
282where
283 F: Fn(Scalar) -> GeomResult<Scalar>,
284{
285 let width = b - a;
286 let mut stack = vec![(a, b, rule(speed, a, b)?)];
287 let mut total = 0.0;
288 while let Some((lo, hi, whole)) = stack.pop() {
289 *panels += 1;
290 if *panels > MAX_PANELS {
291 return Err(GeomError::BudgetExceeded {
292 resource: "arc-length quadrature panels",
293 });
294 }
295 let mid = 0.5 * (lo + hi);
296 let left = rule(speed, lo, mid)?;
297 let right = rule(speed, mid, hi)?;
298 let share = tolerance * (hi - lo) / width;
299 if (left + right - whole).abs() <= share || !(mid > lo && mid < hi) {
301 total += left + right;
302 } else {
303 stack.push((mid, hi, right));
304 stack.push((lo, mid, left));
305 }
306 }
307 Ok(total)
308}
309
310fn parameter_at<F>(family: &Family, speed: &F, from: Scalar, length: Scalar) -> GeomResult<Scalar>
311where
312 F: Fn(Scalar) -> GeomResult<Scalar>,
313{
314 check_finite(from, length, "a finite arc length")?;
315 let (domain, breaks) = match family {
316 Family::Proportional(rate) => return Ok(from + length / rate),
317 Family::Numeric { domain, breaks } => (*domain, breaks),
318 };
319 if !inside(domain, from) {
320 return Err(invalid(format!(
321 "arc length is measured from parameter {from}, outside the curve's domain"
322 )));
323 }
324 if length == 0.0 {
325 return Ok(from);
326 }
327 let direction = length.signum();
328 let target = length.abs();
329 let tolerance = tolerance_for(target);
330 let travelled = |x: Scalar| -> GeomResult<Scalar> {
332 let to = from + direction * x;
333 integrate(speed, breaks, from.min(to), from.max(to))
334 };
335 let (mut hi, mut at_hi) = match domain {
338 Some((lo, hi)) => {
339 let reach = if direction > 0.0 {
340 hi - from
341 } else {
342 from - lo
343 }
344 .max(0.0);
345 let available = travelled(reach)?;
346 if available < target - tolerance {
347 return Err(invalid(format!(
348 "arc length {target} exceeds the {available} available along the curve \
349 from parameter {from}"
350 )));
351 }
352 if available <= target {
353 return Ok(from + direction * reach);
354 }
355 (reach, available)
356 }
357 None => {
358 let mut reach: Scalar = 1.0;
359 let mut grown = 0;
360 loop {
361 let available = travelled(reach)?;
362 if available >= target {
363 break (reach, available);
364 }
365 grown += 1;
366 if grown > 64 {
367 return Err(invalid(format!(
368 "arc length {target} is not reached along the curve from parameter {from}"
369 )));
370 }
371 reach *= 2.0;
372 }
373 }
374 };
375 let mut lo = 0.0;
376 let mut at_lo = 0.0;
377 let mut x = hi * target / at_hi;
379 for _ in 0..MAX_ITERATIONS {
380 if !(x > lo && x < hi) {
381 x = 0.5 * (lo + hi);
382 }
383 let g = travelled(x)?;
384 let residual = g - target;
385 if residual.abs() <= tolerance {
386 let rate = speed(from + direction * x)?;
389 let polished = x - residual / rate;
390 let x = if rate.is_finite() && rate > 0.0 && polished >= lo && polished <= hi {
391 polished
392 } else {
393 x
394 };
395 return Ok(from + direction * x);
396 }
397 if residual < 0.0 {
398 lo = x;
399 at_lo = g;
400 } else {
401 hi = x;
402 at_hi = g;
403 }
404 let midpoint = 0.5 * (lo + hi);
405 if !(midpoint > lo && midpoint < hi) {
406 return Ok(from + direction * x);
409 }
410 let rate = speed(from + direction * x)?;
411 let newton = if rate.is_finite() && rate > 0.0 {
412 x - residual / rate
413 } else {
414 Scalar::NAN
415 };
416 x = if newton > lo && newton < hi {
417 newton
418 } else {
419 let secant = lo + (target - at_lo) * (hi - lo) / (at_hi - at_lo);
421 if secant > lo && secant < hi {
422 secant
423 } else {
424 midpoint
425 }
426 };
427 }
428 Err(GeomError::BudgetExceeded {
429 resource: "arc-length inverse iterations",
430 })
431}