use crate::interpolate_curve;
use crate::{BrepSolid, NurbsCurve, NurbsSurface, Vec3, Vec4};
const MAX_SPANS: usize = 128;
pub(super) fn warp_solid(
solid: &BrepSolid,
map: &dyn Fn(Vec3) -> Vec3,
tol: f64,
) -> Result<BrepSolid, String> {
let mut out = solid.clone();
out.mass_properties_cache = Default::default();
for vertex in &mut out.vertices {
vertex.point = map(vertex.point);
}
for edge in &mut out.edges {
edge.curve = fit_curve(&edge.curve, map, tol)?;
}
for face in out.shells.iter_mut().flat_map(|shell| shell.faces.iter_mut()) {
let (du, dv) = (face.surface.domain_u()?, face.surface.domain_v()?);
let mut lo = [f64::INFINITY; 2];
let mut hi = [f64::NEG_INFINITY; 2];
for coedge in face.loops.iter().flat_map(|l| &l.coedges) {
for p in &coedge.pcurve.control_points {
let q = p.point()?;
lo = [lo[0].min(q.x), lo[1].min(q.y)];
hi = [hi[0].max(q.x), hi[1].max(q.y)];
}
}
let pad = |a: f64, b: f64, range: [f64; 2]| {
let m = 1e-3 * (b - a).max(1e-9);
[(a - m).max(range[0]), (b + m).min(range[1])]
};
let (bu, bv) = if lo[0] <= hi[0] && lo[1] <= hi[1] {
(pad(lo[0], hi[0], du), pad(lo[1], hi[1], dv))
} else {
(du, dv)
};
face.surface = fit_surface(&face.surface, map, tol, bu, bv)?;
}
Ok(out)
}
fn uniform(range: [f64; 2], count: usize) -> Vec<f64> {
(0..count)
.map(|i| range[0] + (range[1] - range[0]) * i as f64 / (count - 1) as f64)
.collect()
}
fn interpolate(points: &[Vec3], params: &[f64]) -> Result<NurbsCurve, String> {
interpolate_curve(points, 3.min(points.len() - 1), params)
}
fn fit_curve(curve: &NurbsCurve, map: &dyn Fn(Vec3) -> Vec3, tol: f64) -> Result<NurbsCurve, String> {
let mut joined: Option<NurbsCurve> = None;
for span in breaks(&curve.knots, curve.domain()?).windows(2) {
let piece = fit_span(curve, map, tol, [span[0], span[1]])?;
joined = Some(match joined {
None => piece,
Some(head) => join(&head, &piece)?,
});
}
joined.ok_or_else(|| "bend relief: an empty edge".to_string())
}
fn join(head: &NurbsCurve, tail: &NurbsCurve) -> Result<NurbsCurve, String> {
let p = head.degree;
let mut knots = head.knots[..head.knots.len() - 1].to_vec();
knots.extend_from_slice(&tail.knots[p + 1..]);
let mut points = head.control_points.clone();
points.extend_from_slice(&tail.control_points[1..]);
NurbsCurve::new(p, knots, points)
}
fn fit_span(curve: &NurbsCurve, map: &dyn Fn(Vec3) -> Vec3, tol: f64, domain: [f64; 2]) -> Result<NurbsCurve, String> {
let target = |t: f64| curve.evaluate(t).map(map);
let mut spans = 4;
loop {
let params = uniform(domain, spans + 3);
let points = params.iter().map(|&t| target(t)).collect::<Result<Vec<_>, _>>()?;
let fitted = interpolate(&points, ¶ms)?;
let mut error: f64 = 0.0;
for pair in params.windows(2) {
let t = 0.5 * (pair[0] + pair[1]);
error = error.max(fitted.evaluate(t)?.sub(target(t)?).length());
}
if error <= tol {
return Ok(fitted);
}
if spans >= MAX_SPANS {
return Err(format!("bend relief: an edge fits to {error:.3e}, not {tol:.1e}"));
}
spans *= 2;
}
}
fn breaks(knots: &[f64], range: [f64; 2]) -> Vec<f64> {
let mut out = vec![range[0]];
for &knot in knots {
if knot > out[out.len() - 1] + 1e-12 && knot < range[1] - 1e-12 {
out.push(knot);
}
}
out.push(range[1]);
out
}
fn interpolate_spans(points: &[Vec<Vec3>], sites: &[Vec<f64>]) -> Result<NurbsCurve, String> {
let mut joined: Option<NurbsCurve> = None;
for (span_points, span_sites) in points.iter().zip(sites) {
let piece = interpolate(span_points, span_sites)?;
joined = Some(match joined {
None => piece,
Some(head) => join(&head, &piece)?,
});
}
joined.ok_or_else(|| "bend relief: an empty wall".to_string())
}
fn fit_surface(
surface: &NurbsSurface,
map: &dyn Fn(Vec3) -> Vec3,
tol: f64,
du: [f64; 2],
dv: [f64; 2],
) -> Result<NurbsSurface, String> {
let target = |u: f64, v: f64| surface.evaluate(u, v).map(map);
let (bu, bv) = (breaks(&surface.knots_u, du), breaks(&surface.knots_v, dv));
let (mut nu, mut nv) = (4, 4);
loop {
let su: Vec<Vec<f64>> = bu.windows(2).map(|w| uniform([w[0], w[1]], nu + 3)).collect();
let sv: Vec<Vec<f64>> = bv.windows(2).map(|w| uniform([w[0], w[1]], nv + 3)).collect();
let mut rows: Vec<Vec<NurbsCurve>> = Vec::with_capacity(sv.len());
for span in &sv {
let mut span_rows = Vec::with_capacity(span.len());
for &v in span {
let points = su
.iter()
.map(|sites| sites.iter().map(|&u| target(u, v)).collect::<Result<Vec<_>, _>>())
.collect::<Result<Vec<_>, _>>()?;
span_rows.push(interpolate_spans(&points, &su)?);
}
rows.push(span_rows);
}
let count_u = rows[0][0].control_points.len();
let mut columns = Vec::with_capacity(count_u);
for i in 0..count_u {
let points = rows
.iter()
.map(|span_rows| span_rows.iter().map(|row| row.control_points[i].point()).collect::<Result<Vec<_>, _>>())
.collect::<Result<Vec<_>, _>>()?;
columns.push(interpolate_spans(&points, &sv)?);
}
let control_points = columns
.iter()
.map(|column| {
column.control_points.iter().map(|p| p.point().map(|q| Vec4::from_point(q, 1.0))).collect()
})
.collect::<Result<Vec<Vec<Vec4>>, String>>()?;
let fitted = NurbsSurface::new(
rows[0][0].degree,
columns[0].degree,
rows[0][0].knots.clone(),
columns[0].knots.clone(),
control_points,
)?;
let mids = |sites: &[Vec<f64>]| -> Vec<f64> {
sites.iter().flat_map(|span| span.windows(2).map(|w| 0.5 * (w[0] + w[1]))).collect()
};
let flat = |sites: &[Vec<f64>]| -> Vec<f64> { sites.iter().flatten().copied().collect() };
let (mu, mv, au, av) = (mids(&su), mids(&sv), flat(&su), flat(&sv));
let (mut along_u, mut along_v, mut center): (f64, f64, f64) = (0.0, 0.0, 0.0);
for &v in &av {
for &u in &mu {
along_u = along_u.max(fitted.evaluate(u, v)?.sub(target(u, v)?).length());
}
}
for &v in &mv {
for &u in &au {
along_v = along_v.max(fitted.evaluate(u, v)?.sub(target(u, v)?).length());
}
for &u in &mu {
center = center.max(fitted.evaluate(u, v)?.sub(target(u, v)?).length());
}
}
if center > tol && along_u <= tol && along_v <= tol {
along_u = center;
along_v = center;
}
if along_u <= tol && along_v <= tol {
return Ok(fitted);
}
if along_u > tol {
if nu >= MAX_SPANS {
return Err(format!("bend relief: a wall fits to {along_u:.3e}, not {tol:.1e}"));
}
nu *= 2;
}
if along_v > tol {
if nv >= MAX_SPANS {
return Err(format!("bend relief: a wall fits to {along_v:.3e}, not {tol:.1e}"));
}
nv *= 2;
}
}
}