1use crate::{
2 interpolate_curve, project_point_to_surface, project_point_to_surface_seeded, AnalyticSurface,
3 DiagnosticSeverity, KernelDiagnostics, KernelStage, KnotVector, NurbsCurve, NurbsSurface, Vec3,
4 Vec4, KNOT_IDENTITY_TOL,
5};
6
7const EPSILON: f64 = crate::tolerance::FP_FLOOR;
8const LINEAR_TOLERANCE: f64 = crate::tolerance::DEFAULT_MODEL;
9
10struct PcurvePhaseClock {
12 started: Option<web_time::Instant>,
13 phase: &'static str,
14 samples: usize,
15 tracking: bool,
16}
17impl PcurvePhaseClock {
18 fn new(phase: &'static str, samples: usize, tracking: bool) -> Self {
19 Self { started: crate::profile::profile_enabled().then(web_time::Instant::now), phase, samples, tracking }
20 }
21}
22impl Drop for PcurvePhaseClock {
23 fn drop(&mut self) {
24 if let Some(started) = self.started {
25 crate::imprint::profile_emit(format_args!("pcurve.phase_time phase={} initial_samples={} tracking={} ms={:.3}", self.phase, self.samples, self.tracking, started.elapsed().as_secs_f64()*1000.0));
26 }
27 }
28}
29
30fn surface_domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), String> {
31 Ok((
32 KnotVector::new(surface.knots_u.clone(), surface.degree_u)?.domain(),
33 KnotVector::new(surface.knots_v.clone(), surface.degree_v)?.domain(),
34 ))
35}
36
37fn surface_closedness(surface: &NurbsSurface) -> Result<(bool, bool), String> {
44 surface.closed_directions()
45}
46
47thread_local! {
48 static PCURVE_REJECT_BOUNDS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
49}
50struct PcurveRejectScope(bool);
51impl PcurveRejectScope {
52 fn open(enabled: bool) -> Self {
53 Self(PCURVE_REJECT_BOUNDS.with(|state| state.replace(enabled)))
54 }
55}
56impl Drop for PcurveRejectScope {
57 fn drop(&mut self) { PCURVE_REJECT_BOUNDS.with(|state| state.set(self.0)); }
58}
59
60thread_local! {
61 static PCURVE_STACK_POINT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
62}
63struct PcurveStackPointScope(bool);
64impl PcurveStackPointScope {
65 fn open(enabled: bool) -> Self { Self(PCURVE_STACK_POINT.with(|state| state.replace(enabled))) }
66}
67impl Drop for PcurveStackPointScope {
68 fn drop(&mut self) { PCURVE_STACK_POINT.with(|state| state.set(self.0)); }
69}
70
71#[cfg_attr(feature = "profile-wall", track_caller)]
78fn section_extended_point(surface: &NurbsSurface, u: f64, v: f64, enabled: bool) -> Result<Vec3, String> {
79 let _reader = crate::reader_profile::Point::open(crate::reader_profile::SURFACE, std::panic::Location::caller());
80 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::POINT);
81 if enabled && u.is_finite() && v.is_finite()
82 && surface.degree_u <= crate::curve::MAX_STACK_DEGREE
83 && surface.degree_v <= crate::curve::MAX_STACK_DEGREE {
84 let [u0,u1] = surface.domain_u()?;
85 let [v0,v1] = surface.domain_v()?;
86 let mut uu = u;
87 let mut vv = v;
88 if u < u0 || u > u1 || v < v0 || v > v1 {
89 let (closed_u, closed_v) = surface_closedness(surface)?;
90 if closed_u && (u < u0 || u > u1) { uu = u0 + (u - u0).rem_euclid(u1 - u0); }
93 if closed_v && (v < v0 || v > v1) { vv = v0 + (v - v0).rem_euclid(v1 - v0); }
94 }
95 if uu >= u0 && uu <= u1 && vv >= v0 && vv <= v1 {
96 let point = surface.evaluate_homogeneous(uu, vv)?;
97 if point.w.abs() <= EPSILON {
98 return Err("NurbsSurface: zero evaluated weight".into());
99 }
100 return Ok(Vec3::new(point.x, point.y, point.z).scale(1.0 / point.w));
101 }
102 }
103 surface.evaluate_extended(u,v)
104}
105
106struct PcurveSampleCache {
109 curve_trace: Option<crate::imprint::reuse_profile::Trace<u64>>,
110 surface_trace: Option<crate::imprint::reuse_profile::Trace<(u64, u64, bool, bool)>>,
111 rollover: bool,
112 point_limit: usize,
113 surface: usize,
114 curve: usize,
115 curves: rustc_hash::FxHashMap<u64, Vec3>,
116 surfaces: rustc_hash::FxHashMap<(u64, u64, bool, bool), Vec3>,
117 inversions: rustc_hash::FxHashMap<[u64; 3], ([f64; 2], f64)>,
118 requests: [usize; 4],
119 hits: [usize; 4],
120 speeds: rustc_hash::FxHashMap<(u64, u64), f64>,
121}
122thread_local! {
123 static PCURVE_SAMPLES: std::cell::RefCell<Vec<Option<PcurveSampleCache>>> = const { std::cell::RefCell::new(Vec::new()) };
124}
125struct PcurveSampleScope<'a>(std::marker::PhantomData<(&'a NurbsSurface, &'a NurbsCurve)>);
126impl<'a> PcurveSampleScope<'a> {
127 fn open(surface: &'a NurbsSurface, curve: &'a NurbsCurve, enabled: bool) -> Self {
128 Self::open_limit(surface, curve, enabled, switch!("BREP_PCURVE_SAMPLE_ROLLOVER", false),
129 if larger_working_set() { 65536 } else { PCURVE_SAMPLE_CACHE_LIMIT })
130 }
131 fn open_config(surface: &'a NurbsSurface, curve: &'a NurbsCurve, enabled: bool, rollover: bool) -> Self {
132 Self::open_limit(surface, curve, enabled, rollover, PCURVE_SAMPLE_CACHE_LIMIT)
133 }
134 fn open_limit(surface: &'a NurbsSurface, curve: &'a NurbsCurve, enabled: bool, rollover: bool, point_limit: usize) -> Self {
135 let cache = enabled
136 .then(|| PcurveSampleCache {
137 curve_trace: crate::imprint::reuse_profile::Trace::open("curve", || serde_json::json!({"curve":curve,"identity":curve as *const _ as usize,"limit":point_limit})),
138 surface_trace: crate::imprint::reuse_profile::Trace::open("surface", || serde_json::json!({"surface":surface,"identity":surface as *const _ as usize,"curve":curve,"limit":point_limit})),
139 rollover, point_limit, surface: surface as *const _ as usize,
140 curve: curve as *const _ as usize, curves: Default::default(),
141 surfaces: Default::default(), inversions: Default::default(), requests: [0; 4], hits: [0; 4], speeds: Default::default() });
142 PCURVE_SAMPLES.with(|stack| stack.borrow_mut().push(cache));
143 Self(std::marker::PhantomData)
144 }
145}
146impl Drop for PcurveSampleScope<'_> {
147 fn drop(&mut self) {
148 PCURVE_SAMPLES.with(|stack| {
149 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
150 if let Some(Some(cache)) = stack.borrow_mut().pop() {
151 if crate::profile::profile_enabled() {
152 profile_line!("pcurve.samples requests={:?} hits={:?} retained={:?}", cache.requests,
153 cache.hits, [cache.curves.len(), cache.surfaces.len(), cache.inversions.len(), cache.speeds.len()]);
154 }
155 }
156 });
157 }
158}
159const PCURVE_SAMPLE_CACHE_LIMIT: usize = 16384;
160
161pub(crate) fn larger_working_set() -> bool {
165 switch!("BREP_PCURVE_WORKING_SET", false)
166}
167
168fn retain_sample<K: std::hash::Hash + Eq, V>(
173 map: &mut rustc_hash::FxHashMap<K, V>, key: K, value: V, rollover: bool, limit: usize,
174) {
175 if rollover && map.len() >= limit { map.clear(); }
176 if map.len() < limit { map.insert(key, value); }
177}
178fn section_u_speed(surface: &NurbsSurface, u: f64, v: f64) -> Result<f64, String> {
179 let key = (u.to_bits(), v.to_bits());
180 let (active, hit) = PCURVE_SAMPLES.with(|stack| {
181 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
182 let mut stack = stack.borrow_mut();
183 let Some(Some(cache)) = stack.last_mut() else { return (false, None) };
184 if cache.surface != surface as *const _ as usize { return (false, None) }
185 cache.requests[3] += 1;
186 let hit = cache.speeds.get(&key).copied();
187 cache.hits[3] += usize::from(hit.is_some());
188 (true, hit)
189 });
190 if let Some(hit) = hit { return Ok(hit) }
191 let speed = surface.deriv1(u, v)?.1.length();
192 if active { PCURVE_SAMPLES.with(|stack| {
193 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
194 let mut stack = stack.borrow_mut();
195 let cache = stack.last_mut().unwrap().as_mut().unwrap();
196 if cache.speeds.len() < PCURVE_SAMPLE_CACHE_LIMIT { cache.speeds.insert(key, speed); }
197 }); }
198 Ok(speed)
199}
200
201#[track_caller]
202fn section_curve_point(curve: &NurbsCurve, parameter: f64) -> Result<Vec3, String> {
203 let caller = std::panic::Location::caller();
204 let key = parameter.to_bits();
205 let (active, hit) = PCURVE_SAMPLES.with(|stack| {
206 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
207 let mut stack = stack.borrow_mut();
208 let Some(Some(cache)) = stack.last_mut() else { return (false, None) };
209 if cache.curve != curve as *const _ as usize { return (false, None) }
210 cache.requests[0] += 1;
211 let hit = cache.curves.get(&key).copied();
212 cache.hits[0] += usize::from(hit.is_some());
213 if hit.is_some() { if let Some(trace) = &mut cache.curve_trace { trace.access(&key, caller, true, true); } }
214 (true, hit)
215 });
216 if let Some(hit) = hit { return Ok(hit) }
217 let result = curve.evaluate(parameter);
218 let point = match result {
219 Ok(point) => point,
220 Err(error) => {
221 if active { PCURVE_SAMPLES.with(|stack| {
222 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP); if let Some(trace) = &mut stack.borrow_mut().last_mut().unwrap().as_mut().unwrap().curve_trace {
223 trace.access(&key, caller, false, false);
224 } }); }
225 return Err(error);
226 }
227 };
228 if active { PCURVE_SAMPLES.with(|stack| {
229 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
230 let mut stack = stack.borrow_mut();
231 let cache = stack.last_mut().unwrap().as_mut().unwrap();
232 if let Some(trace) = &mut cache.curve_trace { trace.access(&key, caller, false, true); }
233 retain_sample(&mut cache.curves, key, point, cache.rollover, cache.point_limit);
234 }); }
235 Ok(point)
236}
237#[track_caller]
238fn section_surface_point(surface: &NurbsSurface, u: f64, v: f64, extended: bool) -> Result<Vec3, String> {
239 let caller = std::panic::Location::caller();
240 let stack_point = PCURVE_STACK_POINT.with(|state| state.get());
243 let key = (u.to_bits(), v.to_bits(), extended, stack_point);
244 let (active, hit) = PCURVE_SAMPLES.with(|stack| {
245 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
246 let mut stack = stack.borrow_mut();
247 let Some(Some(cache)) = stack.last_mut() else { return (false, None) };
248 if cache.surface != surface as *const _ as usize { return (false, None) }
249 cache.requests[1] += 1;
250 let hit = cache.surfaces.get(&key).copied();
251 cache.hits[1] += usize::from(hit.is_some());
252 if hit.is_some() { if let Some(trace) = &mut cache.surface_trace { trace.access(&key, caller, true, true); } }
253 (true, hit)
254 });
255 if let Some(hit) = hit { return Ok(hit) }
256 let result = if extended { section_extended_point(surface, u, v, stack_point) } else { surface.evaluate(u, v) };
257 let point = match result {
258 Ok(point) => point,
259 Err(error) => {
260 if active { PCURVE_SAMPLES.with(|stack| {
261 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP); if let Some(trace) = &mut stack.borrow_mut().last_mut().unwrap().as_mut().unwrap().surface_trace {
262 trace.access(&key, caller, false, false);
263 } }); }
264 return Err(error);
265 }
266 };
267 if active { PCURVE_SAMPLES.with(|stack| {
268 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
269 let mut stack = stack.borrow_mut();
270 let cache = stack.last_mut().unwrap().as_mut().unwrap();
271 if let Some(trace) = &mut cache.surface_trace { trace.access(&key, caller, false, true); }
272 retain_sample(&mut cache.surfaces, key, point, cache.rollover, cache.point_limit);
273 }); }
274 Ok(point)
275}
276fn invert_checked(surface: &NurbsSurface, point: Vec3) -> Result<([f64; 2], f64), String> {
277 let key = [point.x.to_bits(), point.y.to_bits(), point.z.to_bits()];
278 let (active, hit) = PCURVE_SAMPLES.with(|stack| {
279 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
280 let mut stack = stack.borrow_mut();
281 let Some(Some(cache)) = stack.last_mut() else { return (false, None) };
282 if cache.surface != surface as *const _ as usize { return (false, None) }
283 cache.requests[2] += 1;
284 let hit = cache.inversions.get(&key).copied();
285 cache.hits[2] += usize::from(hit.is_some());
286 (true, hit)
287 });
288 if let Some(hit) = hit { return Ok(hit) }
289 let projection = invert_checked_uncached(surface, point)?;
290 if active { PCURVE_SAMPLES.with(|stack| {
291 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
292 let mut stack = stack.borrow_mut();
293 let cache = stack.last_mut().unwrap().as_mut().unwrap();
294 if cache.inversions.len() < PCURVE_SAMPLE_CACHE_LIMIT { cache.inversions.insert(key, projection); }
295 }); }
296 Ok(projection)
297}
298
299fn invert_checked_uncached(surface: &NurbsSurface, point: Vec3) -> Result<([f64; 2], f64), String> {
300 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_INVERT_CHECKED, surface);
301 if surface.is_affine()? {
302 let ([u0, _], [v0, _]) = surface_domains(surface)?;
303 let (origin, du, dv) = surface.deriv1(u0, v0)?;
304 let delta = point.sub(origin);
305 let uu = du.dot(du);
306 let uv = du.dot(dv);
307 let vv = dv.dot(dv);
308 let along_u = delta.dot(du);
309 let along_v = delta.dot(dv);
310 let determinant = uu * vv - uv * uv;
311 if determinant.abs() > EPSILON {
312 let parameter = [
313 u0 + (along_u * vv - along_v * uv) / determinant,
314 v0 + (along_v * uu - along_u * uv) / determinant,
315 ];
316 let foot = origin.add(du.scale(parameter[0] - u0)).add(dv.scale(parameter[1] - v0));
320 return Ok((parameter, point.sub(foot).length()));
321 }
322 }
323 let projection = project_point_to_surface(surface, point)?;
324 Ok(([projection.u, projection.v], projection.distance))
325}
326
327fn cone_apex_u() -> bool {
332 switch!("BREP_PCURVE_CONE_APEX_U", true)
333}
334
335fn worst_first() -> bool {
339 switch!("BREP_PCURVE_WORST_FIRST", true)
340}
341
342fn c0_joins() -> bool {
346 switch!("BREP_PCURVE_C0_JOIN", true)
347}
348
349fn seeded_inserts() -> bool {
350 switch!("BREP_PCURVE_SEEDED_INSERTS", true)
351}
352
353fn invert_near(
363 surface: &NurbsSurface,
364 point: Vec3,
365 seed: [f64; 2],
366 neighbour_gap: f64,
367) -> Result<([f64; 2], f64), String> {
368 if !seeded_inserts() || surface.is_affine()? || surface.analytic().is_some() || surface.affine_sphere().is_some() {
369 return invert_checked(surface, point);
370 }
371 let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
372 if seeded.distance <= neighbour_gap + PCURVE_REFINEMENT_TOLERANCE {
373 return Ok(([seeded.u, seeded.v], seeded.distance));
374 }
375 invert_checked(surface, point)
376}
377
378fn unwrap_periodic(values: &mut [f64], minimum: f64, maximum: f64) {
379 let period = maximum - minimum;
380 for index in 1..values.len() {
381 while values[index] - values[index - 1] > period / 2.0 {
382 values[index] -= period;
383 }
384 while values[index] - values[index - 1] < -period / 2.0 {
385 values[index] += period;
386 }
387 }
388 if values.len() > 1 {
389 while values[0] - values[1] > period / 2.0 {
390 values[0] -= period;
391 }
392 while values[0] - values[1] < -period / 2.0 {
393 values[0] += period;
394 }
395 }
396 if values.len() > 1 {
406 let excursion = |shift: f64| -> f64 {
407 values
408 .iter()
409 .map(|value| {
410 let v = value + shift;
411 (minimum - v).max(0.0) + (v - maximum).max(0.0)
412 })
413 .sum::<f64>()
414 };
415 let mean = values.iter().copied().sum::<f64>() / values.len() as f64;
416 let center = 0.5 * (minimum + maximum);
417 let base_k = ((center - mean) / period).round() as i64;
418 let mut best_shift = 0.0;
419 let mut best_excursion = f64::INFINITY;
420 for k in (base_k - 1)..=(base_k + 1) {
421 let shift = k as f64 * period;
422 let value = excursion(shift);
423 if value < best_excursion {
424 best_excursion = value;
425 best_shift = shift;
426 }
427 }
428 if best_shift != 0.0 {
429 for value in values.iter_mut() {
430 *value += best_shift;
431 }
432 }
433 }
434}
435
436fn build_interpolant(
442 surface: &NurbsSurface,
443 raw_parameters: &[[f64; 2]],
444 curve_parameters: &[f64],
445) -> Result<(NurbsCurve, f64, f64), String> {
446 build_interpolant_joined(surface, raw_parameters, curve_parameters, &[])
447}
448
449fn build_interpolant_joined(
463 surface: &NurbsSurface,
464 raw_parameters: &[[f64; 2]],
465 curve_parameters: &[f64],
466 joins: &[f64],
467) -> Result<(NurbsCurve, f64, f64), String> {
468 let _clock = PcurvePhaseClock::new("interpolant", raw_parameters.len(), false);
469 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
470 let (closed_u, closed_v) = surface_closedness(surface)?;
471 let mut parameters = raw_parameters.to_vec();
472 if cone_apex_u() && matches!(surface.analytic(), Some(AnalyticSurface::RuledRevolution { .. })) && parameters.len() >= 2 {
482 let speeds = parameters
483 .iter()
484 .map(|parameter| section_u_speed(surface, parameter[0], parameter[1]))
485 .collect::<Result<Vec<_>, _>>()?;
486 let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
487 let singular = |index: usize| speeds[index] <= reference * 1e-2;
488 if let (Some(first), Some(last)) = (
489 (0..parameters.len()).find(|&index| !singular(index)),
490 (0..parameters.len()).rfind(|&index| !singular(index)),
491 ) {
492 for index in 0..first {
493 parameters[index][0] = parameters[first][0];
494 }
495 for index in last + 1..parameters.len() {
496 parameters[index][0] = parameters[last][0];
497 }
498 }
499 }
500 let sphere_u_singular = if matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })) {
512 let speeds = parameters
513 .iter()
514 .map(|parameter| {
515 section_u_speed(surface, parameter[0], parameter[1])
516 })
517 .collect::<Result<Vec<_>, _>>()?;
518 let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
519 speeds
520 .into_iter()
521 .map(|speed| speed <= reference * 1e-2)
526 .collect::<Vec<_>>()
527 } else {
528 vec![false; parameters.len()]
529 };
530 let has_interior_pole_crossing = sphere_u_singular
538 .iter()
539 .position(|singular| !singular)
540 .zip(sphere_u_singular.iter().rposition(|singular| !singular))
541 .is_some_and(|(first, last)| sphere_u_singular[first..=last].iter().any(|value| *value));
542 let period = (u1 - u0).abs();
543 let interior_pole_branch_reset = closed_u
544 && has_interior_pole_crossing
545 && parameters.windows(2).zip(sphere_u_singular.windows(2)).any(
546 |(parameter_pair, singular_pair)| {
547 (singular_pair[0] || singular_pair[1])
548 && (parameter_pair[1][0] - parameter_pair[0][0]).abs()
549 >= 0.5 * period - 1e-12 * period.max(1.0)
550 },
551 );
552 if closed_u {
553 let mut values = parameters.iter().map(|value| value[0]).collect::<Vec<_>>();
554 if interior_pole_branch_reset {
555 let mut start = 0;
556 while start < values.len() {
557 while start < values.len() && sphere_u_singular[start] {
558 start += 1;
559 }
560 let mut end = start;
561 while end < values.len() && !sphere_u_singular[end] {
562 end += 1;
563 }
564 unwrap_periodic(&mut values[start..end], u0, u1);
565 start = end;
566 }
567 } else {
568 unwrap_periodic(&mut values, u0, u1);
569 }
570 for (parameter, value) in parameters.iter_mut().zip(values) {
571 parameter[0] = value;
572 }
573 }
574 if closed_v {
575 let mut values = parameters.iter().map(|value| value[1]).collect::<Vec<_>>();
576 unwrap_periodic(&mut values, v0, v1);
577 for (parameter, value) in parameters.iter_mut().zip(values) {
578 parameter[1] = value;
579 }
580 }
581 let mut excursion = 0.0_f64;
598 let mut distance = 0.0_f64;
599 let mut points = Vec::with_capacity(parameters.len());
600 for parameter in parameters {
601 let u = if closed_u {
602 parameter[0]
603 } else {
604 parameter[0].clamp(u0, u1)
605 };
606 let v = if closed_v {
607 parameter[1]
608 } else {
609 parameter[1].clamp(v0, v1)
610 };
611 let pulled = ((parameter[0] - u).powi(2) + (parameter[1] - v).powi(2)).sqrt();
612 if pulled > 0.0 {
613 excursion = excursion.max(pulled);
614 let extended = surface.evaluate_extended(parameter[0], parameter[1])?;
615 distance = distance.max(extended.sub(surface.evaluate(u, v)?).length());
616 }
617 points.push(Vec3::new(u, v, 0.0));
618 }
619 let degree = 3usize.min(points.len() - 1);
620 let mut bounds: Vec<usize> = joins
621 .iter()
622 .filter_map(|join| curve_parameters.iter().position(|parameter| parameter == join))
623 .filter(|&index| index > 0 && index + 1 < curve_parameters.len())
624 .collect();
625 bounds.sort_unstable();
626 bounds.dedup();
627 if degree < 3 || bounds.is_empty() || !c0_joins() {
628 let curve = interpolate_curve(&points, degree, curve_parameters)?;
629 return Ok((curve, excursion, distance));
630 }
631 let mut pieces = vec![0usize];
634 for bound in bounds {
635 if bound + 1 - pieces[pieces.len() - 1] >= 4 {
636 pieces.push(bound);
637 }
638 }
639 if curve_parameters.len() - pieces[pieces.len() - 1] < 4 {
640 pieces.pop();
641 }
642 pieces.push(curve_parameters.len() - 1);
643 if pieces.len() < 3 {
644 let curve = interpolate_curve(&points, degree, curve_parameters)?;
645 return Ok((curve, excursion, distance));
646 }
647 let mut knots: Vec<f64> = Vec::with_capacity(curve_parameters.len() + 4);
648 let mut control: Vec<Vec4> = Vec::with_capacity(curve_parameters.len());
649 let last_piece = pieces.len() - 2;
650 for piece in 0..=last_piece {
651 let (from, to) = (pieces[piece], pieces[piece + 1]);
652 let fitted = interpolate_curve(&points[from..=to], 3, &curve_parameters[from..=to])?;
653 if piece == 0 {
654 knots.extend(fitted.knots[..4].iter().copied());
655 }
656 knots.extend(fitted.knots[4..fitted.knots.len() - 4].iter().copied());
657 let multiplicity = if piece == last_piece { 4 } else { 3 };
658 knots.extend(std::iter::repeat(curve_parameters[to]).take(multiplicity));
659 control.extend(fitted.control_points.iter().skip(usize::from(piece > 0)).copied());
660 }
661 let curve = NurbsCurve::new(3, knots, control)?;
662 Ok((curve, excursion, distance))
663}
664
665pub const PCURVE_REFINEMENT_TOLERANCE: f64 = 1e-7;
713
714const MAX_PCURVE_SAMPLES: usize = 600;
730
731const PCURVE_RAISED_SAMPLES: usize = 2000;
737
738#[derive(Clone, Copy, Debug, PartialEq)]
751pub struct PcurveFitReport {
752 pub residual: f64,
759 pub floor: f64,
761 pub samples: usize,
763 pub exit: PcurveFitExit,
765 pub off_surface: f64,
772 pub raised: bool,
775 pub clamped_excursion: f64,
792 pub clamped_distance: f64,
796}
797
798impl PcurveFitReport {
799 pub fn met_floor(&self) -> bool {
801 self.residual <= self.floor
802 }
803
804 pub fn on_bar(&self) -> bool {
819 self.residual <= self.floor + self.off_surface
820 }
821}
822
823#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
827pub enum PcurveFitExit {
828 Converged,
831 SampleCeiling,
836 RoundBudget,
839 Stalled,
849 SpanFloor,
858}
859
860#[derive(Clone, Debug)]
862pub struct PcurveFit {
863 pub curve: NurbsCurve,
864 pub report: PcurveFitReport,
865}
866
867#[derive(Clone, Copy, Debug, Default, PartialEq)]
887pub struct PcurveFitLedger {
888 pub fits: u64,
890 pub unmet: u64,
892 pub sample_ceiling: u64,
894 pub round_budget: u64,
896 pub stalled: u64,
898 pub span_floor: u64,
901 pub worst_unmet_residual: f64,
903 pub raised: u64,
906 pub raise_declined: u64,
909 pub worst_off_surface: f64,
911 pub clamped: u64,
914 pub worst_clamped_excursion: f64,
916 pub worst_clamped_distance: f64,
918}
919
920impl PcurveFitLedger {
921 fn record(&mut self, report: &PcurveFitReport) {
922 self.fits += 1;
923 if report.raised {
924 self.raised += 1;
925 }
926 self.worst_off_surface = self.worst_off_surface.max(report.off_surface);
927 if report.clamped_excursion > 0.0 {
928 self.clamped += 1;
929 self.worst_clamped_excursion = self.worst_clamped_excursion.max(report.clamped_excursion);
930 self.worst_clamped_distance = self.worst_clamped_distance.max(report.clamped_distance);
931 }
932 if report.met_floor() {
933 return;
934 }
935 self.unmet += 1;
936 match report.exit {
937 PcurveFitExit::Converged => {}
938 PcurveFitExit::SampleCeiling => {
939 self.sample_ceiling += 1;
940 if !report.raised {
941 self.raise_declined += 1;
942 }
943 }
944 PcurveFitExit::RoundBudget => self.round_budget += 1,
945 PcurveFitExit::Stalled => self.stalled += 1,
946 PcurveFitExit::SpanFloor => self.span_floor += 1,
947 }
948 self.worst_unmet_residual = self.worst_unmet_residual.max(report.residual);
949 }
950
951 fn fold(&mut self, child: &PcurveFitLedger) {
952 self.fits += child.fits;
953 self.unmet += child.unmet;
954 self.sample_ceiling += child.sample_ceiling;
955 self.round_budget += child.round_budget;
956 self.stalled += child.stalled;
957 self.span_floor += child.span_floor;
958 self.worst_unmet_residual = self.worst_unmet_residual.max(child.worst_unmet_residual);
959 self.raised += child.raised;
960 self.raise_declined += child.raise_declined;
961 self.worst_off_surface = self.worst_off_surface.max(child.worst_off_surface);
962 self.clamped += child.clamped;
963 self.worst_clamped_excursion = self.worst_clamped_excursion.max(child.worst_clamped_excursion);
964 self.worst_clamped_distance = self.worst_clamped_distance.max(child.worst_clamped_distance);
965 }
966
967 pub fn report_into(&self, diagnostics: &mut KernelDiagnostics) {
973 diagnostics.count_n("pcurve.fits", self.fits);
974 diagnostics.count_n("pcurve.unmet_floor", self.unmet);
975 diagnostics.count_n("pcurve.exit.sample_ceiling", self.sample_ceiling);
976 diagnostics.count_n("pcurve.exit.round_budget", self.round_budget);
977 diagnostics.count_n("pcurve.exit.stalled", self.stalled);
978 diagnostics.count_n("pcurve.exit.span_floor", self.span_floor);
979 diagnostics.count_n("pcurve.budget_raised", self.raised);
980 diagnostics.count_n("pcurve.budget_raise_declined", self.raise_declined);
981 diagnostics.measure_max("pcurve.worst_unmet_residual", self.worst_unmet_residual);
982 diagnostics.measure_max("pcurve.worst_off_surface", self.worst_off_surface);
983 diagnostics.count_n("pcurve.clamped", self.clamped);
984 diagnostics.measure_max("pcurve.worst_clamped_excursion", self.worst_clamped_excursion);
985 diagnostics.measure_max("pcurve.worst_clamped_distance", self.worst_clamped_distance);
986 if self.unmet > 0 {
987 diagnostics.event(
988 DiagnosticSeverity::Degraded,
989 KernelStage::Refine,
990 "pcurve.unmet_floor",
991 format!(
992 "{} of {} pcurve fits did not reach the {:.0e} floor \
993 (sample ceiling {}, round budget {}, stalled {}, span floor {}); \
994 worst residual {:.3e}; budget raised on {}, declined on {}; \
995 edge curves sit off their surfaces by up to {:.3e}{}",
996 self.unmet,
997 self.fits,
998 PCURVE_REFINEMENT_TOLERANCE,
999 self.sample_ceiling,
1000 self.round_budget,
1001 self.stalled,
1002 self.span_floor,
1003 self.worst_unmet_residual,
1004 self.raised,
1005 self.raise_declined,
1006 self.worst_off_surface,
1007 if self.clamped > 0 {
1008 format!(
1009 "; {} fit(s) clamped into their chart by up to {:.3e} in parameter, {:.3e} in 3D",
1010 self.clamped, self.worst_clamped_excursion, self.worst_clamped_distance
1011 )
1012 } else {
1013 String::new()
1014 },
1015 ),
1016 );
1017 }
1018 }
1019}
1020
1021thread_local! {
1022 static FOOT_MEMO: std::cell::RefCell<Option<FootMemo>> = const { std::cell::RefCell::new(None) };
1024}
1025
1026struct FootMemo {
1045 precision: bool,
1046 surface: *const NurbsSurface,
1047 global: rustc_hash::FxHashMap<[u64; 3], Option<crate::SurfaceProjection>>,
1048 seeded: rustc_hash::FxHashMap<[u64; 5], Option<crate::SurfaceProjection>>,
1049 pending_global: Vec<([u64; 3], Option<crate::SurfaceProjection>)>,
1050 pending_seeded: Vec<([u64; 5], Option<crate::SurfaceProjection>)>,
1051}
1052
1053impl FootMemo {
1054 fn new(surface: *const NurbsSurface) -> Self {
1055 Self { precision: false, surface, global: Default::default(), seeded: Default::default(), pending_global: Vec::new(), pending_seeded: Vec::new() }
1056 }
1057
1058 fn fold_pending(&mut self) {
1061 if !self.pending_global.is_empty() {
1062 self.global.reserve(self.pending_global.len());
1063 self.global.extend(self.pending_global.drain(..));
1064 }
1065 if !self.pending_seeded.is_empty() {
1066 self.seeded.reserve(self.pending_seeded.len());
1067 self.seeded.extend(self.pending_seeded.drain(..));
1068 }
1069 }
1070}
1071
1072thread_local! {
1073 static DEFER_FOOT_INSERTS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1074}
1075
1076pub(crate) fn defer_foot_inserts<T>(read: impl FnOnce() -> T) -> T {
1079 let previous = DEFER_FOOT_INSERTS.with(|cell| cell.replace(true));
1080 let result = read();
1081 DEFER_FOOT_INSERTS.with(|cell| cell.set(previous));
1082 result
1083}
1084
1085pub(crate) struct FootMemoScope<'a> {
1092 previous: Option<Option<FootMemo>>,
1093 _surface: std::marker::PhantomData<&'a NurbsSurface>,
1094 _thread_bound: std::marker::PhantomData<*const ()>,
1096}
1097
1098impl<'a> FootMemoScope<'a> {
1099 fn precision(surface: &'a NurbsSurface) -> Self {
1102 let mut next = FootMemo::new(surface);
1103 next.precision = true;
1104 let previous = FOOT_MEMO.with(|memo| memo.replace(Some(next)));
1105 Self { previous: Some(previous), _surface: std::marker::PhantomData, _thread_bound: std::marker::PhantomData }
1106 }
1107
1108 pub(crate) fn open(surface: &'a NurbsSurface) -> Self {
1109 let key = surface as *const NurbsSurface;
1110 let previous = FOOT_MEMO.with(|memo| {
1111 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
1112 let mut memo = memo.borrow_mut();
1113 if memo.as_ref().is_some_and(|open| std::ptr::eq(open.surface, key)) {
1114 None
1115 } else {
1116 Some(memo.replace(FootMemo::new(key)))
1117 }
1118 });
1119 Self { previous, _surface: std::marker::PhantomData, _thread_bound: std::marker::PhantomData }
1120 }
1121}
1122
1123impl Drop for FootMemoScope<'_> {
1124 fn drop(&mut self) {
1125 if let Some(previous) = self.previous.take() {
1126 FOOT_MEMO.with(|memo| *memo.borrow_mut() = previous);
1127 }
1128 }
1129}
1130
1131fn precision_trim_feet(surface: &NurbsSurface) -> bool {
1132 FOOT_MEMO.with(|memo| memo.borrow().as_ref().is_some_and(|m| m.precision && std::ptr::eq(m.surface, surface)))
1133}
1134
1135fn refine_memo_foot(surface: &NurbsSurface, point: Vec3, foot: Option<crate::SurfaceProjection>) -> Option<crate::SurfaceProjection> {
1136 if precision_trim_feet(surface) {
1137 foot.and_then(|foot| crate::projection::refine_trim_foot(surface, point, foot).ok())
1138 } else { foot }
1139}
1140
1141fn point_bits(point: Vec3) -> [u64; 3] {
1142 [point.x.to_bits(), point.y.to_bits(), point.z.to_bits()]
1143}
1144
1145#[track_caller]
1151fn memo_global_foot(surface: &NurbsSurface, point: Vec3) -> Option<crate::SurfaceProjection> {
1152 if crate::projection::global_site_filter_active() {
1153 return refine_memo_foot(surface, point, project_point_to_surface(surface, point).ok());
1154 }
1155 let key = point_bits(point);
1156 let deferring = DEFER_FOOT_INSERTS.with(|cell| cell.get());
1157 let hit = FOOT_MEMO.with(|memo| {
1158 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
1159 memo.borrow_mut().as_mut().filter(|open| std::ptr::eq(open.surface, surface)).map(|open| {
1160 match open.global.get(&key).copied() {
1161 Some(foot) => Some(foot),
1162 None if !deferring && !open.pending_global.is_empty() => {
1163 open.fold_pending();
1164 open.global.get(&key).copied()
1165 }
1166 None => None,
1167 }
1168 })
1169 });
1170 match hit {
1171 Some(Some(foot)) => foot,
1172 Some(None) => {
1173 let foot = refine_memo_foot(surface, point, project_point_to_surface(surface, point).ok());
1174 FOOT_MEMO.with(|memo| {
1175 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
1176 if let Some(open) = memo.borrow_mut().as_mut().filter(|open| std::ptr::eq(open.surface, surface)) {
1177 if deferring {
1178 open.pending_global.push((key, foot));
1179 } else {
1180 open.global.insert(key, foot);
1181 }
1182 }
1183 });
1184 foot
1185 }
1186 None => refine_memo_foot(surface, point, project_point_to_surface(surface, point).ok()),
1187 }
1188}
1189
1190fn memo_seeded_foot(surface: &NurbsSurface, point: Vec3, seed_u: f64, seed_v: f64) -> Option<crate::SurfaceProjection> {
1193 let [x, y, z] = point_bits(point);
1194 let key = [x, y, z, seed_u.to_bits(), seed_v.to_bits()];
1195 let deferring = DEFER_FOOT_INSERTS.with(|cell| cell.get());
1196 let hit = FOOT_MEMO.with(|memo| {
1197 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
1198 memo.borrow_mut().as_mut().filter(|open| std::ptr::eq(open.surface, surface)).map(|open| {
1199 match open.seeded.get(&key).copied() {
1200 Some(foot) => Some(foot),
1201 None if !deferring && !open.pending_seeded.is_empty() => {
1202 open.fold_pending();
1203 open.seeded.get(&key).copied()
1204 }
1205 None => None,
1206 }
1207 })
1208 });
1209 match hit {
1210 Some(Some(foot)) => foot,
1211 Some(None) => {
1212 let foot = refine_memo_foot(surface, point, project_point_to_surface_seeded(surface, point, seed_u, seed_v).ok());
1213 FOOT_MEMO.with(|memo| {
1214 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::LOOKUP);
1215 if let Some(open) = memo.borrow_mut().as_mut().filter(|open| std::ptr::eq(open.surface, surface)) {
1216 if deferring {
1217 open.pending_seeded.push((key, foot));
1218 } else {
1219 open.seeded.insert(key, foot);
1220 }
1221 }
1222 });
1223 foot
1224 }
1225 None => refine_memo_foot(surface, point, project_point_to_surface_seeded(surface, point, seed_u, seed_v).ok()),
1226 }
1227}
1228
1229thread_local! {
1230 static PCURVE_FIT_SCOPES: std::cell::RefCell<Vec<PcurveFitLedger>> =
1232 const { std::cell::RefCell::new(Vec::new()) };
1233}
1234
1235static PCURVE_TRACK_CALL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1239
1240pub struct PcurveFitScope {
1247 _thread_bound: std::marker::PhantomData<*const ()>,
1249}
1250
1251impl PcurveFitScope {
1252 pub fn open() -> Self {
1253 PCURVE_FIT_SCOPES.with(|scopes| scopes.borrow_mut().push(PcurveFitLedger::default()));
1254 Self {
1255 _thread_bound: std::marker::PhantomData,
1256 }
1257 }
1258
1259 pub fn close(self) -> PcurveFitLedger {
1261 let ledger = PCURVE_FIT_SCOPES.with(|scopes| {
1262 let mut scopes = scopes.borrow_mut();
1263 let ledger = scopes.pop().unwrap_or_default();
1264 if let Some(parent) = scopes.last_mut() {
1265 parent.fold(&ledger);
1266 }
1267 ledger
1268 });
1269 std::mem::forget(self);
1270 ledger
1271 }
1272}
1273
1274impl Drop for PcurveFitScope {
1275 fn drop(&mut self) {
1276 PCURVE_FIT_SCOPES.with(|scopes| {
1277 scopes.borrow_mut().pop();
1278 });
1279 }
1280}
1281
1282pub(crate) fn record_fit(report: &PcurveFitReport) {
1283 crate::imprint::work_profile::add(|c| { c.fit_reports += 1; c.fit_samples += report.samples as u64; c.fit_raised += u64::from(report.raised); });
1284 if env_read!("BREP_DEBUG_PCURVE_FIT").is_some() {
1288 eprintln!("PCURVE-FIT {report:?}");
1289 }
1290 PCURVE_FIT_SCOPES.with(|scopes| {
1291 if let Some(open) = scopes.borrow_mut().last_mut() {
1292 open.record(report);
1293 }
1294 });
1295}
1296
1297fn probe_residual(
1302 surface: &NurbsSurface,
1303 curve: &NurbsCurve,
1304 pcurve: &NurbsCurve,
1305 parameters: &[f64],
1306 domain: [f64; 2],
1307 _u_domain: [f64; 2],
1308 _v_domain: [f64; 2],
1309) -> Result<f64, String> {
1310 probe_residual_at(surface, curve, pcurve, parameters, domain, &[0.25, 0.5, 0.75])
1311}
1312
1313const TRACKING_ACCEPTANCE_POINTS: usize = 16;
1322
1323struct ProbeCurveReader<'a> {
1328 curve: &'a NurbsCurve,
1329 enabled: bool,
1330 domain: Option<[f64; 2]>,
1331 span: Option<usize>,
1332 previous: Option<u64>,
1333 basis: [f64; crate::curve::MAX_STACK_ORDER],
1334 left: [f64; crate::curve::MAX_STACK_ORDER],
1335 right: [f64; crate::curve::MAX_STACK_ORDER],
1336}
1337impl<'a> ProbeCurveReader<'a> {
1338 fn new(curve: &'a NurbsCurve, enabled: bool) -> Self {
1339 Self { curve, enabled, domain: None, span: None, previous: None,
1340 basis: [0.0; crate::curve::MAX_STACK_ORDER],
1341 left: [0.0; crate::curve::MAX_STACK_ORDER], right: [0.0; crate::curve::MAX_STACK_ORDER] }
1342 }
1343 #[cfg_attr(feature = "profile-wall", track_caller)]
1344 fn evaluate(&mut self, parameter: f64) -> Result<Vec3, String> {
1345 let _reader = crate::reader_profile::Point::open(crate::reader_profile::PCURVE, std::panic::Location::caller());
1346 if !self.enabled || self.curve.degree > crate::curve::MAX_STACK_DEGREE || !parameter.is_finite() {
1347 return self.curve.evaluate(parameter);
1348 }
1349 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::POINT);
1350 let [start, end] = match self.domain {
1351 Some(domain) => domain,
1352 None => {
1353 let domain = self.curve.domain()?;
1354 if self.curve.knots.windows(2).any(|w| w[1] < w[0]) {
1357 self.enabled = false;
1358 return self.curve.evaluate(parameter);
1359 }
1360 self.domain = Some(domain); domain
1361 }
1362 };
1363 let parameter = parameter.clamp(start, end);
1364 if self.previous != Some(parameter.to_bits()) {
1365 let knots = &self.curve.knots;
1366 let degree = self.curve.degree;
1367 let span = match self.span {
1368 Some(mut span) if parameter > start && parameter < end && parameter >= knots[span] => {
1369 while parameter >= knots[span + 1] { span += 1; }
1370 span
1371 }
1372 _ => crate::curve::knot_find_span(knots, degree, parameter),
1373 };
1374 match degree {
1378 1 => self.fill_basis::<1>(span, parameter),
1379 2 => self.fill_basis::<2>(span, parameter),
1380 3 => self.fill_basis::<3>(span, parameter),
1381 4 => self.fill_basis::<4>(span, parameter),
1382 5 => self.fill_basis::<5>(span, parameter),
1383 6 => self.fill_basis::<6>(span, parameter),
1384 7 => self.fill_basis::<7>(span, parameter),
1385 _ => self.fill_basis::<0>(span, parameter),
1386 }
1387 self.span = Some(span);
1388 self.previous = Some(parameter.to_bits());
1389 }
1390 let span = self.span.unwrap();
1391 let _reader_part = crate::reader_profile::Part::open(crate::reader_profile::ACCUMULATE);
1392 let mut point = Vec4 { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
1393 for (index, value) in self.basis[..=self.curve.degree].iter().enumerate() {
1394 point = point.add(self.curve.control_points[span - self.curve.degree + index].scale(*value));
1395 }
1396 point.point()
1397 }
1398 #[inline(always)]
1399 fn fill_basis<const DEGREE: usize>(&mut self, span: usize, parameter: f64) {
1400 crate::curve::basis_functions_raw_into(&self.curve.knots, DEGREE, span, parameter,
1401 EPSILON, &mut self.basis, &mut self.left, &mut self.right);
1402 }
1403}
1404
1405fn probe_batch_enabled() -> bool { switch!("BREP_PCURVE_PROBE_BATCH", false) }
1406
1407fn probe_residual_at(
1411 surface: &NurbsSurface,
1412 curve: &NurbsCurve,
1413 pcurve: &NurbsCurve,
1414 parameters: &[f64],
1415 [t0, t1]: [f64; 2],
1416 locals: &[f64],
1417) -> Result<f64, String> {
1418 crate::imprint::work_profile::add(|c| { c.pcurve_probe_passes += 1; c.pcurve_probe_samples += (parameters.len().saturating_sub(1) * locals.len()) as u64; });
1419 let mut worst = 0.0_f64;
1420 let mut reader = ProbeCurveReader::new(pcurve, probe_batch_enabled());
1421 for span in parameters.windows(2) {
1422 for &local in locals {
1423 let fraction = span[0] + (span[1] - span[0]) * local;
1424 let parameter = reader.evaluate(fraction)?;
1425 let on_surface =
1426 section_surface_point(surface, parameter.x, parameter.y, true)?;
1427 let on_curve = section_curve_point(curve, t0 + (t1 - t0) * fraction)?;
1428 let distance = on_surface.sub(on_curve).length();
1433 if !distance.is_finite() {
1434 return Err(format!(
1435 "probe_residual: non-finite image distance {distance} at fraction {fraction}"
1436 ));
1437 }
1438 worst = worst.max(distance);
1439 }
1440 }
1441 Ok(worst)
1442}
1443
1444pub(crate) fn native_probe_fractions(curves: &[&NurbsCurve]) -> Result<Vec<f64>, String> {
1451 let locals: Vec<f64> = [0.25, 0.5, 0.75]
1452 .into_iter()
1453 .chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64))
1454 .collect();
1455 let mut fractions = Vec::new();
1456 for curve in curves {
1457 let [q0, q1] = curve.domain()?;
1458 fractions.push(q0);
1459 fractions.push(q1);
1460 let mut knots: Vec<f64> = curve.knots.iter().copied().filter(|k| *k >= q0 && *k <= q1).collect();
1461 knots.dedup();
1462 for span in knots.windows(2).filter(|w| w[1] > w[0]) {
1463 for &local in &locals {
1464 fractions.push(span[0] + (span[1] - span[0]) * local);
1465 }
1466 }
1467 }
1468 fractions.sort_by(f64::total_cmp);
1469 fractions.dedup();
1470 Ok(fractions)
1471}
1472
1473#[derive(Clone, Copy, Debug, PartialEq)]
1482struct NativeRead {
1483 tracking: f64,
1484 deviation: f64,
1485 standoff: f64,
1486}
1487
1488fn native_read(surface: &NurbsSurface, curve: &NurbsCurve, pcurve: &NurbsCurve, [t0, t1]: [f64; 2], fractions: &[f64]) -> Result<NativeRead, String> {
1489 crate::imprint::work_profile::add(|c| { c.pcurve_native_passes += 1; c.pcurve_native_samples += fractions.len() as u64; });
1490 let mut read = NativeRead { tracking: 0.0, deviation: 0.0, standoff: 0.0 };
1491 let mut reader = ProbeCurveReader::new(pcurve, probe_batch_enabled());
1492 for &fraction in fractions {
1493 let parameter = reader.evaluate(fraction)?;
1494 let image = section_surface_point(surface, parameter.x, parameter.y, true)?;
1495 let on_curve = section_curve_point(curve, t0 + (t1 - t0) * fraction)?;
1496 let foot = crate::projection::project_point_to_surface_from_seed(surface, on_curve, [parameter.x, parameter.y])?;
1497 let tracking = image.sub(foot.point).length();
1498 let deviation = image.sub(on_curve).length();
1499 if !(tracking.is_finite() && deviation.is_finite() && foot.distance.is_finite()) {
1500 return Err(format!("native read: non-finite reading at fraction {fraction}"));
1501 }
1502 read.tracking = read.tracking.max(tracking);
1503 read.deviation = read.deviation.max(deviation);
1504 read.standoff = read.standoff.max(foot.distance);
1505 }
1506 Ok(read)
1507}
1508
1509fn standoff_candidate_accepted(standing: NativeRead, candidate: NativeRead, candidate_fields_finite: bool, floor: f64) -> bool {
1515 candidate_fields_finite
1516 && candidate.tracking.is_finite()
1517 && candidate.deviation.is_finite()
1518 && candidate.tracking <= floor
1519 && candidate.tracking < standing.tracking
1520 && candidate.deviation <= standing.deviation
1521}
1522
1523fn tracking_acceptance_residual(
1529 surface: &NurbsSurface,
1530 curve: &NurbsCurve,
1531 pcurve: &NurbsCurve,
1532 parameters: &[f64],
1533 domain: [f64; 2],
1534) -> Result<f64, String> {
1535 let locals: Vec<f64> = [0.25, 0.5, 0.75]
1536 .into_iter()
1537 .chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64))
1538 .collect();
1539 probe_residual_at(surface, curve, pcurve, parameters, domain, &locals)
1540}
1541
1542fn tracking_candidate_accepted(standing: f64, standing_dense: bool, floor: f64, exit: PcurveFitExit, reading: f64) -> bool {
1564 if exit == PcurveFitExit::Converged && !standing_dense {
1565 return reading <= floor && reading < standing;
1566 }
1567 if exit != PcurveFitExit::Converged && !(standing > floor) {
1568 return false;
1569 }
1570 reading.is_finite() && reading <= floor && reading < standing
1571}
1572
1573fn judge_tracking_candidate(
1580 standing: f64,
1581 standing_dense: bool,
1582 floor: f64,
1583 exit: PcurveFitExit,
1584 own: f64,
1585 dense: impl FnOnce() -> Result<f64, String>,
1586) -> (Result<Option<f64>, String>, bool) {
1587 judge_tracking_candidate_with_bounds(standing, standing_dense, floor, exit, own, dense, false)
1588}
1589
1590fn judge_tracking_candidate_with_bounds(
1591 standing: f64,
1592 standing_dense: bool,
1593 floor: f64,
1594 exit: PcurveFitExit,
1595 own: f64,
1596 dense: impl FnOnce() -> Result<f64, String>,
1597 bounds: bool,
1598) -> (Result<Option<f64>, String>, bool) {
1599 if bounds && exit != PcurveFitExit::Converged && own > floor {
1603 if crate::profile::profile_enabled() { profile_line!("pcurve.reject_bound kind=quarter_residual"); }
1604 return (Ok(Some(own)), false);
1605 }
1606 let reading = if exit == PcurveFitExit::Converged && !standing_dense {
1607 Ok(Some(own))
1608 } else if exit == PcurveFitExit::Converged || standing > floor {
1609 dense().map(Some)
1610 } else {
1611 Ok(None)
1612 };
1613 let accepted = matches!(
1614 reading,
1615 Ok(Some(value)) if tracking_candidate_accepted(standing, standing_dense, floor, exit, value)
1616 );
1617 (reading, accepted)
1618}
1619
1620struct Refinement {
1623 exit: PcurveFitExit,
1624 residual: f64,
1625 off_surface: f64,
1626 raised: bool,
1627 clamped: [f64; 2],
1630}
1631
1632#[allow(clippy::too_many_arguments)]
1665fn refine_pcurve(
1666 surface: &NurbsSurface,
1667 curve: &NurbsCurve,
1668 domain: [f64; 2],
1669 u_domain: [f64; 2],
1670 v_domain: [f64; 2],
1671 floor: f64,
1672 joins: &[f64],
1673 parameters: &mut Vec<f64>,
1674 raw: &mut Vec<[f64; 2]>,
1675 pcurve: &mut NurbsCurve,
1676 off_surface: f64,
1677 clamped: [f64; 2],
1678 mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
1679) -> Result<Refinement, String> {
1680 let trace = env_read!("BREP_DEBUG_PCURVE_FIT").is_some();
1683 let call = if trace {
1684 PCURVE_TRACK_CALL.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1
1685 } else {
1686 0
1687 };
1688 let reject_bounds = PCURVE_REJECT_BOUNDS.with(|state| state.get());
1689 let mut best = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain,
1690 floor, joins, parameters, raw, pcurve, off_surface, clamped, false, 1.0, false, &mut insert, false)?;
1691 if trace {
1692 eprintln!(
1693 "PCURVE-TRACK call={call} first-pass exit={:?} residual={:e} floor={floor:e} off_surface={:e} samples={} raised={} tracking={}",
1694 best.exit,
1695 best.residual,
1696 best.off_surface,
1697 parameters.len(),
1698 best.raised,
1699 if best.off_surface <= floor { "eligible" } else { "standoff-tracking" }
1700 );
1701 }
1702 if best.off_surface > floor {
1703 if !standoff_tracking_enabled() {
1723 return Ok(best);
1724 }
1725 return standoff_tracking_rungs(surface, curve, domain, u_domain, v_domain, floor, joins,
1726 parameters, raw, pcurve, best, trace, call, &mut insert);
1727 }
1728 let baseline_parameters = parameters.clone();
1733 let baseline_raw = raw.clone();
1734 let baseline_curve = pcurve.clone();
1735 let baseline_off_surface = best.off_surface;
1736 let baseline_clamped = best.clamped;
1737 let standing_misses = best.residual > floor;
1746 Ok(tracking_rungs(
1747 best,
1748 floor,
1749 trace,
1750 call,
1751 true,
1752 |fraction| {
1753 let mut refined_parameters = baseline_parameters.clone();
1754 let mut refined_raw = baseline_raw.clone();
1755 let mut refined_curve = baseline_curve.clone();
1756 let candidate = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain,
1757 floor, joins, &mut refined_parameters, &mut refined_raw, &mut refined_curve,
1758 baseline_off_surface, baseline_clamped, true, fraction, standing_misses, &mut insert, reject_bounds && !standing_misses);
1759 (candidate, (refined_parameters, refined_raw, refined_curve))
1760 },
1761 |state: &RungState| tracking_acceptance_residual(surface, curve, &state.2, &state.0, domain),
1762 |state: &RungState| state.0.len(),
1763 |(refined_parameters, refined_raw, refined_curve): RungState| {
1764 *parameters = refined_parameters;
1765 *raw = refined_raw;
1766 *pcurve = refined_curve;
1767 },
1768 ))
1769}
1770
1771type RungState = (Vec<f64>, Vec<[f64; 2]>, NurbsCurve);
1774
1775#[allow(clippy::too_many_arguments)]
1785fn tracking_rungs<S>(
1786 mut best: Refinement,
1787 floor: f64,
1788 trace: bool,
1789 call: u64,
1790 thread_dense: bool,
1791 mut run_pass: impl FnMut(f64) -> (Result<Refinement, String>, S),
1792 mut dense: impl FnMut(&S) -> Result<f64, String>,
1793 samples: impl Fn(&S) -> usize,
1794 mut adopt: impl FnMut(S),
1795) -> Refinement {
1796 let mut standing_dense = false;
1799 let reject_bounds = PCURVE_REJECT_BOUNDS.with(|state| state.get());
1800 for fraction in [0.1, 0.01] {
1801 if best.residual <= floor * fraction {
1802 if trace {
1803 eprintln!(
1804 "PCURVE-TRACK call={call} candidate fraction={fraction} skipped best_residual={:e} <= {:e}",
1805 best.residual,
1806 floor * fraction
1807 );
1808 }
1809 continue;
1810 }
1811 let (candidate, state) = run_pass(fraction);
1812 let judged_dense = matches!(&candidate, Ok(seen) if seen.exit != PcurveFitExit::Converged || standing_dense);
1816 let (effective, accepted) = match &candidate {
1817 Ok(seen) => judge_tracking_candidate_with_bounds(best.residual, standing_dense, floor, seen.exit, seen.residual, || {
1818 dense(&state)
1819 }, reject_bounds),
1820 Err(_) => (Ok(None), false),
1821 };
1822 if trace {
1823 let judged_on = match &effective {
1824 Ok(Some(reading)) => format!("{reading:e}"),
1825 Ok(None) => "-".to_string(),
1826 Err(message) => format!("error:{message:?}"),
1827 };
1828 match &candidate {
1829 Ok(seen) => eprintln!(
1830 "PCURVE-TRACK call={call} candidate fraction={fraction} exit={:?} residual={:e} judged_on={judged_on} standing_dense={standing_dense} floor={floor:e} off_surface={:e} samples={} raised={} residual_meets_floor={} decision={}",
1831 seen.exit,
1832 seen.residual,
1833 seen.off_surface,
1834 samples(&state),
1835 seen.raised,
1836 seen.residual <= floor,
1837 if accepted { "accept" } else { "decline-stop" }
1838 ),
1839 Err(message) => eprintln!(
1840 "PCURVE-TRACK call={call} candidate fraction={fraction} error={message:?} decision=decline-stop"
1841 ),
1842 }
1843 }
1844 match candidate {
1845 Ok(mut candidate) if accepted => {
1846 if let Ok(Some(reading)) = effective {
1848 candidate.residual = reading;
1849 }
1850 if thread_dense {
1852 standing_dense |= judged_dense;
1853 }
1854 adopt(state);
1855 best = candidate;
1856 }
1857 _ => break,
1858 }
1859 }
1860 best
1861}
1862
1863fn standoff_tracking_enabled() -> bool {
1867 switch!("BREP_PCURVE_STANDOFF_TRACKING", true)
1868}
1869
1870#[allow(clippy::too_many_arguments)]
1886fn standoff_tracking_rungs(
1887 surface: &NurbsSurface,
1888 curve: &NurbsCurve,
1889 domain: [f64; 2],
1890 u_domain: [f64; 2],
1891 v_domain: [f64; 2],
1892 floor: f64,
1893 joins: &[f64],
1894 parameters: &mut Vec<f64>,
1895 raw: &mut Vec<[f64; 2]>,
1896 pcurve: &mut NurbsCurve,
1897 mut best: Refinement,
1898 trace: bool,
1899 call: u64,
1900 insert: &mut impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
1901) -> Result<Refinement, String> {
1902 let first = native_probe_fractions(&[&*pcurve]).and_then(|probes| native_read(surface, curve, pcurve, domain, &probes));
1903 if trace {
1904 eprintln!("PCURVE-TRACK call={call} standoff-tracking first-pass native={first:?} floor={floor:e}");
1905 }
1906 let Ok(first) = first else { return Ok(best) };
1907 if first.tracking <= floor {
1908 return Ok(best);
1909 }
1910 let baseline_parameters = parameters.clone();
1911 let baseline_raw = raw.clone();
1912 let baseline_curve = pcurve.clone();
1913 for fraction in [0.1, 0.01] {
1914 let mut refined_parameters = baseline_parameters.clone();
1915 let mut refined_raw = baseline_raw.clone();
1916 let mut refined_curve = baseline_curve.clone();
1917 let candidate = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain, floor, joins,
1918 &mut refined_parameters, &mut refined_raw, &mut refined_curve, best.off_surface, best.clamped,
1919 true, fraction, true, &mut *insert, false);
1920 let reads = candidate.as_ref().ok().map(|_| {
1921 native_probe_fractions(&[&*pcurve, &refined_curve]).and_then(|probes| {
1922 Ok((native_read(surface, curve, pcurve, domain, &probes)?, native_read(surface, curve, &refined_curve, domain, &probes)?))
1923 })
1924 });
1925 let fields_finite = matches!(&candidate, Ok(seen) if seen.residual.is_finite() && seen.off_surface.is_finite());
1926 let decision = match &reads {
1927 Some(Ok((standing, read))) => standoff_rung_decision(*standing, *read, fields_finite, floor),
1928 _ => RungDecision::Stop,
1929 };
1930 if trace {
1931 eprintln!(
1932 "PCURVE-TRACK call={call} standoff-tracking fraction={fraction} candidate={} reads={reads:?} decision={}",
1933 match &candidate { Ok(seen) => format!("{:?}/{:e}/raised={}", seen.exit, seen.residual, seen.raised), Err(message) => format!("error:{message:?}") },
1934 match decision { RungDecision::Accept => "accept", RungDecision::Continue => "decline-continue", RungDecision::Stop => "decline-stop" }
1935 );
1936 }
1937 let (Ok(mut taken), Some(Ok((_, read))), RungDecision::Accept) = (candidate, reads, decision) else {
1938 if decision == RungDecision::Continue {
1939 continue;
1940 }
1941 break;
1942 };
1943 taken.residual = read.deviation;
1944 taken.off_surface = taken.off_surface.max(read.standoff);
1945 *parameters = refined_parameters;
1946 *raw = refined_raw;
1947 *pcurve = refined_curve;
1948 best = taken;
1949 if read.tracking <= floor * 0.01 {
1950 break;
1951 }
1952 }
1953 Ok(best)
1954}
1955
1956#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1959enum RungDecision {
1960 Accept,
1962 Continue,
1967 Stop,
1969}
1970
1971fn standoff_rung_decision(standing: NativeRead, candidate: NativeRead, fields_finite: bool, floor: f64) -> RungDecision {
1972 if standoff_candidate_accepted(standing, candidate, fields_finite, floor) {
1973 RungDecision::Accept
1974 } else if candidate.tracking.is_finite() && candidate.tracking < standing.tracking {
1975 RungDecision::Continue
1976 } else {
1977 RungDecision::Stop
1978 }
1979}
1980
1981fn takes_raise(exit: PcurveFitExit, raised: bool, residual: f64, off_surface: f64, floor: f64, unmet_tracking: bool) -> bool {
1988 exit == PcurveFitExit::SampleCeiling && !raised && (residual > off_surface + floor || unmet_tracking)
1989}
1990
1991fn refine_pcurve_pass(
1992 surface: &NurbsSurface,
1993 curve: &NurbsCurve,
1994 [t0, t1]: [f64; 2],
1995 [u0, u1]: [f64; 2],
1996 [v0, v1]: [f64; 2],
1997 floor: f64,
1998 joins: &[f64],
1999 parameters: &mut Vec<f64>,
2000 raw: &mut Vec<[f64; 2]>,
2001 pcurve: &mut NurbsCurve,
2002 mut off_surface: f64,
2003 mut clamped: [f64; 2],
2004 track_projection: bool,
2005 tracking_fraction: f64,
2006 raise_unmet_tracking: bool,
2007 mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
2008 reject_budget: bool,
2009) -> Result<Refinement, String> {
2010 let _clock = PcurvePhaseClock::new("refinement", parameters.len(), track_projection);
2011 let tracking_floor = if track_projection { floor * tracking_fraction } else { floor };
2012 let mut max_samples = MAX_PCURVE_SAMPLES;
2013 let mut raised = false;
2014 loop {
2015 let mut exit = PcurveFitExit::RoundBudget;
2019 let mut residual = None;
2020 for round in 0..4 {
2021 crate::imprint::work_profile::add(|c| c.refinement_rounds += 1);
2022 if parameters.len() >= max_samples {
2023 exit = PcurveFitExit::SampleCeiling;
2024 break;
2025 }
2026 let mut inserts = Vec::new();
2027 let mut round_max = 0.0_f64;
2028 let mut truncated = false;
2029 let mut skipped_narrow = false;
2030 let mut reader = ProbeCurveReader::new(pcurve, probe_batch_enabled());
2031 for index in 0..parameters.len() - 1 {
2032 if parameters.len() + inserts.len() >= max_samples {
2033 truncated = true;
2034 break;
2035 }
2036 let start = parameters[index];
2037 let end = parameters[index + 1];
2038 if end - start < if track_projection { SPAN_FLOOR } else { 1e-3 } {
2042 skipped_narrow = true;
2043 continue;
2044 }
2045 for local_fraction in [0.25, 0.5, 0.75] {
2046 if parameters.len() + inserts.len() >= max_samples {
2047 truncated = true;
2048 break;
2049 }
2050 let fraction = start + (end - start) * local_fraction;
2051 let parameter = reader.evaluate(fraction)?;
2052 let on_surface =
2053 section_surface_point(surface, parameter.x, parameter.y, true)?;
2054 let on_curve = section_curve_point(curve, t0 + (t1 - t0) * fraction)?;
2055 let deviation = on_surface.sub(on_curve).length();
2056 if !deviation.is_finite() {
2062 return Err(format!(
2063 "refine_pcurve_pass: non-finite station distance {deviation} at fraction {fraction}"
2064 ));
2065 }
2066 round_max = round_max.max(deviation);
2067 if deviation <= tracking_floor {
2068 continue;
2069 }
2070 let (surface_parameter, gap) = insert(fraction, parameter, on_curve)?;
2071 off_surface = off_surface.max(gap);
2072 if let Some(surface_parameter) = surface_parameter {
2073 if raised || track_projection {
2082 let projected =
2083 section_surface_point(surface, surface_parameter[0], surface_parameter[1], false)?;
2084 if on_surface.sub(projected).length() <= tracking_floor {
2085 continue;
2086 }
2087 }
2088 inserts.push((index + 1, fraction, surface_parameter));
2089 }
2090 }
2091 }
2092 if inserts.is_empty() {
2093 exit = if truncated {
2098 PcurveFitExit::SampleCeiling
2099 } else if round_max > floor {
2100 PcurveFitExit::Stalled
2101 } else if skipped_narrow {
2102 PcurveFitExit::SpanFloor
2103 } else {
2104 residual = Some(round_max);
2105 PcurveFitExit::Converged
2106 };
2107 break;
2108 }
2109 if reject_budget && round == 3 && !truncated {
2114 if crate::profile::profile_enabled() { profile_line!("pcurve.reject_bound kind=fifth_round"); }
2115 return Err("pcurve tracking candidate requires a fifth round".into());
2116 }
2117 for (at, fraction, surface_parameter) in inserts.into_iter().rev() {
2118 parameters.insert(at, fraction);
2119 raw.insert(at, surface_parameter);
2120 }
2121 let (rebuilt, excursion, distance) = build_interpolant_joined(surface, raw, parameters, joins)?;
2122 *pcurve = rebuilt;
2123 clamped = [excursion, distance];
2124 if truncated {
2125 exit = PcurveFitExit::SampleCeiling;
2126 break;
2127 }
2128 }
2129 let residual = match residual {
2130 Some(measured) => measured,
2131 None => probe_residual(
2135 surface,
2136 curve,
2137 pcurve,
2138 parameters,
2139 [t0, t1],
2140 [u0, u1],
2141 [v0, v1],
2142 )?,
2143 };
2144 if exit == PcurveFitExit::SpanFloor && residual <= floor {
2145 exit = PcurveFitExit::Converged;
2146 }
2147 let unmet_tracking = track_projection
2164 && raise_unmet_tracking
2165 && exit == PcurveFitExit::SampleCeiling
2166 && !raised
2167 && native_probe_fractions(&[&*pcurve])
2168 .and_then(|probes| native_read(surface, curve, pcurve, [t0, t1], &probes))
2169 .is_ok_and(|read| read.tracking > tracking_floor);
2170 if takes_raise(exit, raised, residual, off_surface, floor, unmet_tracking) {
2171 raised = true;
2172 max_samples = PCURVE_RAISED_SAMPLES;
2173 continue;
2174 }
2175 return Ok(Refinement {
2176 exit,
2177 residual,
2178 off_surface,
2179 raised,
2180 clamped,
2181 });
2182 }
2183}
2184
2185struct AffineInversion {
2193 origin: Vec3,
2194 du: Vec3,
2195 dv: Vec3,
2196 u0: f64,
2197 v0: f64,
2198 uu: f64,
2199 uv: f64,
2200 vv: f64,
2201 determinant: f64,
2202}
2203
2204impl AffineInversion {
2205 fn of(surface: &NurbsSurface) -> Result<Option<Self>, String> {
2210 if !surface.is_affine()? {
2211 return Ok(None);
2212 }
2213 let ([u0, _], [v0, _]) = surface_domains(surface)?;
2214 let (origin, du, dv) = surface.deriv1(u0, v0)?;
2215 let uu = du.dot(du);
2216 let uv = du.dot(dv);
2217 let vv = dv.dot(dv);
2218 let determinant = uu * vv - uv * uv;
2219 if determinant.abs() <= 1e-18 {
2220 return Ok(None);
2221 }
2222 Ok(Some(Self {
2223 origin,
2224 du,
2225 dv,
2226 u0,
2227 v0,
2228 uu,
2229 uv,
2230 vv,
2231 determinant,
2232 }))
2233 }
2234
2235 fn uv_of(&self, point: Vec3) -> [f64; 2] {
2236 let delta = point.sub(self.origin);
2237 let along_u = delta.dot(self.du);
2238 let along_v = delta.dot(self.dv);
2239 [
2240 self.u0 + (along_u * self.vv - along_v * self.uv) / self.determinant,
2241 self.v0 + (along_v * self.uu - along_u * self.uv) / self.determinant,
2242 ]
2243 }
2244
2245 fn image_of(&self, curve: &NurbsCurve) -> Result<NurbsCurve, String> {
2248 let control_points = curve
2249 .control_points
2250 .iter()
2251 .map(|control| {
2252 let [u, v] = self.uv_of(control.point()?);
2253 Ok(Vec4::from_point(Vec3::new(u, v, 0.0), control.w))
2254 })
2255 .collect::<Result<Vec<_>, String>>()?;
2256 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
2257 }
2258}
2259
2260fn sphere_iso_enabled() -> bool {
2261 env_read!("BREP_PCURVE_SPHERE_ISO").is_none_or(|mode| mode != "0")
2262}
2263
2264fn sphere_iso_fit(surface: &NurbsSurface, curve: &NurbsCurve) -> Option<PcurveFit> {
2271 if surface.degree_u != 2 { return None; }
2274 let [t0, t1] = curve.domain().ok()?;
2275 let [u0, u1] = surface.domain_u().ok()?;
2276 let [v0, v1] = surface.domain_v().ok()?;
2277 if !matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })) { return None; }
2278 let middle = curve.evaluate(t0 + (t1 - t0) * 0.5).ok()?;
2279 let projected = project_point_to_surface(surface, middle).ok()?;
2280 let mut candidates = vec![projected.u];
2281 for &knot in &surface.knots_u {
2284 if (knot - projected.u).abs() <= 1e-12 && !candidates.contains(&knot) { candidates.push(knot); }
2285 }
2286 for u in candidates {
2287 if !(u >= u0 && u <= u1) { continue; }
2288 let iso = surface.iso_curve_u(u).ok()?;
2289 if iso.degree != curve.degree || iso.knots.len() != curve.knots.len()
2290 || iso.control_points.len() != curve.control_points.len() { continue; }
2291 for reverse in [false, true] {
2292 let n = iso.control_points.len();
2293 fn exact_difference(a: f64, b: f64) -> Option<f64> {
2299 if !(a.is_finite() && b.is_finite()) || a.abs().max(b.abs()) > 1e100 { return None; }
2300 let x = a - b;
2301 let bv = a - x;
2302 let av = x + bv;
2303 let error = (a - av) + (bv - b);
2304 (error == 0.).then_some(x)
2305 }
2306 fn exact_fraction(offset: f64, span: f64) -> Option<f64> {
2307 if !(1e-100..=1e100).contains(&span) { return None; }
2308 let ratio = offset / span;
2309 if !(0.0..=1.0).contains(&ratio) || (ratio != 0. && ratio < 1e-100) { return None; }
2310 (ratio.mul_add(span, -offset) == 0.).then_some(ratio)
2311 }
2312 let knots_match = (|| {
2313 let section_span = exact_difference(t1, t0)?;
2314 let native_span = exact_difference(v1, v0)?;
2315 for (i, &knot) in curve.knots.iter().enumerate() {
2316 let section = exact_difference(knot, t0)?;
2317 let native = if reverse { exact_difference(v1, iso.knots[iso.knots.len() - 1 - i])? }
2318 else { exact_difference(iso.knots[i], v0)? };
2319 if exact_fraction(section, section_span)? != exact_fraction(native, native_span)? { return None; }
2320 }
2321 Some(())
2322 })().is_some();
2323 if !knots_match { continue; }
2324 let mut bound = 0.0_f64;
2325 let mut matches = true;
2326 for (i, control) in curve.control_points.iter().enumerate() {
2327 let native = &iso.control_points[if reverse { n - 1 - i } else { i }];
2328 if !(control.w > 0.) || control.w != native.w { matches = false; break; }
2329 let distance = control.point().ok()?.sub(native.point().ok()?).length();
2330 if !distance.is_finite() { matches = false; break; }
2331 bound = bound.max(distance);
2332 }
2333 if !matches || bound > PCURVE_REFINEMENT_TOLERANCE * 0.0625 { continue; }
2334 let (start, end) = if reverse { (v1, v0) } else { (v0, v1) };
2335 let pcurve = NurbsCurve::new(1, vec![0., 0., 1., 1.], vec![
2336 Vec4::from_point(Vec3::new(u, start, 0.), 1.),
2337 Vec4::from_point(Vec3::new(u, end, 0.), 1.),
2338 ]).ok()?;
2339 for i in 0..=96 {
2344 let fraction = i as f64 / 96.;
2345 let uv = pcurve.evaluate(fraction).ok()?;
2346 let image = surface.evaluate(uv.x, uv.y).ok()?;
2347 let on_curve = curve.evaluate(t0 + (t1 - t0) * fraction).ok()?;
2348 let error = image.sub(on_curve).length();
2349 if !error.is_finite() { matches = false; break; }
2350 bound = bound.max(error);
2351 }
2352 if !matches || bound > PCURVE_REFINEMENT_TOLERANCE * 0.0625 { continue; }
2353 crate::imprint::work_profile::constructed("analytic_certified", 0, pcurve.control_points.len());
2354 return Some(PcurveFit { curve: pcurve, report: PcurveFitReport {
2355 residual: bound, floor: PCURVE_REFINEMENT_TOLERANCE, samples: 2,
2356 exit: PcurveFitExit::Converged, off_surface: bound, raised: false,
2357 clamped_excursion: 0., clamped_distance: 0.,
2358 } });
2359 }
2360 }
2361 None
2362}
2363
2364pub fn fit_pcurve_on_surface(
2374 surface: &NurbsSurface,
2375 curve: &NurbsCurve,
2376) -> Result<PcurveFit, String> {
2377 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::FIT);
2378 crate::imprint::work_profile::add(|c| c.fit_calls += 1);
2379 if sphere_iso_enabled() {
2380 if let Some(fit) = sphere_iso_fit(surface, curve) { return Ok(fit); }
2381 }
2382 let mode = env_read!("BREP_PCURVE_EVAL_CACHE").unwrap_or_default();
2383 let bounds = env_read!("BREP_PCURVE_REJECT_BOUNDS").unwrap_or_else(|| "on".into());
2384 let stack = env_read!("BREP_PCURVE_STACK_POINT").unwrap_or_else(|| "on".into());
2385 if mode == "check" || bounds == "check" || stack == "check" {
2386 let old = fit_pcurve_on_surface_cached(surface, curve, false, false, false)?;
2387 if matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })) {
2388 let candidate = fit_pcurve_on_surface_cached(surface, curve, mode == "check" || mode == "on" || mode == "1", bounds == "check" || bounds == "on" || bounds == "1", stack == "check" || stack == "on" || stack == "1");
2389 let identical = candidate.as_ref().is_ok_and(|new| {
2390 old.curve.degree == new.curve.degree
2391 && old.curve.knots.len() == new.curve.knots.len()
2392 && old.curve.knots.iter().zip(&new.curve.knots).all(|(a, b)| a.to_bits() == b.to_bits())
2393 && old.curve.control_points.len() == new.curve.control_points.len()
2394 && old.curve.control_points.iter().zip(&new.curve.control_points).all(|(a, b)| {
2395 [a.x.to_bits(), a.y.to_bits(), a.z.to_bits(), a.w.to_bits()]
2396 == [b.x.to_bits(), b.y.to_bits(), b.z.to_bits(), b.w.to_bits()]
2397 }) && old.report == new.report
2398 });
2399 if let Ok(path) = std::env::var("BREP_PCURVE_EVAL_CENSUS") {
2400 use std::io::Write;
2401 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
2402 let _ = writeln!(file, "pcurve.eval_census identical={identical} controls={} samples={}", old.curve.control_points.len(), old.report.samples);
2403 }
2404 }
2405 }
2406 return Ok(old);
2407 }
2408 fit_pcurve_on_surface_cached(surface, curve, mode == "1" || mode == "on", bounds == "1" || bounds == "on", stack == "1" || stack == "on")
2409}
2410
2411fn fit_pcurve_on_surface_cached(surface: &NurbsSurface, curve: &NurbsCurve, enabled: bool, bounds: bool, stack: bool) -> Result<PcurveFit, String> {
2412 let _stack = PcurveStackPointScope::open(stack && matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })));
2413 let _samples = PcurveSampleScope::open(surface, curve, enabled
2417 || !matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })));
2418 let _bounds = PcurveRejectScope::open(bounds && matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })));
2419 let [t0, t1] = curve.domain()?;
2420 if surface.is_affine()? {
2421 let Some(inversion) = AffineInversion::of(surface)? else {
2422 return Err("build_pcurve_on_surface: singular affine parameterization".into());
2423 };
2424 let pcurve = inversion.image_of(curve)?;
2425 crate::imprint::work_profile::constructed("affine", 0, pcurve.control_points.len());
2426 return Ok(PcurveFit {
2427 report: PcurveFitReport {
2428 residual: 0.0,
2429 floor: PCURVE_REFINEMENT_TOLERANCE,
2430 samples: pcurve.control_points.len(),
2431 exit: PcurveFitExit::Converged,
2432 off_surface: 0.0,
2433 raised: false,
2434 clamped_excursion: 0.0,
2435 clamped_distance: 0.0,
2436 },
2437 curve: pcurve,
2438 });
2439 }
2440
2441 let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
2442 let drop_tolerance = 1e-4 * scale;
2443 let endpoint_tolerance = 1e-3 * scale;
2444 let refinement_tolerance = PCURVE_REFINEMENT_TOLERANCE;
2445 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
2446 let invert =
2447 |fraction: f64| invert_checked(surface, section_curve_point(curve, t0 + (t1 - t0) * fraction)?);
2448
2449 let mut parameters = Vec::with_capacity(25);
2450 let mut raw_surface_parameters = Vec::with_capacity(25);
2451 let mut off_surface = 0.0_f64;
2454 let corners = range_breaks(curve, t0, t1, true);
2457 let mut stations: Vec<f64> = (0..=24).map(|index| index as f64 / 24.0).collect();
2458 stations.retain(|station| {
2461 *station == 0.0 || *station == 1.0 || corners.contains(station)
2462 || !corners.iter().any(|corner| (*station - corner).abs() <= SPAN_FLOOR)
2463 });
2464 for &corner in &corners {
2465 let at = stations.partition_point(|value| *value < corner);
2466 if stations.get(at) != Some(&corner) {
2467 stations.insert(at, corner);
2468 }
2469 }
2470 let joins = corners;
2471 for (index, &fraction) in stations.iter().enumerate() {
2472 let (parameter, distance) = invert(fraction)?;
2473 off_surface = off_surface.max(distance);
2474 if distance > drop_tolerance && index != 0 && index != stations.len() - 1 {
2475 continue;
2476 }
2477 if distance > endpoint_tolerance {
2478 if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
2479 let p3 = curve.evaluate(t0 + (t1 - t0) * fraction).ok();
2480 let p0 = curve.evaluate(t0).ok();
2481 let p1 = curve.evaluate(t1).ok();
2482 eprintln!(
2483 "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={:?}",
2484 surface.evaluate(parameter[0], parameter[1]).ok()
2485 );
2486 }
2487 return Err(format!(
2488 "build_pcurve_on_surface: endpoint projection failed (distance={distance})"
2489 ));
2490 }
2491 parameters.push(fraction);
2492 raw_surface_parameters.push(parameter);
2493 }
2494
2495 let (mut pcurve, excursion, distance) =
2496 build_interpolant_joined(surface, &raw_surface_parameters, ¶meters, &joins)?;
2497 let refinement = refine_pcurve(
2498 surface,
2499 curve,
2500 [t0, t1],
2501 [u0, u1],
2502 [v0, v1],
2503 refinement_tolerance,
2504 &joins,
2505 &mut parameters,
2506 &mut raw_surface_parameters,
2507 &mut pcurve,
2508 off_surface,
2509 [excursion, distance],
2510 |fraction, _parameter, _on_curve| {
2511 let (surface_parameter, distance) = invert(fraction)?;
2512 Ok((
2513 (distance <= drop_tolerance).then_some(surface_parameter),
2514 distance,
2515 ))
2516 },
2517 )?;
2518 crate::imprint::work_profile::constructed("projected_global", parameters.len(), pcurve.control_points.len());
2519 Ok(PcurveFit {
2520 curve: pcurve,
2521 report: PcurveFitReport {
2522 residual: refinement.residual,
2523 floor: refinement_tolerance,
2524 samples: parameters.len(),
2525 exit: refinement.exit,
2526 off_surface: refinement.off_surface,
2527 raised: refinement.raised,
2528 clamped_excursion: refinement.clamped[0],
2529 clamped_distance: refinement.clamped[1],
2530 },
2531 })
2532}
2533
2534pub fn build_pcurve_on_surface(
2539 surface: &NurbsSurface,
2540 curve: &NurbsCurve,
2541) -> Result<NurbsCurve, String> {
2542 let fit = fit_pcurve_on_surface(surface, curve)?;
2543 record_fit(&fit.report);
2544 Ok(fit.curve)
2545}
2546
2547struct SectionFitClock {
2549 started: Option<web_time::Instant>,
2550 global: std::time::Duration,
2551 seeded: bool,
2552}
2553impl SectionFitClock {
2554 fn new() -> Self {
2555 Self { started: crate::profile::profile_enabled().then(web_time::Instant::now),
2556 global: std::time::Duration::ZERO, seeded: false }
2557 }
2558}
2559impl Drop for SectionFitClock {
2560 fn drop(&mut self) {
2561 if let Some(started) = self.started {
2562 crate::imprint::profile_emit(format_args!("pcurve.section_time global={:.3} seeded={:.3} result={}",
2563 self.global.as_secs_f64() * 1000.0,
2564 started.elapsed().saturating_sub(self.global).as_secs_f64() * 1000.0,
2565 if self.seeded { "seeded" } else { "global" }));
2566 }
2567 }
2568}
2569
2570pub fn fit_pcurve_on_surface_marched(
2601 surface: &NurbsSurface,
2602 curve: &NurbsCurve,
2603) -> Result<PcurveFit, String> {
2604 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::FIT);
2605 crate::imprint::work_profile::add(|c| c.fit_calls += 1);
2606 fn run(surface: &NurbsSurface, curve: &NurbsCurve, eager: bool) -> Result<PcurveFit, String> {
2607 let _samples = PcurveSampleScope::open(surface, curve, true);
2610 let mut clock = SectionFitClock::new();
2611 let mut fallback = || {
2615 let started = clock.started.map(|_| web_time::Instant::now());
2616 let fit = fit_pcurve_on_surface(surface, curve);
2617 if let Some(started) = started { clock.global += started.elapsed(); }
2618 fit
2619 };
2620 let global = if eager { Some(fallback()?) } else { None };
2621 if !eager { curve.domain()?; }
2625 if env_read!("BREP_SECTION_PCURVE_MARCH").as_deref() == Some("0") || surface.is_affine()? {
2626 return global.map_or_else(&mut fallback, Ok);
2627 }
2628 let [t0, t1] = curve.domain()?;
2629 if curve.evaluate(t0)?.sub(curve.evaluate(t1)?).length() <= 1e-6 {
2632 return global.map_or_else(&mut fallback, Ok);
2633 }
2634 const SAMPLES: usize = 24;
2635 let fractions: Vec<f64> = (0..=SAMPLES).map(|i| i as f64 / SAMPLES as f64).collect();
2636 let edge: Vec<Vec3> = fractions
2637 .iter()
2638 .map(|fraction| curve.evaluate(t0 + (t1 - t0) * fraction))
2639 .collect::<Result<Vec<_>, _>>()?;
2640 let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
2641 let endpoint_tolerance = 1e-3 * scale;
2642 let (first, first_distance) = invert_checked(surface, edge[0])?;
2643 let (last, last_distance) = invert_checked(surface, edge[SAMPLES])?;
2644 if first_distance > endpoint_tolerance || last_distance > endpoint_tolerance {
2645 return global.map_or_else(&mut fallback, Ok);
2646 }
2647 let mut off_surface = first_distance.max(last_distance);
2650 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
2651 let u_span = (u1 - u0).abs().max(EPSILON);
2652 let v_span = (v1 - v0).abs().max(EPSILON);
2653 let (closed_u, closed_v) = surface_closedness(surface)?;
2654 let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
2655 let mut du = a[0] - b[0];
2656 if closed_u {
2657 while du > 0.5 * u_span {
2658 du -= u_span;
2659 }
2660 while du < -0.5 * u_span {
2661 du += u_span;
2662 }
2663 }
2664 let mut dv = a[1] - b[1];
2665 if closed_v {
2666 while dv > 0.5 * v_span {
2667 dv -= v_span;
2668 }
2669 while dv < -0.5 * v_span {
2670 dv += v_span;
2671 }
2672 }
2673 ((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
2674 };
2675 let mut raw = vec![first];
2677 let mut previous = first;
2678 for index in 1..SAMPLES {
2679 let seeded =
2680 crate::projection::project_point_to_surface_from_seed(surface, edge[index], previous)?;
2681 let (_, global_distance) = invert_checked(surface, edge[index])?;
2682 if seeded.distance > 4.0 * global_distance + endpoint_tolerance {
2686 return global.map_or_else(&mut fallback, Ok);
2687 }
2688 off_surface = off_surface.max(seeded.distance);
2689 previous = [seeded.u, seeded.v];
2690 raw.push(previous);
2691 }
2692 raw.push(last);
2693 let mut steps: Vec<f64> = (1..raw.len())
2699 .map(|i| norm_step(raw[i], raw[i - 1]))
2700 .collect();
2701 let endpoint_gap = steps.pop().unwrap_or(0.0);
2702 steps.sort_by(f64::total_cmp);
2703 let median = steps
2704 .get(steps.len() / 2)
2705 .copied()
2706 .unwrap_or(0.0)
2707 .max(EPSILON);
2708 if endpoint_gap > (4.0 * median).max(0.05) {
2709 return global.map_or_else(&mut fallback, Ok);
2710 }
2711 let mut parameters = fractions;
2715 let (mut pcurve, excursion, distance) = build_interpolant(surface, &raw, ¶meters)?;
2716 let refinement_tolerance = PCURVE_REFINEMENT_TOLERANCE;
2717 let refinement = refine_pcurve(
2720 surface,
2721 curve,
2722 [t0, t1],
2723 [u0, u1],
2724 [v0, v1],
2725 refinement_tolerance,
2726 &[],
2727 &mut parameters,
2728 &mut raw,
2729 &mut pcurve,
2730 off_surface,
2731 [excursion, distance],
2732 |_fraction, seed, on_curve| {
2733 let seeded = crate::projection::project_point_to_surface_from_seed(surface, on_curve, [seed.x, seed.y])?;
2734 Ok((Some([seeded.u, seeded.v]), seeded.distance))
2735 },
2736 )?;
2737 clock.seeded = true;
2738 crate::imprint::work_profile::constructed("projected_seeded", parameters.len(), pcurve.control_points.len());
2739 Ok(PcurveFit {
2740 curve: pcurve,
2741 report: PcurveFitReport {
2742 residual: refinement.residual,
2743 floor: refinement_tolerance,
2744 samples: parameters.len(),
2745 exit: refinement.exit,
2746 off_surface: refinement.off_surface,
2747 raised: refinement.raised,
2748 clamped_excursion: refinement.clamped[0],
2749 clamped_distance: refinement.clamped[1],
2750 },
2751 })
2752 }
2753
2754 let mode = match std::env::var("BREP_PCURVE_LAZY_GLOBAL") {
2755 Ok(mode) => mode,
2756 Err(std::env::VarError::NotPresent) => "on".to_owned(),
2757 Err(std::env::VarError::NotUnicode(_)) => String::new(),
2758 };
2759 if mode != "check" {
2760 return run(surface, curve, !matches!(mode.as_str(), "1" | "on"));
2761 }
2762 let lazy_surface = surface.clone();
2765 let lazy_curve = curve.clone();
2766 let eager = run(surface, curve, true);
2767 let lazy = run(&lazy_surface, &lazy_curve, false);
2768 let identical = match (&eager, &lazy) {
2769 (Ok(a), Ok(b)) => {
2770 a.curve.degree == b.curve.degree
2771 && a.curve.knots.len() == b.curve.knots.len()
2772 && a.curve.knots.iter().zip(&b.curve.knots).all(|(a,b)| a.to_bits() == b.to_bits())
2773 && a.curve.control_points.len() == b.curve.control_points.len()
2774 && a.curve.control_points.iter().zip(&b.curve.control_points).all(|(a,b)| {
2775 [a.x.to_bits(),a.y.to_bits(),a.z.to_bits(),a.w.to_bits()]
2776 == [b.x.to_bits(),b.y.to_bits(),b.z.to_bits(),b.w.to_bits()]
2777 })
2778 && a.report == b.report
2779 && [a.report.residual, a.report.floor, a.report.off_surface,
2780 a.report.clamped_excursion, a.report.clamped_distance].map(f64::to_bits)
2781 == [b.report.residual, b.report.floor, b.report.off_surface,
2782 b.report.clamped_excursion, b.report.clamped_distance].map(f64::to_bits)
2783 }
2784 (Err(a), Err(b)) => a == b,
2785 _ => false,
2786 };
2787 if let Ok(path) = std::env::var("BREP_PCURVE_LAZY_GLOBAL_CENSUS") {
2788 use std::io::Write;
2789 let mut file = std::fs::OpenOptions::new().create(true).append(true).open(path)
2790 .expect("open lazy-global differential ledger");
2791 writeln!(file, "pcurve.lazy_global identical={identical} eager_ok={} lazy_ok={} degree={} controls={}",
2792 eager.is_ok(), lazy.is_ok(), curve.degree, curve.control_points.len())
2793 .expect("append lazy-global differential ledger");
2794 }
2795 assert!(identical, "lazy global pcurve fallback changed the curve, report or error");
2796 eager
2797}
2798
2799pub fn build_pcurve_on_surface_marched(
2803 surface: &NurbsSurface,
2804 curve: &NurbsCurve,
2805) -> Result<NurbsCurve, String> {
2806 let fit = fit_pcurve_on_surface_marched(surface, curve)?;
2807 record_fit(&fit.report);
2808 Ok(fit.curve)
2809}
2810
2811pub fn fit_pcurve_on_surface_range(
2819 surface: &NurbsSurface,
2820 curve: &NurbsCurve,
2821 edge_start: f64,
2822 edge_end: f64,
2823 forward: bool,
2824 tolerance: f64,
2825) -> Result<PcurveFit, String> {
2826 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::FIT);
2827 crate::imprint::work_profile::add(|c| c.fit_calls += 1);
2828 if let Some(exact) =
2829 affine_pcurve_on_range(surface, curve, edge_start, edge_end, forward, tolerance)?
2830 {
2831 return Ok(exact);
2832 }
2833 fit_pcurve_on_surface_range_dense(
2834 surface, curve, edge_start, edge_end, forward, tolerance, 64, 3, 513,
2835 )
2836}
2837
2838pub(crate) fn refine_import_projected_range(
2842 surface: &NurbsSurface,
2843 curve: &NurbsCurve,
2844 edge_start: f64,
2845 edge_end: f64,
2846 forward: bool,
2847 legacy: PcurveFit,
2848) -> PcurveFit {
2849 let _feet = FootMemoScope::precision(surface);
2850 let evaluate = |fraction: f64| {
2851 let along = if forward { fraction } else { 1.0 - fraction };
2852 curve.evaluate(edge_start + (edge_end - edge_start) * along)
2853 };
2854 let Ok(fixed) = range_use_probes(&[&legacy.curve], curve, edge_start, edge_end, forward) else { return legacy };
2855 let Ok((old, _)) = common_station_read_with_excess(surface, &evaluate, &legacy.curve, &legacy.curve, &fixed) else { return legacy };
2856 if old[0] <= PCURVE_REFINEMENT_TOLERANCE { return legacy; }
2857 let breaks = range_breaks(curve, edge_start, edge_end, forward);
2858 for span_floor in [SPAN_FLOOR, SPAN_FLOOR * 0.01] {
2863 let Ok(candidate) = fit_station_pcurve_mode(surface, &evaluate, &breaks, &breaks,
2864 PCURVE_REFINEMENT_TOLERANCE * 0.1, 64, 3, 513, span_floor, true, Some(&legacy.curve), &fixed) else { return legacy };
2865 let Ok(probes) = range_use_probes(&[&legacy.curve, &candidate.curve], curve, edge_start, edge_end, forward) else { return legacy };
2866 let readings = common_station_read_with_excess(surface, &evaluate, &legacy.curve, &candidate.curve, &probes);
2867 let accepted = matches!(&readings, Ok((old, new)) if floor_admits(*old, *new));
2868 if station_fit_tracing() {
2869 eprintln!("STATION-FIT import projected challenger: span_floor={span_floor:e} old={:?} candidate={:?} common={readings:?} accepted={accepted}", legacy.report, candidate.report);
2870 }
2871 if accepted { return candidate; }
2872 if candidate.report.exit != PcurveFitExit::SpanFloor { break; }
2873 }
2874 legacy
2875}
2876
2877pub fn build_pcurve_on_surface_range(
2881 surface: &NurbsSurface,
2882 curve: &NurbsCurve,
2883 edge_start: f64,
2884 edge_end: f64,
2885 forward: bool,
2886 tolerance: f64,
2887) -> Result<NurbsCurve, String> {
2888 accept_fit(fit_pcurve_on_surface_range(
2889 surface, curve, edge_start, edge_end, forward, tolerance,
2890 )?)
2891}
2892
2893fn station_fit_tracing() -> bool {
2896 debug_flag!("BREP_DEBUG_STATION_FIT")
2897}
2898
2899const SPAN_FLOOR: f64 = 1e-6;
2906
2907const RAISED_ROUNDS: usize = 40;
2910
2911pub const PCURVE_OFF_BAR: &str = "pcurve fit refused: the trim misses its stations by";
2913
2914fn accept_fit(fit: PcurveFit) -> Result<NurbsCurve, String> {
2927 let report = fit.report;
2928 if report.on_bar() {
2929 return Ok(fit.curve);
2930 }
2931 Err(off_bar_message(&report))
2932}
2933
2934fn off_bar_message(report: &PcurveFitReport) -> String {
2935 format!(
2936 "{PCURVE_OFF_BAR} {:.3e} out of sample against a bar of {:.3e} (asked {:.1e}, the \
2937 stations stand {:.3e} off the carrier; {:?} at {} samples{}{})",
2938 report.residual,
2939 report.floor + report.off_surface,
2940 report.floor,
2941 report.off_surface,
2942 report.exit,
2943 report.samples,
2944 if report.raised { ", after the raise" } else { "" },
2945 if report.clamped_excursion > 0.0 {
2946 format!(
2947 "; clamped into the chart by {:.3e} in parameter, {:.3e} in 3D",
2948 report.clamped_excursion, report.clamped_distance
2949 )
2950 } else {
2951 String::new()
2952 },
2953 )
2954}
2955
2956pub(crate) const PCURVE_OFF_BAR_WHAT: &str = "pcurve_fit_off_bar";
2962
2963pub(crate) fn is_pcurve_off_bar(refusal: &crate::KernelRefusal) -> bool {
2965 matches!(&refusal.class, crate::RefusalClass::NonConvergence { what } if what == PCURVE_OFF_BAR_WHAT)
2966}
2967
2968pub(crate) fn build_pcurve_on_surface_range_classed(
2972 surface: &NurbsSurface,
2973 curve: &NurbsCurve,
2974 edge_start: f64,
2975 edge_end: f64,
2976 forward: bool,
2977 tolerance: f64,
2978 stage: KernelStage,
2979 what: &'static str,
2980) -> Result<NurbsCurve, crate::KernelRefusal> {
2981 use crate::OrRefuse;
2982 let fit = fit_pcurve_on_surface_range(surface, curve, edge_start, edge_end, forward, tolerance)
2983 .or_refuse(stage, what)?;
2984 accept_fit_classed(fit, stage)
2985}
2986
2987pub(crate) fn accept_fit_classed(fit: PcurveFit, stage: KernelStage) -> Result<NurbsCurve, crate::KernelRefusal> {
2990 if fit.report.on_bar() {
2991 return Ok(fit.curve);
2992 }
2993 Err(crate::KernelRefusal {
2994 class: crate::RefusalClass::NonConvergence { what: PCURVE_OFF_BAR_WHAT.into() },
2995 stage,
2996 message: off_bar_message(&fit.report),
2997 })
2998}
2999
3000fn affine_pcurve_on_range(
3022 surface: &NurbsSurface,
3023 curve: &NurbsCurve,
3024 edge_start: f64,
3025 edge_end: f64,
3026 forward: bool,
3027 tolerance: f64,
3028) -> Result<Option<PcurveFit>, String> {
3029 if std::env::var("BREP_AFFINE_RANGE_PCURVE").as_deref() == Ok("0") {
3035 return Ok(None);
3036 }
3037 let Some(inversion) = AffineInversion::of(surface)? else {
3038 return Ok(None);
3039 };
3040 if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
3041 return Ok(None);
3043 }
3044 let [t0, t1] = curve.domain()?;
3045 let start = edge_start.max(t0);
3046 let end = edge_end.min(t1);
3047 if end - start <= 2.0 * KNOT_IDENTITY_TOL {
3050 return Ok(None);
3051 }
3052 let mut restricted = curve.clone();
3053 if end < t1 - KNOT_IDENTITY_TOL {
3054 restricted = restricted.split(end)?.0;
3055 }
3056 if start > t0 + KNOT_IDENTITY_TOL {
3057 restricted = restricted.split(start)?.1;
3058 }
3059 let mut pcurve = inversion.image_of(&restricted)?;
3060 if !forward {
3061 pcurve = pcurve.reversed()?;
3062 }
3063 let first = pcurve.knots[0];
3064 let last = pcurve.knots[pcurve.knots.len() - 1];
3065 if !(last - first > 0.0) {
3066 return Ok(None);
3067 }
3068 let knots = pcurve
3069 .knots
3070 .iter()
3071 .map(|knot| (knot - first) / (last - first))
3072 .collect::<Vec<_>>();
3073 let pcurve = NurbsCurve::new(pcurve.degree, knots, pcurve.control_points.clone())?;
3074
3075 let scale = 1.0 + curve.evaluate((start + end) / 2.0)?.length();
3081 let bar = tolerance.max(1e-11 * scale);
3082 let mut residual = 0.0_f64;
3083 for index in 0..=32 {
3084 let fraction = index as f64 / 32.0;
3085 let uv = pcurve.evaluate(fraction)?;
3086 let on_carrier = surface.evaluate_extended(uv.x, uv.y)?;
3090 let edge_fraction = if forward { fraction } else { 1.0 - fraction };
3091 let on_edge = curve.evaluate(start + (end - start) * edge_fraction)?;
3092 residual = residual.max(on_carrier.sub(on_edge).length());
3093 if residual > bar {
3094 return Ok(None);
3095 }
3096 }
3097 let samples = pcurve.control_points.len();
3098 crate::imprint::work_profile::constructed("affine_range", 0, pcurve.control_points.len());
3099 Ok(Some(PcurveFit {
3100 curve: pcurve,
3101 report: PcurveFitReport {
3102 residual,
3103 floor: bar,
3108 samples,
3109 exit: PcurveFitExit::Converged,
3110 off_surface: 0.0,
3111 raised: false,
3112 clamped_excursion: 0.0,
3113 clamped_distance: 0.0,
3114 },
3115 }))
3116}
3117
3118fn align_collapsed_endpoints(surface: &NurbsSurface, raw: &mut [[f64; 2]]) -> Result<(), String> {
3119 let anchor = surface.control_points[0][0].point()?;
3120 let mut extent = 0.0_f64;
3121 for control in surface.control_points.iter().flatten() {
3122 extent = extent.max(control.point()?.sub(anchor).length());
3123 }
3124 let pole_band = (1e-10 * (1.0 + extent)).min(LINEAR_TOLERANCE);
3129 for (endpoint, neighbor) in [(0, 1), (raw.len() - 1, raw.len() - 2)] {
3130 let point = surface.evaluate(raw[endpoint][0], raw[endpoint][1])?;
3131 for axis in 0..2 {
3132 let mut candidate = raw[endpoint];
3133 candidate[axis] = raw[neighbor][axis];
3134 if candidate == raw[endpoint]
3135 || surface
3136 .evaluate(candidate[0], candidate[1])?
3137 .sub(point)
3138 .length()
3139 > pole_band
3140 {
3141 continue;
3142 }
3143 let row = if axis == 0 {
3144 surface.iso_curve_v(raw[endpoint][1])?
3145 } else {
3146 surface.iso_curve_u(raw[endpoint][0])?
3147 };
3148 let mut collapsed = true;
3149 for control in &row.control_points {
3150 if control.point()?.sub(point).length() > pole_band {
3151 collapsed = false;
3152 break;
3153 }
3154 }
3155 if collapsed {
3156 raw[endpoint] = candidate;
3157 }
3158 }
3159 }
3160 Ok(())
3161}
3162
3163fn repair_branch_jumps(
3186 surface: &NurbsSurface,
3187 edge_points: &[Vec3],
3188 raw: &mut [[f64; 2]],
3189 tolerance: f64,
3190) -> Result<(), String> {
3191 if raw.len() < 3 {
3192 return Ok(());
3193 }
3194 if surface.is_affine()? {
3197 return Ok(());
3198 }
3199 if std::env::var("BREP_NO_PCURVE_REPAIR").is_ok() {
3201 return Ok(());
3202 }
3203 align_collapsed_endpoints(surface, raw)?;
3207 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
3208 let u_span = (u1 - u0).abs().max(EPSILON);
3209 let v_span = (v1 - v0).abs().max(EPSILON);
3210 let (closed_u, closed_v) = surface_closedness(surface)?;
3211 let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
3215 let mut du = a[0] - b[0];
3216 if closed_u {
3217 while du > 0.5 * u_span {
3218 du -= u_span;
3219 }
3220 while du < -0.5 * u_span {
3221 du += u_span;
3222 }
3223 }
3224 let mut dv = a[1] - b[1];
3225 if closed_v {
3226 while dv > 0.5 * v_span {
3227 dv -= v_span;
3228 }
3229 while dv < -0.5 * v_span {
3230 dv += v_span;
3231 }
3232 }
3233 ((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
3234 };
3235 const JUMP_THRESHOLD: f64 = 0.2;
3239 let count = raw.len();
3240
3241 let (mut best_start, mut best_len, mut run_start) = (0usize, 1usize, 0usize);
3245 for index in 1..count {
3246 if norm_step(raw[index], raw[index - 1]) > JUMP_THRESHOLD {
3247 if index - run_start > best_len {
3248 best_len = index - run_start;
3249 best_start = run_start;
3250 }
3251 run_start = index;
3252 }
3253 }
3254 if count - run_start > best_len {
3255 best_len = count - run_start;
3256 best_start = run_start;
3257 }
3258 let (spine_lo, spine_hi) = (best_start, best_start + best_len - 1);
3259
3260 let reseat = |edge_point: Vec3,
3266 previous: [f64; 2],
3267 global: [f64; 2]|
3268 -> Result<[f64; 2], String> {
3269 let global_step = norm_step(global, previous);
3270 if global_step <= JUMP_THRESHOLD {
3271 return Ok(global);
3272 }
3273 let seeded =
3274 project_point_to_surface_seeded(surface, edge_point, previous[0], previous[1])?;
3275 let candidate = [seeded.u, seeded.v];
3276 let global_residual = surface
3277 .evaluate(global[0], global[1])?
3278 .sub(edge_point)
3279 .length();
3280 let on_surface = seeded.distance <= 4.0 * global_residual + tolerance.max(1e-12);
3281 if on_surface && norm_step(candidate, previous) < 0.5 * global_step {
3282 if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
3283 eprintln!(
3284 "pcurve repair: jump {global_step:.4} ({global:?}) -> {:.4} ({candidate:?}) res {:.2e}->{:.2e}",
3285 norm_step(candidate, previous),
3286 global_residual,
3287 seeded.distance
3288 );
3289 }
3290 Ok(candidate)
3291 } else {
3292 Ok(global)
3293 }
3294 };
3295
3296 let mut previous = raw[spine_hi];
3310 for index in (spine_hi + 1)..count.saturating_sub(1) {
3311 raw[index] = reseat(edge_points[index], previous, raw[index])?;
3312 previous = raw[index];
3313 }
3314 let mut previous = raw[spine_lo];
3315 for index in (1..spine_lo).rev() {
3316 raw[index] = reseat(edge_points[index], previous, raw[index])?;
3317 previous = raw[index];
3318 }
3319 Ok(())
3320}
3321
3322#[allow(clippy::too_many_arguments)]
3338pub fn fit_pcurve_on_surface_range_dense(
3339 surface: &NurbsSurface,
3340 curve: &NurbsCurve,
3341 edge_start: f64,
3342 edge_end: f64,
3343 forward: bool,
3344 tolerance: f64,
3345 base_samples: usize,
3346 refinement_rounds: usize,
3347 parameter_cap: usize,
3348) -> Result<PcurveFit, String> {
3349 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::FIT);
3350 crate::imprint::work_profile::add(|c| c.fit_calls += 1);
3351 if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
3352 return Err("build_pcurve_on_surface_range: invalid edge interval".into());
3353 }
3354 let evaluate_edge = |fraction: f64| {
3355 let edge_fraction = if forward { fraction } else { 1.0 - fraction };
3356 curve.evaluate(edge_start + (edge_end - edge_start) * edge_fraction)
3357 };
3358 let breaks = range_breaks(curve, edge_start, edge_end, forward);
3359 let ask = tolerance.max(PCURVE_REFINEMENT_TOLERANCE);
3360 let fit = fit_station_pcurve(
3361 surface,
3362 &evaluate_edge,
3363 &breaks,
3364 &breaks,
3365 ask,
3366 base_samples,
3367 refinement_rounds,
3368 parameter_cap,
3369 SPAN_FLOOR,
3370 )?;
3371 end_span_challenger(surface, curve, &evaluate_edge, edge_start, edge_end, forward, &breaks, ask, base_samples, refinement_rounds, parameter_cap, fit)
3372}
3373
3374fn native_end_knot_fractions(curve: &NurbsCurve, edge_start: f64, edge_end: f64, forward: bool, pcurve: &NurbsCurve) -> Result<Vec<f64>, String> {
3379 let [q0, q1] = pcurve.domain()?;
3380 let mut interior: Vec<f64> = pcurve.knots.iter().copied().filter(|k| *k > q0 && *k < q1).collect();
3381 interior.dedup();
3382 let (Some(&first), Some(&last)) = (interior.first(), interior.last()) else { return Ok(Vec::new()) };
3383 let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
3384 let span = edge_end - edge_start;
3385 let mut fractions: Vec<f64> = curve
3386 .knots
3387 .iter()
3388 .copied()
3389 .filter(|k| *k > low && *k < high)
3390 .map(|k| {
3391 let along = (k - edge_start) / span;
3392 if forward { along } else { 1.0 - along }
3393 })
3394 .filter(|f| f.is_finite() && ((*f > q0 && *f < first) || (*f > last && *f < q1)))
3395 .collect();
3396 fractions.sort_by(f64::total_cmp);
3397 fractions.dedup();
3398 Ok(fractions)
3399}
3400
3401fn foot_admitted(foot: &crate::SurfaceProjection, [u0, u1]: [f64; 2], [v0, v1]: [f64; 2]) -> bool {
3405 let finite = |p: Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
3406 foot.distance.is_finite() && finite(foot.point) && foot.u.is_finite() && foot.v.is_finite() && (u0..=u1).contains(&foot.u) && (v0..=v1).contains(&foot.v)
3407}
3408
3409fn verification_fractions(pcurve: &NurbsCurve, verify: &[f64]) -> Result<Vec<f64>, String> {
3415 let mut fractions = native_probe_fractions(&[pcurve])?;
3416 fractions.extend(pcurve.knots.iter().copied());
3417 fractions.extend(verify.iter().copied());
3418 fractions.retain(|f| f.is_finite() && (0.0..=1.0).contains(f));
3419 fractions.sort_by(f64::total_cmp);
3420 fractions.dedup();
3421 Ok(fractions)
3422}
3423
3424pub(crate) fn floor_admits(old: [f64; 3], new: [f64; 3]) -> bool {
3443 let floor = PCURVE_REFINEMENT_TOLERANCE;
3444 old.iter().chain(new.iter()).all(|v| v.is_finite())
3445 && new[0] < old[0]
3446 && (new[1] <= old[1] || (new[0] <= floor && new[2] <= floor))
3447}
3448
3449fn common_foot(
3457 global: Option<crate::SurfaceProjection>,
3458 reference: Option<crate::SurfaceProjection>,
3459 candidate: Option<crate::SurfaceProjection>,
3460 domain_u: [f64; 2],
3461 domain_v: [f64; 2],
3462) -> Option<crate::SurfaceProjection> {
3463 [global, reference, candidate]
3464 .into_iter()
3465 .flatten()
3466 .filter(|f| foot_admitted(f, domain_u, domain_v))
3467 .min_by(|a, b| a.distance.total_cmp(&b.distance))
3468}
3469
3470pub(crate) fn range_use_probes(curves: &[&NurbsCurve], edge_curve: &NurbsCurve, t0: f64, t1: f64, forward: bool) -> Result<Vec<f64>, String> {
3477 let mut probes = native_probe_fractions(curves)?;
3478 for curve in curves {
3479 probes.extend(curve.knots.iter().copied());
3480 }
3481 let span = t1 - t0;
3482 if span.is_finite() && span != 0.0 {
3483 let (low, high) = (t0.min(t1), t0.max(t1));
3484 let mut knots: Vec<f64> = edge_curve.knots.iter().copied().filter(|k| *k >= low && *k <= high).collect();
3485 knots.push(low);
3486 knots.push(high);
3487 knots.sort_by(f64::total_cmp);
3488 knots.dedup();
3489 let locals: Vec<f64> = [0.25, 0.5, 0.75]
3490 .into_iter()
3491 .chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64))
3492 .collect();
3493 let mut ts = knots.clone();
3494 for pair in knots.windows(2).filter(|w| w[1] > w[0]) {
3495 for &local in &locals {
3496 ts.push(pair[0] + (pair[1] - pair[0]) * local);
3497 }
3498 }
3499 for t in ts {
3500 let along = (t - t0) / span;
3501 probes.push(if forward { along } else { 1.0 - along });
3502 }
3503 }
3504 probes.retain(|f| f.is_finite() && (0.0..=1.0).contains(f));
3505 probes.sort_by(f64::total_cmp);
3506 probes.dedup();
3507 Ok(probes)
3508}
3509
3510#[cfg_attr(not(test), allow(dead_code))]
3518pub(crate) fn range_common_read(
3519 surface: &NurbsSurface,
3520 edge_curve: &NurbsCurve,
3521 t0: f64,
3522 t1: f64,
3523 forward: bool,
3524 old: &NurbsCurve,
3525 new: &NurbsCurve,
3526) -> Result<((f64, f64), (f64, f64)), String> {
3527 let (o, n) = range_common_read_with_excess(surface, edge_curve, t0, t1, forward, old, new)?;
3528 Ok(((o[0], o[1]), (n[0], n[1])))
3529}
3530
3531#[cfg_attr(not(test), allow(dead_code))]
3537pub(crate) fn range_common_read_with_excess(
3538 surface: &NurbsSurface,
3539 edge_curve: &NurbsCurve,
3540 t0: f64,
3541 t1: f64,
3542 forward: bool,
3543 old: &NurbsCurve,
3544 new: &NurbsCurve,
3545) -> Result<([f64; 3], [f64; 3]), String> {
3546 for curve in [old, new] {
3547 if !matches!(curve.domain(), Ok([a, b]) if a == 0.0 && b == 1.0) {
3548 return Err("range common read: a trim not on [0, 1]".into());
3549 }
3550 }
3551 let probes = range_use_probes(&[old, new], edge_curve, t0, t1, forward)?;
3552 let evaluate = |fraction: f64| -> Result<Vec3, String> {
3553 let along = if forward { fraction } else { 1.0 - fraction };
3554 edge_curve.evaluate(t0 + (t1 - t0) * along)
3555 };
3556 common_station_read_with_excess(surface, &evaluate, old, new, &probes)
3557}
3558
3559pub(crate) fn common_station_read(
3572 surface: &NurbsSurface,
3573 evaluate_edge: &dyn Fn(f64) -> Result<Vec3, String>,
3574 legacy: &NurbsCurve,
3575 candidate: &NurbsCurve,
3576 probes: &[f64],
3577) -> Result<((f64, f64), (f64, f64)), String> {
3578 let (old, new) = common_station_read_with_excess(surface, evaluate_edge, legacy, candidate, probes)?;
3579 Ok(((old[0], old[1]), (new[0], new[1])))
3580}
3581
3582fn tracking_closed_foot_enabled() -> bool {
3591 let enabled = crate::switch!("BREP_TRACKING_CLOSED_FOOT", true);
3592 enabled
3593}
3594
3595
3596pub(crate) fn common_station_read_with_excess(
3597 surface: &NurbsSurface,
3598 evaluate_edge: &dyn Fn(f64) -> Result<Vec3, String>,
3599 legacy: &NurbsCurve,
3600 candidate: &NurbsCurve,
3601 probes: &[f64],
3602) -> Result<([f64; 3], [f64; 3]), String> {
3603 let (domain_u, domain_v) = surface_domains(surface)?;
3604 let finite = |p: Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
3605 let mut read = ([0.0_f64, 0.0, f64::NEG_INFINITY], [0.0_f64, 0.0, f64::NEG_INFINITY]);
3609 let same = std::ptr::eq(legacy, candidate);
3614 let ellipsoid = if same && surface.analytic().is_none() { surface.affine_sphere() } else { None };
3621 let analytic = if same && ellipsoid.is_none() && tracking_closed_foot_enabled() {
3637 surface.analytic().filter(|analytic| !matches!(analytic, crate::AnalyticSurface::Plane { .. }))
3638 } else {
3639 None
3640 };
3641 for &fraction in probes {
3642 let point = evaluate_edge(fraction)?;
3643 let old_uv = legacy.evaluate(fraction)?;
3644 let new_uv = if same { old_uv } else { candidate.evaluate(fraction)? };
3645 let old_image = surface.evaluate_extended(old_uv.x, old_uv.y)?;
3646 let new_image = if same { old_image } else { surface.evaluate_extended(new_uv.x, new_uv.y)? };
3647 if !(finite(point) && finite(old_image) && finite(new_image)) {
3648 return Err(format!("common station read: non-finite point at fraction {fraction}"));
3649 }
3650 let closed_form = ellipsoid.and_then(|ellipsoid| ellipsoid.project(surface, point)).or_else(|| {
3651 analytic
3652 .and_then(|analytic| analytic.project(surface, point))
3653 .filter(|foot| foot_admitted(foot, domain_u, domain_v))
3654 });
3655 let foot = if let Some(foot) = closed_form {
3656 common_foot(Some(foot), Some(foot), Some(foot), domain_u, domain_v)
3657 } else {
3658 let reference_foot = memo_seeded_foot(surface, point, old_uv.x, old_uv.y);
3659 let candidate_foot = if same {
3660 reference_foot
3661 } else {
3662 memo_seeded_foot(surface, point, new_uv.x, new_uv.y)
3663 };
3664 common_foot(
3665 memo_global_foot(surface, point),
3666 reference_foot,
3667 candidate_foot,
3668 domain_u,
3669 domain_v,
3670 )
3671 }
3672 .ok_or_else(|| format!("common station read: no admitted foot at fraction {fraction}"))?;
3673 let values = [
3674 old_image.sub(foot.point).length(),
3675 old_image.sub(point).length(),
3676 new_image.sub(foot.point).length(),
3677 new_image.sub(point).length(),
3678 ];
3679 if values.iter().any(|v| !v.is_finite()) {
3680 return Err(format!("common station read: non-finite distance at fraction {fraction}"));
3681 }
3682 if !foot.distance.is_finite() {
3683 return Err(format!("common station read: non-finite foot distance at fraction {fraction}"));
3684 }
3685 read.0[0] = read.0[0].max(values[0]);
3686 read.0[1] = read.0[1].max(values[1]);
3687 read.0[2] = read.0[2].max(values[1] - foot.distance);
3688 read.1[0] = read.1[0].max(values[2]);
3689 read.1[1] = read.1[1].max(values[3]);
3690 read.1[2] = read.1[2].max(values[3] - foot.distance);
3691 }
3692 Ok(read)
3693}
3694
3695fn edge_end_span_fractions(curve: &NurbsCurve, edge_start: f64, edge_end: f64, forward: bool) -> Vec<f64> {
3700 let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
3701 let mut knots: Vec<f64> = curve.knots.iter().copied().filter(|k| *k > low && *k < high).collect();
3702 knots.dedup();
3703 let spans = match (knots.first(), knots.last()) {
3704 (Some(&first), Some(&last)) => vec![(low, first), (last, high)],
3705 _ => vec![(low, high)],
3706 };
3707 let span = edge_end - edge_start;
3708 let mut out = Vec::new();
3709 for (a, b) in spans {
3710 if !(b > a) {
3711 continue;
3712 }
3713 for local in [0.25, 0.5, 0.75].into_iter().chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64)) {
3714 let along = (a + (b - a) * local - edge_start) / span;
3715 let fraction = if forward { along } else { 1.0 - along };
3716 if fraction.is_finite() && (0.0..=1.0).contains(&fraction) {
3717 out.push(fraction);
3718 }
3719 }
3720 }
3721 out.sort_by(f64::total_cmp);
3722 out.dedup();
3723 out
3724}
3725
3726const WORST_SPANS: usize = 4;
3728const WORST_SPAN_SEEDS: usize = 7;
3730
3731fn legacy_worst_span_seeds(
3740 evaluate_edge: &dyn Fn(f64) -> Result<Vec3, String>,
3741 surface: &NurbsSurface,
3742 pcurve: &NurbsCurve,
3743 bar: f64,
3744 extra_probes: &[f64],
3745) -> Result<Vec<f64>, String> {
3746 let [q0, q1] = pcurve.domain()?;
3747 let mut knots: Vec<f64> = pcurve.knots.iter().copied().filter(|k| *k >= q0 && *k <= q1).collect();
3748 knots.dedup();
3749 let locals: Vec<f64> = [0.25, 0.5, 0.75]
3750 .into_iter()
3751 .chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64))
3752 .collect();
3753 let mut over: Vec<(f64, f64, f64)> = Vec::new();
3754 for span in knots.windows(2).filter(|w| w[1] > w[0]) {
3755 let mut worst = 0.0_f64;
3756 let inside = extra_probes.iter().copied().filter(|f| *f > span[0] && *f < span[1]);
3757 for fraction in locals.iter().map(|local| span[0] + (span[1] - span[0]) * local).chain(inside) {
3758 let uv = pcurve.evaluate(fraction)?;
3759 let deviation = surface.evaluate_extended(uv.x, uv.y)?.sub(evaluate_edge(fraction)?).length();
3760 if !deviation.is_finite() {
3761 return Err(format!("worst-span read: non-finite deviation at fraction {fraction}"));
3762 }
3763 worst = worst.max(deviation);
3764 }
3765 if worst > bar {
3766 over.push((worst, span[0], span[1]));
3767 }
3768 }
3769 over.sort_by(|a, b| b.0.total_cmp(&a.0).then(a.1.total_cmp(&b.1)));
3770 over.truncate(WORST_SPANS);
3771 Ok(over
3772 .into_iter()
3773 .flat_map(|(_, lo, hi)| (1..=WORST_SPAN_SEEDS).map(move |index| lo + (hi - lo) * index as f64 / (WORST_SPAN_SEEDS + 1) as f64))
3774 .collect())
3775}
3776
3777#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3779enum ChallengerTrigger {
3780 Deviation,
3782 Tracking,
3786}
3787
3788fn challenger_trigger(residual: f64, off_surface: f64, ask: f64, tracking: impl FnOnce() -> Option<f64>) -> Option<ChallengerTrigger> {
3798 if residual > ask + off_surface {
3799 return Some(ChallengerTrigger::Deviation);
3800 }
3801 if !(off_surface > ask) {
3802 return None;
3803 }
3804 match tracking() {
3805 Some(value) if value.is_finite() && value > ask => Some(ChallengerTrigger::Tracking),
3806 _ => None,
3807 }
3808}
3809
3810#[allow(clippy::too_many_arguments)]
3829fn end_span_challenger(
3830 surface: &NurbsSurface,
3831 curve: &NurbsCurve,
3832 evaluate_edge: &dyn Fn(f64) -> Result<Vec3, String>,
3833 edge_start: f64,
3834 edge_end: f64,
3835 forward: bool,
3836 breaks: &[f64],
3837 ask: f64,
3838 base_samples: usize,
3839 refinement_rounds: usize,
3840 parameter_cap: usize,
3841 legacy: PcurveFit,
3842) -> Result<PcurveFit, String> {
3843 if !(legacy.report.residual > ask + legacy.report.off_surface) {
3851 return Ok(legacy);
3852 }
3853 let _feet = FootMemoScope::open(surface);
3856 let Ok(end_knots) = native_end_knot_fractions(curve, edge_start, edge_end, forward, &legacy.curve) else { return Ok(legacy) };
3857 let edge_end_probes = edge_end_span_fractions(curve, edge_start, edge_end, forward);
3871 let Ok(mut fixed) = native_probe_fractions(&[&legacy.curve]) else { return Ok(legacy) };
3878 fixed.extend(edge_end_probes.iter().copied());
3879 let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
3880 let mut edge_knots: Vec<f64> = curve.knots.iter().copied().filter(|k| *k >= low && *k <= high).collect();
3881 edge_knots.push(low);
3882 edge_knots.push(high);
3883 edge_knots.sort_by(f64::total_cmp);
3884 edge_knots.dedup();
3885 let to_fraction = |t: f64| {
3886 let along = (t - edge_start) / (edge_end - edge_start);
3887 if forward { along } else { 1.0 - along }
3888 };
3889 for span in edge_knots.windows(2).filter(|w| w[1] > w[0]) {
3890 for local in [0.25, 0.5, 0.75] {
3891 fixed.push(to_fraction(span[0] + (span[1] - span[0]) * local));
3892 }
3893 }
3894 fixed.extend(edge_knots.iter().map(|&t| to_fraction(t)));
3895 fixed.extend(end_knots.iter().copied());
3896 fixed.extend(legacy.curve.knots.iter().copied());
3897 fixed.retain(|f| f.is_finite() && (0.0..=1.0).contains(f));
3898 fixed.sort_by(f64::total_cmp);
3899 fixed.dedup();
3900 let trigger = challenger_trigger(legacy.report.residual, legacy.report.off_surface, ask, || {
3901 common_station_read(surface, evaluate_edge, &legacy.curve, &legacy.curve, &fixed).ok().map(|((tracking, _), _)| tracking)
3902 });
3903 let Some(trigger) = trigger else { return Ok(legacy) };
3904 let Ok(worst_seeds) = legacy_worst_span_seeds(evaluate_edge, surface, &legacy.curve, ask + legacy.report.off_surface, &edge_end_probes) else { return Ok(legacy) };
3905 if trigger == ChallengerTrigger::Deviation && end_knots.is_empty() && worst_seeds.is_empty() {
3909 return Ok(legacy);
3910 }
3911 let mut stations: Vec<f64> = breaks.iter().copied().chain(end_knots.iter().copied()).chain(worst_seeds.iter().copied()).collect();
3912 stations.sort_by(f64::total_cmp);
3913 stations.dedup();
3914 let initial = base_samples.max(1).checked_add(1).and_then(|count| count.checked_add(stations.len()));
3919 if initial.is_none_or(|count| count > parameter_cap) {
3920 return Ok(legacy);
3921 }
3922 let Ok(candidate) = fit_station_pcurve_mode(surface, evaluate_edge, &stations, breaks, ask, base_samples, refinement_rounds, parameter_cap, SPAN_FLOOR, true, Some(&legacy.curve), &fixed) else {
3923 return Ok(legacy);
3924 };
3925 if !(candidate.report.residual.is_finite() && candidate.report.off_surface.is_finite() && candidate.report.clamped_excursion.is_finite() && candidate.report.clamped_distance.is_finite()) {
3926 return Ok(legacy);
3927 }
3928 let Ok(mut probes) = native_probe_fractions(&[&candidate.curve]) else { return Ok(legacy) };
3932 probes.extend(fixed.iter().copied());
3933 probes.extend(candidate.curve.knots.iter().copied());
3934 probes.retain(|f| f.is_finite() && (0.0..=1.0).contains(f));
3935 probes.sort_by(f64::total_cmp);
3936 probes.dedup();
3937 let readings = common_station_read_with_excess(surface, evaluate_edge, &legacy.curve, &candidate.curve, &probes);
3938 let accepted = matches!(&readings, Ok((old, new)) if floor_admits(*old, *new));
3939 if station_fit_tracing() {
3940 eprintln!(
3941 "STATION-FIT end-span challenger: trigger={trigger:?} end_knots={} worst_span_seeds={} edge_end_probes={} legacy={:?}/{:e} candidate={:?}/{:e} common={readings:?} decision={}",
3942 end_knots.len(), worst_seeds.len(), edge_end_probes.len(), legacy.report.exit, legacy.report.residual, candidate.report.exit, candidate.report.residual,
3943 if accepted { "candidate" } else { "legacy" }
3944 );
3945 }
3946 Ok(if accepted { candidate } else { legacy })
3947}
3948
3949
3950#[allow(clippy::too_many_arguments)]
3953pub fn build_pcurve_on_surface_range_dense(
3954 surface: &NurbsSurface,
3955 curve: &NurbsCurve,
3956 edge_start: f64,
3957 edge_end: f64,
3958 forward: bool,
3959 tolerance: f64,
3960 base_samples: usize,
3961 refinement_rounds: usize,
3962 parameter_cap: usize,
3963) -> Result<NurbsCurve, String> {
3964 accept_fit(fit_pcurve_on_surface_range_dense(
3965 surface,
3966 curve,
3967 edge_start,
3968 edge_end,
3969 forward,
3970 tolerance,
3971 base_samples,
3972 refinement_rounds,
3973 parameter_cap,
3974 )?)
3975}
3976
3977pub(crate) fn range_breaks(
3987 curve: &NurbsCurve,
3988 edge_start: f64,
3989 edge_end: f64,
3990 forward: bool,
3991) -> Vec<f64> {
3992 let mut fractions = Vec::new();
3993 let span = edge_end - edge_start;
3994 if !(span.abs() > 0.0) {
3995 return fractions;
3996 }
3997 let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
3998 let mut index = 0;
3999 while index < curve.knots.len() {
4000 let value = curve.knots[index];
4001 let run = curve.knots[index..]
4002 .iter()
4003 .take_while(|knot| **knot == value)
4004 .count();
4005 if value > low && value < high && run >= curve.degree.max(1) {
4006 let along = (value - edge_start) / span;
4007 fractions.push(if forward { along } else { 1.0 - along });
4008 }
4009 index += run;
4010 }
4011 fractions.sort_by(f64::total_cmp);
4012 fractions
4013}
4014
4015#[allow(clippy::too_many_arguments)]
4066pub fn fit_pcurve_on_surface_stations(
4067 surface: &NurbsSurface,
4068 stations: &dyn Fn(f64) -> Result<Vec3, String>,
4069 breaks: &[f64],
4070 tolerance: f64,
4071 base_samples: usize,
4072 refinement_rounds: usize,
4073 parameter_cap: usize,
4074) -> Result<PcurveFit, String> {
4075 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::FIT);
4076 crate::imprint::work_profile::add(|c| c.fit_calls += 1);
4077 fit_station_pcurve(
4078 surface, stations, breaks, &[], tolerance.max(PCURVE_REFINEMENT_TOLERANCE),
4079 base_samples, refinement_rounds, parameter_cap, SPAN_FLOOR,
4080 )
4081}
4082
4083pub(crate) fn analytic_station_pcurve(
4087 surface: &NurbsSurface,
4088 stations: &dyn Fn(f64) -> Result<Vec3, String>,
4089 tolerance: f64,
4090) -> Result<NurbsCurve, String> {
4091 if surface.analytic().is_none() {
4092 return Err("analytic pcurve requires an analytic carrier".into());
4093 }
4094 let mut breaks = Vec::new();
4095 for _ in 0..4 {
4096 let fit = fit_station_pcurve(surface, stations, &breaks, &[], tolerance, 64, 64, 8192, 1e-10)?;
4097 if fit.report.residual > tolerance || fit.report.off_surface > tolerance {
4098 return Err(format!(
4099 "analytic pcurve construction missed its contract: {:?}",
4100 fit.report
4101 ));
4102 }
4103 let mut failed = Vec::new();
4104 for pair in fit.curve.knots.windows(2) {
4107 if pair[1] <= pair[0] {
4108 continue;
4109 }
4110 for local in [
4111 0.06943184420297371,
4112 0.33000947820757187,
4113 0.6699905217924281,
4114 0.9305681557970262,
4115 ] {
4116 let t = pair[0] + (pair[1] - pair[0]) * local;
4117 let uv = fit.curve.evaluate(t)?;
4118 if surface
4119 .evaluate_extended(uv.x, uv.y)?
4120 .sub(stations(t)?)
4121 .length()
4122 > tolerance
4123 {
4124 failed.push(t);
4125 }
4126 }
4127 }
4128 if failed.is_empty() {
4129 return Ok(fit.curve);
4130 }
4131 if breaks.len() + failed.len() > 8192 {
4132 break;
4133 }
4134 breaks.extend(failed);
4135 breaks.sort_by(f64::total_cmp);
4136 breaks.dedup();
4137 }
4138 Err("analytic pcurve failed independent knot-span witnesses".into())
4139}
4140
4141#[allow(clippy::too_many_arguments)]
4142fn fit_station_pcurve(
4143 surface: &NurbsSurface,
4144 stations: &dyn Fn(f64) -> Result<Vec3, String>,
4145 breaks: &[f64],
4146 corners: &[f64],
4147 tolerance: f64,
4148 base_samples: usize,
4149 refinement_rounds: usize,
4150 parameter_cap: usize,
4151 span_floor: f64,
4152) -> Result<PcurveFit, String> {
4153 fit_station_pcurve_mode(surface, stations, breaks, corners, tolerance, base_samples, refinement_rounds, parameter_cap, span_floor, false, None, &[])
4154}
4155
4156#[allow(clippy::too_many_arguments)]
4175fn fit_station_pcurve_mode(
4176 surface: &NurbsSurface,
4177 stations: &dyn Fn(f64) -> Result<Vec3, String>,
4178 breaks: &[f64],
4179 corners: &[f64],
4180 tolerance: f64,
4181 base_samples: usize,
4182 refinement_rounds: usize,
4183 parameter_cap: usize,
4184 span_floor: f64,
4185 track_projection: bool,
4186 reference: Option<&NurbsCurve>,
4187 verify: &[f64],
4188) -> Result<PcurveFit, String> {
4189 let domains = if track_projection { Some(surface_domains(surface)?) } else { None };
4191 let tracking_of = |represented: Vec3, edge_point: Vec3, uv: Vec3, fraction: f64| -> Result<(f64, [f64; 2], f64), String> {
4198 let (domain_u, domain_v) = domains.ok_or_else(|| "station tracking: legacy mode".to_string())?;
4199 let finite = |p: Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
4200 let reference_seed = match reference {
4201 Some(curve) => curve.evaluate(fraction).ok().and_then(|seed| memo_seeded_foot(surface, edge_point, seed.x, seed.y)),
4202 None => None,
4203 };
4204 let foot = common_foot(
4207 memo_global_foot(surface, edge_point),
4208 reference_seed,
4209 memo_seeded_foot(surface, edge_point, uv.x, uv.y),
4210 domain_u,
4211 domain_v,
4212 )
4213 .ok_or_else(|| "station tracking: no admitted foot".to_string())?;
4214 let tracking = represented.sub(foot.point).length();
4215 if !(finite(represented) && tracking.is_finite()) {
4216 return Err("station tracking: non-finite reading".into());
4217 }
4218 Ok((tracking, [foot.u, foot.v], foot.distance))
4219 };
4220 let mut tracking_unmet = false;
4222 let evaluate_edge = stations;
4223 let base_samples = base_samples.max(1);
4224 let mut parameters = (0..=base_samples)
4233 .map(|index| index as f64 / base_samples as f64)
4234 .collect::<Vec<_>>();
4235 let mut joins = Vec::with_capacity(corners.len());
4248 for &fraction in breaks {
4249 if !(fraction > 0.0 && fraction < 1.0) {
4250 continue;
4251 }
4252 let corner = corners.contains(&fraction);
4253 let at = parameters.partition_point(|parameter| *parameter < fraction);
4254 let near_left = at > 0 && fraction - parameters[at - 1] <= span_floor;
4255 let near_right = at < parameters.len() && parameters[at] - fraction <= span_floor;
4256 if near_left || near_right {
4257 if corner {
4261 joins.push(if near_right { parameters[at] } else { parameters[at - 1] });
4262 }
4263 continue;
4264 }
4265 parameters.insert(at, fraction);
4266 if corner {
4267 joins.push(fraction);
4268 }
4269 }
4270 let mut edge_points = parameters
4273 .iter()
4274 .map(|fraction| evaluate_edge(*fraction))
4275 .collect::<Result<Vec<_>, _>>()?;
4276 let mut off_surface = 0.0_f64;
4279 let mut raw = Vec::with_capacity(edge_points.len());
4280 let mut gaps = Vec::with_capacity(edge_points.len());
4281 for point in &edge_points {
4282 let (parameter, gap) = if track_projection && precision_trim_feet(surface) {
4283 let foot = memo_global_foot(surface, *point).ok_or("precision trim has no foot")?;
4284 ([foot.u, foot.v], foot.distance)
4285 } else { invert_checked(surface, *point)? };
4286 off_surface = off_surface.max(gap);
4287 raw.push(parameter);
4288 gaps.push(gap);
4289 }
4290 repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
4291 let (mut pcurve, mut excursion, mut distance) =
4292 build_interpolant_joined(surface, &raw, ¶meters, &joins)?;
4293
4294 let mut cap = parameter_cap.max(parameters.len());
4295 let mut rounds = refinement_rounds;
4296 let mut raised = false;
4297 loop {
4298 let mut exit = PcurveFitExit::RoundBudget;
4299 let mut converged_max = None;
4300 let mut tracking_met = false;
4303 for _ in 0..rounds {
4304 if parameters.len() >= cap {
4305 exit = PcurveFitExit::SampleCeiling;
4306 break;
4307 }
4308 let target = tolerance;
4316 let mut candidates = Vec::new();
4317 let mut round_max = 0.0_f64;
4318 let mut truncated = false;
4319 let mut skipped_narrow = false;
4320 let worst_first = worst_first();
4321 'spans: for index in 0..parameters.len() - 1 {
4322 if parameters[index + 1] - parameters[index] < span_floor {
4331 skipped_narrow = true;
4332 continue;
4333 }
4334 for local in [0.25, 0.5, 0.75] {
4335 if !worst_first && parameters.len() + candidates.len() >= cap {
4336 truncated = true;
4337 break 'spans;
4338 }
4339 let fraction =
4340 parameters[index] + (parameters[index + 1] - parameters[index]) * local;
4341 let uv = pcurve.evaluate(fraction)?;
4342 let represented = surface.evaluate_extended(uv.x, uv.y)?;
4343 let edge_point = evaluate_edge(fraction)?;
4344 let deviation = represented.sub(edge_point).length();
4345 let (reading, foot) = if track_projection {
4348 let (tracking, foot_uv, foot_gap) = tracking_of(represented, edge_point, uv, fraction)?;
4349 (tracking, Some((foot_uv, foot_gap)))
4350 } else {
4351 (deviation, None)
4352 };
4353 round_max = round_max.max(reading);
4354 if reading <= target {
4355 continue;
4356 }
4357 candidates.push((index, fraction, edge_point, uv, reading, foot));
4358 }
4359 }
4360 let mut verify_blocked = false;
4373 if track_projection && candidates.is_empty() && !truncated && !skipped_narrow {
4374 for fraction in verification_fractions(&pcurve, verify)? {
4375 let index = parameters.partition_point(|parameter| *parameter <= fraction).saturating_sub(1).min(parameters.len() - 2);
4376 let uv = pcurve.evaluate(fraction)?;
4377 let represented = surface.evaluate_extended(uv.x, uv.y)?;
4378 let edge_point = evaluate_edge(fraction)?;
4379 let (tracking, foot_uv, foot_gap) = tracking_of(represented, edge_point, uv, fraction)?;
4380 round_max = round_max.max(tracking);
4381 if tracking <= target {
4382 continue;
4383 }
4384 if fraction - parameters[index] <= span_floor || parameters[index + 1] - fraction <= span_floor {
4385 verify_blocked = true;
4386 continue;
4387 }
4388 if let Some(last) = candidates.last_mut() {
4391 if last.0 == index && fraction - last.1 <= span_floor {
4392 if tracking > last.4 {
4393 *last = (index, fraction, edge_point, uv, tracking, Some((foot_uv, foot_gap)));
4394 }
4395 continue;
4396 }
4397 }
4398 candidates.push((index, fraction, edge_point, uv, tracking, Some((foot_uv, foot_gap))));
4399 }
4400 }
4401 tracking_unmet = track_projection && round_max > target;
4402 let room = cap.saturating_sub(parameters.len());
4412 if candidates.len() > room {
4413 truncated = true;
4414 let mut order: Vec<usize> = (0..candidates.len()).collect();
4415 order.sort_by(|&a, &b| candidates[b].4.total_cmp(&candidates[a].4).then(a.cmp(&b)));
4416 order.truncate(room);
4417 order.sort_unstable();
4418 candidates = order.into_iter().map(|index| candidates[index]).collect();
4419 }
4420 let mut inserts = Vec::with_capacity(candidates.len());
4421 for (index, fraction, edge_point, uv, _, foot) in candidates {
4422 let (parameter, gap) = match foot {
4426 Some(placed) => placed,
4427 None => invert_near(surface, edge_point, [uv.x, uv.y], gaps[index].max(gaps[index + 1]))?,
4428 };
4429 inserts.push((index + 1, fraction, edge_point, parameter, gap));
4430 }
4431 if station_fit_tracing() {
4432 eprintln!(
4433 "STATION-FIT round: samples={} target={target:.3e} round_max={round_max:.3e} inserts={} truncated={truncated}{}",
4434 parameters.len(),
4435 inserts.len(),
4436 if track_projection { " mode=tracking" } else { "" }
4438 );
4439 }
4440 if inserts.is_empty() {
4441 exit = if truncated {
4442 PcurveFitExit::SampleCeiling
4443 } else if skipped_narrow {
4444 PcurveFitExit::SpanFloor
4447 } else if track_projection {
4448 tracking_met = !verify_blocked;
4457 PcurveFitExit::Stalled
4458 } else {
4459 converged_max = Some(round_max);
4460 PcurveFitExit::Converged
4461 };
4462 break;
4463 }
4464 for (index, fraction, edge_point, parameter, gap) in inserts.into_iter().rev() {
4465 parameters.insert(index, fraction);
4466 edge_points.insert(index, edge_point);
4467 raw.insert(index, parameter);
4468 gaps.insert(index, gap);
4469 off_surface = off_surface.max(gap);
4470 }
4471 repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
4475 let rebuilt = build_interpolant_joined(surface, &raw, ¶meters, &joins)?;
4476 pcurve = rebuilt.0;
4477 excursion = rebuilt.1;
4478 distance = rebuilt.2;
4479 if truncated {
4480 exit = PcurveFitExit::SampleCeiling;
4481 break;
4482 }
4483 }
4484 let residual = match converged_max {
4485 Some(measured) => measured,
4486 None => {
4487 station_residual(surface, evaluate_edge, &pcurve, ¶meters, tolerance, &mut off_surface)?
4488 }
4489 };
4490 if station_fit_tracing() {
4491 eprintln!(
4492 "STATION-FIT pass raised={raised} exit={exit:?} samples={} residual={residual:.3e} off_surface={off_surface:.3e}",
4493 parameters.len()
4494 );
4495 if residual > tolerance + off_surface && (exit == PcurveFitExit::Converged || raised) {
4496 let (mut worst, mut at) = (0.0f64, 0usize);
4499 for (index, span) in parameters.windows(2).enumerate() {
4500 let fraction = 0.5 * (span[0] + span[1]);
4501 let uv = pcurve.evaluate(fraction)?;
4502 let deviation = surface.evaluate_extended(uv.x, uv.y)?.sub(evaluate_edge(fraction)?).length();
4503 if deviation > worst {
4504 worst = deviation;
4505 at = index;
4506 }
4507 }
4508 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
4509 let (closed_u, closed_v) = surface_closedness(surface)?;
4510 eprintln!(
4511 "STATION-FIT OFF BAR: worst {worst:.3e} between stations {at} and {}; surface degree {}x{} net {}x{} domain u[{u0},{u1}] v[{v0},{v1}] closed ({closed_u},{closed_v}) analytic {:?}",
4512 at + 1,
4513 surface.degree_u,
4514 surface.degree_v,
4515 surface.control_points.len(),
4516 surface.control_points[0].len(),
4517 surface.analytic().map(|_| "yes").unwrap_or("no")
4518 );
4519 {
4520 let fraction = 0.5 * (parameters[at] + parameters[at + 1]);
4521 let uv = pcurve.evaluate(fraction)?;
4522 let image = surface.evaluate_extended(uv.x, uv.y)?;
4523 let station = evaluate_edge(fraction)?;
4524 let foot = project_point_to_surface(surface, station)?;
4525 let seeded = project_point_to_surface_seeded(surface, station, raw[at][0], raw[at][1])?;
4526 eprintln!(
4527 " at the worst midpoint: deviation {:.4e}, station gap {:.4e} (seeded {:.4e}), image-to-foot {:.4e} (to seeded foot {:.4e}), foot uv ({:.7}, {:.7}) seeded ({:.7}, {:.7}) image uv ({:.7}, {:.7})",
4528 image.sub(station).length(), foot.distance, seeded.distance, image.sub(foot.point).length(), image.sub(seeded.point).length(), foot.u, foot.v, seeded.u, seeded.v, uv.x, uv.y
4529 );
4530 }
4531 let lo = at.saturating_sub(3);
4532 let hi = (at + 4).min(parameters.len() - 1);
4533 for index in lo..=hi {
4534 eprintln!(
4535 " station {index}: fraction {:.6} uv ({:.6}, {:.6}) point ({:.4}, {:.4}, {:.4})",
4536 parameters[index], raw[index][0], raw[index][1], edge_points[index].x, edge_points[index].y, edge_points[index].z
4537 );
4538 }
4539 }
4540 }
4541 if !track_projection && exit == PcurveFitExit::SpanFloor && residual <= tolerance + off_surface {
4548 exit = PcurveFitExit::Converged;
4549 }
4550 if tracking_met && residual <= tolerance {
4551 exit = PcurveFitExit::Converged;
4552 }
4553 if exit != PcurveFitExit::Converged
4554 && !raised
4555 && (residual > tolerance + off_surface || tracking_unmet)
4556 && cap < PCURVE_RAISED_SAMPLES
4557 {
4558 raised = true;
4559 cap = PCURVE_RAISED_SAMPLES;
4560 rounds = RAISED_ROUNDS;
4567 continue;
4568 }
4569 crate::imprint::work_profile::constructed("station_fit", parameters.len(), pcurve.control_points.len());
4570 return Ok(PcurveFit {
4571 curve: pcurve,
4572 report: PcurveFitReport {
4573 residual,
4574 floor: tolerance,
4575 samples: parameters.len(),
4576 exit,
4577 off_surface,
4578 raised,
4579 clamped_excursion: excursion,
4580 clamped_distance: distance,
4581 },
4582 });
4583 }
4584}
4585
4586pub fn build_pcurve_on_surface_stations(
4589 surface: &NurbsSurface,
4590 stations: &dyn Fn(f64) -> Result<Vec3, String>,
4591 breaks: &[f64],
4592 tolerance: f64,
4593 base_samples: usize,
4594 refinement_rounds: usize,
4595 parameter_cap: usize,
4596) -> Result<NurbsCurve, String> {
4597 accept_fit(fit_pcurve_on_surface_stations(
4598 surface,
4599 stations,
4600 breaks,
4601 tolerance,
4602 base_samples,
4603 refinement_rounds,
4604 parameter_cap,
4605 )?)
4606}
4607
4608fn station_residual(
4616 surface: &NurbsSurface,
4617 stations: &dyn Fn(f64) -> Result<Vec3, String>,
4618 pcurve: &NurbsCurve,
4619 parameters: &[f64],
4620 tolerance: f64,
4621 off_surface: &mut f64,
4622) -> Result<f64, String> {
4623 let mut worst = 0.0_f64;
4624 let mut read = |fraction: f64| -> Result<(), String> {
4625 let uv = pcurve.evaluate(fraction)?;
4626 let represented = surface.evaluate_extended(uv.x, uv.y)?;
4627 let station = stations(fraction)?;
4628 let deviation = represented.sub(station).length();
4629 worst = worst.max(deviation);
4630 if deviation > tolerance + *off_surface {
4631 *off_surface = off_surface.max(invert_checked(surface, station)?.1);
4632 }
4633 Ok(())
4634 };
4635 for span in parameters.windows(2) {
4636 for local in [0.25, 0.5, 0.75] {
4637 read(span[0] + (span[1] - span[0]) * local)?;
4638 }
4639 }
4640 Ok(worst)
4641}
4642
4643
4644