use super::*;
pub(in crate::blend) struct CornerContact {
pub(in crate::blend) face_id: u64,
pub(in crate::blend) uv: [f64; 2],
pub(in crate::blend) point: Vec3,
pub(in crate::blend) normal: Vec3,
}
pub(in crate::blend) struct CornerBall {
pub(in crate::blend) center: Vec3,
pub(in crate::blend) contacts: Vec<CornerContact>,
pub(in crate::blend) residual: f64,
}
pub(in crate::blend) struct CornerFace<'a> {
pub(in crate::blend) face: &'a FaceRecord,
pub(in crate::blend) rho: f64,
pub(in crate::blend) seed: [f64; 2],
}
const CORNER_ITERATIONS: usize = 40;
fn centers(faces: &[CornerFace<'_>], x: &[f64]) -> Result<Vec<Vec3>, KernelRefusal> {
faces
.iter()
.enumerate()
.map(|(index, face)| {
blend_offset(&face.face.surface)
.at(x[2 * index], x[2 * index + 1], face.rho)
.map(|sample| sample.point)
.or_refuse(KernelStage::Refine, "offset_at")
})
.collect()
}
fn residual(faces: &[CornerFace<'_>], x: &[f64]) -> Result<Vec<f64>, KernelRefusal> {
let centers = centers(faces, x)?;
let mut rows = Vec::with_capacity(3 * (faces.len() - 1));
for center in ¢ers[1..] {
let delta = center.sub(centers[0]);
rows.extend_from_slice(&[delta.x, delta.y, delta.z]);
}
Ok(rows)
}
fn norm(values: &[f64]) -> f64 {
values.iter().fold(0.0f64, |worst, v| worst.max(v.abs()))
}
pub(in crate::blend) fn solve_corner_ball(
faces: &[CornerFace<'_>],
scale: f64,
) -> Result<CornerBall, KernelRefusal> {
if faces.len() < 3 {
return Err(KernelRefusal::input(
KernelStage::Collect,
"face_count",
format!(
"corner ball: needs at least three faces at the vertex, got {}",
faces.len()
),
));
}
let unknowns = 2 * faces.len();
let equations = 3 * (faces.len() - 1);
let mut x: Vec<f64> = faces
.iter()
.flat_map(|face| [face.seed[0], face.seed[1]])
.collect();
let tolerance = 1e-11 * (1.0 + scale);
let step = 1e-7;
let mut current = residual(faces, &x)?;
for _ in 0..CORNER_ITERATIONS {
if norm(¤t) <= tolerance {
break;
}
let mut jacobian = vec![vec![0.0f64; unknowns]; equations];
for column in 0..unknowns {
let mut probe = x.clone();
probe[column] += step;
let probed = residual(faces, &probe)?;
for row in 0..equations {
jacobian[row][column] = (probed[row] - current[row]) / step;
}
}
let mut normal_matrix = vec![vec![0.0f64; unknowns]; unknowns];
let mut normal_rhs = vec![0.0f64; unknowns];
for row in 0..equations {
for a in 0..unknowns {
normal_rhs[a] += jacobian[row][a] * current[row];
for b in 0..unknowns {
normal_matrix[a][b] += jacobian[row][a] * jacobian[row][b];
}
}
}
let delta = fit::solve_dense(normal_matrix, normal_rhs).map_err(|error| {
KernelRefusal::ill_posed(
KernelStage::Refine,
"singular_jacobian",
format!("corner ball: Gauss-Newton is singular ({error})"),
)
})?;
let mut accepted = false;
let mut damping = 1.0f64;
for _ in 0..8 {
let trial: Vec<f64> = x
.iter()
.zip(&delta)
.map(|(value, correction)| value - damping * correction)
.collect();
if let Ok(trial_residual) = residual(faces, &trial) {
if norm(&trial_residual) < norm(¤t) {
x = trial;
current = trial_residual;
accepted = true;
break;
}
}
damping *= 0.5;
}
if !accepted {
break;
}
}
let final_residual = norm(¤t);
let centers = centers(faces, &x)?;
let center = centers
.iter()
.fold(Vec3::default(), |sum, point| sum.add(*point))
.scale(1.0 / centers.len() as f64);
let mut contacts = Vec::with_capacity(faces.len());
for (index, face) in faces.iter().enumerate() {
let (u, v) = (x[2 * index], x[2 * index + 1]);
let offset = blend_offset(&face.face.surface);
if !offset
.on_real_carrier(u, v, tolerance)
.or_refuse(KernelStage::Refine, "on_real_carrier")?
{
let [u0, u1] = face
.face
.surface
.domain_u()
.or_refuse(KernelStage::Refine, "domain")?;
let [v0, v1] = face
.face
.surface
.domain_v()
.or_refuse(KernelStage::Refine, "domain")?;
trace_corner_ball("REFUSED by on_real_carrier", faces, &x, final_residual, tolerance)?;
return Err(KernelRefusal::unsupported(
KernelStage::Refine,
"carrier_extension",
format!(
"corner ball: the tangency on face {} converged onto a fictitious EXTENSION of \
that carrier — (u={u:.6}, v={v:.6}) against u∈[{u0:.6}, {u1:.6}], \
v∈[{v0:.6}, {v1:.6}], far enough past a curved direction that the evaluator's \
ruled continuation has left the surface. No ball of this radius is tangent to \
the real face here.",
face.face.id
),
));
}
let sample = offset
.at(u, v, face.rho)
.or_refuse(KernelStage::Refine, "offset_at")?;
contacts.push(CornerContact {
face_id: face.face.id,
uv: [u, v],
point: sample.source,
normal: sample.normal,
});
}
trace_corner_ball("ACCEPTED", faces, &x, final_residual, tolerance)?;
Ok(CornerBall {
center,
contacts,
residual: final_residual,
})
}
fn trace_corner_ball(
verdict: &str,
faces: &[CornerFace<'_>],
x: &[f64],
residual: f64,
tolerance: f64,
) -> Result<(), KernelRefusal> {
if std::env::var("BREP_DEBUG_CORNER_BALL").is_err() {
return Ok(());
}
let mut line = format!(
"corner ball {verdict}: n={} residual={residual:.3e} tolerance={tolerance:.3e}",
faces.len()
);
for (index, face) in faces.iter().enumerate() {
let (u, v) = (x[2 * index], x[2 * index + 1]);
let surface = &face.face.surface;
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 out = |value: f64, lo: f64, hi: f64, closed: bool| -> f64 {
if closed {
0.0
} else if value < lo {
value - lo
} else if value > hi {
value - hi
} else {
0.0
}
};
let du = out(u, u0, u1, closed_u);
let dv = out(v, v0, v1, closed_v);
let span = |lo: f64, hi: f64| (hi - lo).abs().max(f64::MIN_POSITIVE);
let faithful = blend_offset(surface)
.on_real_carrier(u, v, tolerance)
.or_refuse(KernelStage::Refine, "on_real_carrier")?;
line.push_str(&format!(
" | face={} u={u:.6} v={v:.6} du=[{u0:.6},{u1:.6}]{} dv=[{v0:.6},{v1:.6}]{} \
out_u={du:.3e} out_v={dv:.3e} out_frac={:.3e} outside={} faithful={faithful}",
face.face.id,
if closed_u { "c" } else { "" },
if closed_v { "c" } else { "" },
(du.abs() / span(u0, u1)).max(dv.abs() / span(v0, v1)),
du != 0.0 || dv != 0.0,
));
}
eprintln!("{line}");
Ok(())
}
pub(in crate::blend) fn corner_faces<'a>(
faces: &[&'a FaceRecord],
rho_of: &dyn Fn(u64) -> Option<f64>,
corner: Vec3,
) -> Result<Vec<CornerFace<'a>>, KernelRefusal> {
let mut prepared = Vec::with_capacity(faces.len());
for face in faces {
let rho = rho_of(face.id).ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"signed_radius",
format!(
"corner ball: no stripe supplies a signed radius for face {}",
face.id
),
)
})?;
let projection = crate::project_point_to_surface(&face.surface, corner)
.or_refuse(KernelStage::Collect, "project")?;
prepared.push(CornerFace {
face,
rho,
seed: [projection.u, projection.v],
});
}
Ok(prepared)
}