use std::collections::{HashMap, HashSet};
use crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
use crate::topology::BrepSolid;
use crate::{AnalyticSurface, NurbsCurve, Vec3};
pub(crate) const PLANAR_CHART_EDGE_OFF_PLANE: &str =
"blend: a trim leaves its planar carrier's chart and its edge is not in that plane —";
pub(crate) const PLANAR_CHART_WIDEN_UNSOUND: &str =
"blend: a widened planar carrier's trim does not follow its edge —";
const CHART_SAMPLES: usize = 256;
const VERIFY_SAMPLES: usize = 128;
struct Widening {
shell: usize,
face: usize,
origin: Vec3,
eu: Vec3,
ev: Vec3,
span_u: f64,
span_v: f64,
excursion: f64,
}
pub(crate) fn fit_planar_charts_to_trims(
input: &BrepSolid,
result: &mut BrepSolid,
) -> Result<usize, String> {
let band = crate::blend::consumed_band(result);
let existing: HashSet<u64> = input.edges.iter().map(|edge| edge.id).collect();
let curves: HashMap<u64, (&NurbsCurve, f64, f64)> = result
.edges
.iter()
.map(|edge| (edge.id, (&edge.curve, edge.t0, edge.t1)))
.collect();
let mut widenings = Vec::new();
for (shell_index, shell) in result.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
if !matches!(
face.surface.analytic(),
Some(AnalyticSurface::Plane { .. })
) {
continue;
}
let touched = face
.loops
.iter()
.flat_map(|hole| &hole.coedges)
.any(|coedge| !existing.contains(&coedge.edge_id));
if !touched {
continue;
}
let [u0, u1] = face.surface.domain_u()?;
let [v0, v1] = face.surface.domain_v()?;
let origin = face.surface.evaluate(u0, v0)?;
let du = face.surface.evaluate(u1, v0)?.sub(origin);
let dv = face.surface.evaluate(u0, v1)?.sub(origin);
let (length_u, length_v) = (du.length(), dv.length());
let (eu, ev) = (du.normalized()?, dv.normalized()?);
let normal = eu.cross(ev).normalized()?;
let (mut low_u, mut high_u) = (0.0_f64, length_u);
let (mut low_v, mut high_v) = (0.0_f64, length_v);
let mut off_plane = 0.0_f64;
let mut worst_edge = 0_u64;
for coedge in face.loops.iter().flat_map(|hole| &hole.coedges) {
let Some((curve, t0, t1)) = curves.get(&coedge.edge_id) else {
continue;
};
for step in 0..=CHART_SAMPLES {
let t = t0 + (t1 - t0) * step as f64 / CHART_SAMPLES as f64;
let point = curve.evaluate(t)?.sub(origin);
low_u = low_u.min(point.dot(eu));
high_u = high_u.max(point.dot(eu));
low_v = low_v.min(point.dot(ev));
high_v = high_v.max(point.dot(ev));
let height = point.dot(normal).abs();
if height > off_plane {
off_plane = height;
worst_edge = coedge.edge_id;
}
}
}
let excursion = (-low_u)
.max(high_u - length_u)
.max(-low_v)
.max(high_v - length_v);
if excursion <= band {
continue;
}
if off_plane > band {
return Err(format!(
"{PLANAR_CHART_EDGE_OFF_PLANE} face {} edge {worst_edge} is {off_plane:.3e} \
off the plane (band {band:.3e}), and its trim runs {excursion:.3e} outside \
the chart",
face.name.clone().unwrap_or_else(|| format!("{}", face.id)),
));
}
let pad = (1e-6 * (1.0 + length_u.max(length_v))).max(band);
let (low_u, low_v) = (low_u - pad, low_v - pad);
widenings.push(Widening {
shell: shell_index,
face: face_index,
origin: origin.add(eu.scale(low_u)).add(ev.scale(low_v)),
eu,
ev,
span_u: high_u + pad - low_u,
span_v: high_v + pad - low_v,
excursion,
});
}
}
if widenings.is_empty() {
return Ok(0);
}
let curves: HashMap<u64, (NurbsCurve, f64, f64)> = result
.edges
.iter()
.map(|edge| (edge.id, (edge.curve.clone(), edge.t0, edge.t1)))
.collect();
let moved = widenings.len();
for widening in widenings {
let plane = crate::make_plane(
widening.origin,
widening.eu,
widening.ev,
widening.span_u,
widening.span_v,
)?;
let face = &mut result.shells[widening.shell].faces[widening.face];
let name = face
.name
.clone()
.unwrap_or_else(|| format!("{}", face.id));
let before = face.surface.normal(
0.5 * (face.surface.domain_u()?[0] + face.surface.domain_u()?[1]),
0.5 * (face.surface.domain_v()?[0] + face.surface.domain_v()?[1]),
)?;
let after = plane.normal(0.5 * widening.span_u, 0.5 * widening.span_v)?;
if before.dot(after) <= 0.0 {
return Err(format!(
"{PLANAR_CHART_WIDEN_UNSOUND} face {name}'s widened carrier reverses its normal",
));
}
for coedge in face.loops.iter_mut().flat_map(|hole| &mut hole.coedges) {
let Some((curve, t0, t1)) = curves.get(&coedge.edge_id) else {
return Err(format!(
"{PLANAR_CHART_WIDEN_UNSOUND} face {name} carries coedge {} whose edge {} is \
not in the solid",
coedge.id, coedge.edge_id,
));
};
coedge.pcurve = crate::build_pcurve_on_surface_range(
&plane,
curve,
*t0,
*t1,
coedge.forward,
PCURVE_REFINEMENT_TOLERANCE,
)
.map_err(|error| {
format!(
"{PLANAR_CHART_WIDEN_UNSOUND} face {name}'s trim for edge {} does not rebuild \
on the widened chart ({error})",
coedge.edge_id,
)
})?;
let [p0, p1] = coedge.pcurve.domain()?;
let mut miss = 0.0_f64;
for step in 0..=VERIFY_SAMPLES {
let t = p0 + (p1 - p0) * step as f64 / VERIFY_SAMPLES as f64;
let uv = coedge.pcurve.evaluate(t)?;
let point = plane.evaluate(uv.x, uv.y)?;
miss = miss.max(crate::project_point_to_curve(curve, point)?.distance);
}
if miss > PCURVE_REFINEMENT_TOLERANCE {
return Err(format!(
"{PLANAR_CHART_WIDEN_UNSOUND} face {name}'s rebuilt trim for edge {} misses it \
by {miss:.3e} (floor {PCURVE_REFINEMENT_TOLERANCE:.0e}); the chart ran \
{:.3e} short",
coedge.edge_id, widening.excursion,
));
}
}
face.surface = plane;
}
Ok(moved)
}