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)
}
pub(in crate::blend) struct RailRailPoint {
pub(in crate::blend) point: Vec3,
pub(in crate::blend) uv: [f64; 2],
pub(in crate::blend) centers: [Vec3; 2],
pub(in crate::blend) residual: f64,
}
pub(in crate::blend) struct RailRailStripe<'a> {
pub(in crate::blend) side: &'a FaceRecord,
pub(in crate::blend) rho_shared: f64,
pub(in crate::blend) rho_side: f64,
}
pub(in crate::blend) fn solve_rail_rail_point(
shared: &FaceRecord,
stripes: [&RailRailStripe<'_>; 2],
corner: Vec3,
scale: f64,
) -> Result<RailRailPoint, KernelRefusal> {
let seed = |face: &FaceRecord| -> Result<[f64; 2], KernelRefusal> {
let projection = crate::project_point_to_surface(&face.surface, corner)
.or_refuse(KernelStage::Collect, "project")?;
Ok([projection.u, projection.v])
};
let shared_seed = seed(shared)?;
let mut x: Vec<f64> = vec![shared_seed[0], shared_seed[1]];
for stripe in stripes {
let side_seed = seed(stripe.side)?;
x.extend_from_slice(&side_seed);
}
let offset_shared = blend_offset(&shared.surface);
let offsets_side = [blend_offset(&stripes[0].side.surface), blend_offset(&stripes[1].side.surface)];
let residual = |x: &[f64]| -> Result<Vec<f64>, KernelRefusal> {
let mut rows = Vec::with_capacity(6);
for (index, stripe) in stripes.iter().enumerate() {
let on_shared = offset_shared
.at(x[0], x[1], stripe.rho_shared)
.or_refuse(KernelStage::Refine, "offset_at")?
.point;
let on_side = offsets_side[index]
.at(x[2 + 2 * index], x[3 + 2 * index], stripe.rho_side)
.or_refuse(KernelStage::Refine, "offset_at")?
.point;
let delta = on_shared.sub(on_side);
rows.extend_from_slice(&[delta.x, delta.y, delta.z]);
}
Ok(rows)
};
let tolerance = 1e-11 * (1.0 + scale);
let step = 1e-7;
let mut current = residual(&x)?;
for _ in 0..CORNER_ITERATIONS {
if norm(¤t) <= tolerance {
break;
}
let mut jacobian = vec![vec![0.0f64; 6]; 6];
for column in 0..6 {
let mut probe = x.clone();
probe[column] += step;
let probed = residual(&probe)?;
for row in 0..6 {
jacobian[row][column] = (probed[row] - current[row]) / step;
}
}
let delta = fit::solve_dense(jacobian, current.clone()).map_err(|error| {
KernelRefusal::ill_posed(
KernelStage::Refine,
"singular_jacobian",
format!("rail/rail point: 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(&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);
if final_residual > tolerance {
return Err(KernelRefusal::non_convergence(
KernelStage::Refine,
"rail_rail_point",
format!(
"rail/rail point: the two balls' contacts on face {} do not meet \
(residual {final_residual:.3e} > {tolerance:.3e})",
shared.id
),
));
}
let carriers = [
(&offset_shared, x[0], x[1], shared.id),
(&offsets_side[0], x[2], x[3], stripes[0].side.id),
(&offsets_side[1], x[4], x[5], stripes[1].side.id),
];
for (offset, u, v, face_id) in carriers {
if !offset
.on_real_carrier(u, v, tolerance)
.or_refuse(KernelStage::Refine, "on_real_carrier")?
{
return Err(KernelRefusal::unsupported(
KernelStage::Refine,
"carrier_extension",
format!(
"rail/rail point: the contact on face {face_id} converged onto a fictitious \
EXTENSION of that carrier (u={u:.6}, v={v:.6}); no pair of balls at these \
radii touches the real faces at one point"
),
));
}
}
let sample = offset_shared
.at(x[0], x[1], stripes[0].rho_shared)
.or_refuse(KernelStage::Refine, "offset_at")?;
let second = offset_shared
.at(x[0], x[1], stripes[1].rho_shared)
.or_refuse(KernelStage::Refine, "offset_at")?;
Ok(RailRailPoint {
point: sample.source,
uv: [x[0], x[1]],
centers: [sample.point, second.point],
residual: final_residual,
})
}