use crate::{KernelRefusal, KernelStage, OrRefuse};
use crate::{fit, NurbsCurve, NurbsSurface, Vec3};
pub(super) fn carve_trace(message: std::fmt::Arguments<'_>) {
if std::env::var("BREP_BLEND_CARVE_TRACE").ok().as_deref() == Some("1") {
eprintln!("{message}");
}
}
const RAIL_FRACTION: f64 = 1.0e-3;
const CREASE_TOLERANCE: f64 = 1.0e-12;
const CREASE_SAMPLES: usize = 33;
const MAX_CREASE_SAMPLES: usize = 513;
#[derive(Clone, Copy, Debug)]
pub(super) struct CreaseSample {
pub(super) first: [f64; 2],
pub(super) second: [f64; 2],
pub(super) point: Vec3,
}
pub(super) struct WallCarve {
pub(super) ends: [[f64; 2]; 2],
pub(super) samples: Vec<CreaseSample>,
pub(super) march_residual: f64,
pub(super) centre: [f64; 3],
}
pub(super) struct CarvedCrease {
pub(super) curve: NurbsCurve,
pub(super) first: NurbsCurve,
pub(super) second: NurbsCurve,
pub(super) start: Vec3,
pub(super) end: Vec3,
pub(super) residual: f64,
}
pub(super) fn band_refusal(radius: f64, reason: &str) -> KernelRefusal {
super::fold::wall_fold_refusal(
Vec::new(),
format!(
"{} {radius} fits this edge: the fold band {reason}, which is a runout rather than \
a crease — the wall does not close on itself there",
super::fold::WALL_FOLDS
),
)
}
pub(super) fn section_centre(surface: &NurbsSurface, u: f64) -> Result<Vec3, KernelRefusal> {
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
let a = surface.evaluate(u, v0).or_refuse(KernelStage::Sew, "evaluate")?;
let b = surface.evaluate(u, 0.5 * (v0 + v1)).or_refuse(KernelStage::Sew, "evaluate")?;
let c = surface.evaluate(u, v1).or_refuse(KernelStage::Sew, "evaluate")?;
let first = b.sub(a);
let second = c.sub(a);
let cross = first.cross(second);
let denominator = 2.0 * cross.dot(cross);
if !(denominator > 0.0) || !denominator.is_finite() {
return Err(KernelRefusal::internal(KernelStage::Sew, "straight_section", "blend carve: the section is straight — a chamfer has no ball centre"));
}
let to_centre = second
.cross(cross)
.scale(first.dot(first))
.add(cross.cross(first).scale(second.dot(second)))
.scale(1.0 / denominator);
Ok(a.add(to_centre))
}
pub(super) fn section_orientation(surface: &NurbsSurface, u: f64) -> Result<f64, KernelRefusal> {
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
let a = surface.derivatives(u, v0, 1).or_refuse(KernelStage::Sew, "derivatives")?;
let b = surface.derivatives(u, 0.5 * (v0 + v1), 1).or_refuse(KernelStage::Sew, "derivatives")?;
let c = surface.derivatives(u, v1, 1).or_refuse(KernelStage::Sew, "derivatives")?;
let first = b[0][0].sub(a[0][0]);
let second = c[0][0].sub(a[0][0]);
let first_du = b[1][0].sub(a[1][0]);
let second_du = c[1][0].sub(a[1][0]);
let cross = first.cross(second);
let cross_du = first_du.cross(second).add(first.cross(second_du));
let denominator = 2.0 * cross.dot(cross);
let denominator_du = 4.0 * cross.dot(cross_du);
let numerator = second.cross(cross).scale(first.dot(first))
.add(cross.cross(first).scale(second.dot(second)));
let numerator_du = second_du.cross(cross).scale(first.dot(first))
.add(second.cross(cross_du).scale(first.dot(first)))
.add(second.cross(cross).scale(2.0 * first.dot(first_du)))
.add(cross_du.cross(first).scale(second.dot(second)))
.add(cross.cross(first_du).scale(second.dot(second)))
.add(cross.cross(first).scale(2.0 * second.dot(second_du)));
let travel = a[1][0].add(numerator_du.scale(1.0 / denominator))
.sub(numerator.scale(denominator_du / (denominator * denominator)));
let orientation = travel.dot(cross);
if !(denominator > 0.0) || !denominator.is_finite()
|| !orientation.is_finite() || orientation == 0.0 {
return Err(KernelRefusal::internal(KernelStage::Sew, "section_orientation",
"blend carve: the section frame has no resolved orientation along its centre curve"));
}
Ok(orientation.signum())
}
fn jacobian(surface: &NurbsSurface, u: f64, v: f64) -> Result<f64, KernelRefusal> {
jacobian_about(surface, u, v, section_centre(surface, u)?)
}
pub(super) fn jacobian_about(surface: &NurbsSurface, u: f64, v: f64, centre: Vec3) -> Result<f64, KernelRefusal> {
let derivatives = surface.derivatives(u, v, 1).or_refuse(KernelStage::Sew, "derivatives")?;
let point = derivatives[0][0];
let du = derivatives[1][0];
let dv = derivatives[0][1];
Ok(du.cross(dv).dot(point.sub(centre)))
}
fn jacobian_du(surface: &NurbsSurface, u: f64, v: f64, step: f64) -> Result<f64, KernelRefusal> {
Ok((jacobian(surface, u + step, v)? - jacobian(surface, u - step, v)?) / (2.0 * step))
}
fn swallowtail(
surface: &NurbsSurface,
seed: [f64; 2],
step: f64,
bounds: [f64; 4],
) -> Result<[f64; 2], KernelRefusal> {
let mut point = seed;
for _ in 0..60 {
let residual = [
jacobian(surface, point[0], point[1])?,
jacobian_du(surface, point[0], point[1], step)?,
];
let scale = residual[0].abs().max(residual[1].abs() * step);
if scale < 1e-14 {
break;
}
let mut matrix = [[0.0f64; 2]; 2];
for column in 0..2 {
let mut high = point;
let mut low = point;
high[column] += step;
low[column] -= step;
matrix[0][column] = (jacobian(surface, high[0], high[1])?
- jacobian(surface, low[0], low[1])?)
/ (2.0 * step);
matrix[1][column] = (jacobian_du(surface, high[0], high[1], step)?
- jacobian_du(surface, low[0], low[1], step)?)
/ (2.0 * step);
}
let determinant = matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
if !(determinant.abs() > 0.0) || !determinant.is_finite() {
return Err(KernelRefusal::internal(KernelStage::Sew, "fold_lens_extremum", "blend carve: the fold lens has no isolated extremal-v point"));
}
let delta = [
(-residual[0] * matrix[1][1] + residual[1] * matrix[0][1]) / determinant,
(-matrix[0][0] * residual[1] + matrix[1][0] * residual[0]) / determinant,
];
point = [
(point[0] + delta[0]).clamp(bounds[0], bounds[1]),
(point[1] + delta[1]).clamp(bounds[2], bounds[3]),
];
if delta[0].abs() + delta[1].abs() < 1e-14 {
break;
}
}
if jacobian(surface, point[0], point[1])?.abs() > 1e-6 * scale_of(surface)? {
return Err(KernelRefusal::non_convergence(KernelStage::Sew, "fold_lens_end", "blend carve: the fold lens end did not converge"));
}
Ok(point)
}
fn scale_of(surface: &NurbsSurface) -> Result<f64, KernelRefusal> {
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
let mut worst: f64 = 0.0;
for index in 0..8 {
let u = u0 + (u1 - u0) * (index as f64 + 0.5) / 8.0;
for step in 0..3 {
let v = v0 + (v1 - v0) * step as f64 / 2.0;
worst = worst.max(jacobian(surface, u, v)?.abs());
}
}
Ok(worst.max(1e-30))
}
fn crease_at(
surface: &NurbsSurface,
v: f64,
seed: [f64; 3],
bounds: [f64; 4],
) -> Result<Option<[f64; 3]>, KernelRefusal> {
let mut point = seed;
let step = 1e-7 * (bounds[1] - bounds[0]).max(bounds[3] - bounds[2]);
let residual = |value: [f64; 3]| -> Result<Vec3, KernelRefusal> {
Ok(surface
.evaluate(value[0], v).or_refuse(KernelStage::Sew, "evaluate")?
.sub(surface.evaluate(value[1], value[2]).or_refuse(KernelStage::Sew, "evaluate")?))
};
for _ in 0..80 {
let r = residual(point)?;
if r.length() < CREASE_TOLERANCE {
break;
}
let mut matrix = [[0.0f64; 3]; 3];
for column in 0..3 {
let mut high = point;
let mut low = point;
high[column] += step;
low[column] -= step;
let derivative = residual(high)?.sub(residual(low)?).scale(1.0 / (2.0 * step));
matrix[0][column] = derivative.x;
matrix[1][column] = derivative.y;
matrix[2][column] = derivative.z;
}
let Some(delta) = solve3(matrix, [-r.x, -r.y, -r.z]) else {
return Ok(None);
};
point = [
(point[0] + delta[0]).clamp(bounds[0], bounds[1]),
(point[1] + delta[1]).clamp(bounds[0], bounds[1]),
(point[2] + delta[2]).clamp(bounds[2], bounds[3]),
];
}
if residual(point)?.length() > 1e-9 {
return Ok(None);
}
let span = (bounds[1] - bounds[0]).max(bounds[3] - bounds[2]);
if (point[0] - point[1]).abs() + (v - point[2]).abs() < 1e-7 * span {
return Ok(None);
}
Ok(Some(point))
}
fn solve3(matrix: [[f64; 3]; 3], rhs: [f64; 3]) -> Option<[f64; 3]> {
let out = crate::fit::solve_small(matrix, rhs, 3).ok()?;
out.iter().all(|value| value.is_finite()).then_some(out)
}
pub(super) fn trace_wall_crease(
surface: &NurbsSurface,
radius: f64,
) -> Result<Option<WallCarve>, KernelRefusal> {
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
let bounds = [u0, u1, v0, v1];
let step = 1e-6 * (u1 - u0);
let samples_u = (surface.control_points.len() * 4).max(256);
let samples_v = 96usize;
let mut inside: Vec<[f64; 2]> = Vec::new();
for index in 0..=samples_u {
let u = u0 + (u1 - u0) * index as f64 / samples_u as f64;
let centre = section_centre(surface, u)?;
let orientation = section_orientation(surface, u)?;
for step_v in 0..=samples_v {
let v = v0 + (v1 - v0) * step_v as f64 / samples_v as f64;
if orientation * jacobian_about(surface, u, v, centre)? > 0.0 {
inside.push([u, v]);
}
}
}
if inside.is_empty() {
return Ok(None);
}
if std::env::var("BREP_BLEND_CARVE_TRACE").ok().as_deref() == Some("1") {
let mut occupied = vec![0usize; samples_u + 1];
for point in &inside {
let index = (((point[0] - u0) / (u1 - u0)) * samples_u as f64).round() as usize;
occupied[index.min(samples_u)] += 1;
}
eprintln!("CARVE_OCCUPANCY {}", serde_json::json!({
"radius": radius, "u_domain": [u0, u1], "v_domain": [v0, v1],
"samples_u": samples_u, "samples_v": samples_v,
"closed_directions": surface.closed_directions().ok(),
"occupied_v_counts_by_u": occupied, "inside": inside, "surface": surface,
}));
}
let cell = (u1 - u0) / samples_u as f64;
let (band_low, band_high) = inside.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |acc, point| {
(acc.0.min(point[0]), acc.1.max(point[0]))
});
if band_low <= u0 + cell && band_high >= u1 - cell {
return Err(band_refusal(
radius,
"wraps the wall's whole u period, so it closes into no lens and its crease \
degenerates to isolated points",
));
}
let mut low = inside[0];
let mut high = inside[0];
for point in &inside {
if point[1] < low[1] {
low = *point;
}
if point[1] > high[1] {
high = *point;
}
}
let rail_band = (v1 - v0) * RAIL_FRACTION;
if low[1] - v0 <= rail_band || v1 - high[1] <= rail_band {
return Err(band_refusal(
radius,
&format!(
"reaches the wall's rail (its v extent is [{:.9}, {:.9}] in a domain of \
[{v0:.9}, {v1:.9}])",
low[1], high[1]
),
));
}
let first_end = swallowtail(surface, low, step, bounds)?;
let second_end = swallowtail(surface, high, step, bounds)?;
if !(second_end[1] - first_end[1] > rail_band) {
return Err(band_refusal(
radius,
&format!(
"closes to nothing between v {:.9} and v {:.9}",
first_end[1], second_end[1]
),
));
}
if first_end[1] - v0 <= rail_band || v1 - second_end[1] <= rail_band {
return Err(band_refusal(
radius,
&format!(
"ends on the wall's rail (v {:.9} and v {:.9} in a domain of [{v0:.9}, {v1:.9}])",
first_end[1], second_end[1]
),
));
}
let middle = 0.5 * (first_end[1] + second_end[1]);
let mut span: Option<[f64; 2]> = None;
for point in &inside {
if (point[1] - middle).abs() > (v1 - v0) / samples_v as f64 {
continue;
}
span = Some(match span {
None => [point[0], point[0]],
Some([lo, hi]) => [lo.min(point[0]), hi.max(point[0])],
});
}
let Some([lens_low, lens_high]) = span else {
return Err(KernelRefusal::internal(KernelStage::Sew, "fold_lens_width", "blend carve: the fold lens has no width at its own middle"));
};
let width = lens_high - lens_low;
let seed = [lens_low - width, lens_high + width, middle];
let Some(centre) = crease_at(surface, middle, seed, bounds)? else {
return Err(KernelRefusal::non_convergence(KernelStage::Sew, "crease_middle", "blend carve: the crease did not solve at the lens's own middle"));
};
let samples = march_crease(
surface,
[first_end, second_end],
centre,
bounds,
CREASE_SAMPLES,
)?;
let march_residual = samples
.iter()
.map(|sample| {
surface
.evaluate(sample.first[0], sample.first[1])
.and_then(|a| Ok(a.sub(surface.evaluate(sample.second[0], sample.second[1])?).length()))
.unwrap_or(f64::INFINITY)
})
.fold(0.0f64, f64::max);
Ok(Some(WallCarve {
ends: [first_end, second_end],
samples,
march_residual,
centre,
}))
}
fn march_crease(
surface: &NurbsSurface,
ends: [[f64; 2]; 2],
centre: [f64; 3],
bounds: [f64; 4],
count: usize,
) -> Result<Vec<CreaseSample>, KernelRefusal> {
let (v_low, v_high) = (ends[0][1], ends[1][1]);
let middle = 0.5 * (v_low + v_high);
let half = 0.5 * (v_high - v_low);
let mut forward: Vec<CreaseSample> = Vec::new();
let mut backward: Vec<CreaseSample> = Vec::new();
for direction in [1.0f64, -1.0] {
let mut seed = centre;
for index in 1..count {
let angle = std::f64::consts::PI * index as f64 / (2.0 * (count - 1) as f64);
let v = middle + direction * half * angle.sin();
let Some(solved) = crease_at(surface, v, seed, bounds)? else {
break;
};
seed = solved;
let point = surface.evaluate(solved[0], v).or_refuse(KernelStage::Sew, "evaluate")?;
let sample = CreaseSample {
first: [solved[0], v],
second: [solved[1], solved[2]],
point,
};
if direction > 0.0 {
forward.push(sample);
} else {
backward.push(sample);
}
}
}
let forward_len = forward.len();
let mut samples = Vec::with_capacity(forward.len() + backward.len() + 3);
let end_point = |uv: [f64; 2]| -> Result<CreaseSample, KernelRefusal> {
Ok(CreaseSample {
first: uv,
second: uv,
point: surface.evaluate(uv[0], uv[1]).or_refuse(KernelStage::Sew, "evaluate")?,
})
};
samples.push(end_point(ends[0])?);
backward.reverse();
samples.extend(backward);
samples.push(CreaseSample {
first: [centre[0], 0.5 * (v_low + v_high)],
second: [centre[1], centre[2]],
point: surface.evaluate(centre[0], 0.5 * (v_low + v_high)).or_refuse(KernelStage::Sew, "evaluate")?,
});
samples.extend(forward);
samples.push(end_point(ends[1])?);
carve_trace(format_args!(
"blend carve: crease marched with {} sample(s) of {count} asked ({} below the middle, \
{} above)",
samples.len(),
samples.len().saturating_sub(forward_len + 3),
forward_len
));
for sample in &samples {
carve_trace(format_args!(
" crease uv1 ({:.9}, {:.9}) uv2 ({:.9}, {:.9}) at ({:.9}, {:.9}, {:.9})",
sample.first[0],
sample.first[1],
sample.second[0],
sample.second[1],
sample.point.x,
sample.point.y,
sample.point.z
));
}
Ok(samples)
}
pub(super) fn fit_crease(
surface: &NurbsSurface,
carve: &WallCarve,
tolerance: f64,
) -> Result<CarvedCrease, KernelRefusal> {
let mut samples = carve.samples.clone();
let mut count = CREASE_SAMPLES;
let mut best: Option<(usize, CarvedCrease)> = None;
loop {
let fitted = fit_once(surface, &samples, tolerance)?;
carve_trace(format_args!(
"blend carve: crease fitted through {} sample(s), worst pcurve-vs-curve {:.6e} \
against a bar of {tolerance:.6e} (the march itself closed to {:.3e})",
samples.len(),
fitted.residual,
carve.march_residual
));
if fitted.residual <= tolerance {
return Ok(fitted);
}
if best
.as_ref()
.is_none_or(|(_, previous)| fitted.residual < previous.residual)
{
best = Some((samples.len(), fitted));
}
if count * 2 > MAX_CREASE_SAMPLES {
let (best_count, best_fit) = best.expect("at least one rung was measured");
if best_fit.residual <= tolerance {
return Ok(best_fit);
}
let _ = best_count;
return Err(KernelRefusal::non_convergence(KernelStage::Sew, "crease_fit", format!(
"blend carve: the crease's pcurves stay {:.6e} from the crease curve at \
{} marched sample(s), against a bar of {tolerance:.6e} — the fold band is \
not resolved well enough to trim along",
best_fit.residual,
best_count
)));
}
count = (count - 1) * 2 + 1;
samples = march_crease(
surface,
carve.ends,
carve.centre,
[
surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?[0],
surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?[1],
surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?[0],
surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?[1],
],
count,
)?;
}
}
fn fit_once(
surface: &NurbsSurface,
samples: &[CreaseSample],
_tolerance: f64,
) -> Result<CarvedCrease, KernelRefusal> {
if samples.len() < 4 {
return Err(KernelRefusal::internal(KernelStage::Sew, "crease_points", format!(
"blend carve: a crease of {} point(s) is not a curve",
samples.len()
)));
}
let points: Vec<Vec3> = samples.iter().map(|sample| sample.point).collect();
let mut parameters = Vec::with_capacity(points.len());
let mut running = 0.0;
parameters.push(0.0);
for index in 1..points.len() {
let previous = samples[index - 1];
let current = samples[index];
let first = (current.first[0] - previous.first[0])
.hypot(current.first[1] - previous.first[1]);
let second = (current.second[0] - previous.second[0])
.hypot(current.second[1] - previous.second[1]);
running += first + second;
parameters.push(running);
}
if !(running > 0.0) {
return Err(KernelRefusal::internal(KernelStage::Sew, "crease_length", "blend carve: the crease has no length"));
}
for parameter in parameters.iter_mut() {
*parameter /= running;
}
if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
return Err(KernelRefusal::internal(KernelStage::Sew, "crease_doubles_back", "blend carve: the crease doubles back on itself"));
}
let degree = 3.min(points.len() - 1);
let curve = fit::interpolate_curve(&points, degree, ¶meters).or_refuse(KernelStage::Sew, "interpolate_curve")?;
let first = fit::interpolate_curve(
&samples
.iter()
.map(|sample| Vec3::new(sample.first[0], sample.first[1], 0.0))
.collect::<Vec<_>>(),
degree,
¶meters,
).or_refuse(KernelStage::Sew, "interpolate_curve")?;
let second = fit::interpolate_curve(
&samples
.iter()
.map(|sample| Vec3::new(sample.second[0], sample.second[1], 0.0))
.collect::<Vec<_>>(),
degree,
¶meters,
).or_refuse(KernelStage::Sew, "interpolate_curve")?;
let mut residual: f64 = 0.0;
let dense = (samples.len() * 4).max(64);
for index in 0..=dense {
let t = index as f64 / dense as f64;
let target = curve.evaluate(t).or_refuse(KernelStage::Sew, "evaluate")?;
for pcurve in [&first, &second] {
let uv = pcurve.evaluate(t).or_refuse(KernelStage::Sew, "evaluate")?;
residual = residual.max(surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?.sub(target).length());
}
}
Ok(CarvedCrease {
curve,
first,
second,
start: points[0],
end: points[points.len() - 1],
residual,
})
}