use crate::topology::{EdgeRecord, FaceRecord};
use crate::{
build_pcurve_on_surface, build_pcurve_on_surface_range, make_plane, BrepSolid, KnotVector,
NurbsSurface, Vec3,
};
use rustc_hash::FxHashMap as HashMap;
pub(crate) const PARALLEL_EPS: f64 = 1e-9;
#[derive(Clone, Copy)]
pub(crate) struct Plane {
pub(crate) origin: Vec3,
pub(crate) u_dir: Vec3,
pub(crate) v_dir: Vec3,
pub(crate) normal: Vec3,
}
fn surface_domain_mid(surface: &NurbsSurface) -> Result<(f64, f64), String> {
let ku = KnotVector::new(surface.knots_u.clone(), surface.degree_u)?;
let kv = KnotVector::new(surface.knots_v.clone(), surface.degree_v)?;
let [u0, u1] = ku.domain();
let [v0, v1] = kv.domain();
Ok(((u0 + u1) * 0.5, (v0 + v1) * 0.5))
}
pub(crate) fn plane_of_surface(
surface: &NurbsSurface,
tolerance: f64,
op: &str,
) -> Result<Plane, String> {
let (u_mid, v_mid) = surface_domain_mid(surface)?;
let derivatives = surface.derivatives(u_mid, v_mid, 1)?;
let origin = derivatives[0][0];
let du = derivatives[1][0];
let dv = derivatives[0][1];
let raw_normal = du.cross(dv);
if raw_normal.length() <= PARALLEL_EPS {
return Err(format!("{op}: degenerate face surface"));
}
let normal = raw_normal.normalized()?;
for row in &surface.control_points {
for control in row {
let point = control.point()?;
if point.sub(origin).dot(normal).abs() > tolerance {
return Err(format!(
"{op}: face is not planar (curved neighbours are deferred in this slice)"
));
}
}
}
let u_dir = du.normalized()?;
let v_dir = normal.cross(u_dir).normalized()?;
Ok(Plane {
origin,
u_dir,
v_dir,
normal,
})
}
pub(crate) fn boundary_samples(
face: &FaceRecord,
edges: &HashMap<u64, EdgeRecord>,
op: &str,
) -> Result<Vec<Vec3>, String> {
let mut points: Vec<Vec3> = Vec::new();
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
let edge = edges
.get(&coedge.edge_id)
.ok_or_else(|| format!("{op}: missing edge {}", coedge.edge_id))?;
for step in 0..=4 {
let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
points.push(edge.curve.evaluate(t)?);
}
}
}
Ok(points)
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum PcurveFit {
WholeCurve,
SubrangeAware {
tolerance: f64,
},
}
pub(crate) fn rebuild_loop_pcurves(
face: &mut FaceRecord,
edges: &HashMap<u64, EdgeRecord>,
surface: &NurbsSurface,
fit: PcurveFit,
op: &str,
) -> Result<(), String> {
for loop_record in &mut face.loops {
for coedge in &mut loop_record.coedges {
let edge = edges
.get(&coedge.edge_id)
.ok_or_else(|| format!("{op}: missing edge {}", coedge.edge_id))?;
let subrange_tolerance = match fit {
PcurveFit::WholeCurve => None,
PcurveFit::SubrangeAware { tolerance } => {
let [d0, d1] = edge.curve.domain()?;
let span = (d1 - d0).max(1e-12);
let is_subrange =
(edge.t0 - d0).abs() > 1e-9 * span || (edge.t1 - d1).abs() > 1e-9 * span;
is_subrange.then_some(tolerance)
}
};
coedge.pcurve = if let Some(tolerance) = subrange_tolerance {
build_pcurve_on_surface_range(
surface,
&edge.curve,
edge.t0,
edge.t1,
coedge.forward,
tolerance,
)?
} else {
let mut pcurve = build_pcurve_on_surface(surface, &edge.curve)?;
if !coedge.forward {
pcurve = pcurve.reversed()?;
}
pcurve
};
}
}
Ok(())
}
pub(crate) fn retrim_planar_face(
face: &mut FaceRecord,
plane: &Plane,
edges: &HashMap<u64, EdgeRecord>,
scale: f64,
op: &str,
) -> Result<(), String> {
let mut u_min = f64::INFINITY;
let mut u_max = f64::NEG_INFINITY;
let mut v_min = f64::INFINITY;
let mut v_max = f64::NEG_INFINITY;
for point in boundary_samples(face, edges, op)? {
let delta = point.sub(plane.origin);
let u = delta.dot(plane.u_dir);
let v = delta.dot(plane.v_dir);
u_min = u_min.min(u);
u_max = u_max.max(u);
v_min = v_min.min(v);
v_max = v_max.max(v);
}
if !(u_min.is_finite() && u_max.is_finite() && v_min.is_finite() && v_max.is_finite()) {
return Err(format!("{op}: empty face boundary"));
}
let margin = ((u_max - u_min).max(v_max - v_min) * 0.25).max(scale * 1e-3);
let new_origin = plane
.origin
.add(plane.u_dir.scale(u_min - margin))
.add(plane.v_dir.scale(v_min - margin));
let width = (u_max - u_min) + 2.0 * margin;
let height = (v_max - v_min) + 2.0 * margin;
let surface = make_plane(new_origin, plane.u_dir, plane.v_dir, width, height)?;
rebuild_loop_pcurves(face, edges, &surface, PcurveFit::WholeCurve, op)?;
face.surface = surface;
Ok(())
}
pub(crate) fn retrim_face_in_solid<G>(
solid: &mut BrepSolid,
shell: usize,
face_pos: usize,
edges: &HashMap<u64, EdgeRecord>,
grow: G,
fit: PcurveFit,
op: &str,
) -> Result<(), String>
where
G: FnOnce(&mut BrepSolid, &[Vec3]) -> Result<(), String>,
{
let points = boundary_samples(&solid.shells[shell].faces[face_pos], edges, op)?;
grow(solid, &points)?;
let surface = solid.shells[shell].faces[face_pos].surface.clone();
rebuild_loop_pcurves(
&mut solid.shells[shell].faces[face_pos],
edges,
&surface,
fit,
op,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::topology::BrepSolid;
use crate::{make_box_brep, make_cylinder_brep, solid_scale};
fn edge_table(solid: &BrepSolid) -> HashMap<u64, EdgeRecord> {
solid
.edges
.iter()
.map(|edge| (edge.id, edge.clone()))
.collect()
}
fn face_with<'a>(solid: &'a BrepSolid, pick: impl Fn(&FaceRecord) -> bool) -> &'a FaceRecord {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| pick(face))
.expect("a face matching the predicate")
}
#[test]
fn boundary_samples_are_five_per_coedge_over_every_loop() {
let cube = make_box_brep(Vec3::new(0.0, 0.0, 0.0), 2.0, 3.0, 4.0).unwrap();
let edges = edge_table(&cube);
for shell in &cube.shells {
for face in &shell.faces {
let coedges: usize = face.loops.iter().map(|l| l.coedges.len()).sum();
let samples = boundary_samples(face, &edges, "probe").unwrap();
assert_eq!(
samples.len(),
5 * coedges,
"face {} has {coedges} coedges",
face.id
);
}
}
}
#[test]
fn boundary_samples_keep_degenerate_edges() {
let cylinder =
make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 1.0, 2.0).unwrap();
let mut edges = edge_table(&cylinder);
let face = face_with(&cylinder, |face| {
face.loops.iter().map(|l| l.coedges.len()).sum::<usize>() > 1
})
.clone();
let victim = face.loops[0].coedges[0].edge_id;
edges.get_mut(&victim).unwrap().degenerate = true;
let coedges: usize = face.loops.iter().map(|l| l.coedges.len()).sum();
assert_eq!(
boundary_samples(&face, &edges, "probe").unwrap().len(),
5 * coedges,
"a degenerate edge still contributes its five samples"
);
}
#[test]
fn a_missing_edge_refuses_under_the_callers_op_name() {
let cube = make_box_brep(Vec3::new(0.0, 0.0, 0.0), 1.0, 1.0, 1.0).unwrap();
let face = cube.shells[0].faces[0].clone();
let empty: HashMap<u64, EdgeRecord> = HashMap::default();
let error = boundary_samples(&face, &empty, "offset_ruled_face").unwrap_err();
assert!(
error.starts_with("offset_ruled_face: missing edge "),
"got: {error}"
);
}
#[test]
fn the_planar_retrim_outsets_by_a_quarter_of_the_longest_extent() {
let cube = make_box_brep(Vec3::new(0.0, 0.0, 0.0), 2.0, 6.0, 4.0).unwrap();
let edges = edge_table(&cube);
let scale = solid_scale(&cube);
let mut face =
face_with(&cube, |face| face.surface.evaluate(0.5, 0.5).unwrap().z > 3.9).clone();
let plane = plane_of_surface(&face.surface, 1e-9, "probe").unwrap();
retrim_planar_face(&mut face, &plane, &edges, scale, "probe").unwrap();
let mut low = f64::INFINITY;
let mut high = f64::NEG_INFINITY;
let mut narrow = f64::INFINITY;
let mut wide = f64::NEG_INFINITY;
for row in &face.surface.control_points {
for control in row {
let point = control.point().unwrap();
low = low.min(point.y);
high = high.max(point.y);
narrow = narrow.min(point.x);
wide = wide.max(point.x);
}
}
assert!((high - low - 9.0).abs() < 1e-12, "y span {}", high - low);
assert!((wide - narrow - 5.0).abs() < 1e-12, "x span {}", wide - narrow);
}
#[test]
fn the_subrange_lane_diverges_from_whole_curve_only_on_a_partial_edge() {
let cylinder =
make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 2.0, 5.0).unwrap();
let mut edges = edge_table(&cylinder);
let face = face_with(&cylinder, |face| {
matches!(
face.surface.analytic(),
Some(crate::AnalyticSurface::RuledRevolution { .. })
)
})
.clone();
let surface = face.surface.clone();
let mut whole = face.clone();
rebuild_loop_pcurves(&mut whole, &edges, &surface, PcurveFit::WholeCurve, "probe").unwrap();
let mut ranged = face.clone();
rebuild_loop_pcurves(
&mut ranged,
&edges,
&surface,
PcurveFit::SubrangeAware { tolerance: 1e-7 },
"probe",
)
.unwrap();
for (a, b) in whole.loops.iter().zip(&ranged.loops) {
for (ca, cb) in a.coedges.iter().zip(&b.coedges) {
assert_eq!(
ca.pcurve.control_points.len(),
cb.pcurve.control_points.len(),
"a full-domain rim takes the same whole-curve build in both lanes"
);
}
}
let rim = face.loops[0]
.coedges
.iter()
.map(|coedge| coedge.edge_id)
.find(|id| {
let edge = &edges[id];
!edge.degenerate && edge.curve.control_points.len() > 2
})
.expect("a curved rim");
{
let edge = edges.get_mut(&rim).unwrap();
edge.t1 = edge.t0 + (edge.t1 - edge.t0) * 0.5;
}
let mut clipped_whole = face.clone();
rebuild_loop_pcurves(
&mut clipped_whole,
&edges,
&surface,
PcurveFit::WholeCurve,
"probe",
)
.unwrap();
let mut clipped_ranged = face.clone();
rebuild_loop_pcurves(
&mut clipped_ranged,
&edges,
&surface,
PcurveFit::SubrangeAware { tolerance: 1e-7 },
"probe",
)
.unwrap();
let rim_pcurve = |f: &FaceRecord| {
f.loops
.iter()
.flat_map(|l| &l.coedges)
.find(|c| c.edge_id == rim)
.unwrap()
.pcurve
.clone()
};
let a = rim_pcurve(&clipped_whole);
let b = rim_pcurve(&clipped_ranged);
let ends = a
.evaluate(a.domain().unwrap()[1])
.unwrap()
.sub(b.evaluate(b.domain().unwrap()[1]).unwrap())
.length();
assert!(
ends > 1e-6,
"a half-domain rim must end somewhere else under the two lanes (got {ends})"
);
}
#[test]
fn the_plane_frame_normal_matches_the_surface_normal() {
let cube = make_box_brep(Vec3::new(0.0, 0.0, 0.0), 2.0, 3.0, 4.0).unwrap();
for shell in &cube.shells {
for face in &shell.faces {
let plane = plane_of_surface(&face.surface, 1e-9, "probe").unwrap();
let normal = face.surface.normal(0.5, 0.5).unwrap();
assert!(
plane.normal.dot(normal) > 1.0 - 1e-12,
"face {}: the frame normal must agree with the surface normal",
face.id
);
assert!(
plane.u_dir.cross(plane.v_dir).dot(plane.normal) > 1.0 - 1e-12,
"face {}: the frame is right-handed about its normal",
face.id
);
}
}
}
#[test]
fn a_curved_carrier_is_not_a_plane() {
let cylinder =
make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 2.0, 5.0).unwrap();
let wall = face_with(&cylinder, |face| {
matches!(
face.surface.analytic(),
Some(crate::AnalyticSurface::RuledRevolution { .. })
)
});
let error = plane_of_surface(&wall.surface, 1e-9, "delete_face_and_heal")
.err()
.expect("a cylinder wall must not read as a plane");
assert!(
error.starts_with("delete_face_and_heal: face is not planar"),
"got: {error}"
);
}
}