1use crate::{
2 interpolate_curve, project_point_to_surface, project_point_to_surface_seeded, AnalyticSurface,
3 KnotVector, NurbsCurve, NurbsSurface, Vec3, Vec4,
4};
5
6const EPSILON: f64 = 1e-12;
7const LINEAR_TOLERANCE: f64 = 1e-7;
8
9fn surface_domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), String> {
10 Ok((
11 KnotVector::new(surface.knots_u.clone(), surface.degree_u)?.domain(),
12 KnotVector::new(surface.knots_v.clone(), surface.degree_v)?.domain(),
13 ))
14}
15
16fn surface_closedness(surface: &NurbsSurface) -> Result<(bool, bool), String> {
17 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
18 let mut closed_u = true;
19 let mut closed_v = true;
20 for fraction in [0.19, 0.52, 0.87] {
21 let v = v0 + (v1 - v0) * fraction;
22 if surface
23 .evaluate(u0, v)?
24 .sub(surface.evaluate(u1, v)?)
25 .length()
26 > LINEAR_TOLERANCE * 10.0
27 {
28 closed_u = false;
29 }
30 let u = u0 + (u1 - u0) * fraction;
31 if surface
32 .evaluate(u, v0)?
33 .sub(surface.evaluate(u, v1)?)
34 .length()
35 > LINEAR_TOLERANCE * 10.0
36 {
37 closed_v = false;
38 }
39 }
40 Ok((closed_u, closed_v))
41}
42
43fn invert_checked(surface: &NurbsSurface, point: Vec3) -> Result<([f64; 2], f64), String> {
44 if surface.is_affine()? {
45 let ([u0, _], [v0, _]) = surface_domains(surface)?;
46 let (origin, du, dv) = surface.deriv1(u0, v0)?;
47 let delta = point.sub(origin);
48 let uu = du.dot(du);
49 let uv = du.dot(dv);
50 let vv = dv.dot(dv);
51 let along_u = delta.dot(du);
52 let along_v = delta.dot(dv);
53 let determinant = uu * vv - uv * uv;
54 if determinant.abs() > EPSILON {
55 return Ok((
56 [
57 u0 + (along_u * vv - along_v * uv) / determinant,
58 v0 + (along_v * uu - along_u * uv) / determinant,
59 ],
60 0.0,
61 ));
62 }
63 }
64 let projection = project_point_to_surface(surface, point)?;
65 Ok(([projection.u, projection.v], projection.distance))
66}
67
68fn unwrap_periodic(values: &mut [f64], minimum: f64, maximum: f64) {
69 let period = maximum - minimum;
70 for index in 1..values.len() {
71 while values[index] - values[index - 1] > period / 2.0 {
72 values[index] -= period;
73 }
74 while values[index] - values[index - 1] < -period / 2.0 {
75 values[index] += period;
76 }
77 }
78 if values.len() > 1 {
79 while values[0] - values[1] > period / 2.0 {
80 values[0] -= period;
81 }
82 while values[0] - values[1] < -period / 2.0 {
83 values[0] += period;
84 }
85 }
86 if values.len() > 1 {
96 let excursion = |shift: f64| -> f64 {
97 values
98 .iter()
99 .map(|value| {
100 let v = value + shift;
101 (minimum - v).max(0.0) + (v - maximum).max(0.0)
102 })
103 .sum::<f64>()
104 };
105 let mean = values.iter().copied().sum::<f64>() / values.len() as f64;
106 let center = 0.5 * (minimum + maximum);
107 let base_k = ((center - mean) / period).round() as i64;
108 let mut best_shift = 0.0;
109 let mut best_excursion = f64::INFINITY;
110 for k in (base_k - 1)..=(base_k + 1) {
111 let shift = k as f64 * period;
112 let value = excursion(shift);
113 if value < best_excursion {
114 best_excursion = value;
115 best_shift = shift;
116 }
117 }
118 if best_shift != 0.0 {
119 for value in values.iter_mut() {
120 *value += best_shift;
121 }
122 }
123 }
124}
125
126fn build_interpolant(
127 surface: &NurbsSurface,
128 raw_parameters: &[[f64; 2]],
129 curve_parameters: &[f64],
130) -> Result<NurbsCurve, String> {
131 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
132 let (closed_u, closed_v) = surface_closedness(surface)?;
133 let mut parameters = raw_parameters.to_vec();
134 let sphere_u_singular = if matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })) {
146 let speeds = parameters
147 .iter()
148 .map(|parameter| {
149 surface
150 .deriv1(parameter[0], parameter[1])
151 .map(|(_, su, _)| su.length())
152 })
153 .collect::<Result<Vec<_>, _>>()?;
154 let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
155 speeds
156 .into_iter()
157 .map(|speed| speed <= reference * 1e-2)
162 .collect::<Vec<_>>()
163 } else {
164 vec![false; parameters.len()]
165 };
166 let has_interior_pole_crossing = sphere_u_singular
174 .iter()
175 .position(|singular| !singular)
176 .zip(sphere_u_singular.iter().rposition(|singular| !singular))
177 .is_some_and(|(first, last)| sphere_u_singular[first..=last].iter().any(|value| *value));
178 let period = (u1 - u0).abs();
179 let interior_pole_branch_reset = closed_u
180 && has_interior_pole_crossing
181 && parameters.windows(2).zip(sphere_u_singular.windows(2)).any(
182 |(parameter_pair, singular_pair)| {
183 (singular_pair[0] || singular_pair[1])
184 && (parameter_pair[1][0] - parameter_pair[0][0]).abs()
185 >= 0.5 * period - 1e-12 * period.max(1.0)
186 },
187 );
188 if closed_u {
189 let mut values = parameters.iter().map(|value| value[0]).collect::<Vec<_>>();
190 if interior_pole_branch_reset {
191 let mut start = 0;
192 while start < values.len() {
193 while start < values.len() && sphere_u_singular[start] {
194 start += 1;
195 }
196 let mut end = start;
197 while end < values.len() && !sphere_u_singular[end] {
198 end += 1;
199 }
200 unwrap_periodic(&mut values[start..end], u0, u1);
201 start = end;
202 }
203 } else {
204 unwrap_periodic(&mut values, u0, u1);
205 }
206 for (parameter, value) in parameters.iter_mut().zip(values) {
207 parameter[0] = value;
208 }
209 }
210 if closed_v {
211 let mut values = parameters.iter().map(|value| value[1]).collect::<Vec<_>>();
212 unwrap_periodic(&mut values, v0, v1);
213 for (parameter, value) in parameters.iter_mut().zip(values) {
214 parameter[1] = value;
215 }
216 }
217 let wrap = surface.analytic().is_none();
227 let points = parameters
228 .into_iter()
229 .map(|parameter| {
230 let u = if closed_u && wrap {
231 parameter[0]
232 } else {
233 parameter[0].clamp(u0, u1)
234 };
235 let v = if closed_v && wrap {
236 parameter[1]
237 } else {
238 parameter[1].clamp(v0, v1)
239 };
240 Vec3::new(u, v, 0.0)
241 })
242 .collect::<Vec<_>>();
243 interpolate_curve(&points, 3usize.min(points.len() - 1), curve_parameters)
244}
245
246pub fn build_pcurve_on_surface(
252 surface: &NurbsSurface,
253 curve: &NurbsCurve,
254) -> Result<NurbsCurve, String> {
255 let [t0, t1] = curve.domain()?;
256 if surface.is_affine()? {
257 let ([u0, _], [v0, _]) = surface_domains(surface)?;
258 let (origin, du, dv) = surface.deriv1(u0, v0)?;
259 let uu = du.dot(du);
260 let uv = du.dot(dv);
261 let vv = dv.dot(dv);
262 let determinant = uu * vv - uv * uv;
263 if determinant.abs() <= 1e-18 {
264 return Err("build_pcurve_on_surface: singular affine parameterization".into());
265 }
266 let control_points = curve
267 .control_points
268 .iter()
269 .map(|control| {
270 let delta = control.point()?.sub(origin);
271 let along_u = delta.dot(du);
272 let along_v = delta.dot(dv);
273 Ok(Vec4::from_point(
274 Vec3::new(
275 u0 + (along_u * vv - along_v * uv) / determinant,
276 v0 + (along_v * uu - along_u * uv) / determinant,
277 0.0,
278 ),
279 control.w,
280 ))
281 })
282 .collect::<Result<Vec<_>, String>>()?;
283 return NurbsCurve::new(curve.degree, curve.knots.clone(), control_points);
284 }
285
286 let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
287 let drop_tolerance = 1e-4 * scale;
288 let endpoint_tolerance = 1e-3 * scale;
289 let refinement_tolerance = 1e-3f64.min(1e-4 * scale);
290 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
291 let invert =
292 |fraction: f64| invert_checked(surface, curve.evaluate(t0 + (t1 - t0) * fraction)?);
293
294 let mut parameters = Vec::with_capacity(25);
295 let mut raw_surface_parameters = Vec::with_capacity(25);
296 for index in 0..=24 {
297 let fraction = index as f64 / 24.0;
298 let (parameter, distance) = invert(fraction)?;
299 if distance > drop_tolerance && index != 0 && index != 24 {
300 continue;
301 }
302 if distance > endpoint_tolerance {
303 if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
304 let p3 = curve.evaluate(t0 + (t1 - t0) * fraction).ok();
305 let p0 = curve.evaluate(t0).ok();
306 let p1 = curve.evaluate(t1).ok();
307 eprintln!(
308 "PCURVE-FAIL idx={index} frac={fraction} dist={distance} endpt_tol={endpoint_tolerance} scale={scale}\n curve3D@frac={p3:?}\n curve3D@t0={p0:?} curve3D@t1={p1:?}\n surf_domain=u[{u0},{u1}] v[{v0},{v1}] invpar={parameter:?}\n surf@invpar={:?}",
309 surface.evaluate(parameter[0], parameter[1]).ok()
310 );
311 }
312 return Err(format!(
313 "build_pcurve_on_surface: endpoint projection failed (distance={distance})"
314 ));
315 }
316 parameters.push(fraction);
317 raw_surface_parameters.push(parameter);
318 }
319
320 let mut pcurve = build_interpolant(surface, &raw_surface_parameters, ¶meters)?;
321 const MAX_SAMPLES: usize = 160;
322 for _ in 0..4 {
323 if parameters.len() >= MAX_SAMPLES {
324 break;
325 }
326 let mut inserts = Vec::new();
327 for index in 0..parameters.len() - 1 {
328 if parameters.len() + inserts.len() >= MAX_SAMPLES {
329 break;
330 }
331 let start = parameters[index];
332 let end = parameters[index + 1];
333 if end - start < 1e-3 {
334 continue;
335 }
336 for local_fraction in [0.25, 0.5, 0.75] {
337 if parameters.len() + inserts.len() >= MAX_SAMPLES {
338 break;
339 }
340 let fraction = start + (end - start) * local_fraction;
341 let parameter = pcurve.evaluate(fraction)?;
342 let on_surface =
343 surface.evaluate(parameter.x.clamp(u0, u1), parameter.y.clamp(v0, v1))?;
344 let on_curve = curve.evaluate(t0 + (t1 - t0) * fraction)?;
345 if on_surface.sub(on_curve).length() <= refinement_tolerance {
346 continue;
347 }
348 let (surface_parameter, distance) = invert(fraction)?;
349 if distance <= drop_tolerance {
350 inserts.push((index + 1, fraction, surface_parameter));
351 }
352 }
353 }
354 if inserts.is_empty() {
355 break;
356 }
357 for (at, fraction, surface_parameter) in inserts.into_iter().rev() {
358 parameters.insert(at, fraction);
359 raw_surface_parameters.insert(at, surface_parameter);
360 }
361 pcurve = build_interpolant(surface, &raw_surface_parameters, ¶meters)?;
362 }
363 Ok(pcurve)
364}
365
366pub fn build_pcurve_on_surface_marched(
395 surface: &NurbsSurface,
396 curve: &NurbsCurve,
397) -> Result<NurbsCurve, String> {
398 let global = build_pcurve_on_surface(surface, curve)?;
399 if std::env::var("BREP_SECTION_PCURVE_MARCH").as_deref() == Ok("0") || surface.is_affine()? {
400 return Ok(global);
401 }
402 let [t0, t1] = curve.domain()?;
403 if curve.evaluate(t0)?.sub(curve.evaluate(t1)?).length() <= 1e-6 {
406 return Ok(global);
407 }
408 const SAMPLES: usize = 24;
409 let fractions: Vec<f64> = (0..=SAMPLES).map(|i| i as f64 / SAMPLES as f64).collect();
410 let edge: Vec<Vec3> = fractions
411 .iter()
412 .map(|fraction| curve.evaluate(t0 + (t1 - t0) * fraction))
413 .collect::<Result<Vec<_>, _>>()?;
414 let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
415 let endpoint_tolerance = 1e-3 * scale;
416 let (first, first_distance) = invert_checked(surface, edge[0])?;
417 let (last, last_distance) = invert_checked(surface, edge[SAMPLES])?;
418 if first_distance > endpoint_tolerance || last_distance > endpoint_tolerance {
419 return Ok(global);
420 }
421 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
422 let u_span = (u1 - u0).abs().max(EPSILON);
423 let v_span = (v1 - v0).abs().max(EPSILON);
424 let (closed_u, closed_v) = surface_closedness(surface)?;
425 let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
426 let mut du = a[0] - b[0];
427 if closed_u {
428 while du > 0.5 * u_span {
429 du -= u_span;
430 }
431 while du < -0.5 * u_span {
432 du += u_span;
433 }
434 }
435 let mut dv = a[1] - b[1];
436 if closed_v {
437 while dv > 0.5 * v_span {
438 dv -= v_span;
439 }
440 while dv < -0.5 * v_span {
441 dv += v_span;
442 }
443 }
444 ((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
445 };
446 let mut raw = vec![first];
448 let mut previous = first;
449 for index in 1..SAMPLES {
450 let seeded =
451 project_point_to_surface_seeded(surface, edge[index], previous[0], previous[1])?;
452 let (_, global_distance) = invert_checked(surface, edge[index])?;
453 if seeded.distance > 4.0 * global_distance + endpoint_tolerance {
457 return Ok(global);
458 }
459 previous = [seeded.u, seeded.v];
460 raw.push(previous);
461 }
462 raw.push(last);
463 let mut steps: Vec<f64> = (1..raw.len()).map(|i| norm_step(raw[i], raw[i - 1])).collect();
469 let endpoint_gap = steps.pop().unwrap_or(0.0);
470 steps.sort_by(f64::total_cmp);
471 let median = steps.get(steps.len() / 2).copied().unwrap_or(0.0).max(EPSILON);
472 if endpoint_gap > (4.0 * median).max(0.05) {
473 return Ok(global);
474 }
475 let mut parameters = fractions;
479 let mut pcurve = build_interpolant(surface, &raw, ¶meters)?;
480 let refinement_tolerance = 1e-3f64.min(1e-4 * scale);
481 const MAX_SAMPLES: usize = 160;
482 for _ in 0..4 {
483 if parameters.len() >= MAX_SAMPLES {
484 break;
485 }
486 let mut inserts = Vec::new();
487 for index in 0..parameters.len() - 1 {
488 if parameters.len() + inserts.len() >= MAX_SAMPLES {
489 break;
490 }
491 if parameters[index + 1] - parameters[index] < 1e-3 {
492 continue;
493 }
494 for local in [0.25, 0.5, 0.75] {
495 if parameters.len() + inserts.len() >= MAX_SAMPLES {
496 break;
497 }
498 let fraction =
499 parameters[index] + (parameters[index + 1] - parameters[index]) * local;
500 let seed = pcurve.evaluate(fraction)?;
501 let on_curve = curve.evaluate(t0 + (t1 - t0) * fraction)?;
502 let on_surface = surface.evaluate(seed.x.clamp(u0, u1), seed.y.clamp(v0, v1))?;
503 if on_surface.sub(on_curve).length() <= refinement_tolerance {
504 continue;
505 }
506 let seeded = project_point_to_surface_seeded(surface, on_curve, seed.x, seed.y)?;
507 inserts.push((index + 1, fraction, [seeded.u, seeded.v]));
508 }
509 }
510 if inserts.is_empty() {
511 break;
512 }
513 for (at, fraction, uv) in inserts.into_iter().rev() {
514 parameters.insert(at, fraction);
515 raw.insert(at, uv);
516 }
517 pcurve = build_interpolant(surface, &raw, ¶meters)?;
518 }
519 Ok(pcurve)
520}
521
522pub fn build_pcurve_on_surface_range(
527 surface: &NurbsSurface,
528 curve: &NurbsCurve,
529 edge_start: f64,
530 edge_end: f64,
531 forward: bool,
532 tolerance: f64,
533) -> Result<NurbsCurve, String> {
534 build_pcurve_on_surface_range_dense(
535 surface, curve, edge_start, edge_end, forward, tolerance, 64, 3, 513,
536 )
537}
538
539fn repair_branch_jumps(
562 surface: &NurbsSurface,
563 edge_points: &[Vec3],
564 raw: &mut [[f64; 2]],
565 tolerance: f64,
566) -> Result<(), String> {
567 if raw.len() < 3 {
568 return Ok(());
569 }
570 if surface.is_affine()? {
573 return Ok(());
574 }
575 if std::env::var("BREP_NO_PCURVE_REPAIR").is_ok() {
577 return Ok(());
578 }
579 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
580 let u_span = (u1 - u0).abs().max(EPSILON);
581 let v_span = (v1 - v0).abs().max(EPSILON);
582 let (closed_u, closed_v) = surface_closedness(surface)?;
583 let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
587 let mut du = a[0] - b[0];
588 if closed_u {
589 while du > 0.5 * u_span {
590 du -= u_span;
591 }
592 while du < -0.5 * u_span {
593 du += u_span;
594 }
595 }
596 let mut dv = a[1] - b[1];
597 if closed_v {
598 while dv > 0.5 * v_span {
599 dv -= v_span;
600 }
601 while dv < -0.5 * v_span {
602 dv += v_span;
603 }
604 }
605 ((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
606 };
607 const JUMP_THRESHOLD: f64 = 0.2;
611 let count = raw.len();
612
613 let (mut best_start, mut best_len, mut run_start) = (0usize, 1usize, 0usize);
617 for index in 1..count {
618 if norm_step(raw[index], raw[index - 1]) > JUMP_THRESHOLD {
619 if index - run_start > best_len {
620 best_len = index - run_start;
621 best_start = run_start;
622 }
623 run_start = index;
624 }
625 }
626 if count - run_start > best_len {
627 best_len = count - run_start;
628 best_start = run_start;
629 }
630 let (spine_lo, spine_hi) = (best_start, best_start + best_len - 1);
631
632 let reseat = |edge_point: Vec3,
638 previous: [f64; 2],
639 global: [f64; 2]|
640 -> Result<[f64; 2], String> {
641 let global_step = norm_step(global, previous);
642 if global_step <= JUMP_THRESHOLD {
643 return Ok(global);
644 }
645 let seeded =
646 project_point_to_surface_seeded(surface, edge_point, previous[0], previous[1])?;
647 let candidate = [seeded.u, seeded.v];
648 let global_residual = surface
649 .evaluate(global[0], global[1])?
650 .sub(edge_point)
651 .length();
652 let on_surface = seeded.distance <= 4.0 * global_residual + tolerance.max(1e-12);
653 if on_surface && norm_step(candidate, previous) < 0.5 * global_step {
654 if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
655 eprintln!(
656 "pcurve repair: jump {global_step:.4} ({global:?}) -> {:.4} ({candidate:?}) res {:.2e}->{:.2e}",
657 norm_step(candidate, previous),
658 global_residual,
659 seeded.distance
660 );
661 }
662 Ok(candidate)
663 } else {
664 Ok(global)
665 }
666 };
667
668 let mut previous = raw[spine_hi];
681 for index in (spine_hi + 1)..count.saturating_sub(1) {
682 raw[index] = reseat(edge_points[index], previous, raw[index])?;
683 previous = raw[index];
684 }
685 let mut previous = raw[spine_lo];
686 for index in (1..spine_lo).rev() {
687 raw[index] = reseat(edge_points[index], previous, raw[index])?;
688 previous = raw[index];
689 }
690 Ok(())
691}
692
693#[allow(clippy::too_many_arguments)]
706pub fn build_pcurve_on_surface_range_dense(
707 surface: &NurbsSurface,
708 curve: &NurbsCurve,
709 edge_start: f64,
710 edge_end: f64,
711 forward: bool,
712 tolerance: f64,
713 base_samples: usize,
714 refinement_rounds: usize,
715 parameter_cap: usize,
716) -> Result<NurbsCurve, String> {
717 if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
718 return Err("build_pcurve_on_surface_range: invalid edge interval".into());
719 }
720 let evaluate_edge = |fraction: f64| {
721 let edge_fraction = if forward { fraction } else { 1.0 - fraction };
722 curve.evaluate(edge_start + (edge_end - edge_start) * edge_fraction)
723 };
724 let mut parameters = (0..=base_samples)
725 .map(|index| index as f64 / base_samples as f64)
726 .collect::<Vec<_>>();
727 let mut edge_points = parameters
730 .iter()
731 .map(|fraction| evaluate_edge(*fraction))
732 .collect::<Result<Vec<_>, _>>()?;
733 let mut raw = edge_points
734 .iter()
735 .map(|point| invert_checked(surface, *point).map(|value| value.0))
736 .collect::<Result<Vec<_>, _>>()?;
737 repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
738 let mut pcurve = build_interpolant(surface, &raw, ¶meters)?;
739 for _ in 0..refinement_rounds {
740 let mut inserts = Vec::new();
741 for index in 0..parameters.len() - 1 {
742 if parameters.len() + inserts.len() >= parameter_cap {
743 break;
744 }
745 for local in [0.25, 0.5, 0.75] {
746 let fraction =
747 parameters[index] + (parameters[index + 1] - parameters[index]) * local;
748 let uv = pcurve.evaluate(fraction)?;
749 let represented = surface.evaluate(uv.x, uv.y)?;
750 let edge_point = evaluate_edge(fraction)?;
751 if represented.sub(edge_point).length() <= tolerance {
752 continue;
753 }
754 inserts.push((
755 index + 1,
756 fraction,
757 edge_point,
758 invert_checked(surface, edge_point)?.0,
759 ));
760 }
761 }
762 if inserts.is_empty() {
763 break;
764 }
765 for (index, fraction, edge_point, value) in inserts.into_iter().rev() {
766 parameters.insert(index, fraction);
767 edge_points.insert(index, edge_point);
768 raw.insert(index, value);
769 }
770 repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
774 pcurve = build_interpolant(surface, &raw, ¶meters)?;
775 }
776 Ok(pcurve)
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use crate::{make_cylinder_surface, make_line, make_plane, make_sphere_surface};
783
784 #[test]
785 fn affine_pcurve_preserves_curve_representation() {
786 let surface = make_plane(
787 Vec3::new(2.0, -3.0, 5.0),
788 Vec3::new(1.0, 0.2, 0.0),
789 Vec3::new(0.0, 0.3, 1.0),
790 7.0,
791 4.0,
792 )
793 .unwrap();
794 let curve = make_line(
795 surface.evaluate(0.2, 0.7).unwrap(),
796 surface.evaluate(0.9, 0.1).unwrap(),
797 )
798 .unwrap();
799 let pcurve = build_pcurve_on_surface(&surface, &curve).unwrap();
800 assert_eq!(pcurve.degree, curve.degree);
801 assert_eq!(pcurve.knots, curve.knots);
802 for fraction in [0.0, 0.2, 0.7, 1.0] {
803 let uv = pcurve.evaluate(fraction).unwrap();
804 let expected = curve.evaluate(fraction).unwrap();
805 assert!(surface.evaluate(uv.x, uv.y).unwrap().sub(expected).length() < 1e-10);
806 }
807 }
808
809 #[test]
810 fn cylinder_pcurve_tracks_iso_curve_across_seam() {
811 let surface =
812 make_cylinder_surface(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 3.0, 5.0).unwrap();
813 let curve = surface.iso_curve_v(0.4).unwrap();
814 let pcurve = build_pcurve_on_surface(&surface, &curve).unwrap();
815 for index in 0..=64 {
816 let fraction = index as f64 / 64.0;
817 let uv = pcurve.evaluate(fraction).unwrap();
818 assert!(
819 surface
820 .evaluate(uv.x, uv.y)
821 .unwrap()
822 .sub(curve.evaluate(fraction).unwrap())
823 .length()
824 < 1e-4
825 );
826 }
827 }
828
829 #[test]
830 fn sphere_pole_does_not_choose_the_adjacent_meridian_unwrap() {
831 let surface =
832 make_sphere_surface(Vec3::default(), 0.009, Vec3::new(0.0, 0.0, 1.0)).unwrap();
833 let raw = [
838 [0.0, 1.0],
839 [0.75, 0.959],
840 [0.75, 0.60],
841 [0.75, 0.02],
842 [0.25, 0.0005],
843 [0.25, 0.045],
844 [0.25, 0.282],
845 ];
846 let stations = [0.0, 0.1, 0.4, 0.65, 0.7, 0.8, 1.0];
847 let pcurve = build_interpolant(&surface, &raw, &stations).unwrap();
848
849 for (station, expected) in stations.into_iter().zip(raw) {
850 let uv = pcurve.evaluate(station).unwrap();
851 assert!((uv.x - expected[0]).abs() < 1e-12, "u at {station}: {uv:?}");
852 assert!((uv.y - expected[1]).abs() < 1e-12, "v at {station}: {uv:?}");
853 }
854 }
855
856 #[test]
857 fn sphere_interior_pole_keeps_exactly_opposite_meridian_runs_independent() {
858 let surface =
859 make_sphere_surface(Vec3::default(), 0.0127, Vec3::new(0.0, 0.0, 1.0)).unwrap();
860 let raw = [
865 [0.75, 0.5],
866 [0.75, 0.2],
867 [0.75, 0.02],
868 [0.25, 0.0005],
869 [0.25, 0.02],
870 [0.25, 0.0665598],
871 ];
872 let stations = [0.0, 0.3, 0.48, 0.52, 0.7, 1.0];
873 let pcurve = build_interpolant(&surface, &raw, &stations).unwrap();
874
875 for (station, expected) in stations.into_iter().zip(raw) {
876 let uv = pcurve.evaluate(station).unwrap();
877 assert!((uv.x - expected[0]).abs() < 1e-12, "u at {station}: {uv:?}");
878 assert!((uv.y - expected[1]).abs() < 1e-12, "v at {station}: {uv:?}");
879 }
880 }
881}