use crate::{
interpolate_curve, project_point_to_surface, project_point_to_surface_seeded, AnalyticSurface,
KnotVector, NurbsCurve, NurbsSurface, Vec3, Vec4,
};
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 fn build_pcurve_on_surface(
surface: &NurbsSurface,
curve: &NurbsCurve,
) -> Result<NurbsCurve, String> {
let [t0, t1] = curve.domain()?;
if surface.is_affine()? {
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 Err("build_pcurve_on_surface: singular affine parameterization".into());
}
let control_points = curve
.control_points
.iter()
.map(|control| {
let delta = control.point()?.sub(origin);
let along_u = delta.dot(du);
let along_v = delta.dot(dv);
Ok(Vec4::from_point(
Vec3::new(
u0 + (along_u * vv - along_v * uv) / determinant,
v0 + (along_v * uu - along_u * uv) / determinant,
0.0,
),
control.w,
))
})
.collect::<Result<Vec<_>, String>>()?;
return NurbsCurve::new(curve.degree, curve.knots.clone(), control_points);
}
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 = 1e-3f64.min(1e-4 * scale);
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);
for index in 0..=24 {
let fraction = index as f64 / 24.0;
let (parameter, distance) = invert(fraction)?;
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)?;
const MAX_SAMPLES: usize = 160;
for _ in 0..4 {
if parameters.len() >= MAX_SAMPLES {
break;
}
let mut inserts = Vec::new();
for index in 0..parameters.len() - 1 {
if parameters.len() + inserts.len() >= MAX_SAMPLES {
break;
}
let start = parameters[index];
let end = parameters[index + 1];
if end - start < 1e-3 {
continue;
}
for local_fraction in [0.25, 0.5, 0.75] {
if parameters.len() + inserts.len() >= MAX_SAMPLES {
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)?;
if on_surface.sub(on_curve).length() <= refinement_tolerance {
continue;
}
let (surface_parameter, distance) = invert(fraction)?;
if distance <= drop_tolerance {
inserts.push((index + 1, fraction, surface_parameter));
}
}
}
if inserts.is_empty() {
break;
}
for (at, fraction, surface_parameter) in inserts.into_iter().rev() {
parameters.insert(at, fraction);
raw_surface_parameters.insert(at, surface_parameter);
}
pcurve = build_interpolant(surface, &raw_surface_parameters, ¶meters)?;
}
Ok(pcurve)
}
pub fn build_pcurve_on_surface_marched(
surface: &NurbsSurface,
curve: &NurbsCurve,
) -> Result<NurbsCurve, String> {
let global = build_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 ([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);
}
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 = 1e-3f64.min(1e-4 * scale);
const MAX_SAMPLES: usize = 160;
for _ in 0..4 {
if parameters.len() >= MAX_SAMPLES {
break;
}
let mut inserts = Vec::new();
for index in 0..parameters.len() - 1 {
if parameters.len() + inserts.len() >= MAX_SAMPLES {
break;
}
if parameters[index + 1] - parameters[index] < 1e-3 {
continue;
}
for local in [0.25, 0.5, 0.75] {
if parameters.len() + inserts.len() >= MAX_SAMPLES {
break;
}
let fraction =
parameters[index] + (parameters[index + 1] - parameters[index]) * local;
let seed = pcurve.evaluate(fraction)?;
let on_curve = curve.evaluate(t0 + (t1 - t0) * fraction)?;
let on_surface = surface.evaluate(seed.x.clamp(u0, u1), seed.y.clamp(v0, v1))?;
if on_surface.sub(on_curve).length() <= refinement_tolerance {
continue;
}
let seeded = project_point_to_surface_seeded(surface, on_curve, seed.x, seed.y)?;
inserts.push((index + 1, fraction, [seeded.u, seeded.v]));
}
}
if inserts.is_empty() {
break;
}
for (at, fraction, uv) in inserts.into_iter().rev() {
parameters.insert(at, fraction);
raw.insert(at, uv);
}
pcurve = build_interpolant(surface, &raw, ¶meters)?;
}
Ok(pcurve)
}
pub fn build_pcurve_on_surface_range(
surface: &NurbsSurface,
curve: &NurbsCurve,
edge_start: f64,
edge_end: f64,
forward: bool,
tolerance: f64,
) -> Result<NurbsCurve, String> {
build_pcurve_on_surface_range_dense(
surface, curve, edge_start, edge_end, forward, tolerance, 64, 3, 513,
)
}
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)
};
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)
}