use super::*;
struct HealPlan {
primary: [usize; 2],
lateral: [usize; 2],
lateral_point: HashMap<usize, Vec3>,
}
fn plan_heal(planes: &[Plane; 4], f_center: Vec3, f_reach: f64) -> Result<HealPlan, String> {
let mut candidates: Vec<HealPlan> = Vec::new();
for start in 0..2usize {
let primary = [start, start + 2];
let lateral = [(start + 1) % 4, (start + 3) % 4];
let Some(line) = intersect_planes(&planes[primary[0]], &planes[primary[1]]) else {
continue;
};
let mut lateral_point = HashMap::default();
let mut ok = true;
for &lat in &lateral {
match intersect_line_plane(&line, &planes[lat]) {
Some(point) if point.sub(f_center).length() <= f_reach => {
lateral_point.insert(lat, point);
}
_ => {
ok = false;
break;
}
}
}
if !ok {
continue;
}
candidates.push(HealPlan {
primary,
lateral,
lateral_point,
});
}
match candidates.len() {
1 => Ok(candidates.pop().unwrap()),
0 => Err(
"delete_face_and_heal: the neighbours do not re-intersect cleanly \
(parallel planes, non-adjacent healing, or a multi-face gap) — refusing \
rather than emitting an invalid solid"
.into(),
),
_ => Err(
"delete_face_and_heal: healing is ambiguous — both opposite neighbour \
pairs re-intersect through the deleted face"
.into(),
),
}
}
pub(super) use crate::offset_retrim::retrim_planar_face;
pub(super) fn locate_coedge(face: &FaceRecord, edge_id: u64) -> Option<(usize, usize)> {
for (loop_index, loop_record) in face.loops.iter().enumerate() {
for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
if coedge.edge_id == edge_id {
return Some((loop_index, coedge_index));
}
}
}
None
}
pub(super) fn coedge_from_vertex(coedge: &CoedgeRecord, edges: &HashMap<u64, EdgeRecord>) -> Option<u64> {
let edge = edges.get(&coedge.edge_id)?;
Some(if coedge.forward {
edge.start_vertex_id
} else {
edge.end_vertex_id
})
}
pub(super) fn coedge_to_vertex(coedge: &CoedgeRecord, edges: &HashMap<u64, EdgeRecord>) -> Option<u64> {
let edge = edges.get(&coedge.edge_id)?;
Some(if coedge.forward {
edge.end_vertex_id
} else {
edge.start_vertex_id
})
}
pub fn delete_face_and_heal(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, String> {
let mut solid = solid.clone();
let scale = solid_model_scale(&solid);
let tolerance = (scale * 1e-7).max(1e-9);
let (shell_index, face_index) = find_face(&solid, face_id)
.ok_or_else(|| format!("delete_face_and_heal: no face with id {face_id}"))?;
let boundary: Vec<(u64, bool)> = {
let face = &solid.shells[shell_index].faces[face_index];
if face.loops.len() != 1 {
return Err(format!(
"delete_face_and_heal: face {face_id} has {} loops; only a simple \
single-loop transition face is supported",
face.loops.len()
));
}
face.loops[0]
.coedges
.iter()
.map(|coedge| (coedge.edge_id, coedge.forward))
.collect()
};
if boundary.len() != 4 {
return Err(format!(
"delete_face_and_heal: face {face_id} has {} boundary edges; only 4-sided \
transition faces (a single chamfer/fillet edge) are supported \
(deferred: multi-face gaps)",
boundary.len()
));
}
let boundary_edge_ids: HashSet<u64> = boundary.iter().map(|(edge_id, _)| *edge_id).collect();
if boundary_edge_ids.len() == 3 {
return heal_closed_transition(&solid, shell_index, face_index, &boundary);
}
if boundary_edge_ids.len() != 4 {
return Err(
"delete_face_and_heal: transition face uses an edge more than once \
(deferred: periodic/closed transition)"
.into(),
);
}
let mut neighbour_ids = [0u64; 4];
for (index, (edge_id, _)) in boundary.iter().enumerate() {
neighbour_ids[index] = other_face_of_edge(&solid, *edge_id, face_id)?;
}
let unique: HashSet<u64> = neighbour_ids.iter().copied().collect();
if unique.len() != 4 {
return Err(
"delete_face_and_heal: the transition face touches a neighbour more \
than once (deferred: periodic/closed transition)"
.into(),
);
}
let neighbour_planes: [Plane; 4] = {
let mut planes: Vec<Option<Plane>> = Vec::with_capacity(4);
for &neighbour_id in &neighbour_ids {
let (ns, nf) = find_face(&solid, neighbour_id)
.ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
planes.push(
plane_of_surface(
&solid.shells[ns].faces[nf].surface,
(scale * 1e-6).max(1e-7),
"delete_face_and_heal",
)
.ok(),
);
}
if planes.iter().any(Option::is_none) {
return heal_open_transition_mixed(
&solid,
shell_index,
face_index,
&boundary,
&neighbour_ids,
);
}
[
planes[0].unwrap(),
planes[1].unwrap(),
planes[2].unwrap(),
planes[3].unwrap(),
]
};
let transition_vertices: Vec<u64> = boundary
.iter()
.map(|(edge_id, forward)| {
let edge = solid
.edges
.iter()
.find(|edge| edge.id == *edge_id)
.ok_or_else(|| format!("delete_face_and_heal: missing edge {edge_id}"))?;
Ok(if *forward {
edge.start_vertex_id
} else {
edge.end_vertex_id
})
})
.collect::<Result<Vec<u64>, String>>()?;
if transition_vertices.iter().collect::<HashSet<_>>().len() != 4 {
return Err(
"delete_face_and_heal: transition face has repeated corner vertices \
(deferred: degenerate transition)"
.into(),
);
}
let mut f_center = Vec3::default();
for &vertex_id in &transition_vertices {
f_center = f_center.add(edge_point(&solid, vertex_id)?);
}
f_center = f_center.scale(0.25);
let mut f_reach = 0.0f64;
for &vertex_id in &transition_vertices {
f_reach = f_reach.max(edge_point(&solid, vertex_id)?.sub(f_center).length());
}
let f_reach = f_reach * 3.0 + tolerance;
let plan = plan_heal(&neighbour_planes, f_center, f_reach)?;
let mut next_id = max_topology_id(&solid) + 1;
let mut alloc = || {
let value = next_id;
next_id += 1;
value
};
let lateral_a = plan.lateral[0];
let lateral_b = plan.lateral[1];
let point_a = plan.lateral_point[&lateral_a];
let point_b = plan.lateral_point[&lateral_b];
if point_a.sub(point_b).length() <= tolerance {
return Err(
"delete_face_and_heal: recovered corners coincide — the neighbours \
do not bound a clean edge"
.into(),
);
}
let vertex_a = alloc();
let vertex_b = alloc();
let mut lateral_vertex: HashMap<usize, u64> = HashMap::default();
lateral_vertex.insert(lateral_a, vertex_a);
lateral_vertex.insert(lateral_b, vertex_b);
let sharp_edge_id = alloc();
let sharp_edge = EdgeRecord {
id: sharp_edge_id,
curve: make_line(point_a, point_b)?,
t0: 0.0,
t1: 1.0,
start_vertex_id: vertex_a,
end_vertex_id: vertex_b,
degenerate: false,
name: None,
};
let lateral_set: HashSet<usize> = plan.lateral.iter().copied().collect();
let mut collapse: HashMap<u64, u64> = HashMap::default();
for index in 0..4usize {
let previous = (index + 3) % 4;
let lateral_index = if lateral_set.contains(&previous) {
previous
} else if lateral_set.contains(&index) {
index
} else {
return Err(
"delete_face_and_heal: transition corner is not flanked by a \
lateral face (unexpected neighbour ordering)"
.into(),
);
};
let target = lateral_vertex[&lateral_index];
collapse.insert(transition_vertices[index], target);
}
let new_vertex_points: HashMap<u64, Vec3> = [(vertex_a, point_a), (vertex_b, point_b)]
.into_iter()
.collect();
for edge in &mut solid.edges {
if boundary_edge_ids.contains(&edge.id) {
continue;
}
let start_target = collapse.get(&edge.start_vertex_id).copied();
let end_target = collapse.get(&edge.end_vertex_id).copied();
if start_target.is_none() && end_target.is_none() {
continue;
}
if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
return Err(
"delete_face_and_heal: a side edge meeting the transition face is \
not a straight line (deferred: curved neighbour edges)"
.into(),
);
}
let mut start_point = edge.curve.control_points[0].point()?;
let mut end_point = edge.curve.control_points[1].point()?;
if let Some(target) = start_target {
edge.start_vertex_id = target;
start_point = new_vertex_points[&target];
}
if let Some(target) = end_target {
edge.end_vertex_id = target;
end_point = new_vertex_points[&target];
}
if start_point.sub(end_point).length() <= tolerance {
return Err(
"delete_face_and_heal: healing would collapse a side edge to zero \
length (deferred: degenerate transition)"
.into(),
);
}
edge.curve = make_line(start_point, end_point)?;
edge.t0 = 0.0;
edge.t1 = 1.0;
}
let mut edges_by_id: HashMap<u64, EdgeRecord> = solid
.edges
.iter()
.map(|edge| (edge.id, edge.clone()))
.collect();
edges_by_id.insert(sharp_edge_id, sharp_edge.clone());
for &primary_index in &plan.primary {
let primary_edge = boundary[primary_index].0;
let neighbour_id = neighbour_ids[primary_index];
let (ns, nf) = find_face(&solid, neighbour_id)
.ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
let face = &mut solid.shells[ns].faces[nf];
let (loop_index, coedge_index) = locate_coedge(face, primary_edge).ok_or_else(|| {
format!(
"delete_face_and_heal: neighbour {neighbour_id} does not use edge {primary_edge}"
)
})?;
let coedges = &face.loops[loop_index].coedges;
let count = coedges.len();
let previous = &coedges[(coedge_index + count - 1) % count];
let next = &coedges[(coedge_index + 1) % count];
let required_from = coedge_to_vertex(previous, &edges_by_id).ok_or_else(|| {
"delete_face_and_heal: could not resolve loop connectivity".to_string()
})?;
let required_to = coedge_from_vertex(next, &edges_by_id).ok_or_else(|| {
"delete_face_and_heal: could not resolve loop connectivity".to_string()
})?;
let forward = if required_from == vertex_a && required_to == vertex_b {
true
} else if required_from == vertex_b && required_to == vertex_a {
false
} else {
return Err(
"delete_face_and_heal: new edge does not close the primary loop \
(unexpected connectivity)"
.into(),
);
};
let new_coedge = CoedgeRecord {
id: alloc(),
edge_id: sharp_edge_id,
forward,
pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0))?,
};
face.loops[loop_index].coedges[coedge_index] = new_coedge;
}
for &lateral_index in &plan.lateral {
let lateral_edge = boundary[lateral_index].0;
let neighbour_id = neighbour_ids[lateral_index];
let (ns, nf) = find_face(&solid, neighbour_id)
.ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
let face = &mut solid.shells[ns].faces[nf];
let (loop_index, coedge_index) = locate_coedge(face, lateral_edge).ok_or_else(|| {
format!(
"delete_face_and_heal: neighbour {neighbour_id} does not use edge {lateral_edge}"
)
})?;
face.loops[loop_index].coedges.remove(coedge_index);
if face.loops[loop_index].coedges.is_empty() {
return Err("delete_face_and_heal: healing emptied a lateral face loop".into());
}
}
solid.shells[shell_index]
.faces
.retain(|face| face.id != face_id);
solid
.edges
.retain(|edge| !boundary_edge_ids.contains(&edge.id));
let removed_vertices: HashSet<u64> = transition_vertices.iter().copied().collect();
solid.edges.push(sharp_edge);
solid
.vertices
.retain(|vertex| !removed_vertices.contains(&vertex.id));
solid.vertices.push(VertexRecord {
id: vertex_a,
point: point_a,
});
solid.vertices.push(VertexRecord {
id: vertex_b,
point: point_b,
});
let final_edges: HashMap<u64, EdgeRecord> = solid
.edges
.iter()
.map(|edge| (edge.id, edge.clone()))
.collect();
for (index, &neighbour_id) in neighbour_ids.iter().enumerate() {
let plane = neighbour_planes[index];
let (ns, nf) = find_face(&solid, neighbour_id)
.ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
retrim_planar_face(
&mut solid.shells[ns].faces[nf],
&plane,
&final_edges,
scale,
"delete_face_and_heal",
)?;
}
let issues = solid.validate();
if !issues.is_empty() {
return Err(format!(
"delete_face_and_heal: healed solid failed validation: {issues:?}"
));
}
Ok(solid)
}
pub fn resolve_face_by_point(solid: &BrepSolid, point: Vec3) -> Result<u64, String> {
let scale = solid_model_scale(&solid);
let mut best: Option<(u64, f64)> = None;
for shell in &solid.shells {
for face in &shell.faces {
let Ok(projection) = crate::project_point_to_surface(&face.surface, point) else {
continue;
};
if best
.map(|(_, known)| projection.distance < known)
.unwrap_or(true)
{
best = Some((face.id, projection.distance));
}
}
}
match best {
Some((face_id, distance)) if distance <= (scale * 1e-3).max(1e-4) => Ok(face_id),
Some((_, distance)) => Err(format!(
"delete_face_and_heal: no face within tolerance of the point (nearest {distance:.6})"
)),
None => Err("delete_face_and_heal: solid has no faces".into()),
}
}
#[cfg(test)]
mod delete_face_tests {
use super::*;
use crate::{
boolean_operation, chamfer_edge, fillet_edge, make_box_brep, make_cone_brep,
make_cylinder_brep, make_sphere_brep, solid_mass_properties, BooleanOperation,
BooleanOptions,
};
fn closed_rim_at(solid: &BrepSolid, z: f64) -> u64 {
solid
.edges
.iter()
.find(|edge| {
!edge.degenerate
&& edge.start_vertex_id == edge.end_vertex_id
&& edge
.curve
.evaluate(0.5 * (edge.t0 + edge.t1))
.map(|point| (point.z - z).abs() < 1e-6)
.unwrap_or(false)
})
.expect("closed rim")
.id
}
fn named_face(solid: &BrepSolid, name: &str) -> u64 {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.name.as_deref() == Some(name))
.expect("named blend face")
.id
}
fn volume(solid: &BrepSolid) -> f64 {
solid_mass_properties(solid)
.expect("mass properties")
.volume
}
fn round_trip(
sharp: &BrepSolid,
rim: u64,
blend: fn(&BrepSolid, u64, f64, Option<&str>) -> Result<BrepSolid, String>,
size: f64,
) -> BrepSolid {
let faces_before: usize = sharp.shells.iter().map(|shell| shell.faces.len()).sum();
let volume_before = volume(sharp);
let blended = blend(sharp, rim, size, Some("B1")).expect("blend");
assert!(blended.validate().is_empty(), "{:?}", blended.validate());
let strip = named_face(&blended, "B1");
let healed = delete_face_and_heal(&blended, strip).expect("heal");
assert!(
healed.validate().is_empty(),
"healed solid must validate: {:?}",
healed.validate()
);
assert_eq!(
healed
.shells
.iter()
.map(|shell| shell.faces.len())
.sum::<usize>(),
faces_before,
"face count must return to the pre-blend count"
);
let volume_after = volume(&healed);
assert!(
(volume_after - volume_before).abs() <= 1e-6 * volume_before.abs(),
"expected the pre-blend volume {volume_before}, got {volume_after}"
);
healed
}
#[test]
fn closed_heal_restores_the_cylinder_after_a_rim_chamfer() {
let cylinder =
make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 4.0, 6.0).unwrap();
let rim = closed_rim_at(&cylinder, 6.0);
let healed = round_trip(&cylinder, rim, chamfer_edge, 1.0);
assert!(healed.edges.iter().any(|edge| {
let Ok(point) = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)) else {
return false;
};
(point.z - 6.0).abs() < 1e-6 && (point.x.hypot(point.y) - 4.0).abs() < 1e-6
}));
}
#[test]
fn closed_heal_restores_the_cone_frustum_after_a_rim_fillet() {
let cone =
make_cone_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 5.0, 2.5, 6.0).unwrap();
let rim = closed_rim_at(&cone, 6.0);
let healed = round_trip(&cone, rim, fillet_edge, 0.8);
assert!(healed.edges.iter().any(|edge| {
let Ok(point) = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)) else {
return false;
};
(point.z - 6.0).abs() < 1e-6 && (point.x.hypot(point.y) - 2.5).abs() < 1e-6
}));
}
fn upper_half_ball(radius: f64) -> BrepSolid {
let sphere = make_sphere_brep(Vec3::default(), radius, Vec3::new(0.0, 0.0, 1.0)).unwrap();
let box_up = make_box_brep(
Vec3::new(-2.0 * radius, -2.0 * radius, 0.0),
4.0 * radius,
4.0 * radius,
2.0 * radius,
)
.unwrap();
let options = BooleanOptions {
merge_coplanar_faces: true,
..BooleanOptions::default()
};
boolean_operation(&sphere, &box_up, BooleanOperation::Intersect, &options).unwrap()
}
#[test]
fn closed_heal_rebinds_a_sphere_neighbour_after_a_rim_fillet() {
let ball = upper_half_ball(5.0);
let rim = closed_rim_at(&ball, 0.0);
let healed = round_trip(&ball, rim, fillet_edge, 0.8);
assert!(healed.edges.iter().any(|edge| {
let Ok(point) = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)) else {
return false;
};
point.z.abs() < 1e-6 && (point.x.hypot(point.y) - 5.0).abs() < 1e-6
}));
for edge in &healed.edges {
if edge.degenerate {
continue;
}
for step in 0..=8 {
let t = edge.t0 + (edge.t1 - edge.t0) * f64::from(step) / 8.0;
let point = edge.curve.evaluate(t).unwrap();
let on_sphere = (point.length() - 5.0).abs() < 1e-6;
let on_base = point.z.abs() < 1e-6 && point.length() <= 5.0 + 1e-6;
assert!(
on_sphere || on_base,
"edge {} left both carriers at t={t}: {point:?}",
edge.id
);
}
}
}
#[test]
fn closed_heal_rebinds_a_sphere_neighbour_after_a_rim_chamfer() {
let ball = upper_half_ball(5.0);
let rim = closed_rim_at(&ball, 0.0);
round_trip(&ball, rim, chamfer_edge, 0.8);
}
#[test]
fn closed_heal_rebinds_a_torus_neighbour_after_a_rim_fillet() {
let torus =
crate::make_torus_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 5.0, 2.0).unwrap();
let box_up = make_box_brep(Vec3::new(-20.0, -20.0, 0.0), 40.0, 40.0, 20.0).unwrap();
let options = BooleanOptions {
merge_coplanar_faces: true,
..BooleanOptions::default()
};
let half =
boolean_operation(&torus, &box_up, BooleanOperation::Intersect, &options).unwrap();
let expected = std::f64::consts::PI.powi(2) * 5.0 * 4.0;
assert!((volume(&half) - expected).abs() <= 1e-6 * expected);
let rim = closed_rim_at(&half, 0.0);
let healed = round_trip(&half, rim, fillet_edge, 0.4);
assert!((volume(&healed) - expected).abs() <= 1e-6 * expected);
}
#[test]
fn refuses_a_face_that_does_not_exist() {
let cube = make_box_brep(Vec3::default(), 1.0, 1.0, 1.0).unwrap();
let error = delete_face_and_heal(&cube, 999_999).unwrap_err();
assert!(
error.contains("no face with id 999999"),
"unexpected refusal: {error}"
);
}
#[test]
fn refuses_a_multi_loop_face_naming_the_loop_count() {
let plate = make_box_brep(Vec3::default(), 20.0, 20.0, 4.0).unwrap();
let drill = make_cylinder_brep(
Vec3::new(10.0, 10.0, -1.0),
Vec3::new(0.0, 0.0, 1.0),
3.0,
6.0,
)
.unwrap();
let drilled = boolean_operation(
&plate,
&drill,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let top = resolve_face_by_point(&drilled, Vec3::new(2.0, 2.0, 4.0)).unwrap();
let error = delete_face_and_heal(&drilled, top).unwrap_err();
assert!(
error.contains("2 loops") && error.contains("single-loop"),
"refusal must name the loop count: {error}"
);
}
#[test]
fn refuses_a_face_that_is_not_four_sided() {
let cube = make_box_brep(Vec3::default(), 10.0, 10.0, 10.0).unwrap();
let profile = [
make_line(Vec3::new(8.0, 0.0, 10.0), Vec3::new(0.0, 8.0, 10.0)).unwrap(),
make_line(Vec3::new(0.0, 8.0, 10.0), Vec3::new(20.0, 20.0, 10.0)).unwrap(),
make_line(Vec3::new(20.0, 20.0, 10.0), Vec3::new(8.0, 0.0, 10.0)).unwrap(),
];
let cutter =
crate::extrude_profile_brep(&profile, Vec3::new(0.0, 0.0, -1.0), 8.0).unwrap();
let cut = boolean_operation(
&cube,
&cutter,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let triangle = cut
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.loops.len() == 1 && face.loops[0].coedges.len() == 3)
.expect("3-sided cut face")
.id;
let error = delete_face_and_heal(&cut, triangle).unwrap_err();
assert!(
error.contains("3 boundary edges") && error.contains("4-sided"),
"refusal must name the side count: {error}"
);
}
#[test]
fn the_shared_reintersect_seam_declines_the_chamfered_cube_primaries() {
let cube = make_box_brep(Vec3::default(), 1.0, 1.0, 1.0).unwrap();
let edge = cube
.edges
.iter()
.find(|edge| {
edge.curve
.evaluate(0.5 * (edge.t0 + edge.t1))
.map(|point| (point.x - 1.0).abs() < 1e-9 && (point.z - 1.0).abs() < 1e-9)
.unwrap_or(false)
})
.expect("top-right cube edge")
.id;
let chamfered = chamfer_edge(&cube, edge, 0.2, Some("C1")).unwrap();
let strip = named_face(&chamfered, "C1");
let top = resolve_face_by_point(&chamfered, Vec3::new(0.4, 0.5, 1.0)).unwrap();
let right = resolve_face_by_point(&chamfered, Vec3::new(1.0, 0.5, 0.4)).unwrap();
assert_ne!(top, strip);
assert_ne!(right, strip);
let surface_of = |id: u64| {
chamfered
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == id)
.map(|face| face.surface.clone())
.expect("face")
};
let mut seeds: Vec<Vec3> = Vec::new();
for edge in &chamfered.edges {
for step in 0..=4 {
let t = edge.t0 + (edge.t1 - edge.t0) * f64::from(step) / 4.0;
if let Ok(point) = edge.curve.evaluate(t) {
seeds.push(point);
}
}
}
let policy = MarchPolicy {
tolerance: 1e-7,
residual_tolerance: 1e-5,
seeds,
};
let found = reintersect_carriers(&surface_of(top), &surface_of(right), &policy)
.expect("the seam answers this pair");
assert!(
matches!(found.lane, RimLane::Marched),
"plane x plane has no closed form, so the answer must come from the \
marched lane; got the analytic one"
);
assert_eq!(found.sections.len(), 1, "one line, traced");
let section = &found.sections[0];
let [s0, s1] = section.curve.domain().unwrap();
let mut worst = 0.0f64;
for step in 0..=16 {
let point = section
.curve
.evaluate(s0 + (s1 - s0) * f64::from(step) / 16.0)
.unwrap();
worst = worst.max((point.x - 1.0).abs().max((point.z - 1.0).abs()));
}
assert!(worst <= 1e-5, "the marched line drifts {worst} off x=1,z=1");
let healed = delete_face_and_heal(&chamfered, strip).unwrap();
assert!(healed.validate().is_empty());
assert!((volume(&healed) - 1.0).abs() < 1e-9);
}
}