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> {
let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
let mut closed_u = true;
let mut closed_v = true;
for fraction in [0.19, 0.52, 0.87] {
let v = v0 + (v1 - v0) * fraction;
if surface
.evaluate(u0, v)?
.sub(surface.evaluate(u1, v)?)
.length()
> LINEAR_TOLERANCE * 10.0
{
closed_u = false;
}
let u = u0 + (u1 - u0) * fraction;
if surface
.evaluate(u, v0)?
.sub(surface.evaluate(u, v1)?)
.length()
> LINEAR_TOLERANCE * 10.0
{
closed_v = false;
}
}
Ok((closed_u, closed_v))
}
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 {
return Ok((
[
u0 + (along_u * vv - along_v * uv) / determinant,
v0 + (along_v * uu - along_u * uv) / determinant,
],
0.0,
));
}
}
let projection = project_point_to_surface(surface, point)?;
Ok(([projection.u, projection.v], projection.distance))
}
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();
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 wrap = surface.analytic().is_none();
let points = parameters
.into_iter()
.map(|parameter| {
let u = if closed_u && wrap {
parameter[0]
} else {
parameter[0].clamp(u0, u1)
};
let v = if closed_v && wrap {
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(crate) 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
}
}
#[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,
[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,
mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
) -> Result<Refinement, String> {
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 < 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 <= 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 {
let projected =
surface.evaluate(surface_parameter[0], surface_parameter[1])?;
if on_surface.sub(projected).length() <= 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 =
project_point_to_surface_seeded(surface, edge[index], previous[0], previous[1])?;
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 = project_point_to_surface_seeded(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 build_pcurve_on_surface_range(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
) -> Result<NurbsCurve, String> {
if let Some(exact) =
affine_pcurve_on_range(surface, curve, edge_start, edge_end, forward, tolerance)?
{
return Ok(exact);
}
build_pcurve_on_surface_range_dense(
surface, curve, edge_start, edge_end, forward, tolerance, 64, 3, 513,
)
}
fn affine_pcurve_on_range(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
) -> Result<Option<NurbsCurve>, 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);
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)?;
if on_carrier.sub(on_edge).length() > bar {
return Ok(None);
}
}
Ok(Some(pcurve))
}
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 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> {
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)
};
build_pcurve_on_surface_stations(
surface,
&evaluate_edge,
tolerance,
base_samples,
refinement_rounds,
parameter_cap,
)
}
pub fn build_pcurve_on_surface_stations(
surface: &NurbsSurface,
stations: &dyn Fn(f64) -> Result<Vec3, String>,
tolerance: f64,
base_samples: usize,
refinement_rounds: usize,
parameter_cap: usize,
) -> Result<NurbsCurve, String> {
let evaluate_edge = stations;
let mut parameters = (0..=base_samples)
.map(|index| index as f64 / base_samples as f64)
.collect::<Vec<_>>();
let mut edge_points = parameters
.iter()
.map(|fraction| evaluate_edge(*fraction))
.collect::<Result<Vec<_>, _>>()?;
let mut raw = edge_points
.iter()
.map(|point| invert_checked(surface, *point).map(|value| value.0))
.collect::<Result<Vec<_>, _>>()?;
repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
let mut pcurve = build_interpolant(surface, &raw, ¶meters)?;
for _ in 0..refinement_rounds {
let mut inserts = Vec::new();
for index in 0..parameters.len() - 1 {
if parameters.len() + inserts.len() >= parameter_cap {
break;
}
for local in [0.25, 0.5, 0.75] {
let fraction =
parameters[index] + (parameters[index + 1] - parameters[index]) * local;
let uv = pcurve.evaluate(fraction)?;
let represented = surface.evaluate(uv.x, uv.y)?;
let edge_point = evaluate_edge(fraction)?;
if represented.sub(edge_point).length() <= tolerance {
continue;
}
inserts.push((
index + 1,
fraction,
edge_point,
invert_checked(surface, edge_point)?.0,
));
}
}
if inserts.is_empty() {
break;
}
for (index, fraction, edge_point, value) in inserts.into_iter().rev() {
parameters.insert(index, fraction);
edge_points.insert(index, edge_point);
raw.insert(index, value);
}
repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
pcurve = build_interpolant(surface, &raw, ¶meters)?;
}
Ok(pcurve)
}