use crate::{
interpolate_curve, project_point_to_surface, project_point_to_surface_seeded, AnalyticSurface,
DiagnosticSeverity, KernelDiagnostics, KernelStage, KnotVector, NurbsCurve, NurbsSurface, Vec3,
Vec4, KNOT_IDENTITY_TOL,
};
const EPSILON: f64 = 1e-12;
const LINEAR_TOLERANCE: f64 = 1e-7;
fn surface_domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), String> {
Ok((
KnotVector::new(surface.knots_u.clone(), surface.degree_u)?.domain(),
KnotVector::new(surface.knots_v.clone(), surface.degree_v)?.domain(),
))
}
fn surface_closedness(surface: &NurbsSurface) -> Result<(bool, bool), String> {
surface.closed_directions()
}
fn invert_checked(surface: &NurbsSurface, point: Vec3) -> Result<([f64; 2], f64), String> {
if surface.is_affine()? {
let ([u0, _], [v0, _]) = surface_domains(surface)?;
let (origin, du, dv) = surface.deriv1(u0, v0)?;
let delta = point.sub(origin);
let uu = du.dot(du);
let uv = du.dot(dv);
let vv = dv.dot(dv);
let along_u = delta.dot(du);
let along_v = delta.dot(dv);
let determinant = uu * vv - uv * uv;
if determinant.abs() > EPSILON {
let parameter = [
u0 + (along_u * vv - along_v * uv) / determinant,
v0 + (along_v * uu - along_u * uv) / determinant,
];
let foot = origin.add(du.scale(parameter[0] - u0)).add(dv.scale(parameter[1] - v0));
return Ok((parameter, point.sub(foot).length()));
}
}
let projection = project_point_to_surface(surface, point)?;
Ok(([projection.u, projection.v], projection.distance))
}
fn cone_apex_u() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var_os("BREP_PCURVE_CONE_APEX_U").is_none_or(|value| value != "0"))
}
fn worst_first() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var_os("BREP_PCURVE_WORST_FIRST").is_none_or(|value| value != "0"))
}
fn seeded_inserts() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var_os("BREP_PCURVE_SEEDED_INSERTS").is_none_or(|value| value != "0"))
}
fn invert_near(
surface: &NurbsSurface,
point: Vec3,
seed: [f64; 2],
neighbour_gap: f64,
) -> Result<([f64; 2], f64), String> {
if !seeded_inserts() || surface.is_affine()? || surface.analytic().is_some() {
return invert_checked(surface, point);
}
let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
if seeded.distance <= neighbour_gap + PCURVE_REFINEMENT_TOLERANCE {
return Ok(([seeded.u, seeded.v], seeded.distance));
}
invert_checked(surface, point)
}
fn unwrap_periodic(values: &mut [f64], minimum: f64, maximum: f64) {
let period = maximum - minimum;
for index in 1..values.len() {
while values[index] - values[index - 1] > period / 2.0 {
values[index] -= period;
}
while values[index] - values[index - 1] < -period / 2.0 {
values[index] += period;
}
}
if values.len() > 1 {
while values[0] - values[1] > period / 2.0 {
values[0] -= period;
}
while values[0] - values[1] < -period / 2.0 {
values[0] += period;
}
}
if values.len() > 1 {
let excursion = |shift: f64| -> f64 {
values
.iter()
.map(|value| {
let v = value + shift;
(minimum - v).max(0.0) + (v - maximum).max(0.0)
})
.sum::<f64>()
};
let mean = values.iter().copied().sum::<f64>() / values.len() as f64;
let center = 0.5 * (minimum + maximum);
let base_k = ((center - mean) / period).round() as i64;
let mut best_shift = 0.0;
let mut best_excursion = f64::INFINITY;
for k in (base_k - 1)..=(base_k + 1) {
let shift = k as f64 * period;
let value = excursion(shift);
if value < best_excursion {
best_excursion = value;
best_shift = shift;
}
}
if best_shift != 0.0 {
for value in values.iter_mut() {
*value += best_shift;
}
}
}
}
fn build_interpolant(
surface: &NurbsSurface,
raw_parameters: &[[f64; 2]],
curve_parameters: &[f64],
) -> Result<NurbsCurve, String> {
let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
let (closed_u, closed_v) = surface_closedness(surface)?;
let mut parameters = raw_parameters.to_vec();
if cone_apex_u() && matches!(surface.analytic(), Some(AnalyticSurface::RuledRevolution { .. })) && parameters.len() >= 2 {
let speeds = parameters
.iter()
.map(|parameter| surface.deriv1(parameter[0], parameter[1]).map(|(_, su, _)| su.length()))
.collect::<Result<Vec<_>, _>>()?;
let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
let singular = |index: usize| speeds[index] <= reference * 1e-2;
if let (Some(first), Some(last)) = (
(0..parameters.len()).find(|&index| !singular(index)),
(0..parameters.len()).rfind(|&index| !singular(index)),
) {
for index in 0..first {
parameters[index][0] = parameters[first][0];
}
for index in last + 1..parameters.len() {
parameters[index][0] = parameters[last][0];
}
}
}
let sphere_u_singular = if matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })) {
let speeds = parameters
.iter()
.map(|parameter| {
surface
.deriv1(parameter[0], parameter[1])
.map(|(_, su, _)| su.length())
})
.collect::<Result<Vec<_>, _>>()?;
let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
speeds
.into_iter()
.map(|speed| speed <= reference * 1e-2)
.collect::<Vec<_>>()
} else {
vec![false; parameters.len()]
};
let has_interior_pole_crossing = sphere_u_singular
.iter()
.position(|singular| !singular)
.zip(sphere_u_singular.iter().rposition(|singular| !singular))
.is_some_and(|(first, last)| sphere_u_singular[first..=last].iter().any(|value| *value));
let period = (u1 - u0).abs();
let interior_pole_branch_reset = closed_u
&& has_interior_pole_crossing
&& parameters.windows(2).zip(sphere_u_singular.windows(2)).any(
|(parameter_pair, singular_pair)| {
(singular_pair[0] || singular_pair[1])
&& (parameter_pair[1][0] - parameter_pair[0][0]).abs()
>= 0.5 * period - 1e-12 * period.max(1.0)
},
);
if closed_u {
let mut values = parameters.iter().map(|value| value[0]).collect::<Vec<_>>();
if interior_pole_branch_reset {
let mut start = 0;
while start < values.len() {
while start < values.len() && sphere_u_singular[start] {
start += 1;
}
let mut end = start;
while end < values.len() && !sphere_u_singular[end] {
end += 1;
}
unwrap_periodic(&mut values[start..end], u0, u1);
start = end;
}
} else {
unwrap_periodic(&mut values, u0, u1);
}
for (parameter, value) in parameters.iter_mut().zip(values) {
parameter[0] = value;
}
}
if closed_v {
let mut values = parameters.iter().map(|value| value[1]).collect::<Vec<_>>();
unwrap_periodic(&mut values, v0, v1);
for (parameter, value) in parameters.iter_mut().zip(values) {
parameter[1] = value;
}
}
let points = parameters
.into_iter()
.map(|parameter| {
let u = if closed_u {
parameter[0]
} else {
parameter[0].clamp(u0, u1)
};
let v = if closed_v {
parameter[1]
} else {
parameter[1].clamp(v0, v1)
};
Vec3::new(u, v, 0.0)
})
.collect::<Vec<_>>();
interpolate_curve(&points, 3usize.min(points.len() - 1), curve_parameters)
}
pub const PCURVE_REFINEMENT_TOLERANCE: f64 = 1e-7;
const MAX_PCURVE_SAMPLES: usize = 600;
const PCURVE_RAISED_SAMPLES: usize = 2000;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PcurveFitReport {
pub residual: f64,
pub floor: f64,
pub samples: usize,
pub exit: PcurveFitExit,
pub off_surface: f64,
pub raised: bool,
}
impl PcurveFitReport {
pub fn met_floor(&self) -> bool {
self.residual <= self.floor
}
pub fn on_bar(&self) -> bool {
self.residual <= self.floor + self.off_surface
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PcurveFitExit {
Converged,
SampleCeiling,
RoundBudget,
Stalled,
SpanFloor,
}
#[derive(Clone, Debug)]
pub struct PcurveFit {
pub curve: NurbsCurve,
pub report: PcurveFitReport,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PcurveFitLedger {
pub fits: u64,
pub unmet: u64,
pub sample_ceiling: u64,
pub round_budget: u64,
pub stalled: u64,
pub span_floor: u64,
pub worst_unmet_residual: f64,
pub raised: u64,
pub raise_declined: u64,
pub worst_off_surface: f64,
}
impl PcurveFitLedger {
fn record(&mut self, report: &PcurveFitReport) {
self.fits += 1;
if report.raised {
self.raised += 1;
}
self.worst_off_surface = self.worst_off_surface.max(report.off_surface);
if report.met_floor() {
return;
}
self.unmet += 1;
match report.exit {
PcurveFitExit::Converged => {}
PcurveFitExit::SampleCeiling => {
self.sample_ceiling += 1;
if !report.raised {
self.raise_declined += 1;
}
}
PcurveFitExit::RoundBudget => self.round_budget += 1,
PcurveFitExit::Stalled => self.stalled += 1,
PcurveFitExit::SpanFloor => self.span_floor += 1,
}
self.worst_unmet_residual = self.worst_unmet_residual.max(report.residual);
}
fn fold(&mut self, child: &PcurveFitLedger) {
self.fits += child.fits;
self.unmet += child.unmet;
self.sample_ceiling += child.sample_ceiling;
self.round_budget += child.round_budget;
self.stalled += child.stalled;
self.span_floor += child.span_floor;
self.worst_unmet_residual = self.worst_unmet_residual.max(child.worst_unmet_residual);
self.raised += child.raised;
self.raise_declined += child.raise_declined;
self.worst_off_surface = self.worst_off_surface.max(child.worst_off_surface);
}
pub fn report_into(&self, diagnostics: &mut KernelDiagnostics) {
diagnostics.count_n("pcurve.fits", self.fits);
diagnostics.count_n("pcurve.unmet_floor", self.unmet);
diagnostics.count_n("pcurve.exit.sample_ceiling", self.sample_ceiling);
diagnostics.count_n("pcurve.exit.round_budget", self.round_budget);
diagnostics.count_n("pcurve.exit.stalled", self.stalled);
diagnostics.count_n("pcurve.exit.span_floor", self.span_floor);
diagnostics.count_n("pcurve.budget_raised", self.raised);
diagnostics.count_n("pcurve.budget_raise_declined", self.raise_declined);
diagnostics.measure_max("pcurve.worst_unmet_residual", self.worst_unmet_residual);
diagnostics.measure_max("pcurve.worst_off_surface", self.worst_off_surface);
if self.unmet > 0 {
diagnostics.event(
DiagnosticSeverity::Degraded,
KernelStage::Refine,
"pcurve.unmet_floor",
format!(
"{} of {} pcurve fits did not reach the {:.0e} floor \
(sample ceiling {}, round budget {}, stalled {}, span floor {}); \
worst residual {:.3e}; budget raised on {}, declined on {}; \
edge curves sit off their surfaces by up to {:.3e}",
self.unmet,
self.fits,
PCURVE_REFINEMENT_TOLERANCE,
self.sample_ceiling,
self.round_budget,
self.stalled,
self.span_floor,
self.worst_unmet_residual,
self.raised,
self.raise_declined,
self.worst_off_surface,
),
);
}
}
}
thread_local! {
static PCURVE_FIT_SCOPES: std::cell::RefCell<Vec<PcurveFitLedger>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub struct PcurveFitScope {
_thread_bound: std::marker::PhantomData<*const ()>,
}
impl PcurveFitScope {
pub fn open() -> Self {
PCURVE_FIT_SCOPES.with(|scopes| scopes.borrow_mut().push(PcurveFitLedger::default()));
Self {
_thread_bound: std::marker::PhantomData,
}
}
pub fn close(self) -> PcurveFitLedger {
let ledger = PCURVE_FIT_SCOPES.with(|scopes| {
let mut scopes = scopes.borrow_mut();
let ledger = scopes.pop().unwrap_or_default();
if let Some(parent) = scopes.last_mut() {
parent.fold(&ledger);
}
ledger
});
std::mem::forget(self);
ledger
}
}
impl Drop for PcurveFitScope {
fn drop(&mut self) {
PCURVE_FIT_SCOPES.with(|scopes| {
scopes.borrow_mut().pop();
});
}
}
fn record_fit(report: &PcurveFitReport) {
if std::env::var_os("BREP_DEBUG_PCURVE_FIT").is_some() {
eprintln!("PCURVE-FIT {report:?}");
}
PCURVE_FIT_SCOPES.with(|scopes| {
if let Some(open) = scopes.borrow_mut().last_mut() {
open.record(report);
}
});
}
fn probe_residual(
surface: &NurbsSurface,
curve: &NurbsCurve,
pcurve: &NurbsCurve,
parameters: &[f64],
[t0, t1]: [f64; 2],
[u0, u1]: [f64; 2],
[v0, v1]: [f64; 2],
) -> Result<f64, String> {
let mut worst = 0.0_f64;
for span in parameters.windows(2) {
for local in [0.25, 0.5, 0.75] {
let fraction = span[0] + (span[1] - span[0]) * local;
let parameter = pcurve.evaluate(fraction)?;
let on_surface =
surface.evaluate(parameter.x.clamp(u0, u1), parameter.y.clamp(v0, v1))?;
let on_curve = curve.evaluate(t0 + (t1 - t0) * fraction)?;
worst = worst.max(on_surface.sub(on_curve).length());
}
}
Ok(worst)
}
struct Refinement {
exit: PcurveFitExit,
residual: f64,
off_surface: f64,
raised: bool,
}
#[allow(clippy::too_many_arguments)]
fn refine_pcurve(
surface: &NurbsSurface,
curve: &NurbsCurve,
domain: [f64; 2],
u_domain: [f64; 2],
v_domain: [f64; 2],
floor: f64,
parameters: &mut Vec<f64>,
raw: &mut Vec<[f64; 2]>,
pcurve: &mut NurbsCurve,
off_surface: f64,
mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
) -> Result<Refinement, String> {
let mut best = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain,
floor, parameters, raw, pcurve, off_surface, false, 1.0, &mut insert)?;
if best.off_surface > floor { return Ok(best); }
let baseline_parameters = parameters.clone();
let baseline_raw = raw.clone();
let baseline_curve = pcurve.clone();
let baseline_off_surface = best.off_surface;
for fraction in [0.1, 0.01] {
if best.residual <= floor * fraction { continue; }
let mut refined_parameters = baseline_parameters.clone();
let mut refined_raw = baseline_raw.clone();
let mut refined_curve = baseline_curve.clone();
let candidate = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain,
floor, &mut refined_parameters, &mut refined_raw, &mut refined_curve,
baseline_off_surface, true, fraction, &mut insert);
match candidate {
Ok(candidate) if candidate.exit == PcurveFitExit::Converged
&& candidate.residual <= floor && candidate.residual < best.residual => {
*parameters = refined_parameters;
*raw = refined_raw;
*pcurve = refined_curve;
best = candidate;
}
_ => break,
}
}
Ok(best)
}
fn refine_pcurve_pass(
surface: &NurbsSurface,
curve: &NurbsCurve,
[t0, t1]: [f64; 2],
[u0, u1]: [f64; 2],
[v0, v1]: [f64; 2],
floor: f64,
parameters: &mut Vec<f64>,
raw: &mut Vec<[f64; 2]>,
pcurve: &mut NurbsCurve,
mut off_surface: f64,
track_projection: bool,
tracking_fraction: f64,
mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
) -> Result<Refinement, String> {
let tracking_floor = if track_projection { floor * tracking_fraction } else { floor };
let mut max_samples = MAX_PCURVE_SAMPLES;
let mut raised = false;
loop {
let mut exit = PcurveFitExit::RoundBudget;
let mut residual = None;
for _ in 0..4 {
if parameters.len() >= max_samples {
exit = PcurveFitExit::SampleCeiling;
break;
}
let mut inserts = Vec::new();
let mut round_max = 0.0_f64;
let mut truncated = false;
let mut skipped_narrow = false;
for index in 0..parameters.len() - 1 {
if parameters.len() + inserts.len() >= max_samples {
truncated = true;
break;
}
let start = parameters[index];
let end = parameters[index + 1];
if end - start < if track_projection { SPAN_FLOOR } else { 1e-3 } {
skipped_narrow = true;
continue;
}
for local_fraction in [0.25, 0.5, 0.75] {
if parameters.len() + inserts.len() >= max_samples {
truncated = true;
break;
}
let fraction = start + (end - start) * local_fraction;
let parameter = pcurve.evaluate(fraction)?;
let on_surface =
surface.evaluate(parameter.x.clamp(u0, u1), parameter.y.clamp(v0, v1))?;
let on_curve = curve.evaluate(t0 + (t1 - t0) * fraction)?;
let deviation = on_surface.sub(on_curve).length();
round_max = round_max.max(deviation);
if deviation <= tracking_floor {
continue;
}
let (surface_parameter, gap) = insert(fraction, parameter, on_curve)?;
off_surface = off_surface.max(gap);
if let Some(surface_parameter) = surface_parameter {
if raised || track_projection {
let projected =
surface.evaluate(surface_parameter[0], surface_parameter[1])?;
if on_surface.sub(projected).length() <= tracking_floor {
continue;
}
}
inserts.push((index + 1, fraction, surface_parameter));
}
}
}
if inserts.is_empty() {
exit = if truncated {
PcurveFitExit::SampleCeiling
} else if round_max > floor {
PcurveFitExit::Stalled
} else if skipped_narrow {
PcurveFitExit::SpanFloor
} else {
residual = Some(round_max);
PcurveFitExit::Converged
};
break;
}
for (at, fraction, surface_parameter) in inserts.into_iter().rev() {
parameters.insert(at, fraction);
raw.insert(at, surface_parameter);
}
*pcurve = build_interpolant(surface, raw, parameters)?;
if truncated {
exit = PcurveFitExit::SampleCeiling;
break;
}
}
let residual = match residual {
Some(measured) => measured,
None => probe_residual(
surface,
curve,
pcurve,
parameters,
[t0, t1],
[u0, u1],
[v0, v1],
)?,
};
if exit == PcurveFitExit::SpanFloor && residual <= floor {
exit = PcurveFitExit::Converged;
}
if exit == PcurveFitExit::SampleCeiling && !raised && residual > off_surface + floor {
raised = true;
max_samples = PCURVE_RAISED_SAMPLES;
continue;
}
return Ok(Refinement {
exit,
residual,
off_surface,
raised,
});
}
}
struct AffineInversion {
origin: Vec3,
du: Vec3,
dv: Vec3,
u0: f64,
v0: f64,
uu: f64,
uv: f64,
vv: f64,
determinant: f64,
}
impl AffineInversion {
fn of(surface: &NurbsSurface) -> Result<Option<Self>, String> {
if !surface.is_affine()? {
return Ok(None);
}
let ([u0, _], [v0, _]) = surface_domains(surface)?;
let (origin, du, dv) = surface.deriv1(u0, v0)?;
let uu = du.dot(du);
let uv = du.dot(dv);
let vv = dv.dot(dv);
let determinant = uu * vv - uv * uv;
if determinant.abs() <= 1e-18 {
return Ok(None);
}
Ok(Some(Self {
origin,
du,
dv,
u0,
v0,
uu,
uv,
vv,
determinant,
}))
}
fn uv_of(&self, point: Vec3) -> [f64; 2] {
let delta = point.sub(self.origin);
let along_u = delta.dot(self.du);
let along_v = delta.dot(self.dv);
[
self.u0 + (along_u * self.vv - along_v * self.uv) / self.determinant,
self.v0 + (along_v * self.uu - along_u * self.uv) / self.determinant,
]
}
fn image_of(&self, curve: &NurbsCurve) -> Result<NurbsCurve, String> {
let control_points = curve
.control_points
.iter()
.map(|control| {
let [u, v] = self.uv_of(control.point()?);
Ok(Vec4::from_point(Vec3::new(u, v, 0.0), control.w))
})
.collect::<Result<Vec<_>, String>>()?;
NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
}
}
pub fn fit_pcurve_on_surface(
surface: &NurbsSurface,
curve: &NurbsCurve,
) -> Result<PcurveFit, String> {
let [t0, t1] = curve.domain()?;
if surface.is_affine()? {
let Some(inversion) = AffineInversion::of(surface)? else {
return Err("build_pcurve_on_surface: singular affine parameterization".into());
};
let pcurve = inversion.image_of(curve)?;
return Ok(PcurveFit {
report: PcurveFitReport {
residual: 0.0,
floor: PCURVE_REFINEMENT_TOLERANCE,
samples: pcurve.control_points.len(),
exit: PcurveFitExit::Converged,
off_surface: 0.0,
raised: false,
},
curve: pcurve,
});
}
let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
let drop_tolerance = 1e-4 * scale;
let endpoint_tolerance = 1e-3 * scale;
let refinement_tolerance = PCURVE_REFINEMENT_TOLERANCE;
let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
let invert =
|fraction: f64| invert_checked(surface, curve.evaluate(t0 + (t1 - t0) * fraction)?);
let mut parameters = Vec::with_capacity(25);
let mut raw_surface_parameters = Vec::with_capacity(25);
let mut off_surface = 0.0_f64;
for index in 0..=24 {
let fraction = index as f64 / 24.0;
let (parameter, distance) = invert(fraction)?;
off_surface = off_surface.max(distance);
if distance > drop_tolerance && index != 0 && index != 24 {
continue;
}
if distance > endpoint_tolerance {
if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
let p3 = curve.evaluate(t0 + (t1 - t0) * fraction).ok();
let p0 = curve.evaluate(t0).ok();
let p1 = curve.evaluate(t1).ok();
eprintln!(
"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={:?}",
surface.evaluate(parameter[0], parameter[1]).ok()
);
}
return Err(format!(
"build_pcurve_on_surface: endpoint projection failed (distance={distance})"
));
}
parameters.push(fraction);
raw_surface_parameters.push(parameter);
}
let mut pcurve = build_interpolant(surface, &raw_surface_parameters, ¶meters)?;
let refinement = refine_pcurve(
surface,
curve,
[t0, t1],
[u0, u1],
[v0, v1],
refinement_tolerance,
&mut parameters,
&mut raw_surface_parameters,
&mut pcurve,
off_surface,
|fraction, _parameter, _on_curve| {
let (surface_parameter, distance) = invert(fraction)?;
Ok((
(distance <= drop_tolerance).then_some(surface_parameter),
distance,
))
},
)?;
Ok(PcurveFit {
curve: pcurve,
report: PcurveFitReport {
residual: refinement.residual,
floor: refinement_tolerance,
samples: parameters.len(),
exit: refinement.exit,
off_surface: refinement.off_surface,
raised: refinement.raised,
},
})
}
pub fn build_pcurve_on_surface(
surface: &NurbsSurface,
curve: &NurbsCurve,
) -> Result<NurbsCurve, String> {
let fit = fit_pcurve_on_surface(surface, curve)?;
record_fit(&fit.report);
Ok(fit.curve)
}
pub fn fit_pcurve_on_surface_marched(
surface: &NurbsSurface,
curve: &NurbsCurve,
) -> Result<PcurveFit, String> {
let global = fit_pcurve_on_surface(surface, curve)?;
if std::env::var("BREP_SECTION_PCURVE_MARCH").as_deref() == Ok("0") || surface.is_affine()? {
return Ok(global);
}
let [t0, t1] = curve.domain()?;
if curve.evaluate(t0)?.sub(curve.evaluate(t1)?).length() <= 1e-6 {
return Ok(global);
}
const SAMPLES: usize = 24;
let fractions: Vec<f64> = (0..=SAMPLES).map(|i| i as f64 / SAMPLES as f64).collect();
let edge: Vec<Vec3> = fractions
.iter()
.map(|fraction| curve.evaluate(t0 + (t1 - t0) * fraction))
.collect::<Result<Vec<_>, _>>()?;
let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
let endpoint_tolerance = 1e-3 * scale;
let (first, first_distance) = invert_checked(surface, edge[0])?;
let (last, last_distance) = invert_checked(surface, edge[SAMPLES])?;
if first_distance > endpoint_tolerance || last_distance > endpoint_tolerance {
return Ok(global);
}
let mut off_surface = first_distance.max(last_distance);
let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
let u_span = (u1 - u0).abs().max(EPSILON);
let v_span = (v1 - v0).abs().max(EPSILON);
let (closed_u, closed_v) = surface_closedness(surface)?;
let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
let mut du = a[0] - b[0];
if closed_u {
while du > 0.5 * u_span {
du -= u_span;
}
while du < -0.5 * u_span {
du += u_span;
}
}
let mut dv = a[1] - b[1];
if closed_v {
while dv > 0.5 * v_span {
dv -= v_span;
}
while dv < -0.5 * v_span {
dv += v_span;
}
}
((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
};
let mut raw = vec![first];
let mut previous = first;
for index in 1..SAMPLES {
let seeded =
crate::projection::project_point_to_surface_from_seed(surface, edge[index], previous)?;
let (_, global_distance) = invert_checked(surface, edge[index])?;
if seeded.distance > 4.0 * global_distance + endpoint_tolerance {
return Ok(global);
}
off_surface = off_surface.max(seeded.distance);
previous = [seeded.u, seeded.v];
raw.push(previous);
}
raw.push(last);
let mut steps: Vec<f64> = (1..raw.len())
.map(|i| norm_step(raw[i], raw[i - 1]))
.collect();
let endpoint_gap = steps.pop().unwrap_or(0.0);
steps.sort_by(f64::total_cmp);
let median = steps
.get(steps.len() / 2)
.copied()
.unwrap_or(0.0)
.max(EPSILON);
if endpoint_gap > (4.0 * median).max(0.05) {
return Ok(global);
}
let mut parameters = fractions;
let mut pcurve = build_interpolant(surface, &raw, ¶meters)?;
let refinement_tolerance = PCURVE_REFINEMENT_TOLERANCE;
let refinement = refine_pcurve(
surface,
curve,
[t0, t1],
[u0, u1],
[v0, v1],
refinement_tolerance,
&mut parameters,
&mut raw,
&mut pcurve,
off_surface,
|_fraction, seed, on_curve| {
let seeded = crate::projection::project_point_to_surface_from_seed(surface, on_curve, [seed.x, seed.y])?;
Ok((Some([seeded.u, seeded.v]), seeded.distance))
},
)?;
Ok(PcurveFit {
curve: pcurve,
report: PcurveFitReport {
residual: refinement.residual,
floor: refinement_tolerance,
samples: parameters.len(),
exit: refinement.exit,
off_surface: refinement.off_surface,
raised: refinement.raised,
},
})
}
pub fn build_pcurve_on_surface_marched(
surface: &NurbsSurface,
curve: &NurbsCurve,
) -> Result<NurbsCurve, String> {
let fit = fit_pcurve_on_surface_marched(surface, curve)?;
record_fit(&fit.report);
Ok(fit.curve)
}
pub fn fit_pcurve_on_surface_range(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
) -> Result<PcurveFit, String> {
if let Some(exact) =
affine_pcurve_on_range(surface, curve, edge_start, edge_end, forward, tolerance)?
{
return Ok(exact);
}
fit_pcurve_on_surface_range_dense(
surface, curve, edge_start, edge_end, forward, tolerance, 64, 3, 513,
)
}
pub fn build_pcurve_on_surface_range(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
) -> Result<NurbsCurve, String> {
accept_fit(fit_pcurve_on_surface_range(
surface, curve, edge_start, edge_end, forward, tolerance,
)?)
}
fn station_fit_tracing() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var_os("BREP_DEBUG_STATION_FIT").is_some())
}
const SPAN_FLOOR: f64 = 1e-6;
const RAISED_ROUNDS: usize = 40;
pub const PCURVE_OFF_BAR: &str = "pcurve fit refused: the trim misses its stations by";
fn accept_fit(fit: PcurveFit) -> Result<NurbsCurve, String> {
let report = fit.report;
if report.on_bar() {
return Ok(fit.curve);
}
Err(off_bar_message(&report))
}
fn off_bar_message(report: &PcurveFitReport) -> String {
format!(
"{PCURVE_OFF_BAR} {:.3e} out of sample against a bar of {:.3e} (asked {:.1e}, the \
stations stand {:.3e} off the carrier; {:?} at {} samples{})",
report.residual,
report.floor + report.off_surface,
report.floor,
report.off_surface,
report.exit,
report.samples,
if report.raised { ", after the raise" } else { "" },
)
}
pub(crate) const PCURVE_OFF_BAR_WHAT: &str = "pcurve_fit_off_bar";
pub(crate) fn is_pcurve_off_bar(refusal: &crate::KernelRefusal) -> bool {
matches!(&refusal.class, crate::RefusalClass::NonConvergence { what } if what == PCURVE_OFF_BAR_WHAT)
}
pub(crate) fn build_pcurve_on_surface_range_classed(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
stage: KernelStage,
what: &'static str,
) -> Result<NurbsCurve, crate::KernelRefusal> {
use crate::OrRefuse;
let fit = fit_pcurve_on_surface_range(surface, curve, edge_start, edge_end, forward, tolerance)
.or_refuse(stage, what)?;
accept_fit_classed(fit, stage)
}
pub(crate) fn accept_fit_classed(fit: PcurveFit, stage: KernelStage) -> Result<NurbsCurve, crate::KernelRefusal> {
if fit.report.on_bar() {
return Ok(fit.curve);
}
Err(crate::KernelRefusal {
class: crate::RefusalClass::NonConvergence { what: PCURVE_OFF_BAR_WHAT.into() },
stage,
message: off_bar_message(&fit.report),
})
}
fn affine_pcurve_on_range(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
) -> Result<Option<PcurveFit>, String> {
if std::env::var("BREP_AFFINE_RANGE_PCURVE").as_deref() == Ok("0") {
return Ok(None);
}
let Some(inversion) = AffineInversion::of(surface)? else {
return Ok(None);
};
if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
return Ok(None);
}
let [t0, t1] = curve.domain()?;
let start = edge_start.max(t0);
let end = edge_end.min(t1);
if end - start <= 2.0 * KNOT_IDENTITY_TOL {
return Ok(None);
}
let mut restricted = curve.clone();
if end < t1 - KNOT_IDENTITY_TOL {
restricted = restricted.split(end)?.0;
}
if start > t0 + KNOT_IDENTITY_TOL {
restricted = restricted.split(start)?.1;
}
let mut pcurve = inversion.image_of(&restricted)?;
if !forward {
pcurve = pcurve.reversed()?;
}
let first = pcurve.knots[0];
let last = pcurve.knots[pcurve.knots.len() - 1];
if !(last - first > 0.0) {
return Ok(None);
}
let knots = pcurve
.knots
.iter()
.map(|knot| (knot - first) / (last - first))
.collect::<Vec<_>>();
let pcurve = NurbsCurve::new(pcurve.degree, knots, pcurve.control_points.clone())?;
let scale = 1.0 + curve.evaluate((start + end) / 2.0)?.length();
let bar = tolerance.max(1e-11 * scale);
let mut residual = 0.0_f64;
for index in 0..=32 {
let fraction = index as f64 / 32.0;
let uv = pcurve.evaluate(fraction)?;
let on_carrier = surface.evaluate_extended(uv.x, uv.y)?;
let edge_fraction = if forward { fraction } else { 1.0 - fraction };
let on_edge = curve.evaluate(start + (end - start) * edge_fraction)?;
residual = residual.max(on_carrier.sub(on_edge).length());
if residual > bar {
return Ok(None);
}
}
let samples = pcurve.control_points.len();
Ok(Some(PcurveFit {
curve: pcurve,
report: PcurveFitReport {
residual,
floor: bar,
samples,
exit: PcurveFitExit::Converged,
off_surface: 0.0,
raised: false,
},
}))
}
fn align_collapsed_endpoints(surface: &NurbsSurface, raw: &mut [[f64; 2]]) -> Result<(), String> {
let anchor = surface.control_points[0][0].point()?;
let mut extent = 0.0_f64;
for control in surface.control_points.iter().flatten() {
extent = extent.max(control.point()?.sub(anchor).length());
}
let pole_band = (1e-10 * (1.0 + extent)).min(LINEAR_TOLERANCE);
for (endpoint, neighbor) in [(0, 1), (raw.len() - 1, raw.len() - 2)] {
let point = surface.evaluate(raw[endpoint][0], raw[endpoint][1])?;
for axis in 0..2 {
let mut candidate = raw[endpoint];
candidate[axis] = raw[neighbor][axis];
if candidate == raw[endpoint]
|| surface
.evaluate(candidate[0], candidate[1])?
.sub(point)
.length()
> pole_band
{
continue;
}
let row = if axis == 0 {
surface.iso_curve_v(raw[endpoint][1])?
} else {
surface.iso_curve_u(raw[endpoint][0])?
};
let mut collapsed = true;
for control in &row.control_points {
if control.point()?.sub(point).length() > pole_band {
collapsed = false;
break;
}
}
if collapsed {
raw[endpoint] = candidate;
}
}
}
Ok(())
}
fn repair_branch_jumps(
surface: &NurbsSurface,
edge_points: &[Vec3],
raw: &mut [[f64; 2]],
tolerance: f64,
) -> Result<(), String> {
if raw.len() < 3 {
return Ok(());
}
if surface.is_affine()? {
return Ok(());
}
if std::env::var("BREP_NO_PCURVE_REPAIR").is_ok() {
return Ok(());
}
align_collapsed_endpoints(surface, raw)?;
let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
let u_span = (u1 - u0).abs().max(EPSILON);
let v_span = (v1 - v0).abs().max(EPSILON);
let (closed_u, closed_v) = surface_closedness(surface)?;
let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
let mut du = a[0] - b[0];
if closed_u {
while du > 0.5 * u_span {
du -= u_span;
}
while du < -0.5 * u_span {
du += u_span;
}
}
let mut dv = a[1] - b[1];
if closed_v {
while dv > 0.5 * v_span {
dv -= v_span;
}
while dv < -0.5 * v_span {
dv += v_span;
}
}
((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
};
const JUMP_THRESHOLD: f64 = 0.2;
let count = raw.len();
let (mut best_start, mut best_len, mut run_start) = (0usize, 1usize, 0usize);
for index in 1..count {
if norm_step(raw[index], raw[index - 1]) > JUMP_THRESHOLD {
if index - run_start > best_len {
best_len = index - run_start;
best_start = run_start;
}
run_start = index;
}
}
if count - run_start > best_len {
best_len = count - run_start;
best_start = run_start;
}
let (spine_lo, spine_hi) = (best_start, best_start + best_len - 1);
let reseat = |edge_point: Vec3,
previous: [f64; 2],
global: [f64; 2]|
-> Result<[f64; 2], String> {
let global_step = norm_step(global, previous);
if global_step <= JUMP_THRESHOLD {
return Ok(global);
}
let seeded =
project_point_to_surface_seeded(surface, edge_point, previous[0], previous[1])?;
let candidate = [seeded.u, seeded.v];
let global_residual = surface
.evaluate(global[0], global[1])?
.sub(edge_point)
.length();
let on_surface = seeded.distance <= 4.0 * global_residual + tolerance.max(1e-12);
if on_surface && norm_step(candidate, previous) < 0.5 * global_step {
if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
eprintln!(
"pcurve repair: jump {global_step:.4} ({global:?}) -> {:.4} ({candidate:?}) res {:.2e}->{:.2e}",
norm_step(candidate, previous),
global_residual,
seeded.distance
);
}
Ok(candidate)
} else {
Ok(global)
}
};
let mut previous = raw[spine_hi];
for index in (spine_hi + 1)..count.saturating_sub(1) {
raw[index] = reseat(edge_points[index], previous, raw[index])?;
previous = raw[index];
}
let mut previous = raw[spine_lo];
for index in (1..spine_lo).rev() {
raw[index] = reseat(edge_points[index], previous, raw[index])?;
previous = raw[index];
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn fit_pcurve_on_surface_range_dense(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
base_samples: usize,
refinement_rounds: usize,
parameter_cap: usize,
) -> Result<PcurveFit, String> {
if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
return Err("build_pcurve_on_surface_range: invalid edge interval".into());
}
let evaluate_edge = |fraction: f64| {
let edge_fraction = if forward { fraction } else { 1.0 - fraction };
curve.evaluate(edge_start + (edge_end - edge_start) * edge_fraction)
};
let breaks = range_breaks(curve, edge_start, edge_end, forward);
fit_pcurve_on_surface_stations(
surface,
&evaluate_edge,
&breaks,
tolerance,
base_samples,
refinement_rounds,
parameter_cap,
)
}
#[allow(clippy::too_many_arguments)]
pub fn build_pcurve_on_surface_range_dense(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
base_samples: usize,
refinement_rounds: usize,
parameter_cap: usize,
) -> Result<NurbsCurve, String> {
accept_fit(fit_pcurve_on_surface_range_dense(
surface,
curve,
edge_start,
edge_end,
forward,
tolerance,
base_samples,
refinement_rounds,
parameter_cap,
)?)
}
pub(crate) fn range_breaks(
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
) -> Vec<f64> {
let mut fractions = Vec::new();
let span = edge_end - edge_start;
if !(span.abs() > 0.0) {
return fractions;
}
let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
let mut index = 0;
while index < curve.knots.len() {
let value = curve.knots[index];
let run = curve.knots[index..]
.iter()
.take_while(|knot| **knot == value)
.count();
if value > low && value < high && run >= curve.degree.max(1) {
let along = (value - edge_start) / span;
fractions.push(if forward { along } else { 1.0 - along });
}
index += run;
}
fractions.sort_by(f64::total_cmp);
fractions
}
#[allow(clippy::too_many_arguments)]
pub fn fit_pcurve_on_surface_stations(
surface: &NurbsSurface,
stations: &dyn Fn(f64) -> Result<Vec3, String>,
breaks: &[f64],
tolerance: f64,
base_samples: usize,
refinement_rounds: usize,
parameter_cap: usize,
) -> Result<PcurveFit, String> {
let evaluate_edge = stations;
let base_samples = base_samples.max(1);
let tolerance = tolerance.max(PCURVE_REFINEMENT_TOLERANCE);
let mut parameters = (0..=base_samples)
.map(|index| index as f64 / base_samples as f64)
.collect::<Vec<_>>();
for &fraction in breaks {
if !(fraction > 0.0 && fraction < 1.0) {
continue;
}
let at = parameters.partition_point(|parameter| *parameter < fraction);
let near_left = at > 0 && fraction - parameters[at - 1] <= SPAN_FLOOR;
let near_right = at < parameters.len() && parameters[at] - fraction <= SPAN_FLOOR;
if near_left || near_right {
continue;
}
parameters.insert(at, fraction);
}
let mut edge_points = parameters
.iter()
.map(|fraction| evaluate_edge(*fraction))
.collect::<Result<Vec<_>, _>>()?;
let mut off_surface = 0.0_f64;
let mut raw = Vec::with_capacity(edge_points.len());
let mut gaps = Vec::with_capacity(edge_points.len());
for point in &edge_points {
let (parameter, gap) = invert_checked(surface, *point)?;
off_surface = off_surface.max(gap);
raw.push(parameter);
gaps.push(gap);
}
repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
let mut pcurve = build_interpolant(surface, &raw, ¶meters)?;
let mut cap = parameter_cap.max(parameters.len());
let mut rounds = refinement_rounds;
let mut raised = false;
loop {
let mut exit = PcurveFitExit::RoundBudget;
let mut converged_max = None;
for _ in 0..rounds {
if parameters.len() >= cap {
exit = PcurveFitExit::SampleCeiling;
break;
}
let target = tolerance;
let mut candidates = Vec::new();
let mut round_max = 0.0_f64;
let mut truncated = false;
let mut skipped_narrow = false;
let worst_first = worst_first();
'spans: for index in 0..parameters.len() - 1 {
if parameters[index + 1] - parameters[index] < SPAN_FLOOR {
skipped_narrow = true;
continue;
}
for local in [0.25, 0.5, 0.75] {
if !worst_first && parameters.len() + candidates.len() >= cap {
truncated = true;
break 'spans;
}
let fraction =
parameters[index] + (parameters[index + 1] - parameters[index]) * local;
let uv = pcurve.evaluate(fraction)?;
let represented = surface.evaluate_extended(uv.x, uv.y)?;
let edge_point = evaluate_edge(fraction)?;
let deviation = represented.sub(edge_point).length();
round_max = round_max.max(deviation);
if deviation <= target {
continue;
}
candidates.push((index, fraction, edge_point, uv, deviation));
}
}
let room = cap.saturating_sub(parameters.len());
if candidates.len() > room {
truncated = true;
let mut order: Vec<usize> = (0..candidates.len()).collect();
order.sort_by(|&a, &b| candidates[b].4.total_cmp(&candidates[a].4).then(a.cmp(&b)));
order.truncate(room);
order.sort_unstable();
candidates = order.into_iter().map(|index| candidates[index]).collect();
}
let mut inserts = Vec::with_capacity(candidates.len());
for (index, fraction, edge_point, uv, _) in candidates {
let (parameter, gap) = invert_near(
surface,
edge_point,
[uv.x, uv.y],
gaps[index].max(gaps[index + 1]),
)?;
inserts.push((index + 1, fraction, edge_point, parameter, gap));
}
if station_fit_tracing() {
eprintln!(
"STATION-FIT round: samples={} target={target:.3e} round_max={round_max:.3e} inserts={} truncated={truncated}",
parameters.len(),
inserts.len()
);
}
if inserts.is_empty() {
exit = if truncated {
PcurveFitExit::SampleCeiling
} else if skipped_narrow {
PcurveFitExit::SpanFloor
} else {
converged_max = Some(round_max);
PcurveFitExit::Converged
};
break;
}
for (index, fraction, edge_point, parameter, gap) in inserts.into_iter().rev() {
parameters.insert(index, fraction);
edge_points.insert(index, edge_point);
raw.insert(index, parameter);
gaps.insert(index, gap);
off_surface = off_surface.max(gap);
}
repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
pcurve = build_interpolant(surface, &raw, ¶meters)?;
if truncated {
exit = PcurveFitExit::SampleCeiling;
break;
}
}
let residual = match converged_max {
Some(measured) => measured,
None => {
station_residual(surface, evaluate_edge, &pcurve, ¶meters, tolerance, &mut off_surface)?
}
};
if station_fit_tracing() {
eprintln!(
"STATION-FIT pass raised={raised} exit={exit:?} samples={} residual={residual:.3e} off_surface={off_surface:.3e}",
parameters.len()
);
if residual > tolerance + off_surface && (exit == PcurveFitExit::Converged || raised) {
let (mut worst, mut at) = (0.0f64, 0usize);
for (index, span) in parameters.windows(2).enumerate() {
let fraction = 0.5 * (span[0] + span[1]);
let uv = pcurve.evaluate(fraction)?;
let deviation = surface.evaluate_extended(uv.x, uv.y)?.sub(evaluate_edge(fraction)?).length();
if deviation > worst {
worst = deviation;
at = index;
}
}
let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
let (closed_u, closed_v) = surface_closedness(surface)?;
eprintln!(
"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 {:?}",
at + 1,
surface.degree_u,
surface.degree_v,
surface.control_points.len(),
surface.control_points[0].len(),
surface.analytic().map(|_| "yes").unwrap_or("no")
);
{
let fraction = 0.5 * (parameters[at] + parameters[at + 1]);
let uv = pcurve.evaluate(fraction)?;
let image = surface.evaluate_extended(uv.x, uv.y)?;
let station = evaluate_edge(fraction)?;
let foot = project_point_to_surface(surface, station)?;
let seeded = project_point_to_surface_seeded(surface, station, raw[at][0], raw[at][1])?;
eprintln!(
" 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})",
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
);
}
let lo = at.saturating_sub(3);
let hi = (at + 4).min(parameters.len() - 1);
for index in lo..=hi {
eprintln!(
" station {index}: fraction {:.6} uv ({:.6}, {:.6}) point ({:.4}, {:.4}, {:.4})",
parameters[index], raw[index][0], raw[index][1], edge_points[index].x, edge_points[index].y, edge_points[index].z
);
}
}
}
if exit == PcurveFitExit::SpanFloor && residual <= tolerance + off_surface {
exit = PcurveFitExit::Converged;
}
if exit != PcurveFitExit::Converged
&& !raised
&& residual > tolerance + off_surface
&& cap < PCURVE_RAISED_SAMPLES
{
raised = true;
cap = PCURVE_RAISED_SAMPLES;
rounds = RAISED_ROUNDS;
continue;
}
return Ok(PcurveFit {
curve: pcurve,
report: PcurveFitReport {
residual,
floor: tolerance,
samples: parameters.len(),
exit,
off_surface,
raised,
},
});
}
}
pub fn build_pcurve_on_surface_stations(
surface: &NurbsSurface,
stations: &dyn Fn(f64) -> Result<Vec3, String>,
breaks: &[f64],
tolerance: f64,
base_samples: usize,
refinement_rounds: usize,
parameter_cap: usize,
) -> Result<NurbsCurve, String> {
accept_fit(fit_pcurve_on_surface_stations(
surface,
stations,
breaks,
tolerance,
base_samples,
refinement_rounds,
parameter_cap,
)?)
}
fn station_residual(
surface: &NurbsSurface,
stations: &dyn Fn(f64) -> Result<Vec3, String>,
pcurve: &NurbsCurve,
parameters: &[f64],
tolerance: f64,
off_surface: &mut f64,
) -> Result<f64, String> {
let mut worst = 0.0_f64;
for span in parameters.windows(2) {
for local in [0.25, 0.5, 0.75] {
let fraction = span[0] + (span[1] - span[0]) * local;
let uv = pcurve.evaluate(fraction)?;
let represented = surface.evaluate_extended(uv.x, uv.y)?;
let station = stations(fraction)?;
let deviation = represented.sub(station).length();
worst = worst.max(deviation);
if deviation > tolerance + *off_surface {
*off_surface = off_surface.max(invert_checked(surface, station)?.1);
}
}
}
Ok(worst)
}