1use crate::curve::{NurbsCurve, Vec4};
60use crate::fit::interpolate_curve;
61use crate::surface::NurbsSurface;
62use crate::Vec3;
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum ImageCurveTier {
67 Affine,
69 Iso,
71 Approximated,
73}
74
75#[derive(Clone, Debug)]
78pub struct ImageCurve {
79 pub curve: NurbsCurve,
80 pub t0: f64,
82 pub t1: f64,
84 pub tier: ImageCurveTier,
85 pub deviation: f64,
87}
88
89const VERIFY_SAMPLES: usize = 24;
92
93const PROBES_PER_INTERVAL: usize = 5;
97
98const MAX_ROUNDS: usize = 14;
100
101const MAX_SAMPLES: usize = 4096;
103
104pub fn affine_image_curve(
110 sheet: &NurbsSurface,
111 pcurve: &NurbsCurve,
112) -> Result<NurbsCurve, String> {
113 let [u0, _] = sheet.domain_u()?;
114 let [v0, _] = sheet.domain_v()?;
115 let frame = sheet.derivatives(u0, v0, 1)?;
116 let origin = frame[0][0];
117 let du = frame[1][0];
118 let dv = frame[0][1];
119 let control_points = pcurve
120 .control_points
121 .iter()
122 .map(|control| {
123 let position = origin
124 .scale(control.w)
125 .add(du.scale(control.x - control.w * u0))
126 .add(dv.scale(control.y - control.w * v0));
127 Vec4 {
128 x: position.x,
129 y: position.y,
130 z: position.z,
131 w: control.w,
132 }
133 })
134 .collect();
135 NurbsCurve::new(pcurve.degree, pcurve.knots.clone(), control_points)
136}
137
138fn compose(surface: &NurbsSurface, pcurve: &NurbsCurve, q: f64) -> Result<Vec3, String> {
145 let uv = pcurve.evaluate(q)?;
146 surface.evaluate_extended(uv.x, uv.y)
147}
148
149fn sweep_deviation(
152 surface: &NurbsSurface,
153 pcurve: &NurbsCurve,
154 curve: &NurbsCurve,
155 t0: f64,
156 t1: f64,
157 count: usize,
158) -> Result<f64, String> {
159 let [q0, q1] = pcurve.domain()?;
160 let mut worst = 0.0f64;
161 for index in 0..=count {
162 let fraction = index as f64 / count as f64;
163 let target = compose(surface, pcurve, q0 + (q1 - q0) * fraction)?;
164 let value = curve.evaluate(t0 + (t1 - t0) * fraction)?;
165 worst = worst.max(value.sub(target).length());
166 }
167 Ok(worst)
168}
169
170fn iso_line(pcurve: &NurbsCurve, eps_u: f64, eps_v: f64) -> Result<Option<(bool, f64, f64)>, String> {
181 let [q0, q1] = pcurve.domain()?;
182 let first = pcurve.evaluate(q0)?;
183 let last = pcurve.evaluate(q1)?;
184 let constant_u = (first.x - last.x).abs() <= eps_u;
185 let constant_v = (first.y - last.y).abs() <= eps_v;
186 if constant_u == constant_v {
189 return Ok(None);
190 }
191 let span = q1 - q0;
192 for index in 1..8 {
193 let fraction = index as f64 / 8.0;
194 let uv = pcurve.evaluate(q0 + span * fraction)?;
195 let (held, varying, expected, eps_held, eps_vary) = if constant_u {
196 (
197 uv.x - first.x,
198 uv.y,
199 first.y + (last.y - first.y) * fraction,
200 eps_u,
201 eps_v,
202 )
203 } else {
204 (
205 uv.y - first.y,
206 uv.x,
207 first.x + (last.x - first.x) * fraction,
208 eps_v,
209 eps_u,
210 )
211 };
212 if held.abs() > eps_held || (varying - expected).abs() > eps_vary {
213 return Ok(None);
214 }
215 }
216 Ok(Some(if constant_u {
217 (true, first.y, last.y)
218 } else {
219 (false, first.x, last.x)
220 }))
221}
222
223fn iso_constant(pcurve: &NurbsCurve, constant_u: bool) -> Result<f64, String> {
225 let [q0, q1] = pcurve.domain()?;
226 let first = pcurve.evaluate(q0)?;
227 let last = pcurve.evaluate(q1)?;
228 Ok(if constant_u {
229 0.5 * (first.x + last.x)
230 } else {
231 0.5 * (first.y + last.y)
232 })
233}
234
235fn iso_epsilons(surface: &NurbsSurface) -> Result<(f64, f64), String> {
238 let [u0, u1] = surface.domain_u()?;
239 let [v0, v1] = surface.domain_v()?;
240 Ok((1e-7 * (u1 - u0).abs(), 1e-7 * (v1 - v0).abs()))
241}
242
243fn seed_parameters(pcurve: &NurbsCurve) -> Result<Vec<f64>, String> {
248 let [q0, q1] = pcurve.domain()?;
249 let span = q1 - q0;
250 let mut parameters = vec![q0, q1];
251 for knot in &pcurve.knots {
252 if *knot > q0 && *knot < q1 {
253 parameters.push(*knot);
254 }
255 }
256 for index in 1..8 {
257 parameters.push(q0 + span * index as f64 / 8.0);
258 }
259 parameters.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
260 parameters.dedup_by(|a, b| (*a - *b).abs() <= span.abs() * 1e-9);
261 Ok(parameters)
262}
263
264fn fit_and_measure(
268 surfaces: &[&NurbsSurface],
269 pcurve: &NurbsCurve,
270 parameters: &[f64],
271 which: usize,
272) -> Result<(NurbsCurve, f64, Vec<f64>), String> {
273 let surface = surfaces[which];
274 let points = parameters
275 .iter()
276 .map(|q| compose(surface, pcurve, *q))
277 .collect::<Result<Vec<_>, String>>()?;
278 let degree = 3.min(points.len() - 1);
279 let curve = interpolate_curve(&points, degree, parameters)?;
280 let mut worst = 0.0f64;
281 let mut per_interval = Vec::with_capacity(parameters.len() - 1);
282 for window in parameters.windows(2) {
283 let (a, b) = (window[0], window[1]);
284 let mut local = 0.0f64;
285 for probe in 1..=PROBES_PER_INTERVAL {
286 let q = a + (b - a) * probe as f64 / (PROBES_PER_INTERVAL + 1) as f64;
287 let target = compose(surface, pcurve, q)?;
288 local = local.max(curve.evaluate(q)?.sub(target).length());
289 }
290 worst = worst.max(local);
291 per_interval.push(local);
292 }
293 Ok((curve, worst, per_interval))
294}
295
296fn approximate(
301 surfaces: &[&NurbsSurface],
302 pcurve: &NurbsCurve,
303 tolerance: f64,
304 site: &str,
305) -> Result<Vec<ImageCurve>, String> {
306 let [q0, q1] = pcurve.domain()?;
307 let mut parameters = seed_parameters(pcurve)?;
308 let mut best = f64::INFINITY;
309 for _ in 0..MAX_ROUNDS {
310 let mut fits = Vec::with_capacity(surfaces.len());
311 let mut worst = 0.0f64;
312 let mut per_interval = vec![0.0f64; parameters.len() - 1];
313 for which in 0..surfaces.len() {
314 let (curve, sheet_worst, sheet_intervals) =
315 fit_and_measure(surfaces, pcurve, ¶meters, which)?;
316 worst = worst.max(sheet_worst);
317 for (slot, value) in per_interval.iter_mut().zip(&sheet_intervals) {
318 *slot = slot.max(*value);
319 }
320 fits.push(curve);
321 }
322 best = best.min(worst);
323 if worst <= tolerance {
324 return Ok(fits
325 .into_iter()
326 .map(|curve| ImageCurve {
327 curve,
328 t0: q0,
329 t1: q1,
330 tier: ImageCurveTier::Approximated,
331 deviation: worst,
332 })
333 .collect());
334 }
335 let mut refined = Vec::with_capacity(parameters.len() * 2);
339 for (index, window) in parameters.windows(2).enumerate() {
340 refined.push(window[0]);
341 if per_interval[index] > tolerance {
342 refined.push(0.5 * (window[0] + window[1]));
343 }
344 }
345 refined.push(parameters[parameters.len() - 1]);
346 if refined.len() == parameters.len() || refined.len() > MAX_SAMPLES {
347 break;
348 }
349 parameters = refined;
350 }
351 Err(format!(
352 "{site}: the 3D image of a general pcurve could not be fitted to tolerance \
353 (worst off-node deviation {best:.3e} > {tolerance:.3e} after {} samples) — refusing",
354 parameters.len()
355 ))
356}
357
358pub fn image_curve(
366 surface: &NurbsSurface,
367 pcurve: &NurbsCurve,
368 tolerance: f64,
369 site: &str,
370) -> Result<ImageCurve, String> {
371 let [q0, q1] = pcurve.domain()?;
372 if !(q1 - q0).is_finite() || (q1 - q0).abs() <= 0.0 {
373 return Err(format!("{site}: pcurve has an empty parameter domain"));
374 }
375
376 if surface.is_affine()? {
377 let curve = affine_image_curve(surface, pcurve)?;
378 let deviation = sweep_deviation(surface, pcurve, &curve, q0, q1, VERIFY_SAMPLES)?;
379 if deviation <= tolerance {
380 return Ok(ImageCurve {
381 curve,
382 t0: q0,
383 t1: q1,
384 tier: ImageCurveTier::Affine,
385 deviation,
386 });
387 }
388 }
389
390 let (eps_u, eps_v) = iso_epsilons(surface)?;
391 if let Some((constant_u, start, end)) = iso_line(pcurve, eps_u, eps_v)? {
392 let constant = iso_constant(pcurve, constant_u)?;
393 let curve = if constant_u {
394 surface.iso_curve_u(constant)?
395 } else {
396 surface.iso_curve_v(constant)?
397 };
398 let deviation = sweep_deviation(surface, pcurve, &curve, start, end, VERIFY_SAMPLES)?;
399 if deviation <= tolerance {
400 return Ok(ImageCurve {
401 curve,
402 t0: start,
403 t1: end,
404 tier: ImageCurveTier::Iso,
405 deviation,
406 });
407 }
408 }
409
410 Ok(approximate(&[surface], pcurve, tolerance, site)?
411 .pop()
412 .expect("one surface in, one image out"))
413}
414
415pub fn image_curve_pair(
423 first: &NurbsSurface,
424 second: &NurbsSurface,
425 pcurve: &NurbsCurve,
426 tolerance: f64,
427 site: &str,
428) -> Result<(ImageCurve, ImageCurve), String> {
429 let [q0, q1] = pcurve.domain()?;
430 if !(q1 - q0).is_finite() || (q1 - q0).abs() <= 0.0 {
431 return Err(format!("{site}: pcurve has an empty parameter domain"));
432 }
433
434 if first.is_affine()? && second.is_affine()? {
435 let a = affine_image_curve(first, pcurve)?;
436 let b = affine_image_curve(second, pcurve)?;
437 let deviation = sweep_deviation(first, pcurve, &a, q0, q1, VERIFY_SAMPLES)?
438 .max(sweep_deviation(second, pcurve, &b, q0, q1, VERIFY_SAMPLES)?);
439 if deviation <= tolerance {
440 return Ok((
441 ImageCurve {
442 curve: a,
443 t0: q0,
444 t1: q1,
445 tier: ImageCurveTier::Affine,
446 deviation,
447 },
448 ImageCurve {
449 curve: b,
450 t0: q0,
451 t1: q1,
452 tier: ImageCurveTier::Affine,
453 deviation,
454 },
455 ));
456 }
457 }
458
459 let (eps_u, eps_v) = iso_epsilons(first)?;
460 if let Some((constant_u, start, end)) = iso_line(pcurve, eps_u, eps_v)? {
461 let constant = iso_constant(pcurve, constant_u)?;
462 let (a, b) = if constant_u {
463 (first.iso_curve_u(constant)?, second.iso_curve_u(constant)?)
464 } else {
465 (first.iso_curve_v(constant)?, second.iso_curve_v(constant)?)
466 };
467 let deviation = sweep_deviation(first, pcurve, &a, start, end, VERIFY_SAMPLES)?
468 .max(sweep_deviation(second, pcurve, &b, start, end, VERIFY_SAMPLES)?);
469 if deviation <= tolerance {
470 return Ok((
471 ImageCurve {
472 curve: a,
473 t0: start,
474 t1: end,
475 tier: ImageCurveTier::Iso,
476 deviation,
477 },
478 ImageCurve {
479 curve: b,
480 t0: start,
481 t1: end,
482 tier: ImageCurveTier::Iso,
483 deviation,
484 },
485 ));
486 }
487 }
488
489 let mut images = approximate(&[first, second], pcurve, tolerance, site)?;
490 let second_image = images.pop().expect("two surfaces in, two images out");
491 let first_image = images.pop().expect("two surfaces in, two images out");
492 Ok((first_image, second_image))
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::curve::make_line;
499 use crate::surface::{make_cylinder_surface, make_plane, make_sphere_surface};
500
501 fn z() -> Vec3 {
502 Vec3::new(0.0, 0.0, 1.0)
503 }
504
505 fn diagonal(u0: f64, v0: f64, u1: f64, v1: f64) -> NurbsCurve {
508 make_line(Vec3::new(u0, v0, 0.0), Vec3::new(u1, v1, 0.0)).unwrap()
509 }
510
511 #[test]
512 fn affine_tier_is_exact_for_a_general_pcurve() {
513 let plane = make_plane(
514 Vec3::new(1.0, 2.0, 3.0),
515 Vec3::new(1.0, 0.0, 0.0),
516 Vec3::new(0.0, 1.0, 0.0),
517 4.0,
518 5.0,
519 )
520 .unwrap();
521 let image = image_curve(&plane, &diagonal(0.1, 0.2, 0.8, 0.9), 1e-9, "test").unwrap();
522 assert_eq!(image.tier, ImageCurveTier::Affine);
523 assert!(image.deviation <= 1e-12, "{}", image.deviation);
524 }
525
526 #[test]
527 fn iso_tier_is_taken_for_a_constant_u_line() {
528 let cylinder =
529 make_cylinder_surface(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0, 10.0)
530 .unwrap();
531 let [u0, u1] = cylinder.domain_u().unwrap();
532 let [v0, v1] = cylinder.domain_v().unwrap();
533 let u = 0.5 * (u0 + u1);
534 let pcurve = diagonal(u, v0 + 0.1 * (v1 - v0), u, v0 + 0.7 * (v1 - v0));
535 let image = image_curve(&cylinder, &pcurve, 1e-9, "test").unwrap();
536 assert_eq!(image.tier, ImageCurveTier::Iso);
537 assert!(image.deviation <= 1e-9, "{}", image.deviation);
538 }
539
540 #[test]
544 fn a_nonlinearly_parametrised_iso_pcurve_falls_through_to_the_general_tier() {
545 let cylinder =
546 make_cylinder_surface(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0, 10.0)
547 .unwrap();
548 let [u0, u1] = cylinder.domain_u().unwrap();
549 let [v0, v1] = cylinder.domain_v().unwrap();
550 let u = 0.5 * (u0 + u1);
551 let pcurve = NurbsCurve::new(
554 2,
555 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
556 vec![
557 Vec4::from_point(Vec3::new(u, v0, 0.0), 1.0),
558 Vec4::from_point(Vec3::new(u, v0 + 0.9 * (v1 - v0), 0.0), 1.0),
559 Vec4::from_point(Vec3::new(u, v1, 0.0), 1.0),
560 ],
561 )
562 .unwrap();
563 let image = image_curve(&cylinder, &pcurve, 1e-6, "test").unwrap();
564 assert_eq!(image.tier, ImageCurveTier::Approximated);
565 assert!(image.deviation <= 1e-6, "{}", image.deviation);
566 }
567
568 #[test]
569 fn general_tier_fits_a_diagonal_pcurve_on_a_cylinder() {
570 let cylinder =
571 make_cylinder_surface(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0, 10.0)
572 .unwrap();
573 let [u0, u1] = cylinder.domain_u().unwrap();
574 let [v0, v1] = cylinder.domain_v().unwrap();
575 let pcurve = diagonal(
576 u0 + 0.1 * (u1 - u0),
577 v0 + 0.1 * (v1 - v0),
578 u0 + 0.8 * (u1 - u0),
579 v0 + 0.9 * (v1 - v0),
580 );
581 let image = image_curve(&cylinder, &pcurve, 1e-6, "test").unwrap();
582 assert_eq!(image.tier, ImageCurveTier::Approximated);
583 assert!(image.deviation <= 1e-6, "{}", image.deviation);
584 let [q0, q1] = pcurve.domain().unwrap();
586 for index in 0..=37 {
587 let fraction = index as f64 / 37.0;
588 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction).unwrap();
589 let target = cylinder.evaluate(uv.x, uv.y).unwrap();
590 let value = image
591 .curve
592 .evaluate(image.t0 + (image.t1 - image.t0) * fraction)
593 .unwrap();
594 assert!(value.sub(target).length() <= 1e-6);
595 }
596 }
597
598 #[test]
602 fn general_tier_fits_a_diagonal_pcurve_on_an_exact_torus() {
603 let torus =
604 crate::surface::make_torus_surface(Vec3::new(0.0, 0.0, 0.0), z(), 6.0, 2.0).unwrap();
605 let [u0, u1] = torus.domain_u().unwrap();
606 let [v0, v1] = torus.domain_v().unwrap();
607 let pcurve = diagonal(
608 u0 + 0.15 * (u1 - u0),
609 v0 + 0.10 * (v1 - v0),
610 u0 + 0.85 * (u1 - u0),
611 v0 + 0.80 * (v1 - v0),
612 );
613 let image = image_curve(&torus, &pcurve, 1e-6, "test").unwrap();
614 assert_eq!(image.tier, ImageCurveTier::Approximated);
615 assert!(image.deviation <= 1e-6, "{}", image.deviation);
616 let [t0, t1] = image.curve.domain().unwrap();
619 for index in 0..=53 {
620 let point = image
621 .curve
622 .evaluate(t0 + (t1 - t0) * index as f64 / 53.0)
623 .unwrap();
624 let radial = (point.x * point.x + point.y * point.y).sqrt();
625 let tube = ((radial - 6.0).powi(2) + point.z * point.z).sqrt();
626 assert!((tube - 2.0).abs() <= 1e-6, "off the torus by {}", tube - 2.0);
627 }
628 }
629
630 #[test]
631 fn general_tier_refuses_with_the_measured_deviation_when_the_bar_is_unreachable() {
632 let sphere = make_sphere_surface(Vec3::new(0.0, 0.0, 0.0), 5.0, Vec3::new(0.0, 0.0, 1.0)).unwrap();
633 let [u0, u1] = sphere.domain_u().unwrap();
634 let [v0, v1] = sphere.domain_v().unwrap();
635 let pcurve = diagonal(
636 u0 + 0.05 * (u1 - u0),
637 v0 + 0.05 * (v1 - v0),
638 u0 + 0.95 * (u1 - u0),
639 v0 + 0.95 * (v1 - v0),
640 );
641 let error = image_curve(&sphere, &pcurve, 1e-18, "unit").unwrap_err();
643 assert!(error.starts_with("unit: "), "{error}");
644 assert!(error.contains("worst off-node deviation"), "{error}");
645 assert!(error.contains("refusing"), "{error}");
646 }
647
648 #[test]
649 fn a_paired_image_shares_one_basis() {
650 let inner =
651 make_cylinder_surface(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0, 10.0)
652 .unwrap();
653 let outer =
654 make_cylinder_surface(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.5, 10.0)
655 .unwrap();
656 let [u0, u1] = inner.domain_u().unwrap();
657 let [v0, v1] = inner.domain_v().unwrap();
658 let pcurve = diagonal(
659 u0 + 0.1 * (u1 - u0),
660 v0 + 0.1 * (v1 - v0),
661 u0 + 0.8 * (u1 - u0),
662 v0 + 0.9 * (v1 - v0),
663 );
664 let (a, b) = image_curve_pair(&inner, &outer, &pcurve, 1e-6, "test").unwrap();
665 assert_eq!(a.tier, ImageCurveTier::Approximated);
666 assert_eq!(a.curve.degree, b.curve.degree);
667 assert_eq!(a.curve.knots, b.curve.knots);
668 assert_eq!(
669 a.curve.control_points.len(),
670 b.curve.control_points.len()
671 );
672 for (first, second) in a.curve.control_points.iter().zip(&b.curve.control_points) {
673 assert!((first.w - second.w).abs() <= 1e-12);
674 }
675 }
676}