1use super::*;
2
3enum EdgeMoveClass {
10 Fixed,
11 Interior,
12 Boundary { moved_face: u64, fixed_face: u64 },
13}
14
15enum EdgeMoveAction {
16 Translate,
18 Rebuild { start: Vec3, end: Vec3 },
20}
21
22enum FaceMoveAction {
23 TranslateSurface,
25 Retrim(Plane),
28}
29
30fn cached_plane(
33 cache: &mut HashMap<u64, Plane>,
34 solid: &BrepSolid,
35 face_lookup: &HashMap<u64, (usize, usize)>,
36 face_id: u64,
37 tolerance: f64,
38) -> Result<Plane, String> {
39 if let Some(plane) = cache.get(&face_id) {
40 return Ok(*plane);
41 }
42 let (shell_index, face_index) = *face_lookup
43 .get(&face_id)
44 .ok_or_else(|| format!("move_faces: missing face {face_id}"))?;
45 let plane = plane_of_surface(
46 &solid.shells[shell_index].faces[face_index].surface,
47 tolerance,
48 "move_faces",
49 )?;
50 cache.insert(face_id, plane);
51 Ok(plane)
52}
53
54fn solve_corner(planes: &[Plane]) -> Option<Vec3> {
59 let mut best_pair: Option<(usize, usize, f64)> = None;
60 for first in 0..planes.len() {
61 for second in first + 1..planes.len() {
62 let spread = planes[first].normal.cross(planes[second].normal).length();
63 if best_pair.map(|(_, _, best)| spread > best).unwrap_or(true) {
64 best_pair = Some((first, second, spread));
65 }
66 }
67 }
68 let (first, second, spread) = best_pair?;
69 if spread <= PARALLEL_EPS {
70 return None;
71 }
72 let line = intersect_planes(&planes[first], &planes[second])?;
73 let mut best_third: Option<(usize, f64)> = None;
74 for third in 0..planes.len() {
75 if third == first || third == second {
76 continue;
77 }
78 let transversality = line.dir.dot(planes[third].normal).abs();
79 if best_third
80 .map(|(_, best)| transversality > best)
81 .unwrap_or(true)
82 {
83 best_third = Some((third, transversality));
84 }
85 }
86 let (third, transversality) = best_third?;
87 if transversality <= PARALLEL_EPS {
88 return None;
89 }
90 intersect_line_plane(&line, &planes[third])
91}
92
93fn plan_straight_rebuild(
98 edge: &EdgeRecord,
99 start_old: Vec3,
100 end_old: Vec3,
101 start_new: Vec3,
102 end_new: Vec3,
103 tolerance: f64,
104) -> Result<EdgeMoveAction, String> {
105 if edge.degenerate {
106 return Err(format!(
107 "move_faces: degenerate edge {} would need re-stretching (deferred)",
108 edge.id
109 ));
110 }
111 if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
112 return Err(format!(
113 "move_faces: edge {} must be re-stretched but is not a straight line \
114 (curved re-intersection edges are deferred in this slice)",
115 edge.id
116 ));
117 }
118 let new_chord = end_new.sub(start_new);
119 if new_chord.length() <= tolerance {
120 return Err(format!(
121 "move_faces: the translation collapses edge {} to zero length (a moved \
122 face lands exactly on its neighbour) — refusing",
123 edge.id
124 ));
125 }
126 if end_old.sub(start_old).dot(new_chord) <= 0.0 {
127 return Err(format!(
128 "move_faces: the translation inverts edge {} (a moved face passes beyond \
129 its neighbour) — refusing",
130 edge.id
131 ));
132 }
133 Ok(EdgeMoveAction::Rebuild {
134 start: start_new,
135 end: end_new,
136 })
137}
138
139pub fn move_faces(
166 solid: &BrepSolid,
167 face_ids: &[u64],
168 translation: Vec3,
169) -> Result<BrepSolid, String> {
170 if !(translation.x.is_finite() && translation.y.is_finite() && translation.z.is_finite()) {
171 return Err("move_faces: translation must be finite".into());
172 }
173 if face_ids.is_empty() {
174 return Err("move_faces: no faces selected".into());
175 }
176 let moved: HashSet<u64> = face_ids.iter().copied().collect();
177 let mut face_lookup: HashMap<u64, (usize, usize)> = HashMap::default();
182 for (shell_index, shell) in solid.shells.iter().enumerate() {
183 for (face_index, face) in shell.faces.iter().enumerate() {
184 face_lookup
185 .entry(face.id)
186 .or_insert((shell_index, face_index));
187 }
188 }
189 for &face_id in face_ids {
190 if !face_lookup.contains_key(&face_id) {
191 return Err(format!("move_faces: no face with id {face_id}"));
192 }
193 }
194
195 let scale = solid_scale(solid);
196 let tolerance = (scale * 1e-7).max(1e-9);
197 let plane_tolerance = (scale * 1e-6).max(1e-7);
198 let parallel_tolerance = (translation.length() * 1e-9).max(1e-12);
201 let rigid_tolerance = (scale * 1e-9).max(1e-12);
204
205 let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
207 for shell in &solid.shells {
208 for face in &shell.faces {
209 for loop_record in &face.loops {
210 for coedge in &loop_record.coedges {
211 faces_of_edge
212 .entry(coedge.edge_id)
213 .or_default()
214 .push(face.id);
215 }
216 }
217 }
218 }
219 let mut classes: HashMap<u64, EdgeMoveClass> = HashMap::default();
220 let mut planes: HashMap<u64, Plane> = HashMap::default();
221 for edge in &solid.edges {
222 let uses = faces_of_edge
223 .get(&edge.id)
224 .map(Vec::as_slice)
225 .unwrap_or(&[]);
226 let expected = if edge.degenerate { 1 } else { 2 };
227 if uses.len() != expected {
228 return Err(format!(
229 "move_faces: edge {} is used {} times (non-manifold input)",
230 edge.id,
231 uses.len()
232 ));
233 }
234 let moved_uses = uses
235 .iter()
236 .filter(|face_id| moved.contains(*face_id))
237 .count();
238 let class = if moved_uses == 0 {
239 EdgeMoveClass::Fixed
240 } else if moved_uses == uses.len() {
241 EdgeMoveClass::Interior
242 } else {
243 let moved_face = *uses
244 .iter()
245 .find(|face_id| moved.contains(*face_id))
246 .unwrap();
247 let fixed_face = *uses
248 .iter()
249 .find(|face_id| !moved.contains(*face_id))
250 .unwrap();
251 cached_plane(
254 &mut planes,
255 solid,
256 &face_lookup,
257 fixed_face,
258 plane_tolerance,
259 )?;
260 EdgeMoveClass::Boundary {
261 moved_face,
262 fixed_face,
263 }
264 };
265 classes.insert(edge.id, class);
266 }
267
268 let mut vertex_faces: HashMap<u64, HashSet<u64>> = HashMap::default();
270 for edge in &solid.edges {
271 if let Some(uses) = faces_of_edge.get(&edge.id) {
272 for vertex_id in [edge.start_vertex_id, edge.end_vertex_id] {
273 vertex_faces
274 .entry(vertex_id)
275 .or_default()
276 .extend(uses.iter().copied());
277 }
278 }
279 }
280 let mut new_vertex: HashMap<u64, Vec3> = HashMap::default();
281 for vertex in &solid.vertices {
282 let Some(adjacent) = vertex_faces.get(&vertex.id) else {
283 continue;
284 };
285 if !adjacent.iter().any(|face_id| moved.contains(face_id)) {
286 continue;
287 }
288 let fixed_at: Vec<u64> = adjacent
289 .iter()
290 .copied()
291 .filter(|face_id| !moved.contains(face_id))
292 .collect();
293 if fixed_at.is_empty() {
294 new_vertex.insert(vertex.id, vertex.point.add(translation));
296 continue;
297 }
298 let mut fixed_planes = Vec::with_capacity(fixed_at.len());
299 for &face_id in &fixed_at {
300 fixed_planes.push(cached_plane(
301 &mut planes,
302 solid,
303 &face_lookup,
304 face_id,
305 plane_tolerance,
306 )?);
307 }
308 if fixed_planes
309 .iter()
310 .all(|plane| translation.dot(plane.normal).abs() <= parallel_tolerance)
311 {
312 new_vertex.insert(vertex.id, vertex.point.add(translation));
317 continue;
318 }
319 let mut corner_planes = fixed_planes;
322 for face_id in adjacent
323 .iter()
324 .copied()
325 .filter(|face_id| moved.contains(face_id))
326 {
327 let mut plane =
328 cached_plane(&mut planes, solid, &face_lookup, face_id, plane_tolerance)?;
329 plane.origin = plane.origin.add(translation);
330 corner_planes.push(plane);
331 }
332 let corner = solve_corner(&corner_planes).ok_or_else(|| {
333 format!(
334 "move_faces: cannot re-intersect the carriers meeting at vertex {} \
335 (parallel or under-constrained planes)",
336 vertex.id
337 )
338 })?;
339 for plane in &corner_planes {
342 if corner.sub(plane.origin).dot(plane.normal).abs() > tolerance {
343 return Err(format!(
344 "move_faces: the moved group tears away from its neighbours at \
345 vertex {} — refusing rather than emitting an invalid solid",
346 vertex.id
347 ));
348 }
349 }
350 new_vertex.insert(vertex.id, corner);
351 }
352
353 let vertex_position: HashMap<u64, Vec3> = solid
355 .vertices
356 .iter()
357 .map(|vertex| (vertex.id, vertex.point))
358 .collect();
359 let mut actions: HashMap<u64, EdgeMoveAction> = HashMap::default();
360 for edge in &solid.edges {
361 let position = |vertex_id: u64| -> Result<Vec3, String> {
362 vertex_position
363 .get(&vertex_id)
364 .copied()
365 .ok_or_else(|| format!("move_faces: missing vertex {vertex_id}"))
366 };
367 let start_old = position(edge.start_vertex_id)?;
368 let end_old = position(edge.end_vertex_id)?;
369 let start_new = new_vertex
370 .get(&edge.start_vertex_id)
371 .copied()
372 .unwrap_or(start_old);
373 let end_new = new_vertex
374 .get(&edge.end_vertex_id)
375 .copied()
376 .unwrap_or(end_old);
377 let rigid = start_new.sub(start_old.add(translation)).length() <= rigid_tolerance
378 && end_new.sub(end_old.add(translation)).length() <= rigid_tolerance;
379 match &classes[&edge.id] {
380 EdgeMoveClass::Fixed => {
381 if start_new.sub(start_old).length() == 0.0 && end_new.sub(end_old).length() == 0.0
382 {
383 continue; }
385 for &face_id in &faces_of_edge[&edge.id] {
389 cached_plane(&mut planes, solid, &face_lookup, face_id, plane_tolerance)?;
390 }
391 actions.insert(
392 edge.id,
393 plan_straight_rebuild(edge, start_old, end_old, start_new, end_new, tolerance)?,
394 );
395 }
396 EdgeMoveClass::Interior => {
397 if rigid {
398 actions.insert(edge.id, EdgeMoveAction::Translate);
399 } else {
400 for &face_id in &faces_of_edge[&edge.id] {
404 cached_plane(&mut planes, solid, &face_lookup, face_id, plane_tolerance)?;
405 }
406 actions.insert(
407 edge.id,
408 plan_straight_rebuild(
409 edge, start_old, end_old, start_new, end_new, tolerance,
410 )?,
411 );
412 }
413 }
414 EdgeMoveClass::Boundary {
415 moved_face,
416 fixed_face,
417 } => {
418 let fixed_plane = planes[fixed_face];
419 if rigid && translation.dot(fixed_plane.normal).abs() <= parallel_tolerance {
420 actions.insert(edge.id, EdgeMoveAction::Translate);
424 } else {
425 cached_plane(
429 &mut planes,
430 solid,
431 &face_lookup,
432 *moved_face,
433 plane_tolerance,
434 )?;
435 actions.insert(
436 edge.id,
437 plan_straight_rebuild(
438 edge, start_old, end_old, start_new, end_new, tolerance,
439 )?,
440 );
441 }
442 }
443 }
444 }
445
446 let rebuilt: HashSet<u64> = actions
448 .iter()
449 .filter(|(_, action)| matches!(action, EdgeMoveAction::Rebuild { .. }))
450 .map(|(edge_id, _)| *edge_id)
451 .collect();
452 let dirty: HashSet<u64> = actions.keys().copied().collect();
453 let mut face_actions: Vec<(usize, usize, FaceMoveAction)> = Vec::new();
454 for (shell_index, shell) in solid.shells.iter().enumerate() {
455 for (face_index, face) in shell.faces.iter().enumerate() {
456 let edge_ids = || {
457 face.loops
458 .iter()
459 .flat_map(|loop_record| &loop_record.coedges)
460 .map(|coedge| coedge.edge_id)
461 };
462 if moved.contains(&face.id) {
463 if edge_ids().any(|edge_id| rebuilt.contains(&edge_id)) {
464 let mut plane =
467 cached_plane(&mut planes, solid, &face_lookup, face.id, plane_tolerance)?;
468 plane.origin = plane.origin.add(translation);
469 face_actions.push((shell_index, face_index, FaceMoveAction::Retrim(plane)));
470 } else {
471 face_actions.push((shell_index, face_index, FaceMoveAction::TranslateSurface));
475 }
476 } else if edge_ids().any(|edge_id| dirty.contains(&edge_id)) {
477 let plane =
478 cached_plane(&mut planes, solid, &face_lookup, face.id, plane_tolerance)?;
479 face_actions.push((shell_index, face_index, FaceMoveAction::Retrim(plane)));
480 }
481 }
482 }
483
484 let translate = AffineTransform::new([
486 1.0,
487 0.0,
488 0.0,
489 translation.x,
490 0.0,
491 1.0,
492 0.0,
493 translation.y,
494 0.0,
495 0.0,
496 1.0,
497 translation.z,
498 0.0,
499 0.0,
500 0.0,
501 1.0,
502 ])?;
503 let mut result = solid.clone();
504 for edge in &mut result.edges {
505 match actions.get(&edge.id) {
506 Some(EdgeMoveAction::Translate) => {
507 edge.curve = transform_curve(&edge.curve, translate)?;
508 }
509 Some(EdgeMoveAction::Rebuild { start, end }) => {
510 edge.curve = make_line(*start, *end)?;
511 edge.t0 = 0.0;
512 edge.t1 = 1.0;
513 }
514 None => {}
515 }
516 }
517 for vertex in &mut result.vertices {
518 if let Some(point) = new_vertex.get(&vertex.id) {
519 vertex.point = *point;
520 }
521 }
522 let final_edges: HashMap<u64, EdgeRecord> = result
523 .edges
524 .iter()
525 .map(|edge| (edge.id, edge.clone()))
526 .collect();
527 for (shell_index, face_index, action) in face_actions {
528 let face = &mut result.shells[shell_index].faces[face_index];
529 match action {
530 FaceMoveAction::TranslateSurface => {
531 face.surface = transform_surface(&face.surface, translate)?;
532 }
533 FaceMoveAction::Retrim(plane) => {
534 retrim_planar_face(face, &plane, &final_edges, scale, "move_faces")?;
535 }
536 }
537 }
538
539 let issues = result.validate();
542 if !issues.is_empty() {
543 return Err(format!(
544 "move_faces: moved solid failed validation: {issues:?}"
545 ));
546 }
547 if let (Ok(before), Ok(after)) = (solid_signed_volume(solid), solid_signed_volume(&result)) {
550 if before * after <= 0.0 {
551 return Err(
552 "move_faces: the translation inverts the solid (signed volume changed sign) \
553 — refusing"
554 .into(),
555 );
556 }
557 }
558 Ok(result)
559}