use crate::{KernelRefusal, KernelStage, NurbsCurve, NurbsSurface, OrRefuse, Vec3, Vec4};
use super::stations::FIT_DEGREE;
pub(crate) const PCURVE_OFF_FLOOR: &str =
"blend: a pcurve does not reach the refinement floor out of sample —";
const MIN_PIECE_INTERVALS: usize = 6;
const MERGE: f64 = 1e-6;
#[derive(Clone)]
pub(in crate::blend) struct Track {
pub(in crate::blend) fractions: Vec<f64>,
pub(in crate::blend) uv: Vec<[f64; 2]>,
pub(in crate::blend) feet: Vec<[f64; 2]>,
pub(in crate::blend) breaks: Vec<usize>,
}
pub(in crate::blend) struct TrackFit {
pub(in crate::blend) curve: NurbsCurve,
pub(in crate::blend) miss: f64,
pub(in crate::blend) samples: usize,
pub(in crate::blend) on_floor: bool,
pub(in crate::blend) worst: Option<WorstCheck>,
pub(in crate::blend) over: Vec<usize>,
pub(in crate::blend) sections: usize,
pub(in crate::blend) kept: Vec<f64>,
}
#[derive(Clone, Copy, Debug)]
pub(in crate::blend) struct WorstCheck {
pub(in crate::blend) fraction: f64,
pub(in crate::blend) interval: usize,
pub(in crate::blend) bracket: [f64; 2],
}
pub(in crate::blend) fn project_track(
surface: &NurbsSurface,
at: &dyn Fn(f64) -> Result<Vec3, String>,
dense: usize,
curve_breaks: &[f64],
) -> Result<Track, KernelRefusal> {
let dense = dense.max(2 * MIN_PIECE_INTERVALS);
let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain")?;
let periods = [
closed_u.then_some(u1 - u0),
closed_v.then_some(v1 - v0),
];
let mut track = Track {
fractions: Vec::with_capacity(dense + 1),
uv: Vec::with_capacity(dense + 1),
feet: Vec::with_capacity(dense + 1),
breaks: Vec::new(),
};
for index in 0..=dense {
let fraction = index as f64 / dense as f64;
let point = at(fraction).or_refuse(KernelStage::Refine, "at")?;
let raw = foot(surface, point, track.feet.last().copied())?;
let mut uv = raw;
if let Some(previous) = track.uv.last() {
unwrap_near(&mut uv, previous, periods);
}
track.fractions.push(fraction);
track.uv.push(uv);
track.feet.push(raw);
}
insert_knot_crossings(surface, at, &mut track, curve_breaks)?;
Ok(track)
}
fn polish(surface: &NurbsSurface, point: Vec3, uv: [f64; 2]) -> Result<[f64; 2], KernelRefusal> {
if surface.analytic().is_some() {
return Ok(uv);
}
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain")?;
let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let wrap = |value: f64, low: f64, high: f64, closed: bool| {
if closed && (value < low || value > high) {
low + (value - low).rem_euclid(high - low)
} else {
value
}
};
let mut current = uv;
for _ in 0..8 {
let d = surface.derivatives_extended_small(current[0], current[1], 2).or_refuse(KernelStage::Refine, "derivatives")?;
let residual = d[0][0].sub(point);
let (su, sv) = (d[1][0], d[0][1]);
let (f, g) = (su.dot(residual), sv.dot(residual));
let j00 = d[2][0].dot(residual) + su.length_squared();
let j01 = d[1][1].dot(residual) + su.dot(sv);
let j11 = d[0][2].dot(residual) + sv.length_squared();
let determinant = j00 * j11 - j01 * j01;
if !(determinant.abs() > 1e-300) {
break;
}
let du = (-f * j11 + g * j01) / determinant;
let dv = (-g * j00 + f * j01) / determinant;
if !(du.abs() <= 1e-3 * (u1 - u0) && dv.abs() <= 1e-3 * (v1 - v0)) {
break;
}
current = [
wrap(current[0] + du, u0, u1, closed_u),
wrap(current[1] + dv, v0, v1, closed_v),
];
if du.abs() <= 1e-15 * (u1 - u0) && dv.abs() <= 1e-15 * (v1 - v0) {
break;
}
}
Ok(current)
}
fn wrap_closed(surface: &NurbsSurface, uv: [f64; 2]) -> Result<[f64; 2], KernelRefusal> {
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
if uv[0] >= u0 && uv[0] <= u1 && uv[1] >= v0 && uv[1] <= v1 {
return Ok(uv);
}
let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let wrap = |value: f64, closed: bool, low: f64, high: f64| {
if closed && (value < low || value > high) && high > low {
low + (value - low).rem_euclid(high - low)
} else {
value
}
};
Ok([wrap(uv[0], closed_u, u0, u1), wrap(uv[1], closed_v, v0, v1)])
}
fn foot(surface: &NurbsSurface, point: Vec3, seed: Option<[f64; 2]>) -> Result<[f64; 2], KernelRefusal> {
let projected = match seed {
None => crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?,
Some(seed) => crate::projection::project_point_to_surface_from_seed(surface, point, wrap_closed(surface, seed)?)
.or_refuse(KernelStage::Refine, "project")?,
};
polish(surface, point, [projected.u, projected.v])
}
fn foot_among(surface: &NurbsSurface, point: Vec3, seeds: &[[f64; 2]]) -> Result<[f64; 2], KernelRefusal> {
foot_among_with_point(surface, point, seeds, None)
}
fn foot_among_with_point(surface: &NurbsSurface, point: Vec3, seeds: &[[f64; 2]], retained: Option<&mut Option<Vec3>>) -> Result<[f64; 2], KernelRefusal> {
if let (Some(analytic), Some(first)) = (surface.analytic(), seeds.first()) {
#[cfg(not(test))]
let once = true;
if once {
if let Some(projection) = analytic.project(surface, point) {
wrap_closed(surface, *first)?;
note_foot_projection();
if let Some(retained) = retained { *retained = Some(projection.point); }
return Ok([projection.u, projection.v]);
}
}
}
let on_carrier = 1e-12 * (1.0 + point.length());
let mut best: Option<([f64; 2], f64)> = None;
for (index, seed) in seeds.iter().enumerate() {
if seeds[..index].contains(seed) {
continue;
}
note_foot_projection();
let found = foot(surface, point, Some(*seed))?;
let distance = surface.evaluate_extended(found[0], found[1]).or_refuse(KernelStage::Refine, "evaluate_extended")?.sub(point).length();
if best.map_or(true, |(_, known)| distance < known) {
best = Some((found, distance));
}
if distance <= on_carrier {
break;
}
}
best.map(|(found, _)| found).ok_or_else(|| KernelRefusal::internal(KernelStage::Refine, "foot_seeds", "blend: a foot with no seed"))
}
thread_local! {
static FOOT_PROJECTIONS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
fn note_foot_projection() {
if crate::blend::timing::enabled() {
FOOT_PROJECTIONS.with(|count| count.set(count.get() + 1));
}
}
fn flush_foot_projections(measure: &str) {
let count = FOOT_PROJECTIONS.with(|count| count.replace(0));
if count > 0 {
crate::blend::record_count(measure, count);
}
}
pub(in crate::blend) fn polish_track(
surface: &NurbsSurface,
at: &dyn Fn(f64) -> Result<Vec3, String>,
track: &mut Track,
) -> Result<(), KernelRefusal> {
let last = track.fractions.len() - 1;
for index in 1..last {
let raw = track.feet[index];
let polished = polish(surface, at(track.fractions[index]).or_refuse(KernelStage::Refine, "at")?, raw)?;
let (du, dv) = (polished[0] - raw[0], polished[1] - raw[1]);
if du.abs() > 0.25 || dv.abs() > 0.25 {
continue;
}
track.feet[index] = polished;
track.uv[index] = [track.uv[index][0] + du, track.uv[index][1] + dv];
}
Ok(())
}
fn pcurve_image(surface: &NurbsSurface, u: f64, v: f64) -> Result<Vec3, String> {
let ([u0, u1], [v0, v1]) = (surface.domain_u()?, surface.domain_v()?);
if u >= u0 && u <= u1 && v >= v0 && v <= v1 {
return surface.evaluate(u, v);
}
let (closed_u, closed_v) = surface.closed_directions()?;
let wrap = |value: f64, closed: bool, low: f64, high: f64| {
if closed && (value < low || value > high) && high > low {
low + (value - low).rem_euclid(high - low)
} else {
value
}
};
surface.evaluate(wrap(u, closed_u, u0, u1), wrap(v, closed_v, v0, v1))
}
fn unwrap_near(uv: &mut [f64; 2], near: &[f64; 2], periods: [Option<f64>; 2]) {
for axis in 0..2 {
let Some(period) = periods[axis] else {
continue;
};
while uv[axis] - near[axis] > 0.5 * period {
uv[axis] -= period;
}
while near[axis] - uv[axis] > 0.5 * period {
uv[axis] += period;
}
}
}
fn knot_lines(surface: &NurbsSurface) -> Result<[Vec<f64>; 2], KernelRefusal> {
let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let lines = |knots: &[f64], degree: usize, closed: bool| -> Vec<f64> {
let (low, high) = (knots[0], knots[knots.len() - 1]);
let mut lines = Vec::new();
let mut index = 0;
while index < knots.len() {
let value = knots[index];
let run = knots[index..].iter().take_while(|knot| **knot == value).count();
if value > low && value < high && run >= line_multiplicity(degree) {
lines.push(value);
}
index += run;
}
if closed {
lines.push(low);
}
lines
};
Ok([
lines(&surface.knots_u, surface.degree_u, closed_u),
lines(&surface.knots_v, surface.degree_v, closed_v),
])
}
pub(in crate::blend) fn line_multiplicity(degree: usize) -> usize {
if c1_knot_lines_on() {
degree.saturating_sub(1).max(1)
} else {
degree.max(1)
}
}
const C1_KNOT_LINES_DEFAULT: bool = false;
pub(crate) fn c1_knot_lines_on() -> bool {
switch!(live "BREP_BLEND_TRACK_C1_KNOT_LINES", C1_KNOT_LINES_DEFAULT)
}
pub(in crate::blend) fn curve_breaks(curve: &NurbsCurve, t0: f64, t1: f64, forward: bool) -> Vec<f64> {
let mut fractions = Vec::new();
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();
let (low, high) = (t0.min(t1), t0.max(t1));
if value > low && value < high && run >= curve.degree.max(1) {
let along = (value - t0) / (t1 - t0);
fractions.push(if forward { along } else { 1.0 - along });
}
index += run;
}
fractions
}
pub(in crate::blend) fn insert_knot_crossings(
surface: &NurbsSurface,
at: &dyn Fn(f64) -> Result<Vec3, String>,
track: &mut Track,
curve_breaks: &[f64],
) -> Result<(), KernelRefusal> {
let lines = knot_lines(surface)?;
let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain")?;
let periods = [
closed_u.then_some(u1 - u0),
closed_v.then_some(v1 - v0),
];
let images = |axis: usize, low: f64, high: f64| -> Vec<f64> {
let mut images = Vec::new();
for &line in &lines[axis] {
match periods[axis] {
None => images.push(line),
Some(period) => {
let first = ((low - line) / period).floor() as i64 - 1;
let last = ((high - line) / period).ceil() as i64 + 1;
for k in first..=last {
images.push(line + k as f64 * period);
}
}
}
}
images
};
let resolution = 1e-12;
let mut inserts: Vec<(usize, f64, [f64; 2], [f64; 2])> = Vec::new();
let mut on_line: Vec<usize> = Vec::new();
let last = track.fractions.len() - 1;
for index in 0..last {
for axis in 0..2 {
let (a, b) = (track.uv[index][axis], track.uv[index + 1][axis]);
for line in images(axis, a.min(b), a.max(b)) {
let (da, db) = (a - line, b - line);
if da.abs() <= resolution {
if index > 0 {
on_line.push(index);
}
continue;
}
if db.abs() <= resolution || da.signum() == db.signum() {
continue;
}
let (mut low, mut high) = (track.fractions[index], track.fractions[index + 1]);
let mut low_value = da;
let mut found = (low, track.uv[index], track.feet[index]);
for _ in 0..80 {
if high - low <= 1e-15 {
break;
}
let middle = 0.5 * (low + high);
let raw = foot(surface, at(middle).or_refuse(KernelStage::Refine, "at")?, Some(track.feet[index]))?;
let mut uv = raw;
unwrap_near(&mut uv, &track.uv[index], periods);
found = (middle, uv, raw);
let value = uv[axis] - line;
if value.signum() == low_value.signum() {
low = middle;
low_value = value;
} else {
high = middle;
}
}
inserts.push((index, found.0, found.1, found.2));
}
}
}
for &fraction in curve_breaks {
if !(fraction > 0.0 && fraction < 1.0) {
continue;
}
let index = track
.fractions
.windows(2)
.position(|pair| pair[0] <= fraction && fraction <= pair[1])
.ok_or_else(|| KernelRefusal::internal(KernelStage::Refine, "curve_break", "blend: a curve break outside its track"))?;
let raw = foot_among(surface, at(fraction).or_refuse(KernelStage::Refine, "at")?, &[track.feet[index], track.feet[index + 1]])?;
let mut uv = raw;
unwrap_near(&mut uv, &track.uv[index], periods);
inserts.push((index, fraction, uv, raw));
}
let mut breaks: Vec<f64> = on_line.iter().map(|&index| track.fractions[index]).collect();
inserts.sort_by(|a, b| b.1.total_cmp(&a.1));
let spacing = 1.0 / last as f64;
for (index, fraction, uv, foot) in inserts {
let near_left = fraction - track.fractions[index] <= 1e-9 * spacing;
let near_right = track.fractions[index + 1] - fraction <= 1e-9 * spacing;
if inserted_at(&track.fractions, fraction) {
breaks.push(fraction);
continue;
}
if near_left || near_right {
breaks.push(track.fractions[if near_left { index } else { index + 1 }]);
continue;
}
track.fractions.insert(index + 1, fraction);
track.uv.insert(index + 1, uv);
track.feet.insert(index + 1, foot);
breaks.push(fraction);
}
breaks.sort_by(f64::total_cmp);
let mut fractions: Vec<f64> = Vec::new();
for fraction in breaks {
let previous = fractions.last().copied().unwrap_or(0.0);
if fraction - previous > MERGE && 1.0 - fraction > MERGE {
fractions.push(fraction);
}
}
let mut bounds = vec![0.0];
bounds.extend(fractions.iter().copied());
bounds.push(1.0);
for pair in bounds.windows(2) {
let (low, high) = (pair[0], pair[1]);
let inside = track.fractions.iter().filter(|f| **f > low && **f < high).count();
if inside + 1 >= MIN_PIECE_INTERVALS {
continue;
}
for k in 1..MIN_PIECE_INTERVALS {
let fraction = low + (high - low) * k as f64 / MIN_PIECE_INTERVALS as f64;
if inserted_at(&track.fractions, fraction) {
continue;
}
let index = track
.fractions
.windows(2)
.position(|pair| pair[0] < fraction && fraction < pair[1])
.ok_or_else(|| {
KernelRefusal::internal(KernelStage::Refine, "densified_sample", "blend: a densified sample outside its track")
})?;
let raw = foot_among(surface, at(fraction).or_refuse(KernelStage::Refine, "at")?, &[track.feet[index], track.feet[index + 1]])?;
let mut uv = raw;
unwrap_near(&mut uv, &track.uv[index], periods);
track.fractions.insert(index + 1, fraction);
track.uv.insert(index + 1, uv);
track.feet.insert(index + 1, raw);
}
}
let kept: Vec<usize> = fractions
.iter()
.filter_map(|fraction| track.fractions.iter().position(|f| f == fraction))
.collect();
track.breaks = kept;
Ok(())
}
pub(in crate::blend) fn fit_track(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
tolerance: f64,
) -> Result<TrackFit, KernelRefusal> {
fit_track_from(surface, track, at, tolerance, 8)
}
pub(in crate::blend) fn fit_track_from(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
tolerance: f64,
start: usize,
) -> Result<TrackFit, KernelRefusal> {
let checks = rung_checks(surface, track, at)?;
fit_track_with(surface, track, at, &checks, tolerance, start)
}
fn fit_track_from_cached(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
tolerance: f64,
start: usize,
cache: &mut CheckCache,
) -> Result<TrackFit, KernelRefusal> {
let checks = rung_checks_cached(surface, track, at, Some(cache))?;
fit_track_with(surface, track, at, &checks, tolerance, start)
}
pub(in crate::blend) struct RungChecks {
checks: Vec<(f64, Vec3)>,
tangents: Vec<[[f64; 2]; 2]>,
}
pub(in crate::blend) fn rung_checks(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
) -> Result<RungChecks, KernelRefusal> {
rung_checks_cached(surface, track, at, None)
}
#[derive(Default)]
struct CheckCache {
points: std::collections::HashMap<[u64; 5], Vec3>,
stencils: Vec<([u64; 6], [Vec3; LOCAL_STENCIL - 1])>,
analytic_readings: u64,
hits: u64,
}
impl Drop for CheckCache {
fn drop(&mut self) {
crate::blend::record_count("count.track.check_cache_hits", self.hits);
crate::blend::record_count("count.track.stencil_cache_intervals", self.stencils.len() as u64);
crate::blend::record_count("count.track.analytic_check_points_reused", self.analytic_readings);
}
}
fn analytic_check_reuse_on() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("BREP_TRACK_ANALYTIC_CHECK_REUSE").ok().as_deref() != Some("0"))
}
fn check_point(
surface: &NurbsSurface,
at: &dyn Fn(f64) -> Result<Vec3, String>,
fraction: f64,
seeds: [[f64; 2]; 2],
cache: &mut Option<&mut CheckCache>,
) -> Result<Vec3, KernelRefusal> {
check_point_with_storage(surface, at, fraction, seeds, cache, true)
}
fn check_point_with_storage(
surface: &NurbsSurface,
at: &dyn Fn(f64) -> Result<Vec3, String>,
fraction: f64,
seeds: [[f64; 2]; 2],
cache: &mut Option<&mut CheckCache>,
store: bool,
) -> Result<Vec3, KernelRefusal> {
let key = [fraction.to_bits(), seeds[0][0].to_bits(), seeds[0][1].to_bits(), seeds[1][0].to_bits(), seeds[1][1].to_bits()];
if let Some(cache) = cache.as_mut() {
if store {
if let Some(known) = cache.points.get(&key) {
let known = *known;
cache.hits += 1;
return Ok(known);
}
}
}
let reuse = analytic_check_reuse_on();
let mut retained = None;
let source = at(fraction).or_refuse(KernelStage::Refine, "at")?;
let [u, v] = if reuse {
foot_among_with_point(surface, source, &seeds, Some(&mut retained))?
} else {
foot_among(surface, source, &seeds)?
};
let point = match retained {
Some(point) => point,
None => surface.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?,
};
if let Some(cache) = cache.as_mut() {
if store { cache.points.insert(key, point); }
if retained.is_some() {
cache.analytic_readings += 1;
}
}
Ok(point)
}
fn rung_checks_cached(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
mut cache: Option<&mut CheckCache>,
) -> Result<RungChecks, KernelRefusal> {
let timer = crate::blend::BlendTimer::start("timing.track.rung_checks_ms");
flush_foot_projections("count.track.feet_elsewhere");
let last = track.fractions.len() - 1;
let mut checks: Vec<(f64, Vec3)> = Vec::with_capacity(last);
for index in 0..last {
let fraction = 0.5 * (track.fractions[index] + track.fractions[index + 1]);
checks.push((fraction, check_point(surface, at, fraction, [track.feet[index], track.feet[index + 1]], &mut cache)?));
}
let tangents: Vec<[[f64; 2]; 2]> = track
.breaks
.iter()
.map(|&index| track_tangent(surface, track, at, index))
.collect::<Result<_, _>>()?;
crate::blend::record_count("count.track.rung_check_points", last as u64);
flush_foot_projections("count.track.rung_check_feet");
timer.stop();
Ok(RungChecks { checks, tangents })
}
fn fit_track_with(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
checks: &RungChecks,
tolerance: f64,
start: usize,
) -> Result<TrackFit, KernelRefusal> {
let _timer = crate::blend::BlendTimer::start("timing.track.ladder_ms");
let last = track.fractions.len() - 1;
let mut best: Option<TrackFit> = None;
let mut sections = start.max(8);
loop {
let sections_here = sections.min(last);
let pieces = ladder_pieces(track, sections_here);
crate::blend::record_count("count.track.rungs", 1);
let fit = measure_rung(surface, track, at, checks, &pieces, sections_here, tolerance)?;
let miss = fit.miss;
if best.as_ref().map_or(true, |known| miss < known.miss) {
best = Some(fit);
}
if miss <= tolerance || sections_here == last {
break;
}
sections *= 2;
}
best.ok_or_else(|| KernelRefusal::internal(KernelStage::Refine, "no_rung", "blend: the track fit produced no rung"))
}
const BOUND_SLIVER: f64 = 1e-3;
fn drop_bound_slivers(track: &Track, indices: Vec<usize>) -> Vec<usize> {
let mut indices = indices;
let f = |index: usize| track.fractions[index];
if indices.len() > FIT_DEGREE + 1 {
let (a, b, c) = (indices[0], indices[1], indices[2]);
if f(b) - f(a) < BOUND_SLIVER * (f(c) - f(b)) {
indices.remove(1);
}
}
let n = indices.len();
if n > FIT_DEGREE + 1 {
let (a, b, c) = (indices[n - 3], indices[n - 2], indices[n - 1]);
if f(c) - f(b) < BOUND_SLIVER * (f(b) - f(a)) {
indices.remove(n - 2);
}
}
indices
}
fn ladder_pieces(track: &Track, sections: usize) -> Vec<Vec<usize>> {
let last = track.fractions.len() - 1;
if track.breaks.is_empty() {
return vec![drop_bound_slivers(track, (0..=sections).map(|k| (k * last / sections).min(last)).collect())];
}
let mut bounds = vec![0usize];
bounds.extend(track.breaks.iter().copied());
bounds.push(last);
bounds
.windows(2)
.map(|pair| {
let (from, to) = (pair[0], pair[1]);
let span = to - from;
let share = ((sections as f64 * span as f64 / last as f64).ceil() as usize)
.max(MIN_PIECE_INTERVALS)
.min(span);
let mut indices: Vec<usize> = (0..=share).map(|k| from + k * span / share).collect();
indices.dedup();
drop_bound_slivers(track, indices)
})
.collect()
}
fn nested_pieces(track: &Track, kept: &[f64]) -> Option<Vec<Vec<usize>>> {
let last = track.fractions.len() - 1;
let wanted: std::collections::HashSet<u64> = kept.iter().map(|fraction| fraction.to_bits()).collect();
let mut bounds = vec![0usize];
bounds.extend(track.breaks.iter().copied());
bounds.push(last);
let mut pieces = Vec::with_capacity(bounds.len() - 1);
for pair in bounds.windows(2) {
let (from, to) = (pair[0], pair[1]);
let indices: Vec<usize> =
(from..=to).filter(|&index| index == from || index == to || wanted.contains(&track.fractions[index].to_bits())).collect();
if indices.len() < FIT_DEGREE + 1 {
return None;
}
pieces.push(drop_bound_slivers(track, indices));
}
Some(pieces)
}
fn measure_rung(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
checks: &RungChecks,
pieces: &[Vec<usize>],
sections: usize,
tolerance: f64,
) -> Result<TrackFit, KernelRefusal> {
let last = track.fractions.len() - 1;
let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
let (curve, samples) = if track.breaks.is_empty() {
let indices = &pieces[0];
let points: Vec<Vec4> = indices
.iter()
.map(|&i| Vec4::from_point(Vec3::new(track.uv[i][0], track.uv[i][1], 0.0), 1.0))
.collect();
let parameters: Vec<f64> = indices.iter().map(|&i| track.fractions[i]).collect();
(interpolate_track(&points, ¶meters)?, indices.len())
} else {
fit_in_c1_pieces(track, &checks.tangents, pieces)?
};
let mut worst: Option<(usize, f64)> = None;
let mut distances = Vec::with_capacity(checks.checks.len());
for (index, (fraction, on_carrier)) in checks.checks.iter().enumerate() {
let uv = curve.evaluate(*fraction).or_refuse(KernelStage::Refine, "evaluate")?;
let distance = pcurve_image(surface, uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")?.sub(*on_carrier).length();
if debug && worst.map_or(true, |(_, known)| distance > known) {
worst = Some((index, distance));
}
distances.push(distance);
}
let (miss, over) = classify_checks(&distances, tolerance)?;
let worst = worst.map(|(index, _)| WorstCheck {
fraction: checks.checks[index].0,
interval: index,
bracket: [track.fractions[index], track.fractions[index + 1]],
});
let kept: Vec<f64> = pieces.iter().flatten().map(|&index| track.fractions[index]).collect();
if let Some(at_worst) = worst {
eprintln!(
"TRACK_FIT rung: sections {sections} samples {samples} miss {miss:.3e} worst at fraction {:.9} \
(dense interval {} of {}: [{:.9}, {:.9}]; {} breaks)",
at_worst.fraction, at_worst.interval, last, at_worst.bracket[0], at_worst.bracket[1], track.breaks.len()
);
if std::env::var_os("BREP_DEBUG_TRACK_WINDOW").is_some() {
debug_rung_window(surface, track, at, &curve, &kept, at_worst.interval);
}
}
Ok(TrackFit {
curve,
miss,
samples,
on_floor: miss <= tolerance,
worst,
over,
sections,
kept,
})
}
fn debug_rung_window(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
curve: &NurbsCurve,
kept: &[f64],
interval: usize,
) {
let last = track.fractions.len() - 1;
let kept: std::collections::HashSet<u64> = kept.iter().map(|fraction| fraction.to_bits()).collect();
let breaks: Vec<String> = track.breaks.iter().map(|&index| format!("{:.12}", track.fractions[index])).collect();
eprintln!("TRACK_WINDOW breaks [{}]", breaks.join(", "));
for index in interval.saturating_sub(3)..=(interval + 4).min(last) {
let fraction = track.fractions[index];
let [u, v] = track.feet[index];
let standoff = at(fraction).ok().zip(surface.evaluate(u, v).ok()).map(|(point, image)| image.sub(point).length());
let image_error = curve
.evaluate(fraction)
.ok()
.and_then(|uv| pcurve_image(surface, uv.x, uv.y).ok())
.zip(surface.evaluate(u, v).ok())
.map(|(image, on)| image.sub(on).length());
let is_break = track.breaks.contains(&index);
eprintln!(
"TRACK_WINDOW sample {index} fraction {fraction:.12} uv [{:.12}, {:.12}] break {is_break} kept {} standoff {standoff:?} rung error {image_error:?}",
track.uv[index][0], track.uv[index][1], kept.contains(&fraction.to_bits())
);
if is_break && index > 0 && index < last {
let secant = |a: usize, b: usize| {
let h = track.fractions[b] - track.fractions[a];
[(track.uv[b][0] - track.uv[a][0]) / h, (track.uv[b][1] - track.uv[a][1]) / h]
};
let metric = track_tangent(surface, track, at, index).ok();
let exact = one_sided_derivatives(at, fraction).ok().map(|(left, right)| {
[exact_track_derivative(surface, track.feet[index], at(fraction).ok(), left), exact_track_derivative(surface, track.feet[index], at(fraction).ok(), right)]
});
eprintln!(
"TRACK_WINDOW break {index}: metric tangent {metric:?} exact {exact:?} secant left {:?} right {:?}",
secant(index - 1, index),
secant(index, index + 1)
);
}
}
}
pub(in crate::blend) fn exact_track_derivative(surface: &NurbsSurface, uv: [f64; 2], point: Option<Vec3>, derivative: Vec3) -> Option<[f64; 2]> {
let point = point?;
let d = surface.derivatives_small(uv[0], uv[1], 2).ok()?;
let residual = d[0][0].sub(point);
let (su, sv) = (d[1][0], d[0][1]);
let j00 = d[2][0].dot(residual) + su.length_squared();
let j01 = d[1][1].dot(residual) + su.dot(sv);
let j11 = d[0][2].dot(residual) + sv.length_squared();
let determinant = j00 * j11 - j01 * j01;
if !(determinant.abs() > 1e-300) {
return None;
}
let (x, y) = (su.dot(derivative), sv.dot(derivative));
let result = [(j11 * x - j01 * y) / determinant, (j00 * y - j01 * x) / determinant];
(result[0].is_finite() && result[1].is_finite()).then_some(result)
}
pub(in crate::blend) fn refine_track_intervals(
surface: &NurbsSurface,
at: &dyn Fn(f64) -> Result<Vec3, String>,
track: &mut Track,
intervals: &[usize],
max_intervals: usize,
) -> Result<Option<usize>, KernelRefusal> {
let lines = knot_lines(surface)?;
let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain")?;
let periods = [closed_u.then_some(u1 - u0), closed_v.then_some(v1 - v0)];
let images = |axis: usize, low: f64, high: f64| -> Vec<f64> {
let mut images = Vec::new();
for &line in &lines[axis] {
match periods[axis] {
None => images.push(line),
Some(period) => {
let first = ((low - line) / period).floor() as i64 - 1;
let last = ((high - line) / period).ceil() as i64 + 1;
for k in first..=last {
images.push(line + k as f64 * period);
}
}
}
}
images
};
let resolution = 1e-12;
let original = &mut *track;
let mut staged = original.clone();
let track = &mut staged;
let mut intervals: Vec<usize> = intervals.iter().copied().filter(|&index| index + 1 < track.fractions.len()).collect();
intervals.sort_unstable();
intervals.dedup();
let mut inserted = 0;
let mut new_breaks: Vec<f64> = Vec::new();
let insert_at = |track: &mut Track, index: usize, fraction: f64, uv: [f64; 2], raw: [f64; 2]| -> bool {
if track.fractions.len() - 1 >= max_intervals {
return false;
}
track.fractions.insert(index + 1, fraction);
track.uv.insert(index + 1, uv);
track.feet.insert(index + 1, raw);
for brk in track.breaks.iter_mut() {
if *brk > index {
*brk += 1;
}
}
true
};
for &index in intervals.iter().rev() {
let fraction = 0.5 * (track.fractions[index] + track.fractions[index + 1]);
if !(fraction > track.fractions[index] && fraction < track.fractions[index + 1]) {
continue;
}
let raw = foot_among(surface, at(fraction).or_refuse(KernelStage::Refine, "at")?, &[track.feet[index], track.feet[index + 1]])?;
let mut uv = raw;
unwrap_near(&mut uv, &track.uv[index], periods);
if !insert_at(track, index, fraction, uv, raw) {
return Ok(None);
}
inserted += 1;
for half in [index + 1, index] {
let spacing = 1.0 / (track.fractions.len() - 1) as f64;
let mut crossings: Vec<(f64, [f64; 2], [f64; 2])> = Vec::new();
for axis in 0..2 {
let (a, b) = (track.uv[half][axis], track.uv[half + 1][axis]);
for line in images(axis, a.min(b), a.max(b)) {
let (da, db) = (a - line, b - line);
if da.abs() <= resolution {
if half == index + 1 {
new_breaks.push(track.fractions[half]);
}
continue;
}
if db.abs() <= resolution || da.signum() == db.signum() {
continue;
}
let (mut low, mut high) = (track.fractions[half], track.fractions[half + 1]);
let mut low_value = da;
let mut found = (low, track.uv[half], track.feet[half]);
for _ in 0..80 {
if high - low <= 1e-15 {
break;
}
let middle = 0.5 * (low + high);
let raw = foot(surface, at(middle).or_refuse(KernelStage::Refine, "at")?, Some(track.feet[half]))?;
let mut probe = raw;
unwrap_near(&mut probe, &track.uv[half], periods);
found = (middle, probe, raw);
let value = probe[axis] - line;
if value.signum() == low_value.signum() {
low = middle;
low_value = value;
} else {
high = middle;
}
}
crossings.push(found);
}
}
crossings.sort_by(|a, b| b.0.total_cmp(&a.0));
for (fraction, uv, raw) in crossings {
let near_left = fraction - track.fractions[half] <= 1e-9 * spacing;
let near_right = track.fractions[half + 1] - fraction <= 1e-9 * spacing;
if inserted_at(&track.fractions, fraction) {
new_breaks.push(fraction);
continue;
}
if near_left || near_right {
new_breaks.push(track.fractions[if near_left { half } else { half + 1 }]);
continue;
}
if !insert_at(track, half, fraction, uv, raw) {
return Ok(None);
}
inserted += 1;
new_breaks.push(fraction);
}
}
}
if new_breaks.is_empty() {
*original = staged;
return Ok(Some(inserted));
}
let mut fractions: Vec<f64> = track.breaks.iter().map(|&index| track.fractions[index]).collect();
new_breaks.sort_by(f64::total_cmp);
for fraction in new_breaks {
let apart = fractions.iter().all(|known| (known - fraction).abs() > MERGE);
if fraction > MERGE && 1.0 - fraction > MERGE && apart {
fractions.push(fraction);
}
}
fractions.sort_by(f64::total_cmp);
let mut bounds = vec![0.0];
bounds.extend(fractions.iter().copied());
bounds.push(1.0);
for pair in bounds.windows(2) {
let (low, high) = (pair[0], pair[1]);
let inside = track.fractions.iter().filter(|f| **f > low && **f < high).count();
if inside + 1 >= MIN_PIECE_INTERVALS {
continue;
}
for k in 1..MIN_PIECE_INTERVALS {
let fraction = low + (high - low) * k as f64 / MIN_PIECE_INTERVALS as f64;
if track.fractions.iter().any(|known| (known - fraction).abs() <= 1e-9 * (high - low)) {
continue;
}
let index = track
.fractions
.windows(2)
.position(|pair| pair[0] < fraction && fraction < pair[1])
.ok_or_else(|| {
KernelRefusal::internal(KernelStage::Refine, "densified_sample", "blend: a densified sample outside its track")
})?;
let raw = foot_among(surface, at(fraction).or_refuse(KernelStage::Refine, "at")?, &[track.feet[index], track.feet[index + 1]])?;
let mut uv = raw;
unwrap_near(&mut uv, &track.uv[index], periods);
if !insert_at(track, index, fraction, uv, raw) {
return Ok(None);
}
inserted += 1;
}
}
track.breaks = fractions
.iter()
.filter_map(|fraction| track.fractions.iter().position(|f| f == fraction))
.collect();
*original = staged;
Ok(Some(inserted))
}
fn classify_checks(distances: &[f64], tolerance: f64) -> Result<(f64, Vec<usize>), KernelRefusal> {
if let Some((index, distance)) = distances.iter().enumerate().find(|(_, distance)| !distance.is_finite()) {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "track_fit_unreadable", format!(
"{PCURVE_OFF_FLOOR} check {index} of {} on the track is unreadable (distance {distance})",
distances.len()
)));
}
let mut miss: f64 = 0.0;
let mut over = Vec::new();
for (index, &distance) in distances.iter().enumerate() {
if !(distance <= tolerance) {
over.push(index);
}
miss = miss.max(distance);
}
Ok((miss, over))
}
pub(in crate::blend) fn read_shipped_pcurve(
surface: &NurbsSurface,
rail: &dyn Fn(f64) -> Result<Vec3, String>,
pcurve: &crate::NurbsCurve,
d0: f64,
d1: f64,
) -> Result<(f64, f64, usize, f64), KernelRefusal> {
read_shipped_pcurve_memo(surface, rail, pcurve, d0, d1, &mut FootMemo::transient())
}
fn small_carrier_homogeneous<const DU: usize, const DV: usize>(surface: &NurbsSurface, u: f64, v: f64) -> Vec4 {
fn basis<const D: usize>(knots: &[f64], span: usize, parameter: f64) -> [f64; 3] {
let mut values = [0.0; 3];
let mut left = [0.0; 3];
let mut right = [0.0; 3];
values[0] = 1.0;
for j in 1..=D {
left[j] = parameter - knots[span + 1 - j];
right[j] = knots[span + j] - parameter;
let mut saved = 0.0;
for r in 0..j {
let denominator = right[r + 1] + left[j - r];
let temporary = if denominator.abs() <= crate::tolerance::FP_FLOOR { 0.0 } else { values[r] / denominator };
values[r] = saved + right[r + 1] * temporary;
saved = left[j - r] * temporary;
}
values[j] = saved;
}
values
}
let u = crate::curve::knot_clamp(&surface.knots_u, DU, u);
let v = crate::curve::knot_clamp(&surface.knots_v, DV, v);
let span_u = crate::curve::knot_find_span(&surface.knots_u, DU, u);
let span_v = crate::curve::knot_find_span(&surface.knots_v, DV, v);
let basis_u = basis::<DU>(&surface.knots_u, span_u, u);
let basis_v = basis::<DV>(&surface.knots_v, span_v, v);
let mut point = Vec4 { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
for (i, value_u) in basis_u[..=DU].iter().enumerate() {
let row = &surface.control_points[span_u - DU + i];
for (j, value_v) in basis_v[..=DV].iter().enumerate() {
point = point.add(row[span_v - DV + j].scale(value_u * value_v));
}
}
point
}
pub(in crate::blend) fn carrier_point(surface: &NurbsSurface, u: f64, v: f64) -> Result<Vec3, String> {
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
if u >= u0 && u <= u1 && v >= v0 && v <= v1 {
let homogeneous = match (surface.degree_u, surface.degree_v) {
(1, 1) => small_carrier_homogeneous::<1, 1>(surface, u, v),
(1, 2) => small_carrier_homogeneous::<1, 2>(surface, u, v),
(2, 1) => small_carrier_homogeneous::<2, 1>(surface, u, v),
(2, 2) => small_carrier_homogeneous::<2, 2>(surface, u, v),
_ => surface.evaluate_homogeneous(u, v)?,
};
let weight = homogeneous.w;
if weight.abs() <= 1e-12 {
return Err("NurbsSurface: zero evaluated weight".into());
}
return Ok(Vec3::new(homogeneous.x, homogeneous.y, homogeneous.z).scale(1.0 / weight));
}
surface.evaluate_extended(u, v)
}
#[derive(Clone)]
struct FootRead {
uv: [f64; 2],
point: Vec3,
on_carrier: Vec3,
}
#[derive(Default)]
pub(in crate::blend) struct FootMemo {
feet: std::collections::HashMap<u64, Result<FootRead, KernelRefusal>>,
transient: bool,
}
impl FootMemo {
pub(in crate::blend) fn transient() -> Self {
FootMemo { feet: Default::default(), transient: true }
}
pub(in crate::blend) fn empty_like(&self) -> Self {
FootMemo { feet: Default::default(), transient: self.transient }
}
pub(in crate::blend) fn stored(&self) -> usize {
self.feet.len()
}
pub(in crate::blend) fn clear(&mut self) {
self.feet.clear();
}
fn at(
&mut self,
surface: &NurbsSurface,
rail: &dyn Fn(f64) -> Result<Vec3, String>,
pcurve: &crate::NurbsCurve,
t: f64,
) -> Result<FootRead, KernelRefusal> {
if !self.transient {
if let Some(read) = self.feet.get(&t.to_bits()) {
return read.clone();
}
}
let read = (|| -> Result<FootRead, KernelRefusal> {
let point = rail(t).or_refuse(KernelStage::Refine, "rail")?;
let uv = pcurve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
let [u, v] = foot(surface, point, Some([uv.x, uv.y]))?;
let on_carrier = carrier_point(surface, u, v).or_refuse(KernelStage::Refine, "evaluate_extended")?;
Ok(FootRead { uv: [uv.x, uv.y], point, on_carrier })
})();
if !self.transient {
self.feet.insert(t.to_bits(), read.clone());
}
read
}
}
pub(in crate::blend) fn read_shipped_pcurve_memo(
surface: &NurbsSurface,
rail: &dyn Fn(f64) -> Result<Vec3, String>,
pcurve: &crate::NurbsCurve,
d0: f64,
d1: f64,
memo: &mut FootMemo,
) -> Result<(f64, f64, usize, f64), KernelRefusal> {
let _timer = crate::blend::BlendTimer::start("timing.track.shipped_read_ms");
let mut parameters: Vec<f64> = pcurve.knots.iter().copied().filter(|knot| *knot >= d0 && *knot <= d1).collect();
parameters.push(d0);
parameters.push(d1);
parameters.sort_by(f64::total_cmp);
parameters.dedup();
let spans: Vec<f64> = parameters.windows(2).flat_map(|pair| (1..16).map(move |j| pair[0] + (pair[1] - pair[0]) * j as f64 / 16.0)).collect();
parameters.extend(spans);
parameters.extend((0..=512).map(|k| d0 + (d1 - d0) * k as f64 / 512.0));
let mut residuals = Vec::with_capacity(parameters.len());
let mut offset = 0.0_f64;
for &t in ¶meters {
let read = (|| -> Result<(f64, f64), KernelRefusal> {
let FootRead { uv, point, on_carrier } = memo.at(surface, rail, pcurve, t)?;
let image = carrier_point(surface, uv[0], uv[1]).or_refuse(KernelStage::Refine, "evaluate_extended")?;
Ok((image.sub(on_carrier).length(), point.sub(on_carrier).length()))
})();
match read {
Ok((residual, standoff)) if residual.is_finite() && standoff.is_finite() => {
residuals.push(residual);
offset = offset.max(standoff);
}
_ => residuals.push(f64::NAN),
}
}
crate::blend::record_count("count.track.shipped_samples", parameters.len() as u64);
let (miss, _) = classify_checks(&residuals, f64::INFINITY)?;
let worst = residuals.iter().zip(¶meters).fold((0.0_f64, d0), |best, (&residual, &t)| if residual > best.0 { (residual, t) } else { best }).1;
Ok((miss, offset, residuals.len(), worst))
}
pub(in crate::blend) fn standoff_excess(
surface: &NurbsSurface,
rail: &dyn Fn(f64) -> Result<Vec3, String>,
reference: &crate::NurbsCurve,
candidate: &crate::NurbsCurve,
d0: f64,
d1: f64,
) -> Result<f64, KernelRefusal> {
standoff_excess_memo(surface, rail, reference, candidate, d0, d1, &mut FootMemo::default(), &mut FootMemo::default())
}
pub(in crate::blend) fn standoff_excess_memo(
surface: &NurbsSurface,
rail: &dyn Fn(f64) -> Result<Vec3, String>,
reference: &crate::NurbsCurve,
candidate: &crate::NurbsCurve,
d0: f64,
d1: f64,
reference_memo: &mut FootMemo,
candidate_memo: &mut FootMemo,
) -> Result<f64, KernelRefusal> {
let mut parameters: Vec<f64> = reference.knots.iter().chain(&candidate.knots).copied().filter(|knot| *knot >= d0 && *knot <= d1).collect();
parameters.push(d0);
parameters.push(d1);
parameters.sort_by(f64::total_cmp);
parameters.dedup();
let spans: Vec<f64> = parameters.windows(2).flat_map(|pair| (1..16).map(move |j| pair[0] + (pair[1] - pair[0]) * j as f64 / 16.0)).collect();
parameters.extend(spans);
parameters.extend((0..=512).map(|k| d0 + (d1 - d0) * k as f64 / 512.0));
let standoff = |memo: &mut FootMemo, pcurve: &crate::NurbsCurve, t: f64| -> Result<f64, KernelRefusal> {
let FootRead { point, on_carrier, .. } = memo.at(surface, rail, pcurve, t)?;
Ok(point.sub(on_carrier).length())
};
let mut excess = f64::NEG_INFINITY;
for &t in ¶meters {
rail(t).or_refuse(KernelStage::Refine, "rail")?;
let difference = standoff(candidate_memo, candidate, t)? - standoff(reference_memo, reference, t)?;
if !difference.is_finite() {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "track_fit_unreadable", format!(
"blend: a support trim's standoff is unreadable at t = {t}"
)));
}
excess = excess.max(difference);
}
Ok(excess)
}
pub(in crate::blend) fn standoff_over_nearest(
surface: &NurbsSurface,
rail: &dyn Fn(f64) -> Result<Vec3, String>,
candidate: &crate::NurbsCurve,
d0: f64,
d1: f64,
) -> Result<f64, KernelRefusal> {
standoff_over_nearest_memo(surface, rail, candidate, d0, d1, &mut FootMemo::default())
}
pub(in crate::blend) fn standoff_over_nearest_memo(
surface: &NurbsSurface,
rail: &dyn Fn(f64) -> Result<Vec3, String>,
candidate: &crate::NurbsCurve,
d0: f64,
d1: f64,
memo: &mut FootMemo,
) -> Result<f64, KernelRefusal> {
let mut parameters: Vec<f64> = candidate.knots.iter().copied().filter(|knot| *knot >= d0 && *knot <= d1).collect();
parameters.extend((0..=512).map(|k| d0 + (d1 - d0) * k as f64 / 512.0));
parameters.sort_by(f64::total_cmp);
parameters.dedup();
let mut excess = f64::NEG_INFINITY;
for &t in ¶meters {
let FootRead { point, on_carrier, .. } = memo.at(surface, rail, candidate, t)?;
let standoff = point.sub(on_carrier).length();
let nearest = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?.distance;
let difference = standoff - nearest;
if !difference.is_finite() {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "track_fit_unreadable", format!(
"blend: a support trim's standoff is unreadable at t = {t}"
)));
}
excess = excess.max(difference);
}
Ok(excess)
}
const LOCAL_STENCIL: usize = 16;
fn stencil_verdict(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
curve: &NurbsCurve,
tolerance: f64,
) -> Result<(f64, Vec<usize>, Option<WorstCheck>, Vec<(f64, f64, f64, f64)>), KernelRefusal> {
stencil_verdict_cached(surface, track, at, curve, tolerance, None)
}
fn stencil_verdict_cached(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
curve: &NurbsCurve,
tolerance: f64,
mut cache: Option<&mut CheckCache>,
) -> Result<(f64, Vec<usize>, Option<WorstCheck>, Vec<(f64, f64, f64, f64)>), KernelRefusal> {
let timer = crate::blend::BlendTimer::start("timing.track.stencil_ms");
let interval_cache = cache.is_some();
let previous_stencils = if interval_cache {
cache.as_mut().map(|c| std::mem::take(&mut c.stencils)).unwrap_or_default()
} else { Vec::new() };
let mut previous_index = 0;
let mut next_stencils = if interval_cache { Vec::with_capacity(track.fractions.len().saturating_sub(1)) } else { Vec::new() };
let profile_image = crate::blend::timing::enabled()
&& std::env::var("BREP_PROFILE_TRACK_IMAGE").ok().as_deref() == Some("1");
let mut image_elapsed = std::time::Duration::ZERO;
let mut image_count = 0u64;
flush_foot_projections("count.track.feet_elsewhere");
crate::blend::record_count("count.track.stencil_points", ((track.fractions.len() - 1) * (LOCAL_STENCIL - 1)) as u64);
let mut per_interval = Vec::with_capacity(track.fractions.len() - 1);
let mut located = Vec::with_capacity(track.fractions.len() - 1);
let mut worst: Option<(f64, WorstCheck)> = None;
for index in 0..track.fractions.len() - 1 {
let (low, high) = (track.fractions[index], track.fractions[index + 1]);
let seeds = [track.feet[index], track.feet[index + 1]];
let key = [low.to_bits(), high.to_bits(), seeds[0][0].to_bits(), seeds[0][1].to_bits(), seeds[1][0].to_bits(), seeds[1][1].to_bits()];
while previous_index < previous_stencils.len()
&& f64::from_bits(previous_stencils[previous_index].0[0]) < low {
previous_index += 1;
}
let known = previous_stencils.get(previous_index)
.filter(|(previous, _)| *previous == key).map(|(_, points)| points);
let mut stencil = [Vec3::default(); LOCAL_STENCIL - 1];
let mut distances = Vec::with_capacity(LOCAL_STENCIL - 1);
let mut interval_worst = (0.0_f64, 0.5 * (low + high));
for j in 1..LOCAL_STENCIL {
let fraction = low + (high - low) * j as f64 / LOCAL_STENCIL as f64;
let on_carrier = match &known {
Some(points) => {
if let Some(cache) = cache.as_mut() { cache.hits += 1; }
points[j - 1]
}
None => check_point_with_storage(surface, at, fraction, seeds, &mut cache,
!interval_cache || j == LOCAL_STENCIL / 2)?,
};
stencil[j - 1] = on_carrier;
let uv = curve.evaluate(fraction).or_refuse(KernelStage::Refine, "evaluate")?;
let image_started = profile_image.then(web_time::Instant::now);
let image = pcurve_image(surface, uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")?;
if let Some(started) = image_started {
image_elapsed += started.elapsed();
image_count += 1;
}
let distance = image.sub(on_carrier).length();
if worst.as_ref().map_or(true, |(known, _)| distance > *known) {
worst = Some((distance, WorstCheck { fraction, interval: index, bracket: [low, high] }));
}
if distance > interval_worst.0 {
interval_worst = (distance, fraction);
}
distances.push(distance);
}
if interval_cache { next_stencils.push((key, stencil)); }
let (miss, _) = classify_checks(&distances, tolerance)?;
per_interval.push(miss);
located.push((miss, interval_worst.1, low, high));
}
if interval_cache {
if let Some(cache) = cache.as_mut() { cache.stencils = next_stencils; }
}
let (miss, over) = classify_checks(&per_interval, tolerance)?;
if profile_image {
crate::blend::timing::record_ms("timing.track.stencil.image_ms", image_elapsed.as_secs_f64() * 1000.0);
crate::blend::record_count("count.track.stencil.image", image_count);
}
flush_foot_projections("count.track.stencil_feet");
if std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some() {
eprintln!(
"TRACK_FIT stencil: {} intervals x {} points on a {} carrier ({} x {} net) in {:.1} ms",
track.fractions.len() - 1,
LOCAL_STENCIL - 1,
if surface.analytic().is_some() { "analytic" } else { "general" },
surface.control_points.len(),
surface.control_points.first().map_or(0, |row| row.len()),
timer.elapsed_ms()
);
}
Ok((miss, over, worst.map(|(_, at)| at), located))
}
pub(in crate::blend) fn fit_track_locally(
surface: &NurbsSurface,
track: &mut Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
tolerance: f64,
rounds: usize,
max_intervals: usize,
) -> Result<(TrackFit, usize), KernelRefusal> {
let mut cache = CheckCache::default();
let mut fit = fit_track_from_cached(surface, track, at, tolerance, 8, &mut cache)?;
if fit.on_floor {
#[cfg(not(test))]
let trust_midpoints = false;
if trust_midpoints {
return Ok((fit, 0));
}
let (_, over, _, _) = stencil_verdict_cached(surface, track, at, &fit.curve, tolerance, Some(&mut cache))?;
if over.is_empty() {
return Ok((fit, 0));
}
}
let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
let mut inserted = 0;
let mut round = 0;
let mut start = 8usize;
let mut judged: Option<(f64, Vec<usize>, Option<WorstCheck>, Vec<(f64, f64, f64, f64)>)> = None;
let mut previous_miss: Option<f64> = None;
let mut over_located: Vec<(f64, f64, f64, f64)> = Vec::new();
loop {
#[cfg(not(test))]
let midpoint_only = false;
let (miss, over) = if midpoint_only {
(fit.miss, fit.over.clone())
} else {
let (miss, over, worst, located) = match judged.take() {
Some(verdict) => verdict,
None => stencil_verdict_cached(surface, track, at, &fit.curve, tolerance, Some(&mut cache))?,
};
over_located = over.iter().filter_map(|&index| located.get(index).copied()).collect();
if fit.worst.is_some() {
fit.worst = worst;
}
(miss, over)
};
let midpoint_over = std::mem::take(&mut fit.over);
fit.miss = miss;
fit.on_floor = miss <= tolerance && over.is_empty();
fit.over = over;
if debug {
eprintln!(
"TRACK_FIT local round {round}: {} intervals, stencil miss {:.3e} ({} over; midpoint checks {} over), on floor {}",
track.fractions.len() - 1, fit.miss, fit.over.len(), midpoint_over.len(), fit.on_floor
);
}
if fit.on_floor || round >= rounds {
break;
}
let last = track.fractions.len() - 1;
if !midpoint_only && midpoint_over.is_empty() && fit.sections < last {
let climb = (fit.sections * 2).min(last);
#[cfg(not(test))]
let climb_fails = false;
let challenger = if climb_fails {
Err(KernelRefusal::internal(KernelStage::Refine, "climb_control", "blend: control: the climbed rung cannot be read"))
} else {
fit_track_from_cached(surface, track, at, tolerance, climb, &mut cache).and_then(|climbed| {
stencil_verdict_cached(surface, track, at, &climbed.curve, tolerance, Some(&mut cache)).map(|verdict| (climbed, verdict))
})
};
match challenger {
Ok((climbed, verdict)) => {
if debug {
eprintln!(
"TRACK_FIT local round {round}: its midpoints passed at {} sections (stencil {:.3e}); the climb from {climb} reads {:.3e}",
fit.sections, fit.miss, verdict.0
);
}
if verdict.0 < fit.miss {
start = climb;
round += 1;
fit = climbed;
judged = Some(verdict);
continue;
}
}
Err(error) => {
if debug {
eprintln!("TRACK_FIT local round {round}: the climb from {climb} was declined: {error}");
}
}
}
}
let mut targets: Vec<usize> = fit.over.iter().chain(&midpoint_over).copied().collect();
targets.sort_unstable();
targets.dedup();
let before: std::collections::HashSet<u64> = track.fractions.iter().map(|fraction| fraction.to_bits()).collect();
let Some(added) = refine_track_intervals(surface, at, track, &targets, max_intervals)? else {
if debug {
eprintln!("TRACK_FIT local round {round}: declined — it does not fit within {max_intervals} intervals");
}
break;
};
if added == 0 {
break;
}
inserted += added;
let low_order = previous_miss.is_some_and(|previous| previous.is_finite() && miss > previous / 2.5);
previous_miss = Some(miss);
if low_order && !midpoint_only {
let anchors: Vec<(u64, bool, usize)> = over_located
.iter()
.map(|&(interval_miss, worst, low, high)| {
let owed = ((interval_miss / tolerance).log2().ceil().max(1.0) as usize).saturating_sub(1).min(64);
if worst - low < high - worst { (low.to_bits(), false, owed) } else { (high.to_bits(), true, owed) }
})
.collect();
let steps = anchors.iter().map(|anchor| anchor.2).max().unwrap_or(0);
for step in 0..steps {
let mut graded: Vec<usize> = anchors
.iter()
.filter(|anchor| anchor.2 > step)
.filter_map(|&(bits, below, _)| {
let at = track.fractions.iter().position(|known| known.to_bits() == bits)?;
if below { at.checked_sub(1) } else { (at + 1 < track.fractions.len()).then_some(at) }
})
.collect();
graded.sort_unstable();
graded.dedup();
if graded.is_empty() {
break;
}
match refine_track_intervals(surface, at, track, &graded, max_intervals)? {
Some(more) if more > 0 => inserted += more,
_ => break,
}
}
if debug {
eprintln!("TRACK_FIT local round {round}: low-order convergence; graded {steps} bisection(s) toward the nearer endpoints");
}
}
round += 1;
let checks = rung_checks_cached(surface, track, at, Some(&mut cache))?;
let ladder = fit_track_with(surface, track, at, &checks, tolerance, start)?;
if midpoint_only {
fit = ladder;
continue;
}
let ladder_verdict = stencil_verdict_cached(surface, track, at, &ladder.curve, tolerance, Some(&mut cache))?;
let mut kept = fit.kept.clone();
kept.extend(track.fractions.iter().copied().filter(|fraction| !before.contains(&fraction.to_bits())));
#[cfg(not(test))]
let nested_off = false;
#[cfg(not(test))]
let nested_fails = false;
let challenger = if nested_off {
None
} else {
nested_pieces(track, &kept).and_then(|pieces| {
let sections = pieces.iter().map(|piece| piece.len() - 1).sum();
let measured = if nested_fails {
Err(KernelRefusal::internal(KernelStage::Refine, "nested_rung_control", "blend: control: the nested rung cannot be read"))
} else {
measure_rung(surface, track, at, &checks, &pieces, sections, tolerance)
};
let nested = measured.ok()?;
let verdict = stencil_verdict_cached(surface, track, at, &nested.curve, tolerance, Some(&mut cache)).ok()?;
Some((nested, verdict))
})
};
if debug {
eprintln!(
"TRACK_FIT local round {round}: ladder {} sections stencil {:.3e}; nested {:?}",
ladder.sections,
ladder_verdict.0,
challenger.as_ref().map(|(nested, verdict)| (nested.sections, verdict.0))
);
}
match challenger {
Some((nested, verdict)) if verdict.0 < ladder_verdict.0 => {
fit = nested;
judged = Some(verdict);
}
_ => {
fit = ladder;
judged = Some(ladder_verdict);
}
}
}
Ok((fit, inserted))
}
pub(in crate::blend) fn fit_curve_track(
surface: &NurbsSurface,
at: &dyn Fn(f64) -> Result<Vec3, String>,
curve_breaks: &[f64],
tolerance: f64,
dense: usize,
dense_limit: usize,
) -> Result<TrackFit, KernelRefusal> {
let mut dense = dense;
loop {
let track = project_track(surface, at, dense, curve_breaks)?;
let fit = fit_track(surface, &track, at, tolerance)?;
if fit.on_floor || dense * 2 > dense_limit {
return Ok(fit);
}
dense *= 2;
}
}
fn track_tangent(
surface: &NurbsSurface,
track: &Track,
at: &dyn Fn(f64) -> Result<Vec3, String>,
index: usize,
) -> Result<[[f64; 2]; 2], KernelRefusal> {
let fraction = track.fractions[index];
let (left, right) = one_sided_derivatives(at, fraction)?;
let resolve = |foot: [f64; 2], derivative: Vec3| -> Result<[f64; 2], KernelRefusal> {
let (_, su, sv) = surface.deriv1(foot[0], foot[1]).or_refuse(KernelStage::Refine, "deriv1")?;
let (a, b, c) = (su.dot(su), su.dot(sv), sv.dot(sv));
let determinant = a * c - b * b;
if !(determinant.abs() > 1e-300) {
return Err(KernelRefusal::unsupported(
KernelStage::Refine,
"pole_tangent",
"blend: the track tangent is undefined at a knot-line crossing (a pole)",
));
}
let (x, y) = (su.dot(derivative), sv.dot(derivative));
Ok([(c * x - b * y) / determinant, (a * y - b * x) / determinant])
};
let [left_foot, right_foot] = side_feet(surface, track, index)?;
let (left, right) = (resolve(left_foot, left)?, resolve(right_foot, right)?);
let scale = (left[0].hypot(left[1])).max(right[0].hypot(right[1])).max(1e-300);
if (left[0] - right[0]).hypot(left[1] - right[1]) <= 1e-7 * scale {
let mean = [0.5 * (left[0] + right[0]), 0.5 * (left[1] + right[1])];
return Ok([mean, mean]);
}
Ok([left, right])
}
fn side_feet(surface: &NurbsSurface, track: &Track, index: usize) -> Result<[[f64; 2]; 2], KernelRefusal> {
let foot = track.feet[index];
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
let hair = 1e-9 * (u1 - u0).max(v1 - v0);
let here = track.uv[index];
let room = |knots: &[f64], value: f64, component: f64, _span: f64| -> f64 {
if component.abs() <= f64::EPSILON {
return f64::INFINITY;
}
let distinct = 0.1 * hair;
let next = knots
.iter()
.map(|knot| (knot - value) * component.signum())
.filter(|ahead| *ahead > distinct)
.fold(f64::INFINITY, f64::min);
0.5 * next / component.abs()
};
let toward = |neighbours: &mut dyn Iterator<Item = usize>| -> [f64; 2] {
for other in neighbours {
let (du, dv) = (track.uv[other][0] - here[0], track.uv[other][1] - here[1]);
let length = du.hypot(dv);
if length > 1e-14 && length.is_finite() {
let (cu, cv) = (du / length, dv / length);
let step = hair
.min(room(&surface.knots_u, foot[0], cu, u1 - u0))
.min(room(&surface.knots_v, foot[1], cv, v1 - v0));
return [foot[0] + step * cu, foot[1] + step * cv];
}
}
foot
};
Ok([toward(&mut (0..index).rev()), toward(&mut (index + 1..track.uv.len()))])
}
fn one_sided_derivatives(at: &dyn Fn(f64) -> Result<Vec3, String>, fraction: f64) -> Result<(Vec3, Vec3), KernelRefusal> {
let step = 1e-5;
let here = at(fraction).or_refuse(KernelStage::Refine, "at")?;
let left = here
.scale(3.0)
.sub(at(fraction - step).or_refuse(KernelStage::Refine, "at")?.scale(4.0))
.add(at(fraction - 2.0 * step).or_refuse(KernelStage::Refine, "at")?)
.scale(1.0 / (2.0 * step));
let right = at(fraction + step).or_refuse(KernelStage::Refine, "at")?
.scale(4.0)
.sub(here.scale(3.0))
.sub(at(fraction + 2.0 * step).or_refuse(KernelStage::Refine, "at")?)
.scale(1.0 / (2.0 * step));
Ok((left, right))
}
fn fit_in_c1_pieces(
track: &Track,
tangents: &[[[f64; 2]; 2]],
piece_indices: &[Vec<usize>],
) -> Result<(NurbsCurve, usize), KernelRefusal> {
let pieces = piece_indices.len();
let mut knots: Vec<f64> = Vec::new();
let mut control: Vec<Vec4> = Vec::new();
let mut samples = 0usize;
for piece in 0..pieces {
let indices = &piece_indices[piece];
samples += indices.len() - usize::from(piece > 0);
let points: Vec<[f64; 2]> = indices.iter().map(|&i| track.uv[i]).collect();
let parameters: Vec<f64> = indices.iter().map(|&i| track.fractions[i]).collect();
let start = (piece > 0).then(|| tangents[piece - 1][1]);
let end = (piece + 1 < pieces).then(|| tangents[piece][0]);
let fitted = interpolate_with_tangents(&points, ¶meters, start, end)?;
if piece == 0 {
knots.extend(std::iter::repeat(parameters[0]).take(FIT_DEGREE + 1));
}
let interior = &fitted.knots[FIT_DEGREE + 1..fitted.knots.len() - FIT_DEGREE - 1];
knots.extend(interior.iter().copied());
let multiplicity = if piece + 1 == pieces { FIT_DEGREE + 1 } else { FIT_DEGREE };
knots.extend(std::iter::repeat(parameters[parameters.len() - 1]).take(multiplicity));
control.extend(fitted.control_points.iter().skip(usize::from(piece > 0)).copied());
}
Ok((NurbsCurve::new(FIT_DEGREE, knots, control).or_refuse(KernelStage::Refine, "curve_new")?, samples))
}
enum BandOrDense {
Band(crate::fit::CollocationBand, usize, Vec<(usize, usize, f64)>),
Dense(Vec<Vec<f64>>),
}
impl BandOrDense {
fn new(count: usize, degree: usize) -> Self {
BandOrDense::Band(crate::fit::CollocationBand::new(count, degree), count, Vec::new())
}
fn set(&mut self, row: usize, column: usize, value: f64) {
match self {
BandOrDense::Band(band, count, entries) => {
if band.set(row, column, value) {
entries.push((row, column, value));
} else {
let mut dense = vec![vec![0.0; *count]; *count];
for &(r, c, v) in entries.iter() {
dense[r][c] = v;
}
dense[row][column] = value;
*self = BandOrDense::Dense(dense);
}
}
BandOrDense::Dense(dense) => dense[row][column] = value,
}
}
fn solve(&self, rhs: &[f64], degree: usize) -> Result<Vec<f64>, String> {
match self {
BandOrDense::Band(band, _, _) => crate::fit::solve_collocation_banded(band, rhs),
BandOrDense::Dense(dense) => crate::fit::solve_collocation(dense, rhs, degree),
}
}
}
pub(in crate::blend) fn interpolate_track(points: &[Vec4], parameters: &[f64]) -> Result<NurbsCurve, KernelRefusal> {
if points.len() < 2 || parameters.len() != points.len() || parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
return crate::fit::interpolate_homogeneous(points, FIT_DEGREE, parameters).or_refuse(KernelStage::Refine, "interpolate");
}
interpolate_track_band(points, parameters).or_refuse(KernelStage::Refine, "interpolate")
}
fn interpolate_track_band(points: &[Vec4], parameters: &[f64]) -> Result<NurbsCurve, String> {
let n = points.len() - 1;
let degree = FIT_DEGREE.min(n);
let m = n + degree + 1;
let mut knots = vec![0.0; m + 1];
for knot in &mut knots[m - degree..=m] {
*knot = 1.0;
}
for j in 1..=n.saturating_sub(degree) {
knots[j + degree] = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
}
let knot_vector = crate::KnotVector::new(knots.clone(), degree)?;
let mut matrix = BandOrDense::new(n + 1, degree);
for (row, ¶meter) in parameters.iter().enumerate() {
let span = knot_vector.find_span(parameter);
let basis = knot_vector.basis_functions(span, parameter);
for (offset, value) in basis.into_iter().enumerate() {
matrix.set(row, span - degree + offset, value);
}
}
let solve_axis = |axis: fn(&Vec4) -> f64| matrix.solve(&points.iter().map(axis).collect::<Vec<_>>(), degree);
let xs = solve_axis(|point| point.x)?;
let ys = solve_axis(|point| point.y)?;
let zs = solve_axis(|point| point.z)?;
let ws = solve_axis(|point| point.w)?;
NurbsCurve::new(
degree,
knots,
(0..=n).map(|index| Vec4 { x: xs[index], y: ys[index], z: zs[index], w: ws[index] }).collect(),
)
}
fn interpolate_with_tangents(
points: &[[f64; 2]],
parameters: &[f64],
start: Option<[f64; 2]>,
end: Option<[f64; 2]>,
) -> Result<NurbsCurve, KernelRefusal> {
let degree = FIT_DEGREE;
let n = points.len() - 1;
if n < degree || parameters.len() != points.len() {
return Err(KernelRefusal::internal(
KernelStage::Refine,
"piece_short",
"blend: a track piece is too short for the fit degree",
));
}
let (t0, t1) = (parameters[0], parameters[n]);
let mut averaged: Vec<f64> = Vec::with_capacity(n + 3);
if start.is_some() {
averaged.push(t0);
}
averaged.extend_from_slice(parameters);
if end.is_some() {
averaged.push(t1);
}
let controls = averaged.len();
let mut knots = vec![t0; degree + 1];
for j in 1..controls - degree {
knots.push(averaged[j..j + degree].iter().sum::<f64>() / degree as f64);
}
knots.extend(std::iter::repeat(t1).take(degree + 1));
let knot_vector = crate::KnotVector::new(knots.clone(), degree).or_refuse(KernelStage::Refine, "knot_vector")?;
let mut matrix = BandOrDense::new(controls, degree);
let mut rhs: Vec<[f64; 2]> = vec![[0.0; 2]; controls];
let mut row = 0usize;
for (index, ¶meter) in parameters.iter().enumerate() {
if index == 0 {
matrix.set(row, 0, 1.0);
rhs[row] = points[0];
row += 1;
if let Some(tangent) = start {
let scale = degree as f64 / (knots[degree + 1] - t0);
matrix.set(row, 0, -scale);
matrix.set(row, 1, scale);
rhs[row] = tangent;
row += 1;
}
continue;
}
if index == n {
if let Some(tangent) = end {
let scale = degree as f64 / (t1 - knots[controls - 1]);
matrix.set(row, controls - 2, -scale);
matrix.set(row, controls - 1, scale);
rhs[row] = tangent;
row += 1;
}
matrix.set(row, controls - 1, 1.0);
rhs[row] = points[n];
continue;
}
let span = knot_vector.find_span(parameter);
for (offset, value) in knot_vector.basis_functions(span, parameter).into_iter().enumerate() {
matrix.set(row, span - degree + offset, value);
}
rhs[row] = points[index];
row += 1;
}
let solve = |axis: usize| matrix.solve(&rhs.iter().map(|value| value[axis]).collect::<Vec<_>>(), degree);
let (us, vs) = (
solve(0).or_refuse(KernelStage::Refine, "collocation")?,
solve(1).or_refuse(KernelStage::Refine, "collocation")?,
);
NurbsCurve::new(
degree,
knots,
(0..controls)
.map(|index| Vec4::from_point(Vec3::new(us[index], vs[index], 0.0), 1.0))
.collect(),
)
.or_refuse(KernelStage::Refine, "curve_new")
}
fn inserted_at(fractions: &[f64], fraction: f64) -> bool {
fractions.iter().any(|known| (known - fraction).abs() <= 1e-15)
}