use super::*;
use crate::RevolutionFrame;
use crate::{KernelRefusal, KernelStage, OrRefuse};
pub(super) fn carrier_road_selected() -> bool {
std::env::var("BREP_FACE_MOVE_ROAD").as_deref() == Ok("carrier")
}
const OP: &str = "move_faces(carrier)";
const RESIDUAL_SLACK: f64 = 10.0;
enum Section {
Straight,
Curve(NurbsCurve),
}
pub(super) fn move_faces_by_carrier(
solid: &BrepSolid,
moved: &HashSet<u64>,
translation: Vec3,
) -> Result<BrepSolid, KernelRefusal> {
let scale = solid_model_scale(solid);
let tolerance = (scale * 1e-7).max(1e-9);
let plane_tolerance = (scale * 1e-6).max(1e-7);
let motion = translate_affine(translation).or_refuse(KernelStage::Collect, "translate_affine")?;
let mut face_lookup: HashMap<u64, (usize, usize)> = HashMap::default();
for (shell_index, shell) in solid.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
face_lookup.entry(face.id).or_insert((shell_index, face_index));
}
}
let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
for shell in &solid.shells {
for face in &shell.faces {
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
faces_of_edge.entry(coedge.edge_id).or_default().push(face.id);
}
}
}
}
for edge in &solid.edges {
let uses = faces_of_edge.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
let expected = if edge.degenerate { 1 } else { 2 };
if uses.len() != expected {
return Err(KernelRefusal::input(KernelStage::Collect, "non_manifold", format!(
"{OP}: edge {} is used {} times (non-manifold input)",
edge.id,
uses.len()
)));
}
}
let (centre, radius) = body_ball(solid)?;
let reach = (radius + translation.length()) * 4.0 + scale;
let mut carriers: HashMap<u64, NurbsSurface> = HashMap::default();
let invariance_tolerance = (scale * 1e-11).max(1e-13);
let mut moves_carrier: HashSet<u64> = HashSet::default();
for shell in &solid.shells {
for face in &shell.faces {
let stretched = unbounded_carrier(&face.surface, centre, reach, plane_tolerance)?;
let carrier = if moved.contains(&face.id)
&& !carrier_invariant_under_motion(
&face.surface,
&stretched,
motion,
invariance_tolerance,
).or_refuse(KernelStage::Classify, "carrier_invariant_under_motion")?
{
moves_carrier.insert(face.id);
transform_surface(&stretched, motion).or_refuse(KernelStage::Classify, "transform_surface")?
} else {
stretched
};
carriers.insert(face.id, carrier);
}
}
let mut vertex_faces: HashMap<u64, HashSet<u64>> = HashMap::default();
for edge in &solid.edges {
if let Some(uses) = faces_of_edge.get(&edge.id) {
for vertex_id in [edge.start_vertex_id, edge.end_vertex_id] {
vertex_faces.entry(vertex_id).or_default().extend(uses.iter().copied());
}
}
}
let mut new_vertex: HashMap<u64, Vec3> = HashMap::default();
for vertex in &solid.vertices {
let Some(adjacent) = vertex_faces.get(&vertex.id) else {
continue;
};
if !adjacent.iter().any(|face_id| moved.contains(face_id)) {
continue;
}
let mut at: Vec<&NurbsSurface> = Vec::with_capacity(adjacent.len());
let mut ids: Vec<u64> = adjacent.iter().copied().collect();
ids.sort_unstable();
for face_id in &ids {
at.push(carriers.get(face_id).ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Refine,
"carrier_lookup",
format!("{OP}: no carrier for face {face_id} at vertex {}", vertex.id),
)
})?);
}
let seed = if ids.iter().any(|face_id| moves_carrier.contains(face_id)) {
vertex.point.add(translation)
} else {
vertex.point
};
if std::env::var("BREP_FACE_MOVE_CARRIER_DEBUG").is_ok() {
eprintln!("CORNER v{} at faces {:?} seed {:?}", vertex.id, ids, seed);
for (face_id, surface) in ids.iter().zip(at.iter()) {
let kind = match surface.analytic() {
Some(AnalyticSurface::Plane { .. }) => "plane",
Some(AnalyticSurface::RuledRevolution { .. }) => "ruled",
Some(AnalyticSurface::Sphere { .. }) => "sphere",
Some(AnalyticSurface::Torus { .. }) => "torus",
Some(AnalyticSurface::Revolution { .. }) => "revolution",
None => "free-form",
};
match crate::project_point_to_surface(surface, seed) {
Ok(projection) => eprintln!(
" face {face_id} [{kind}] foot {:?} dist {:.6e} uv ({:.6},{:.6}) normal {:?}",
projection.point,
projection.point.sub(seed).length(),
projection.u,
projection.v,
surface.normal(projection.u, projection.v),
),
Err(error) => eprintln!(" face {face_id} [{kind}] projection FAILED: {error}"),
}
}
}
let corner = solve_corner(&at, seed, tolerance).map_err(|why| {
let named: Vec<String> = ids
.iter()
.map(|face_id| face_label(solid, &face_lookup, *face_id))
.collect();
let split = if ids.len() > 3 {
" — more than three carriers met there, and a corner like that cannot stay \
ONE corner once any of them moves: the right answer splits it, which is a \
topology change this operation does not make"
} else {
""
};
why.with_message(|why| format!(
"{OP}: the {} carriers meeting at the corner of {} no longer share a point \
after the motion ({why}){split}",
ids.len(),
named.join(", ")
))
})?;
new_vertex.insert(vertex.id, corner);
}
let mut result = solid.clone();
let mut dirty: HashSet<u64> = HashSet::default();
let mut rebuilt: HashMap<u64, (NurbsCurve, f64, f64)> = HashMap::default();
let mut order: Vec<&EdgeRecord> = Vec::new();
for edge in &solid.edges {
let uses = faces_of_edge.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
let moved_uses = uses.iter().filter(|face_id| moves_carrier.contains(*face_id)).count();
let corner_moved = |vertex_id: u64| {
new_vertex
.get(&vertex_id)
.is_some_and(|point| point.sub(point_of(solid, vertex_id)).length() > tolerance)
};
let ends_moved =
corner_moved(edge.start_vertex_id) || corner_moved(edge.end_vertex_id);
if moved_uses == 0 && !ends_moved {
continue;
}
dirty.insert(edge.id);
order.push(edge);
}
for edge in &order {
if edge.start_vertex_id != edge.end_vertex_id || edge.degenerate {
continue;
}
let uses = faces_of_edge.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
let (Some(&first), Some(&second)) = (uses.first(), uses.get(1)) else {
continue;
};
if first == second {
continue;
}
let section = carrier_section(
edge,
carrier_of(&carriers, first)?,
carrier_of(&carriers, second)?,
tolerance,
)?;
let Section::Curve(curve) = section else {
return Err(KernelRefusal::internal(KernelStage::Refine, "closed_rim_straight", format!(
"{OP}: the closed rim {} sections in a straight line, which cannot close — \
refusing",
edge.id
)));
};
let matched = match_marched_rim_direction(curve, edge)?;
let [d0, d1] = matched.domain().or_refuse(KernelStage::Refine, "domain")?;
let anchor = matched.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
new_vertex.insert(edge.start_vertex_id, anchor);
rebuilt.insert(edge.id, (matched, d0, d1));
}
for edge in &order {
if rebuilt.contains_key(&edge.id) {
continue;
}
let uses = faces_of_edge.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
let moved_uses = uses.iter().filter(|face_id| moves_carrier.contains(*face_id)).count();
let old_start = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Refine, "evaluate")?;
let old_end = edge.curve.evaluate(edge.t1).or_refuse(KernelStage::Refine, "evaluate")?;
let start_new = new_vertex
.get(&edge.start_vertex_id)
.copied()
.unwrap_or_else(|| point_of(solid, edge.start_vertex_id));
let end_new = new_vertex
.get(&edge.end_vertex_id)
.copied()
.unwrap_or_else(|| point_of(solid, edge.end_vertex_id));
let first = *uses.first().ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Refine,
"edge_faces",
format!("{OP}: edge {} has no faces", edge.id),
)
})?;
let second = uses.get(1).copied();
let rigid_ends = start_new.sub(old_start.add(translation)).length() <= tolerance
&& end_new.sub(old_end.add(translation)).length() <= tolerance;
if moved_uses == uses.len() && rigid_ends {
rebuilt.insert(
edge.id,
(transform_curve(&edge.curve, motion).or_refuse(KernelStage::Refine, "transform_curve")?, edge.t0, edge.t1),
);
continue;
}
let carrier_pair = match second {
Some(second) if second != first => Some(second),
_ => None,
};
let (curve, t0, t1) = match carrier_pair {
None => {
let carried = if moves_carrier.contains(&first) {
transform_curve(&edge.curve, motion).or_refuse(KernelStage::Refine, "transform_curve")?
} else {
edge.curve.clone()
};
let curve = if carried.straight_segment(tolerance).is_some() {
make_line(start_new, end_new).or_refuse(KernelStage::Refine, "make_line")?
} else {
carried
};
let t0 = parameter_on(&curve, start_new, tolerance, edge.id, "start")?;
let t1 = parameter_on(&curve, end_new, tolerance, edge.id, "end")?;
if (t1 - t0).abs() <= 1e-12 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "seam_collapse", format!(
"{OP}: the corners of the single-carrier edge {} land on the same point \
of it — refusing",
edge.id
)));
}
(curve, t0, t1)
}
Some(second) => rebuild_edge(
edge,
carrier_of(&carriers, first)?,
carrier_of(&carriers, second)?,
start_new,
end_new,
tolerance,
)?,
};
rebuilt.insert(edge.id, (curve, t0, t1));
}
for edge in &solid.edges {
let Some((curve, t0, t1)) = rebuilt.get(&edge.id) else {
continue;
};
if edge.start_vertex_id == edge.end_vertex_id || edge.degenerate {
continue;
}
let old_start = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Refine, "evaluate")?;
let old_end = edge.curve.evaluate(edge.t1).or_refuse(KernelStage::Refine, "evaluate")?;
let new_start = curve.evaluate(*t0).or_refuse(KernelStage::Refine, "evaluate")?;
let new_end = curve.evaluate(*t1).or_refuse(KernelStage::Refine, "evaluate")?;
let old_chord = old_end.sub(old_start);
let new_chord = new_end.sub(new_start);
if new_chord.length() <= tolerance {
return Err(KernelRefusal::unsupported(KernelStage::Validate, "edge_collapse", format!(
"{OP}: the translation collapses edge {} to zero length (a moved face lands \
exactly on its neighbour)",
edge.id
)));
}
if old_chord.length() > tolerance && new_chord.dot(old_chord) < 0.0 {
return Err(KernelRefusal::unsupported(KernelStage::Validate, EDGE_INVERSION, format!(
"{OP}: the translation inverts edge {} (a moved face passes beyond its \
neighbour)",
edge.id
)));
}
}
for edge in &mut result.edges {
if let Some((curve, t0, t1)) = rebuilt.get(&edge.id) {
edge.curve = curve.clone();
edge.t0 = *t0;
edge.t1 = *t1;
}
}
for vertex in &mut result.vertices {
if let Some(point) = new_vertex.get(&vertex.id) {
vertex.point = *point;
}
}
let final_edges: HashMap<u64, EdgeRecord> =
result.edges.iter().map(|edge| (edge.id, edge.clone())).collect();
let mut touched_faces: Vec<(usize, usize)> = Vec::new();
for (shell_index, shell) in result.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
let face_dirty = moves_carrier.contains(&face.id)
|| face.loops.iter().any(|loop_record| {
loop_record.coedges.iter().any(|c| dirty.contains(&c.edge_id))
});
if face_dirty {
touched_faces.push((shell_index, face_index));
}
}
}
for (shell_index, face_index) in touched_faces {
let face = &mut result.shells[shell_index].faces[face_index];
if moves_carrier.contains(&face.id) {
face.surface = transform_surface(&face.surface, motion).or_refuse(KernelStage::Sew, "transform_surface")?;
}
regrow_and_refit_carrier(face, &final_edges, scale, OP)?;
}
let issues = result.validate();
if !issues.is_empty() {
return Err(KernelRefusal::internal(KernelStage::Validate, "validate", format!("{OP}: moved solid failed validation: {issues:?}")));
}
if let (Ok(before), Ok(after)) =
(crate::signed_volume_against(solid, 0.0), crate::signed_volume_against(&result, 0.0))
{
if before * after <= 0.0 {
return Err(KernelRefusal::unsupported(KernelStage::Validate, SOLID_INVERSION, format!(
"{OP}: the translation inverts the solid (signed volume changed sign) — refusing"
)));
}
}
Ok(result)
}
fn carrier_invariant_under_motion(
patch: &NurbsSurface,
carrier: &NurbsSurface,
motion: AffineTransform,
tolerance: f64,
) -> Result<bool, String> {
const STATIONS: usize = 4;
let [u0, u1] = patch.domain_u()?;
let [v0, v1] = patch.domain_v()?;
for i in 0..STATIONS {
for j in 0..STATIONS {
let u = u0 + (u1 - u0) * i as f64 / (STATIONS - 1) as f64;
let v = v0 + (v1 - v0) * j as f64 / (STATIONS - 1) as f64;
let carried = motion.point(patch.evaluate(u, v)?);
let foot = crate::project_point_to_surface(carrier, carried)?;
if foot.point.sub(carried).length() > tolerance {
return Ok(false);
}
}
}
Ok(true)
}
fn face_label(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
face_id: u64,
) -> String {
match face_lookup
.get(&face_id)
.and_then(|&(shell, face)| solid.shells[shell].faces[face].name.as_deref())
{
Some(name) => format!("`{name}` (face {face_id})"),
None => format!("face {face_id}"),
}
}
fn carrier_of<'a>(
carriers: &'a HashMap<u64, NurbsSurface>,
face_id: u64,
) -> Result<&'a NurbsSurface, KernelRefusal> {
carriers.get(&face_id).ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Refine,
"carrier_missing",
format!("{OP}: no carrier for face {face_id}"),
)
})
}
fn point_of(solid: &BrepSolid, vertex_id: u64) -> Vec3 {
solid
.vertices
.iter()
.find(|vertex| vertex.id == vertex_id)
.map(|vertex| vertex.point)
.unwrap_or_default()
}
fn translate_affine(translation: Vec3) -> Result<AffineTransform, String> {
AffineTransform::new([
1.0, 0.0, 0.0, translation.x, 0.0, 1.0, 0.0, translation.y, 0.0, 0.0, 1.0, translation.z, 0.0, 0.0, 0.0, 1.0,
])
}
fn body_ball(solid: &BrepSolid) -> Result<(Vec3, f64), KernelRefusal> {
if solid.vertices.is_empty() {
return Err(KernelRefusal::input(KernelStage::Collect, "empty_solid", format!("{OP}: the body has no vertices")));
}
let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for vertex in &solid.vertices {
low = Vec3::new(
low.x.min(vertex.point.x),
low.y.min(vertex.point.y),
low.z.min(vertex.point.z),
);
high = Vec3::new(
high.x.max(vertex.point.x),
high.y.max(vertex.point.y),
high.z.max(vertex.point.z),
);
}
let centre = low.add(high).scale(0.5);
let radius = high.sub(low).length() * 0.5;
Ok((centre, radius.max(1.0)))
}
fn unbounded_carrier(
surface: &NurbsSurface,
centre: Vec3,
reach: f64,
plane_tolerance: f64,
) -> Result<NurbsSurface, KernelRefusal> {
match surface.analytic() {
Some(AnalyticSurface::Sphere { .. }) | Some(AnalyticSurface::Torus { .. }) => {
return Ok(surface.clone())
}
Some(AnalyticSurface::RuledRevolution {
frame,
rho0,
rho1,
height,
}) => {
let (frame, rho0, rho1, height) = (frame.clone(), *rho0, *rho1, *height);
return stretch_ruled(&frame, rho0, rho1, height, centre, reach);
}
Some(AnalyticSurface::Revolution {
frame,
sweep,
generatrix,
..
}) => {
let sweep = *sweep;
let frame = frame.clone();
if let Some((start, end)) = generatrix.straight_segment(plane_tolerance) {
let (rho0, z0) = axial_radial(&frame, start);
let (rho1, z1) = axial_radial(&frame, end);
let height = z1 - z0;
if height.abs() > 1e-12 {
let base = RevolutionFrame {
origin: frame.origin.add(frame.axis.scale(z0)),
..frame.clone()
};
return stretch_ruled_sweep(&base, rho0, rho1, height, centre, reach, sweep);
}
}
return Ok(surface.clone());
}
_ => {}
}
match plane_of_surface(surface, plane_tolerance, OP) {
Ok(plane) => {
let foot = centre.sub(plane.normal.scale(centre.sub(plane.origin).dot(plane.normal)));
let origin = foot
.sub(plane.u_dir.scale(reach))
.sub(plane.v_dir.scale(reach));
crate::make_plane(origin, plane.u_dir, plane.v_dir, 2.0 * reach, 2.0 * reach)
.or_refuse(KernelStage::Classify, "make_plane")
}
Err(_) => Ok(surface.clone()),
}
}
fn axial_radial(frame: &RevolutionFrame, point: Vec3) -> (f64, f64) {
let delta = point.sub(frame.origin);
let z = delta.dot(frame.axis);
(delta.sub(frame.axis.scale(z)).length(), z)
}
fn stretch_ruled(
frame: &RevolutionFrame,
rho0: f64,
rho1: f64,
height: f64,
centre: Vec3,
reach: f64,
) -> Result<NurbsSurface, KernelRefusal> {
stretch_ruled_sweep(
frame,
rho0,
rho1,
height,
centre,
reach,
std::f64::consts::TAU,
)
}
fn stretch_ruled_sweep(
frame: &RevolutionFrame,
rho0: f64,
rho1: f64,
height: f64,
centre: Vec3,
reach: f64,
sweep: f64,
) -> Result<NurbsSurface, KernelRefusal> {
if height.abs() <= 1e-12 {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "flat_ruled_carrier", format!("{OP}: a ruled carrier with no height cannot be stretched")));
}
let axial = centre.sub(frame.origin).dot(frame.axis);
let low = (axial - reach).min(0.0);
let high = (axial + reach).max(height);
let rho_at = |z: f64| rho0 + (rho1 - rho0) * z / height;
let (low, high) = if (rho1 - rho0).abs() > 1e-12 {
let apex_z = -rho0 * height / (rho1 - rho0);
if apex_z > 0.0 {
(low.max(apex_z), high)
} else {
(low, high.min(apex_z).max(low))
}
} else {
(low, high)
};
let (low, high) = if high - low > 1e-12 * height.abs() {
(low, high)
} else {
(0.0, height)
};
let base = frame.origin.add(frame.axis.scale(low));
let start = base.add(frame.x_axis.scale(rho_at(low)));
let end = frame
.origin
.add(frame.axis.scale(high))
.add(frame.x_axis.scale(rho_at(high)));
let generatrix = make_line(start, end).or_refuse(KernelStage::Classify, "make_line")?;
make_revolution(base, frame.axis, &generatrix, sweep)
.or_refuse(KernelStage::Classify, "make_revolution")
}
fn solve_corner(
at: &[&NurbsSurface],
seed: Vec3,
tolerance: f64,
) -> Result<Vec3, KernelRefusal> {
const DAMPING: f64 = 1e-12;
let mut x = seed;
let mut worst = f64::INFINITY;
for _ in 0..64 {
let mut normal_matrix = [[0.0f64; 3]; 3];
let mut rhs = [0.0f64; 3];
worst = 0.0;
for surface in at {
let projection = crate::project_point_to_surface(surface, x).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let foot = projection.point;
let normal = surface.normal(projection.u, projection.v).or_refuse(KernelStage::Refine, "normal")?;
worst = worst.max(foot.sub(x).length());
let row = [normal.x, normal.y, normal.z];
let value = normal.dot(foot.sub(seed));
for i in 0..3 {
for j in 0..3 {
normal_matrix[i][j] += row[i] * row[j];
}
rhs[i] += row[i] * value;
}
}
if worst <= tolerance {
return Ok(x);
}
let scale3 = (normal_matrix[0][0] + normal_matrix[1][1] + normal_matrix[2][2]) / 3.0;
let full_rank = determinant3(&normal_matrix).abs() > 1e-9 * scale3.powi(3).max(1e-300);
if !full_rank {
for i in 0..3 {
normal_matrix[i][i] += DAMPING * scale3.max(1.0);
}
}
let delta = solve3(&normal_matrix, &rhs)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Refine,
"corner_singular",
"the carriers give no correction",
)
})?;
let next = seed.add(Vec3::new(delta[0], delta[1], delta[2]));
if next.sub(x).length() <= tolerance * 1e-3 {
x = next;
break;
}
x = next;
}
let mut residual = 0.0f64;
for surface in at {
let projection = crate::project_point_to_surface(surface, x).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
residual = residual.max(projection.point.sub(x).length());
}
if residual <= tolerance * RESIDUAL_SLACK {
return Ok(x);
}
let mut spread: f64 = 0.0;
let mut normals: Vec<Vec3> = Vec::with_capacity(at.len());
for surface in at {
let projection = crate::project_point_to_surface(surface, x).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
normals.push(surface.normal(projection.u, projection.v).or_refuse(KernelStage::Refine, "normal")?);
}
for (index, first) in normals.iter().enumerate() {
for second in &normals[index + 1..] {
let sine = first.cross(*second).length();
let cosine = first.dot(*second).abs();
spread = spread.max(sine.atan2(cosine));
}
}
if spread <= 1e-6 {
return Err(KernelRefusal::ill_posed(KernelStage::Refine, "tangent_corner", format!(
"they share a common TANGENT PLANE there (their normals agree to {spread:.1e} rad), \
so together they fix the corner in one direction and pin it in neither of the \
other two; the nearest point to all of them is still {residual:.3e} away"
)));
}
Err(KernelRefusal::unsupported(KernelStage::Refine, "corner_apart", format!(
"the nearest point to all of them is {residual:.3e} off at least one, against a \
tolerance of {:.3e} (their normals there are {spread:.3e} rad apart, so they are \
transverse and it is the MOTION that carried them apart); worst reading while \
iterating {worst:.3e}",
tolerance * RESIDUAL_SLACK
)))
}
fn solve3(matrix: &[[f64; 3]; 3], rhs: &[f64; 3]) -> Option<[f64; 3]> {
let out = crate::fit::solve_small(*matrix, *rhs, 3).ok()?;
out.iter().all(|value| value.is_finite()).then_some(out)
}
fn rebuild_edge(
edge: &EdgeRecord,
first: &NurbsSurface,
second: &NurbsSurface,
start_new: Vec3,
end_new: Vec3,
tolerance: f64,
) -> Result<(NurbsCurve, f64, f64), KernelRefusal> {
let section = carrier_section(edge, first, second, tolerance)?;
match section {
Section::Straight => {
let curve = make_line(start_new, end_new).or_refuse(KernelStage::Refine, "make_line")?;
Ok((curve, 0.0, 1.0))
}
Section::Curve(curve) => {
let start_t = parameter_on(&curve, start_new, tolerance, edge.id, "start")?;
let end_t = parameter_on(&curve, end_new, tolerance, edge.id, "end")?;
if (end_t - start_t).abs() <= 1e-12 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "section_collapse", format!(
"{OP}: the corners of edge {} land on the same point of their section \
— refusing",
edge.id
)));
}
if start_t < end_t {
Ok((curve, start_t, end_t))
} else {
let reversed = curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
let [d0, d1] = reversed.domain().or_refuse(KernelStage::Refine, "domain")?;
Ok((reversed, d0 + d1 - start_t, d0 + d1 - end_t))
}
}
}
}
fn carrier_section(
edge: &EdgeRecord,
first: &NurbsSurface,
second: &NurbsSurface,
tolerance: f64,
) -> Result<Section, KernelRefusal> {
let plane_tolerance = tolerance * 10.0;
if plane_of_surface(first, plane_tolerance, OP).is_ok()
&& plane_of_surface(second, plane_tolerance, OP).is_ok()
{
return Ok(Section::Straight);
}
let seeds = edge_seeds(edge, 9)?;
let policy = MarchPolicy {
tolerance,
residual_tolerance: tolerance * 100.0,
seeds: seeds.clone(),
};
let found = match reintersect_carriers(first, second, &policy) {
Ok(found) => found,
Err(ReintersectRefusal::Separated) => {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "carriers_separated", format!(
"{OP}: the carriers of edge {} no longer meet after the motion — refusing",
edge.id
)))
}
Err(other @ ReintersectRefusal::Residual { .. }) => {
return Err(KernelRefusal::non_convergence(
KernelStage::Refine,
"section_residual",
format!("{OP}: edge {}: {}", edge.id, other.describe()),
));
}
Err(other) => {
return Err(KernelRefusal::internal(
KernelStage::Refine,
"reintersect",
format!("{OP}: edge {}: {}", edge.id, other.describe()),
));
}
};
let section = found
.nearest_section(&seeds)
.map_err(|error| {
KernelRefusal::internal(
KernelStage::Refine,
"nearest_section",
format!("{OP}: edge {}: {error}", edge.id),
)
})?;
Ok(Section::Curve(section.curve.clone()))
}
fn parameter_on(
curve: &NurbsCurve,
point: Vec3,
tolerance: f64,
edge_id: u64,
which: &str,
) -> Result<f64, KernelRefusal> {
let projection = project_point_to_curve(curve, point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
let off = projection.point.sub(point).length();
if off > tolerance * RESIDUAL_SLACK {
return Err(KernelRefusal::internal(KernelStage::Refine, "corner_off_section", format!(
"{OP}: the re-solved {which} corner of edge {edge_id} is {off:.3e} off the section \
its carriers make — refusing"
)));
}
Ok(projection.u)
}
#[derive(Debug, Clone, Default)]
pub struct RouteReading {
pub new_loop_faces: Vec<(u64, Option<String>)>,
pub unreadable: Vec<(u64, Option<String>, String)>,
}
impl RouteReading {
pub fn needs_rebuild(&self) -> bool {
!self.new_loop_faces.is_empty()
}
pub fn is_complete(&self) -> bool {
self.unreadable.is_empty()
}
}
pub fn route_reading(
solid: &BrepSolid,
moved: &[u64],
motion: &AffineTransform,
) -> Result<RouteReading, KernelRefusal> {
let tolerances = crate::KernelTolerances::for_solid(solid, 1e-7);
let tolerance = tolerances.model.max(1e-9);
let scale = crate::solid_scale(solid).max(1.0);
let moved_set: HashSet<u64> = moved.iter().copied().collect();
let mut adjacent: HashSet<u64> = HashSet::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
if moved_set.contains(&face.id) {
continue;
}
let touches = face.loops.iter().flat_map(|wire| &wire.coedges).any(|use_| {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|other| moved_set.contains(&other.id))
.any(|other| {
other
.loops
.iter()
.flat_map(|wire| &wire.coedges)
.any(|theirs| theirs.edge_id == use_.edge_id)
})
});
if touches {
adjacent.insert(face.id);
}
}
let mut centre = Vec3::new(0.0, 0.0, 0.0);
for vertex in &solid.vertices {
centre = centre.add(vertex.point);
}
centre = centre.scale(1.0 / (solid.vertices.len().max(1)) as f64);
let travel = motion.point(centre).sub(centre).length();
let reach = scale + travel + tolerance * 10.0;
let mut before_carriers: Vec<NurbsSurface> = Vec::new();
let mut after_carriers: Vec<NurbsSurface> = Vec::new();
for face_id in moved {
let Some((shell, position)) = find_face(solid, *face_id) else {
return Err(KernelRefusal::input(KernelStage::Collect, "face_id", format!("route_reading: no face with id {face_id}")));
};
let surface = &solid.shells[shell].faces[position].surface;
let unbounded = unbounded_carrier(surface, centre, reach, tolerance * 10.0)?;
after_carriers.push(transform_surface(&unbounded, *motion).or_refuse(KernelStage::Classify, "transform_surface")?);
before_carriers.push(unbounded);
}
let mut unreadable: Vec<(u64, Option<String>, String)> = Vec::new();
let candidates: Vec<&FaceRecord> = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| !moved_set.contains(&face.id) && !adjacent.contains(&face.id))
.collect();
let before = crossed_faces(
&before_carriers,
&candidates,
scale,
tolerance,
&mut unreadable,
);
let after = crossed_faces(
&after_carriers,
&candidates,
scale,
tolerance,
&mut unreadable,
);
let mut new_loop_faces: Vec<(u64, Option<String>)> = candidates
.iter()
.filter(|face| after.contains(&face.id) && !before.contains(&face.id))
.map(|face| (face.id, face.name.clone()))
.collect();
new_loop_faces.sort();
unreadable.sort();
unreadable.dedup();
Ok(RouteReading {
new_loop_faces,
unreadable,
})
}
fn crossed_faces(
carriers: &[NurbsSurface],
candidates: &[&FaceRecord],
scale: f64,
tolerance: f64,
unreadable: &mut Vec<(u64, Option<String>, String)>,
) -> HashSet<u64> {
let mut crossed: HashSet<u64> = HashSet::default();
for face in candidates {
for carrier in carriers {
let mut nearest: Vec<(f64, Vec3)> = Vec::new();
if let (Ok([u0, u1]), Ok([v0, v1])) = (face.surface.domain_u(), face.surface.domain_v())
{
for ui in 0..=10 {
for vi in 0..=10 {
let u = u0 + (u1 - u0) * ui as f64 / 10.0;
let v = v0 + (v1 - v0) * vi as f64 / 10.0;
let Ok(point) = face.surface.evaluate(u, v) else {
continue;
};
let Ok(projection) = crate::project_point_to_surface(carrier, point) else {
continue;
};
if projection.distance <= scale * 0.1 {
if let Ok(on_carrier) = carrier.evaluate(projection.u, projection.v) {
nearest.push((projection.distance, on_carrier));
}
}
}
}
}
nearest.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let options = crate::SurfaceIntersectionOptions {
tolerance,
seed_points: nearest.into_iter().take(12).map(|(_, p)| p).collect(),
..Default::default()
};
let curves = match crate::intersect_surfaces(carrier, &face.surface, &options) {
Ok(curves) => curves,
Err(error) => {
unreadable.push((face.id, face.name.clone(), error));
continue;
}
};
let mut opens = false;
for curve in &curves {
for uv in &curve.params_b {
let point = crate::Vec2 { x: uv[0], y: uv[1] };
if matches!(
crate::parameter_point_in_face(face, point, tolerance),
Ok(crate::PolygonClass::Inside)
) {
opens = true;
break;
}
}
if opens {
break;
}
}
if opens {
crossed.insert(face.id);
break;
}
}
}
crossed
}
fn determinant3(m: &[[f64; 3]; 3]) -> f64 {
crate::AffineTransform { elements: [
m[0][0], m[0][1], m[0][2], 0.0,
m[1][0], m[1][1], m[1][2], 0.0,
m[2][0], m[2][1], m[2][2], 0.0,
0.0, 0.0, 0.0, 1.0,
] }.determinant3()
}