use crate::{KernelRefusal, KernelStage, OrRefuse, RefusalClass};
use crate::topology::{EdgeRecord, FaceRecord};
use crate::Vec3;
use super::stations::*;
pub(super) struct MiterStripe<'a> {
pub(super) rows: &'a FittedRows,
pub(super) shared_first: bool,
pub(super) at_start: bool,
pub(super) u_ball: f64,
pub(super) other_face: &'a FaceRecord,
}
impl MiterStripe<'_> {
fn z(&self, fraction: f64) -> f64 {
if self.shared_first {
fraction
} else {
1.0 - fraction
}
}
fn fraction_of_z(&self, z: f64) -> f64 {
if self.shared_first {
z
} else {
1.0 - z
}
}
fn u_end(&self) -> f64 {
if self.at_start {
self.rows.u_domain[0]
} else {
self.rows.u_domain[1]
}
}
fn u_vertex(&self) -> f64 {
match self.rows.vertex_stations {
Some(stations) => stations[usize::from(!self.at_start)],
None => self.u_end(),
}
}
fn u_limit(&self) -> f64 {
if self.u_vertex() >= self.u_ball {
self.rows.u_domain[1]
} else {
self.rows.u_domain[0]
}
}
fn point(&self, u: f64, fraction: f64) -> Result<Vec3, KernelRefusal> {
self.rows.surface.evaluate_extended(u, self.z(fraction)).or_refuse(KernelStage::Refine, "evaluate_extended")
}
fn center(&self) -> Result<&crate::NurbsCurve, KernelRefusal> {
self.rows
.center
.as_ref()
.ok_or_else(|| "miter: stripe rows carry no centre path".to_string()).or_refuse(KernelStage::Refine, "ok_or_else")
}
}
pub(super) struct MarchedCurve {
pub(super) points: Vec<Vec3>,
pub(super) uv_first: Vec<[f64; 2]>,
pub(super) uv_second: Vec<[f64; 2]>,
}
pub(super) type MarchedFit = (crate::NurbsCurve, crate::NurbsCurve, crate::NurbsCurve);
pub(super) enum MiterClosure {
Symmetric {
seam: MarchedCurve,
seam_fit: MarchedFit,
exit: [f64; 2],
sharp_parameter: f64,
},
Asymmetric {
seam: MarchedCurve,
seam_fit: MarchedFit,
leader: usize,
exit_leader: f64,
connector_fit: MarchedFit,
exit_follower: f64,
sharp_parameter: f64,
},
}
fn off_surface(surface: &crate::NurbsSurface, point: Vec3) -> Result<f64, KernelRefusal> {
let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let normal = raw_normal(surface, foot.u, foot.v)?;
Ok(point.sub(foot.point).dot(normal).abs())
}
fn bevel_level(stripe: &MiterStripe<'_>, point: Vec3, scale: f64) -> Result<f64, KernelRefusal> {
let projection = crate::project_point_to_surface(&stripe.rows.surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let normal = raw_normal(&stripe.rows.surface, projection.u, projection.v)?;
let center = stripe.center()?.evaluate(projection.u).or_refuse(KernelStage::Refine, "evaluate")?;
let seated = center.sub(projection.point).dot(normal);
if seated.abs() <= 1e-9 * (1.0 + scale) {
return Err(
KernelRefusal::unsupported(KernelStage::Refine, "miter_chord_seat", "miter: the chamfer's rolling ball sits on its own chord โ the supports meet too \
sharply to sign the seam's level"),
);
}
Ok(point.sub(projection.point).dot(normal) * -seated.signum())
}
fn bisect(
mut low: f64,
mut high: f64,
mut f_low: f64,
f: &mut dyn FnMut(f64) -> Result<f64, KernelRefusal>,
bar: f64,
) -> Result<f64, KernelRefusal> {
for _ in 0..80 {
let mid = (low + high) * 0.5;
if (high - low).abs() <= bar {
return Ok(mid);
}
let f_mid = f(mid)?;
if (f_mid > 0.0) == (f_low > 0.0) {
low = mid;
f_low = f_mid;
} else {
high = mid;
}
}
Ok((low + high) * 0.5)
}
const SEAM_STEPS: usize = 48;
const SEAM_SAMPLES: usize = 64;
const MARCH_RUNGS: usize = 4;
fn seam_spacing(step: usize, samples: usize) -> f64 {
let s = step as f64 / samples as f64;
1.0 - (1.0 - s) * (1.0 - s)
}
fn fit_on_ladder(
march: &dyn Fn(usize) -> Result<MarchedCurve, KernelRefusal>,
what: &str,
off_carriers: &dyn Fn(Vec3) -> Result<f64, KernelRefusal>,
tolerance: f64,
) -> Result<(MarchedCurve, MarchedFit), KernelRefusal> {
let mut rung = 0;
loop {
let curve = march(rung)?;
match fit_marched(&curve, what, off_carriers, tolerance) {
Ok(fit) => return Ok((curve, fit)),
Err(error) if is_marched_fit_off_carriers(&error) && rung + 1 < MARCH_RUNGS => {
rung += 1;
}
Err(error) => return Err(error),
}
}
}
pub(super) fn solve_miter(
stripes: [&MiterStripe<'_>; 2],
radius: f64,
chamfer: bool,
sharp: &EdgeRecord,
scale: f64,
fit_tolerance: f64,
consumed_band: f64,
) -> Result<MiterClosure, KernelRefusal> {
let [first, second] = stripes;
let bar = 1e-14;
let center_second = second.center()?;
let sibling_level = |point: Vec3| -> Result<f64, KernelRefusal> {
if chamfer {
return bevel_level(second, point, scale);
}
Ok(crate::project_point_to_curve(center_second, point).or_refuse(KernelStage::Refine, "project_point_to_curve")?.distance - radius)
};
let level = |u: f64, fraction: f64| -> Result<f64, KernelRefusal> {
sibling_level(first.point(u, fraction)?)
};
let on_second = |point: Vec3| -> Result<([f64; 2], f64, f64), KernelRefusal> {
let projection = crate::project_point_to_surface(&second.rows.surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
Ok((
[projection.u, projection.v],
second.fraction_of_z(projection.v),
projection.distance,
))
};
let sample_second = |point: Vec3, seed: [f64; 2]| -> Result<[f64; 2], KernelRefusal> {
let projection =
crate::projection::project_point_to_surface_from_seed(&second.rows.surface, point, seed).or_refuse(KernelStage::Refine, "project_point_to_surface_from_seed")?;
Ok([projection.u, projection.v])
};
let past_second = |point: Vec3| -> Result<f64, KernelRefusal> {
let (_, fraction, distance) = on_second(point)?;
Ok(if fraction < 1.0 { fraction - 1.0 } else { distance })
};
let u_start = first.u_ball;
let u_end = first.u_limit();
let span = u_end - u_start;
if !(span.abs() > 1e-9) {
return Err(KernelRefusal::internal(KernelStage::Refine, "miter_vertex_station", "miter: the ball station sits at the vertex end of the rows"));
}
let du = span / SEAM_STEPS as f64;
let mut points = vec![first.point(u_start, 0.0)?];
let mut uv_first = vec![[u_start, first.z(0.0)]];
let mut uv_second = vec![[second.u_ball, second.z(0.0)]];
let station = |u: f64| -> Result<Option<(f64, Vec3)>, KernelRefusal> {
let at_far = level(u, 1.0)?;
if at_far >= 0.0 {
return Ok(None);
}
let at_near = level(u, 0.0)?;
if at_near <= 0.0 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "miter_rail_reentry", "miter: the shared rail re-enters the sibling blend"));
}
let fraction = bisect(0.0, 1.0, at_near, &mut |f| level(u, f), 1e-13)?;
Ok(Some((fraction, first.point(u, fraction)?)))
};
let seam_point = |u: f64| -> Result<Vec3, KernelRefusal> {
Ok(match station(u)? {
Some((_, point)) => point,
None => first.point(u, 1.0)?,
})
};
let mut previous_u = u_start;
let mut exit: Option<(usize, f64)> = None; for step in 1..=SEAM_STEPS + 8 {
let u = u_start + du * step as f64;
if (u - u_end) * span.signum() > du.abs() * 8.0 {
break;
}
let station_here = station(u)?;
let left_first = station_here.is_none();
let left_second = past_second(seam_point(u)?)? >= 0.0;
if let (false, false, Some((fraction, point))) = (left_first, left_second, station_here) {
let uv2 = sample_second(point, uv_second[uv_second.len() - 1])?;
points.push(point);
uv_first.push([u, first.z(fraction)]);
uv_second.push(uv2);
previous_u = u;
continue;
}
let mut candidates: Vec<(usize, f64)> = Vec::new();
if left_first {
let mut predicate =
|candidate: f64| -> Result<f64, KernelRefusal> { level(candidate, 1.0) };
let f_low = predicate(previous_u)?;
candidates.push((0, bisect(previous_u, u, f_low, &mut predicate, bar)?));
}
if left_second {
let mut predicate =
|candidate: f64| -> Result<f64, KernelRefusal> { past_second(seam_point(candidate)?) };
let f_low = predicate(previous_u)?;
candidates.push((1, bisect(previous_u, u, f_low, &mut predicate, bar)?));
}
exit = candidates
.into_iter()
.min_by(|a, b| (a.1 - u_start).abs().total_cmp(&(b.1 - u_start).abs()));
break;
}
let Some((leaves_first, u_exit)) = exit else {
return Err(
KernelRefusal::unsupported(KernelStage::Refine, "miter_radius_too_large", "miter: the seam does not leave either blend within the marched rows โ the \
radius is too large for this corner"),
);
};
let (exit_point, exit_uv_first, exit_uv_second, exit_u_second) = if leaves_first == 0 {
let point = first.point(u_exit, 1.0)?;
let uv2 = sample_second(point, uv_second[uv_second.len() - 1])?;
(point, [u_exit, first.z(1.0)], uv2, uv2[0])
} else {
let (fraction, point) = station(u_exit)?
.ok_or(KernelRefusal::internal(KernelStage::Refine, "miter_exit_seam", "miter: the exit station lost the seam"))?;
let uv2 = sample_second(point, uv_second[uv_second.len() - 1])?;
(point, [u_exit, first.z(fraction)], [uv2[0], second.z(1.0)], uv2[0])
};
let resample = |rung: usize| -> Result<MarchedCurve, KernelRefusal> {
let samples = SEAM_SAMPLES << rung;
let mut points = vec![first.point(u_start, 0.0)?];
let mut uv_first = vec![[u_start, first.z(0.0)]];
let mut uv_second = vec![[second.u_ball, second.z(0.0)]];
for step in 1..samples {
let u = u_start + (u_exit - u_start) * seam_spacing(step, samples);
let Some((fraction, point)) = station(u)? else {
return Err(KernelRefusal::internal(KernelStage::Refine, "miter_seam_resample", "miter: the seam vanished while being resampled"));
};
let uv2 = sample_second(point, *uv_second.last().expect("a seeded sample"))?;
points.push(point);
uv_first.push([u, first.z(fraction)]);
uv_second.push(uv2);
}
points.push(exit_point);
uv_first.push(exit_uv_first);
uv_second.push(exit_uv_second);
Ok(MarchedCurve {
points,
uv_first,
uv_second,
})
};
let seam_off_carriers = |point: Vec3| -> Result<f64, KernelRefusal> {
Ok(off_surface(&first.rows.surface, point)?.max(sibling_level(point)?.abs()))
};
let (seam, seam_fit) = fit_on_ladder(&resample, "seam", &seam_off_carriers, fit_tolerance)?;
check_sharp_extent(sharp, exit_point, consumed_band, radius)?;
let on_sharp = crate::project_point_to_curve(&sharp.curve, exit_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
let symmetric_bar = 1e-6 * (1.0 + radius);
if on_sharp.distance <= symmetric_bar {
return Ok(MiterClosure::Symmetric {
seam,
seam_fit,
exit: [u_exit, exit_u_second],
sharp_parameter: on_sharp.u,
});
}
let (leader, follower) = if leaves_first == 0 {
(first, second)
} else {
(second, first)
};
let (follower_uv_at_exit, follower_fraction_at_exit) = if leaves_first == 0 {
(exit_uv_second, second.fraction_of_z(exit_uv_second[1]))
} else {
(exit_uv_first, first.fraction_of_z(exit_uv_first[1]))
};
let march = |rung: usize| {
march_connector(
follower,
leader.other_face,
follower_uv_at_exit[0],
follower_fraction_at_exit,
exit_point,
scale,
CONNECTOR_STEPS << rung,
)
};
let connector_off_carriers = |point: Vec3| -> Result<f64, KernelRefusal> {
Ok(off_surface(&follower.rows.surface, point)?
.max(off_surface(&leader.other_face.surface, point)?))
};
let (connector, connector_fit) =
fit_on_ladder(&march, "connector", &connector_off_carriers, fit_tolerance)?;
let connector_end = *connector
.points
.last()
.ok_or(KernelRefusal::internal(KernelStage::Refine, "miter_connector_samples", "miter: the connector has no samples"))?;
check_sharp_extent(sharp, connector_end, consumed_band, radius)?;
let on_sharp = crate::project_point_to_curve(&sharp.curve, connector_end).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
if on_sharp.distance > 1e-5 * (1.0 + radius) {
return Err(KernelRefusal::internal(KernelStage::Refine, "miter_connector_end", format!(
"miter: the connector ends {:.3e} off the sharp edge",
on_sharp.distance
)));
}
let exit_follower = connector.uv_first.last().map(|uv| uv[0]).unwrap_or(0.0);
Ok(MiterClosure::Asymmetric {
seam,
seam_fit,
leader: leaves_first,
exit_leader: if leaves_first == 0 { u_exit } else { exit_u_second },
connector_fit,
exit_follower,
sharp_parameter: on_sharp.u,
})
}
fn check_sharp_extent(
sharp: &EdgeRecord,
point: Vec3,
band: f64,
radius: f64,
) -> Result<(), KernelRefusal> {
if sharp.curve.straight_segment(band).is_none() {
return Ok(());
}
let start = sharp.curve.evaluate(sharp.t0).or_refuse(KernelStage::Refine, "evaluate")?;
let end = sharp.curve.evaluate(sharp.t1).or_refuse(KernelStage::Refine, "evaluate")?;
let chord = end.sub(start);
let length = chord.length();
if length <= band {
return Ok(());
}
let direction = chord.scale(1.0 / length);
let offset = point.sub(start);
let along = offset.dot(direction);
if offset.sub(direction.scale(along)).length() > band {
return Ok(()); }
let overrun = (-along).max(along - length);
if overrun > band {
let supported = radius * length / (length + overrun);
return Err(KernelRefusal::unsupported(
KernelStage::Refine,
MITER_SUPPORT_OVERRUN_WHAT,
format!(
"miter: the corner closure runs {overrun:.6} past sharp edge {} \
({length:.6} long) โ the blend has exhausted its support faces and \
needs to roll onto neighbouring faces, which this corner construction \
does not support; reduce the radius: about {supported:.3} closes on \
that edge's far vertex, and anything smaller is the ordinary corner",
sharp.id
),
));
}
Ok(())
}
const MITER_SUPPORT_OVERRUN_WHAT: &str = "miter_support_overrun";
pub(crate) fn is_miter_support_overrun(refusal: &KernelRefusal) -> bool {
matches!(&refusal.class, RefusalClass::UnsupportedGeometry { what } if what == MITER_SUPPORT_OVERRUN_WHAT)
}
const CONNECTOR_STEPS: usize = 24;
fn march_connector(
follower: &MiterStripe<'_>,
face: &FaceRecord,
u0: f64,
fraction0: f64,
start: Vec3,
scale: f64,
steps: usize,
) -> Result<MarchedCurve, KernelRefusal> {
let bar = (1e-11 / (1.0 + scale)).max(1e-15);
let signed_distance = |point: Vec3| -> Result<f64, KernelRefusal> {
let projection = crate::project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let normal = raw_normal(&face.surface, projection.u, projection.v)?;
Ok(point.sub(projection.point).dot(normal))
};
let u_end = follower.u_limit();
let mut points = vec![start];
let mut uv_follower = vec![[u0, follower.z(fraction0)]];
let first_projection = crate::project_point_to_surface(&face.surface, start).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let mut uv_face = vec![[first_projection.u, first_projection.v]];
let mut previous_u = u0;
for step in 1..=steps {
let fraction = fraction0 + (1.0 - fraction0) * step as f64 / steps as f64;
let mut g = |u: f64| -> Result<f64, KernelRefusal> {
signed_distance(follower.point(u, fraction)?)
};
let g_prev = g(previous_u)?;
let reach = (u_end - previous_u).abs().max(1e-9);
let mut found = None;
for direction in [(u_end - previous_u).signum(), -(u_end - previous_u).signum()] {
let mut width = reach * 0.05;
for _ in 0..8 {
let candidate = previous_u + direction * width;
let g_candidate = g(candidate)?;
if (g_candidate > 0.0) != (g_prev > 0.0) {
let (low, high, f_low) = if candidate > previous_u {
(previous_u, candidate, g_prev)
} else {
(candidate, previous_u, g_candidate)
};
found = Some(bisect(low, high, f_low, &mut g, bar)?);
break;
}
width *= 2.0;
}
if found.is_some() {
break;
}
}
let u = match found {
Some(u) => u,
None if g_prev.abs() <= 1e-9 * (1.0 + scale) => previous_u,
None => {
return Err(KernelRefusal::internal(KernelStage::Refine, "miter_connector_face", "miter: the connector left the face without crossing it"))
}
};
let point = follower.point(u, fraction)?;
let seed = *uv_face.last().ok_or(KernelRefusal::internal(KernelStage::Refine, "miter_connector_samples", "miter: the connector has no face sample"))?;
let projection =
crate::projection::project_point_to_surface_from_seed(&face.surface, point, seed).or_refuse(KernelStage::Refine, "project_point_to_surface_from_seed")?;
points.push(point);
uv_follower.push([u, follower.z(fraction)]);
uv_face.push([projection.u, projection.v]);
previous_u = u;
}
Ok(MarchedCurve {
points,
uv_first: uv_follower,
uv_second: uv_face,
})
}
const MARCH_STRAIGHT: f64 = 1e-11;
fn line_deviation(points: &[Vec3], parameters: &[f64]) -> f64 {
let (Some(first), Some(last)) = (points.first(), points.last()) else {
return f64::INFINITY;
};
let travel = last.sub(*first);
points
.iter()
.zip(parameters)
.fold(0.0f64, |worst, (point, parameter)| {
worst.max(point.sub(first.add(travel.scale(*parameter))).length())
})
}
pub(crate) const MARCHED_FIT_OFF_CARRIERS: &str = "miter: a marched curve's fit leaves its carriers";
pub(crate) const MARCHED_FIT_OFF_CARRIERS_WHAT: &str = "marched_fit_off_carriers";
pub(crate) fn is_marched_fit_off_carriers(refusal: &KernelRefusal) -> bool {
matches!(&refusal.class, RefusalClass::NonConvergence { what } if what == MARCHED_FIT_OFF_CARRIERS_WHAT)
}
pub(super) fn fit_marched(
curve: &MarchedCurve,
what: &str,
off_carriers: &dyn Fn(Vec3) -> Result<f64, KernelRefusal>,
tolerance: f64,
) -> Result<(crate::NurbsCurve, crate::NurbsCurve, crate::NurbsCurve), KernelRefusal> {
use crate::Vec4;
let count = curve.points.len();
if count < 2 {
return Err(KernelRefusal::internal(KernelStage::Refine, "marched_samples", "miter: a marched curve needs at least two samples"));
}
let mut parameters = Vec::with_capacity(count);
let mut accumulated = 0.0;
parameters.push(0.0);
for pair in curve.points.windows(2) {
accumulated += pair[1].sub(pair[0]).length();
parameters.push(accumulated);
}
let total = accumulated.max(1e-12);
for parameter in &mut parameters {
*parameter /= total;
}
let degree = FIT_DEGREE.min(count - 1);
let flatten = |samples: &[[f64; 2]]| {
samples
.iter()
.map(|uv| Vec3::new(uv[0], uv[1], 0.0))
.collect::<Vec<_>>()
};
let as_points = |samples: &[Vec3]| {
samples
.iter()
.map(|point| Vec4::from_point(*point, 1.0))
.collect::<Vec<_>>()
};
let first_image = flatten(&curve.uv_first);
let second_image = flatten(&curve.uv_second);
let extent = |points: &[Vec3]| {
let (Some(first), Some(last)) = (points.first(), points.last()) else {
return 0.0;
};
last.sub(*first).length()
};
let straight: Vec<(f64, f64)> = [&curve.points[..], &first_image[..], &second_image[..]]
.into_iter()
.map(|points| {
(
line_deviation(points, ¶meters),
MARCH_STRAIGHT * (1.0 + extent(points)),
)
})
.collect();
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"marched curve: {count} samples, off its own chord 3d {:.3e} / first {:.3e} / \
second {:.3e} (bars {:.3e} / {:.3e} / {:.3e})",
straight[0].0, straight[1].0, straight[2].0,
straight[0].1, straight[1].1, straight[2].1,
);
}
let fitted = if straight.iter().all(|(deviation, bar)| deviation <= bar) {
let line = |points: &[Vec3]| -> Result<crate::NurbsCurve, KernelRefusal> {
crate::make_line(points[0], points[count - 1]).or_refuse(KernelStage::Refine, "make_line")
};
(
line(&curve.points)?,
line(&first_image)?,
line(&second_image)?,
)
} else {
(
crate::fit::interpolate_homogeneous(&as_points(&curve.points), degree, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?,
crate::fit::interpolate_homogeneous(&as_points(&first_image), degree, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?,
crate::fit::interpolate_homogeneous(&as_points(&second_image), degree, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?,
)
};
let worse = |worst: f64, off: f64| !worst.is_nan() && (off.is_nan() || off > worst);
let mut worst = 0.0f64;
let mut worst_span = 0;
for (span, pair) in parameters.windows(2).enumerate() {
for fraction in [0.25, 0.5, 0.75] {
let off = off_carriers(fitted.0.evaluate(pair[0] + (pair[1] - pair[0]) * fraction).or_refuse(KernelStage::Refine, "evaluate")?)?;
if worse(worst, off) {
(worst, worst_span) = (off, span);
}
}
}
let debug = std::env::var("BREP_DEBUG_NETWORK").is_ok();
let mut at_samples = f64::NAN;
if debug || !(worst <= tolerance) {
at_samples = 0.0;
for point in &curve.points {
let off = off_carriers(*point)?;
if worse(at_samples, off) {
at_samples = off;
}
}
}
if debug {
eprintln!(
"marched {what} [{}]: {count} samples, fit off its carriers {worst:.3e} between them \
(span {worst_span}), {at_samples:.3e} at them, tolerance {tolerance:.3e}",
std::thread::current().name().unwrap_or("main"),
);
}
if !(worst <= tolerance) {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, MARCHED_FIT_OFF_CARRIERS_WHAT, format!(
"{MARCHED_FIT_OFF_CARRIERS}: the {what}'s fit stands {worst:.3e} off them between its \
{count} samples (span {worst_span}; the samples themselves {at_samples:.3e}), over the \
fit tolerance {tolerance:.3e}"
)));
}
Ok(fitted)
}