use super::*;
enum EdgeMoveClass {
Fixed,
Interior,
Boundary { moved_face: u64, fixed_face: u64 },
}
enum EdgeMoveAction {
Translate,
Rebuild { start: Vec3, end: Vec3 },
}
enum FaceMoveAction {
TranslateSurface,
Retrim(Plane),
}
fn cached_plane(
cache: &mut HashMap<u64, Plane>,
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
face_id: u64,
tolerance: f64,
) -> Result<Plane, String> {
if let Some(plane) = cache.get(&face_id) {
return Ok(*plane);
}
let (shell_index, face_index) = *face_lookup
.get(&face_id)
.ok_or_else(|| format!("move_faces: missing face {face_id}"))?;
let plane = plane_of_surface(
&solid.shells[shell_index].faces[face_index].surface,
tolerance,
"move_faces",
)?;
cache.insert(face_id, plane);
Ok(plane)
}
fn solve_corner(planes: &[Plane]) -> Option<Vec3> {
let mut best_pair: Option<(usize, usize, f64)> = None;
for first in 0..planes.len() {
for second in first + 1..planes.len() {
let spread = planes[first].normal.cross(planes[second].normal).length();
if best_pair.map(|(_, _, best)| spread > best).unwrap_or(true) {
best_pair = Some((first, second, spread));
}
}
}
let (first, second, spread) = best_pair?;
if spread <= PARALLEL_EPS {
return None;
}
let line = intersect_planes(&planes[first], &planes[second])?;
let mut best_third: Option<(usize, f64)> = None;
for third in 0..planes.len() {
if third == first || third == second {
continue;
}
let transversality = line.dir.dot(planes[third].normal).abs();
if best_third
.map(|(_, best)| transversality > best)
.unwrap_or(true)
{
best_third = Some((third, transversality));
}
}
let (third, transversality) = best_third?;
if transversality <= PARALLEL_EPS {
return None;
}
intersect_line_plane(&line, &planes[third])
}
fn plan_straight_rebuild(
edge: &EdgeRecord,
start_old: Vec3,
end_old: Vec3,
start_new: Vec3,
end_new: Vec3,
tolerance: f64,
) -> Result<EdgeMoveAction, String> {
if edge.degenerate {
return Err(format!(
"move_faces: degenerate edge {} would need re-stretching (deferred)",
edge.id
));
}
if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
return Err(format!(
"move_faces: edge {} must be re-stretched but is not a straight line \
(curved re-intersection edges are deferred in this slice)",
edge.id
));
}
let new_chord = end_new.sub(start_new);
if new_chord.length() <= tolerance {
return Err(format!(
"move_faces: the translation collapses edge {} to zero length (a moved \
face lands exactly on its neighbour) — refusing",
edge.id
));
}
if end_old.sub(start_old).dot(new_chord) <= 0.0 {
return Err(format!(
"move_faces: the translation inverts edge {} (a moved face passes beyond \
its neighbour) — refusing",
edge.id
));
}
Ok(EdgeMoveAction::Rebuild {
start: start_new,
end: end_new,
})
}
pub fn move_faces(
solid: &BrepSolid,
face_ids: &[u64],
translation: Vec3,
) -> Result<BrepSolid, String> {
if !(translation.x.is_finite() && translation.y.is_finite() && translation.z.is_finite()) {
return Err("move_faces: translation must be finite".into());
}
if face_ids.is_empty() {
return Err("move_faces: no faces selected".into());
}
let moved: HashSet<u64> = face_ids.iter().copied().collect();
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));
}
}
for &face_id in face_ids {
if !face_lookup.contains_key(&face_id) {
return Err(format!("move_faces: no face with id {face_id}"));
}
}
let scale = solid_scale(solid);
let tolerance = (scale * 1e-7).max(1e-9);
let plane_tolerance = (scale * 1e-6).max(1e-7);
let parallel_tolerance = (translation.length() * 1e-9).max(1e-12);
let rigid_tolerance = (scale * 1e-9).max(1e-12);
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);
}
}
}
}
let mut classes: HashMap<u64, EdgeMoveClass> = HashMap::default();
let mut planes: HashMap<u64, Plane> = HashMap::default();
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(format!(
"move_faces: edge {} is used {} times (non-manifold input)",
edge.id,
uses.len()
));
}
let moved_uses = uses
.iter()
.filter(|face_id| moved.contains(*face_id))
.count();
let class = if moved_uses == 0 {
EdgeMoveClass::Fixed
} else if moved_uses == uses.len() {
EdgeMoveClass::Interior
} else {
let moved_face = *uses
.iter()
.find(|face_id| moved.contains(*face_id))
.unwrap();
let fixed_face = *uses
.iter()
.find(|face_id| !moved.contains(*face_id))
.unwrap();
cached_plane(
&mut planes,
solid,
&face_lookup,
fixed_face,
plane_tolerance,
)?;
EdgeMoveClass::Boundary {
moved_face,
fixed_face,
}
};
classes.insert(edge.id, class);
}
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 fixed_at: Vec<u64> = adjacent
.iter()
.copied()
.filter(|face_id| !moved.contains(face_id))
.collect();
if fixed_at.is_empty() {
new_vertex.insert(vertex.id, vertex.point.add(translation));
continue;
}
let mut fixed_planes = Vec::with_capacity(fixed_at.len());
for &face_id in &fixed_at {
fixed_planes.push(cached_plane(
&mut planes,
solid,
&face_lookup,
face_id,
plane_tolerance,
)?);
}
if fixed_planes
.iter()
.all(|plane| translation.dot(plane.normal).abs() <= parallel_tolerance)
{
new_vertex.insert(vertex.id, vertex.point.add(translation));
continue;
}
let mut corner_planes = fixed_planes;
for face_id in adjacent
.iter()
.copied()
.filter(|face_id| moved.contains(face_id))
{
let mut plane =
cached_plane(&mut planes, solid, &face_lookup, face_id, plane_tolerance)?;
plane.origin = plane.origin.add(translation);
corner_planes.push(plane);
}
let corner = solve_corner(&corner_planes).ok_or_else(|| {
format!(
"move_faces: cannot re-intersect the carriers meeting at vertex {} \
(parallel or under-constrained planes)",
vertex.id
)
})?;
for plane in &corner_planes {
if corner.sub(plane.origin).dot(plane.normal).abs() > tolerance {
return Err(format!(
"move_faces: the moved group tears away from its neighbours at \
vertex {} — refusing rather than emitting an invalid solid",
vertex.id
));
}
}
new_vertex.insert(vertex.id, corner);
}
let vertex_position: HashMap<u64, Vec3> = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect();
let mut actions: HashMap<u64, EdgeMoveAction> = HashMap::default();
for edge in &solid.edges {
let position = |vertex_id: u64| -> Result<Vec3, String> {
vertex_position
.get(&vertex_id)
.copied()
.ok_or_else(|| format!("move_faces: missing vertex {vertex_id}"))
};
let start_old = position(edge.start_vertex_id)?;
let end_old = position(edge.end_vertex_id)?;
let start_new = new_vertex
.get(&edge.start_vertex_id)
.copied()
.unwrap_or(start_old);
let end_new = new_vertex
.get(&edge.end_vertex_id)
.copied()
.unwrap_or(end_old);
let rigid = start_new.sub(start_old.add(translation)).length() <= rigid_tolerance
&& end_new.sub(end_old.add(translation)).length() <= rigid_tolerance;
match &classes[&edge.id] {
EdgeMoveClass::Fixed => {
if start_new.sub(start_old).length() == 0.0 && end_new.sub(end_old).length() == 0.0
{
continue; }
for &face_id in &faces_of_edge[&edge.id] {
cached_plane(&mut planes, solid, &face_lookup, face_id, plane_tolerance)?;
}
actions.insert(
edge.id,
plan_straight_rebuild(edge, start_old, end_old, start_new, end_new, tolerance)?,
);
}
EdgeMoveClass::Interior => {
if rigid {
actions.insert(edge.id, EdgeMoveAction::Translate);
} else {
for &face_id in &faces_of_edge[&edge.id] {
cached_plane(&mut planes, solid, &face_lookup, face_id, plane_tolerance)?;
}
actions.insert(
edge.id,
plan_straight_rebuild(
edge, start_old, end_old, start_new, end_new, tolerance,
)?,
);
}
}
EdgeMoveClass::Boundary {
moved_face,
fixed_face,
} => {
let fixed_plane = planes[fixed_face];
if rigid && translation.dot(fixed_plane.normal).abs() <= parallel_tolerance {
actions.insert(edge.id, EdgeMoveAction::Translate);
} else {
cached_plane(
&mut planes,
solid,
&face_lookup,
*moved_face,
plane_tolerance,
)?;
actions.insert(
edge.id,
plan_straight_rebuild(
edge, start_old, end_old, start_new, end_new, tolerance,
)?,
);
}
}
}
}
let rebuilt: HashSet<u64> = actions
.iter()
.filter(|(_, action)| matches!(action, EdgeMoveAction::Rebuild { .. }))
.map(|(edge_id, _)| *edge_id)
.collect();
let dirty: HashSet<u64> = actions.keys().copied().collect();
let mut face_actions: Vec<(usize, usize, FaceMoveAction)> = Vec::new();
for (shell_index, shell) in solid.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
let edge_ids = || {
face.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.map(|coedge| coedge.edge_id)
};
if moved.contains(&face.id) {
if edge_ids().any(|edge_id| rebuilt.contains(&edge_id)) {
let mut plane =
cached_plane(&mut planes, solid, &face_lookup, face.id, plane_tolerance)?;
plane.origin = plane.origin.add(translation);
face_actions.push((shell_index, face_index, FaceMoveAction::Retrim(plane)));
} else {
face_actions.push((shell_index, face_index, FaceMoveAction::TranslateSurface));
}
} else if edge_ids().any(|edge_id| dirty.contains(&edge_id)) {
let plane =
cached_plane(&mut planes, solid, &face_lookup, face.id, plane_tolerance)?;
face_actions.push((shell_index, face_index, FaceMoveAction::Retrim(plane)));
}
}
}
let translate = 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,
])?;
let mut result = solid.clone();
for edge in &mut result.edges {
match actions.get(&edge.id) {
Some(EdgeMoveAction::Translate) => {
edge.curve = transform_curve(&edge.curve, translate)?;
}
Some(EdgeMoveAction::Rebuild { start, end }) => {
edge.curve = make_line(*start, *end)?;
edge.t0 = 0.0;
edge.t1 = 1.0;
}
None => {}
}
}
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();
for (shell_index, face_index, action) in face_actions {
let face = &mut result.shells[shell_index].faces[face_index];
match action {
FaceMoveAction::TranslateSurface => {
face.surface = transform_surface(&face.surface, translate)?;
}
FaceMoveAction::Retrim(plane) => {
retrim_planar_face(face, &plane, &final_edges, scale, "move_faces")?;
}
}
}
let issues = result.validate();
if !issues.is_empty() {
return Err(format!(
"move_faces: moved solid failed validation: {issues:?}"
));
}
if let (Ok(before), Ok(after)) = (solid_signed_volume(solid), solid_signed_volume(&result)) {
if before * after <= 0.0 {
return Err(
"move_faces: the translation inverts the solid (signed volume changed sign) \
— refusing"
.into(),
);
}
}
Ok(result)
}