1use super::*;
2use crate::{KernelRefusal, KernelStage, OrRefuse};
3
4struct HealPlan {
5 primary: [usize; 2],
7 lateral: [usize; 2],
9 lateral_point: HashMap<usize, Vec3>,
11}
12
13fn plan_heal(planes: &[Plane; 4], f_center: Vec3, f_reach: f64) -> Result<HealPlan, KernelRefusal> {
16 let mut candidates: Vec<HealPlan> = Vec::new();
17 for start in 0..2usize {
18 let primary = [start, start + 2];
19 let lateral = [(start + 1) % 4, (start + 3) % 4];
20 let Some(line) = intersect_planes(&planes[primary[0]], &planes[primary[1]]) else {
21 continue;
22 };
23 let mut lateral_point = HashMap::default();
24 let mut ok = true;
25 for &lat in &lateral {
26 match intersect_line_plane(&line, &planes[lat]) {
27 Some(point) if point.sub(f_center).length() <= f_reach => {
28 lateral_point.insert(lat, point);
29 }
30 _ => {
31 ok = false;
32 break;
33 }
34 }
35 }
36 if !ok {
37 continue;
38 }
39 candidates.push(HealPlan {
40 primary,
41 lateral,
42 lateral_point,
43 });
44 }
45 match candidates.len() {
46 1 => Ok(candidates.pop().unwrap()),
47 0 => Err(KernelRefusal::ill_posed(
48 KernelStage::Classify,
49 "no_reintersection",
50 "delete_face_and_heal: the neighbours do not re-intersect cleanly \
51 (parallel planes, non-adjacent healing, or a multi-face gap) — refusing \
52 rather than emitting an invalid solid",
53 )),
54 _ => Err(KernelRefusal::ill_posed(
55 KernelStage::Classify,
56 "ambiguous_pair",
57 "delete_face_and_heal: healing is ambiguous — both opposite neighbour \
58 pairs re-intersect through the deleted face",
59 )),
60 }
61}
62
63fn neighbour_position(solid: &BrepSolid, neighbour_id: u64) -> Result<(usize, usize), KernelRefusal> {
66 find_face(solid, neighbour_id).ok_or_else(|| {
67 KernelRefusal::internal(
68 KernelStage::Collect,
69 "missing_neighbour",
70 format!("delete_face_and_heal: missing neighbour {neighbour_id}"),
71 )
72 })
73}
74
75pub(super) use crate::offset_retrim::retrim_planar_face;
80
81pub(super) fn locate_coedge(face: &FaceRecord, edge_id: u64) -> Option<(usize, usize)> {
84 for (loop_index, loop_record) in face.loops.iter().enumerate() {
85 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
86 if coedge.edge_id == edge_id {
87 return Some((loop_index, coedge_index));
88 }
89 }
90 }
91 None
92}
93
94pub(super) fn coedge_from_vertex(coedge: &CoedgeRecord, edges: &HashMap<u64, EdgeRecord>) -> Option<u64> {
95 let edge = edges.get(&coedge.edge_id)?;
96 Some(if coedge.forward {
97 edge.start_vertex_id
98 } else {
99 edge.end_vertex_id
100 })
101}
102
103pub(super) fn coedge_to_vertex(coedge: &CoedgeRecord, edges: &HashMap<u64, EdgeRecord>) -> Option<u64> {
104 let edge = edges.get(&coedge.edge_id)?;
105 Some(if coedge.forward {
106 edge.end_vertex_id
107 } else {
108 edge.start_vertex_id
109 })
110}
111
112fn anchored_corners(
119 solid: &BrepSolid,
120 boundary_edges: &HashSet<u64>,
121 corners: &[u64],
122 neighbours: &[u64; 4],
123 plan: &HealPlan,
124 plane_tolerance: f64,
125) -> HashMap<usize, u64> {
126 let held_at = |corner: u64, target: Vec3| -> bool {
127 solid.edges.iter().filter(|edge| {
128 !boundary_edges.contains(&edge.id)
129 && (edge.start_vertex_id == corner || edge.end_vertex_id == corner)
130 }).any(|edge| {
131 solid.shells.iter().flat_map(|shell| &shell.faces).filter(|face| {
132 !neighbours.contains(&face.id)
133 && face.loops.iter().flat_map(|l| &l.coedges).any(|coedge| coedge.edge_id == edge.id)
134 }).any(|face| {
135 plane_of_surface(&face.surface, plane_tolerance, "delete_face_and_heal").is_ok_and(|plane| {
136 target.sub(plane.origin).dot(plane.normal).abs() > plane_tolerance
137 })
138 })
139 })
140 };
141 let mut anchored = HashMap::default();
142 for &lateral in &plan.lateral {
143 let target = plan.lateral_point[&lateral];
144 let ends = [corners[lateral], corners[(lateral + 1) % 4]];
145 let held: Vec<u64> = ends.into_iter().filter(|corner| held_at(*corner, target)).collect();
146 if let [corner] = held[..] {
147 let at_target = solid.vertices.iter().any(|vertex| vertex.id == corner && vertex.point.sub(target).length() <= plane_tolerance);
148 if !at_target {
149 anchored.insert(lateral, corner);
150 }
151 }
152 }
153 anchored
154}
155
156pub fn delete_face_and_heal(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, KernelRefusal> {
170 crate::accept_sound(delete_face_and_heal_impl(solid, face_id)?, "deleteFace")
171}
172
173pub(super) fn delete_face_and_heal_impl(
175 solid: &BrepSolid,
176 face_id: u64,
177) -> Result<BrepSolid, KernelRefusal> {
178 let mut solid = solid.clone();
179 let scale = solid_model_scale(&solid);
180 let tolerance = (scale * 1e-7).max(1e-9);
181
182 let (shell_index, face_index) = find_face(&solid, face_id).ok_or_else(|| {
183 KernelRefusal::input(
184 KernelStage::Collect,
185 "face_id",
186 format!("delete_face_and_heal: no face with id {face_id}"),
187 )
188 })?;
189
190 let boundary: Vec<(u64, bool)> = {
192 let face = &solid.shells[shell_index].faces[face_index];
193 if face.loops.len() != 1 {
194 return Err(KernelRefusal::unsupported(
195 KernelStage::Classify,
196 "multi_loop",
197 format!(
198 "delete_face_and_heal: face {face_id} has {} loops; only a simple \
199 single-loop transition face is supported",
200 face.loops.len()
201 ),
202 ));
203 }
204 face.loops[0]
205 .coedges
206 .iter()
207 .map(|coedge| (coedge.edge_id, coedge.forward))
208 .collect()
209 };
210 let degenerate = |edge_id: u64| solid.edges.iter().any(|edge| edge.id == edge_id && edge.degenerate);
213 let sides = boundary.iter().filter(|(edge_id, _)| !degenerate(*edge_id)).count();
214 if boundary.len() != 4 || (sides != boundary.len() && sides != 4) {
215 return Err(KernelRefusal::unsupported(
216 KernelStage::Classify,
217 "side_count",
218 format!(
219 "delete_face_and_heal: face {face_id} has {} boundary edges; only 4-sided \
220 transition faces (a single chamfer/fillet edge) are supported \
221 (deferred: multi-face gaps)",
222 if sides != boundary.len() { sides } else { boundary.len() }
223 ),
224 ));
225 }
226 let boundary_edge_ids: HashSet<u64> = boundary.iter().map(|(edge_id, _)| *edge_id).collect();
227 if boundary_edge_ids.len() == 3 {
228 return heal_closed_transition(&solid, shell_index, face_index, &boundary)
232 ;
233 }
234 if boundary_edge_ids.len() != 4 {
235 return Err(KernelRefusal::unsupported(
236 KernelStage::Classify,
237 "repeated_edge",
238 "delete_face_and_heal: transition face uses an edge more than once \
239 (deferred: periodic/closed transition)",
240 ));
241 }
242
243 let mut neighbour_ids = [0u64; 4];
245 for (index, (edge_id, _)) in boundary.iter().enumerate() {
246 neighbour_ids[index] =
247 other_face_of_edge(&solid, *edge_id, face_id)?;
248 }
249 let unique: HashSet<u64> = neighbour_ids.iter().copied().collect();
250 if unique.len() != 4 {
251 return Err(KernelRefusal::unsupported(
252 KernelStage::Classify,
253 "repeated_neighbour",
254 "delete_face_and_heal: the transition face touches a neighbour more \
255 than once (deferred: periodic/closed transition)",
256 ));
257 }
258
259 let neighbour_planes: [Plane; 4] = {
263 let mut planes: Vec<Option<Plane>> = Vec::with_capacity(4);
264 for &neighbour_id in &neighbour_ids {
265 let (ns, nf) = neighbour_position(&solid, neighbour_id)?;
266 planes.push(
267 plane_of_surface(
268 &solid.shells[ns].faces[nf].surface,
269 (scale * 1e-6).max(1e-7),
270 "delete_face_and_heal",
271 )
272 .ok(),
273 );
274 }
275 if planes.iter().any(Option::is_none) {
276 return heal_open_transition_mixed(
277 &solid,
278 shell_index,
279 face_index,
280 &boundary,
281 &neighbour_ids,
282 )
283 ;
284 }
285 [
286 planes[0].unwrap(),
287 planes[1].unwrap(),
288 planes[2].unwrap(),
289 planes[3].unwrap(),
290 ]
291 };
292
293 let transition_vertices: Vec<u64> = boundary
295 .iter()
296 .map(|(edge_id, forward)| {
297 let edge = solid
298 .edges
299 .iter()
300 .find(|edge| edge.id == *edge_id)
301 .ok_or_else(|| {
302 KernelRefusal::internal(
303 KernelStage::Collect,
304 "missing_edge",
305 format!("delete_face_and_heal: missing edge {edge_id}"),
306 )
307 })?;
308 Ok(if *forward {
309 edge.start_vertex_id
310 } else {
311 edge.end_vertex_id
312 })
313 })
314 .collect::<Result<Vec<u64>, KernelRefusal>>()?;
315 if transition_vertices.iter().collect::<HashSet<_>>().len() != 4 {
316 return Err(KernelRefusal::unsupported(
317 KernelStage::Classify,
318 "repeated_corner",
319 "delete_face_and_heal: transition face has repeated corner vertices \
320 (deferred: degenerate transition)",
321 ));
322 }
323 let mut f_center = Vec3::default();
324 for &vertex_id in &transition_vertices {
325 f_center = f_center.add(edge_point(&solid, vertex_id)?);
326 }
327 f_center = f_center.scale(0.25);
328 let mut f_reach = 0.0f64;
329 for &vertex_id in &transition_vertices {
330 f_reach = f_reach.max(
331 edge_point(&solid, vertex_id)
332 ?
333 .sub(f_center)
334 .length(),
335 );
336 }
337 let f_reach = f_reach * 3.0 + tolerance;
338
339 if let Some(directory) = census_directory() {
340 record_planar_open_heal(&directory, &solid, face_id);
341 }
342 let plan = plan_heal(&neighbour_planes, f_center, f_reach)?;
343
344 let mut next_id = max_topology_id(&solid) + 1;
346 let mut alloc = || {
347 let value = next_id;
348 next_id += 1;
349 value
350 };
351
352 let lateral_a = plan.lateral[0];
353 let lateral_b = plan.lateral[1];
354 let point_a = plan.lateral_point[&lateral_a];
355 let point_b = plan.lateral_point[&lateral_b];
356 if point_a.sub(point_b).length() <= tolerance {
357 return Err(KernelRefusal::unsupported(
358 KernelStage::Classify,
359 "coincident_corners",
360 "delete_face_and_heal: recovered corners coincide — the neighbours \
361 do not bound a clean edge",
362 ));
363 }
364 let vertex_a = alloc();
365 let vertex_b = alloc();
366 let mut lateral_vertex: HashMap<usize, u64> = HashMap::default();
367 lateral_vertex.insert(lateral_a, vertex_a);
368 lateral_vertex.insert(lateral_b, vertex_b);
369
370 let sharp_edge_id = alloc();
372 let sharp_edge = EdgeRecord {
373 id: sharp_edge_id,
374 curve: make_line(point_a, point_b).or_refuse(KernelStage::Fragment, "make_line")?,
375 t0: 0.0,
376 t1: 1.0,
377 start_vertex_id: vertex_a,
378 end_vertex_id: vertex_b,
379 degenerate: false,
380 name: None,
381 };
382
383 let lateral_set: HashSet<usize> = plan.lateral.iter().copied().collect();
388 let plane_tolerance = (scale * 1e-6).max(1e-7);
389 let anchored_corners =
390 anchored_corners(&solid, &boundary_edge_ids, &transition_vertices, &neighbour_ids, &plan, plane_tolerance);
391 let mut collapse: HashMap<u64, u64> = HashMap::default();
392 for index in 0..4usize {
393 let previous = (index + 3) % 4;
394 let lateral_index = if lateral_set.contains(&previous) {
395 previous
396 } else if lateral_set.contains(&index) {
397 index
398 } else {
399 return Err(KernelRefusal::internal(
400 KernelStage::Classify,
401 "corner_flank",
402 "delete_face_and_heal: transition corner is not flanked by a \
403 lateral face (unexpected neighbour ordering)",
404 ));
405 };
406 if anchored_corners.get(&lateral_index) == Some(&transition_vertices[index]) {
407 continue;
408 }
409 let target = lateral_vertex[&lateral_index];
410 collapse.insert(transition_vertices[index], target);
411 }
412 let mut bridges: HashMap<u64, (u64, u64)> = HashMap::default();
414 let mut bridge_of_lateral: HashMap<usize, (u64, u64)> = HashMap::default();
415 let mut bridge_edges: Vec<EdgeRecord> = Vec::new();
416 for (lateral_index, corner) in &anchored_corners {
417 let target = lateral_vertex[lateral_index];
418 let edge_id = alloc();
419 let from = edge_point(&solid, *corner)?;
420 let to = plan.lateral_point[lateral_index];
421 bridge_edges.push(EdgeRecord {
422 id: edge_id,
423 curve: make_line(from, to).or_refuse(KernelStage::Fragment, "make_line")?,
424 t0: 0.0,
425 t1: 1.0,
426 start_vertex_id: *corner,
427 end_vertex_id: target,
428 degenerate: false,
429 name: None,
430 });
431 bridges.insert(*corner, (edge_id, target));
432 bridge_of_lateral.insert(*lateral_index, (edge_id, *corner));
433 }
434 let new_vertex_points: HashMap<u64, Vec3> = [(vertex_a, point_a), (vertex_b, point_b)]
435 .into_iter()
436 .collect();
437
438 let unmoved = solid.edges.clone();
440 let mut moved: HashSet<u64> = HashSet::default();
441 for edge in &mut solid.edges {
442 if boundary_edge_ids.contains(&edge.id) {
443 continue;
444 }
445 let start_target = collapse.get(&edge.start_vertex_id).copied();
446 let end_target = collapse.get(&edge.end_vertex_id).copied();
447 if start_target.is_none() && end_target.is_none() {
448 continue;
449 }
450 moved.insert(edge.id);
451 if edge.curve.straight_segment(tolerance).is_none() {
452 return Err(KernelRefusal::unsupported(
453 KernelStage::Classify,
454 "curved_side_edge",
455 "delete_face_and_heal: a side edge meeting the transition face is \
456 not a straight line (deferred: curved neighbour edges)",
457 ));
458 }
459 let mut start_point = edge
467 .curve
468 .evaluate(edge.t0)
469 .or_refuse(KernelStage::Fragment, "evaluate")?;
470 let mut end_point = edge
471 .curve
472 .evaluate(edge.t1)
473 .or_refuse(KernelStage::Fragment, "evaluate")?;
474 if let Some(target) = start_target {
475 edge.start_vertex_id = target;
476 start_point = new_vertex_points[&target];
477 }
478 if let Some(target) = end_target {
479 edge.end_vertex_id = target;
480 end_point = new_vertex_points[&target];
481 }
482 if start_point.sub(end_point).length() <= tolerance {
483 return Err(KernelRefusal::unsupported(
484 KernelStage::Fragment,
485 "collapsed_side_edge",
486 "delete_face_and_heal: healing would collapse a side edge to zero \
487 length (deferred: degenerate transition)",
488 ));
489 }
490 edge.curve = make_line(start_point, end_point).or_refuse(KernelStage::Fragment, "make_line")?;
491 edge.t0 = 0.0;
492 edge.t1 = 1.0;
493 }
494 let retrimmed: HashMap<u64, Plane> =
496 neighbour_ids.iter().copied().zip(neighbour_planes.iter().copied()).collect();
497 moved_edges_lie_on_their_faces(
498 &unmoved,
499 &solid,
500 &moved,
501 &retrimmed,
502 (scale * 1e-6).max(1e-7),
503 "delete_face_and_heal",
504 )
505 ?;
506
507 let mut edges_by_id: HashMap<u64, EdgeRecord> = solid
509 .edges
510 .iter()
511 .map(|edge| (edge.id, edge.clone()))
512 .collect();
513 edges_by_id.insert(sharp_edge_id, sharp_edge.clone());
514 let old_boundary: HashMap<u64, (u64, u64)> = unmoved
515 .iter()
516 .filter(|edge| boundary_edge_ids.contains(&edge.id))
517 .map(|edge| (edge.id, (edge.start_vertex_id, edge.end_vertex_id)))
518 .collect();
519
520 for &primary_index in &plan.primary {
524 let primary_edge = boundary[primary_index].0;
525 let neighbour_id = neighbour_ids[primary_index];
526 let (ns, nf) = neighbour_position(&solid, neighbour_id)?;
527 let face = &mut solid.shells[ns].faces[nf];
528 let (loop_index, coedge_index) = locate_coedge(face, primary_edge).ok_or_else(|| {
529 KernelRefusal::internal(
530 KernelStage::Sew,
531 "primary_coedge",
532 format!(
533 "delete_face_and_heal: neighbour {neighbour_id} does not use edge {primary_edge}"
534 ),
535 )
536 })?;
537 let coedges = &face.loops[loop_index].coedges;
538 let count = coedges.len();
539 let previous = &coedges[(coedge_index + count - 1) % count];
540 let next = &coedges[(coedge_index + 1) % count];
541 let required_from = coedge_to_vertex(previous, &edges_by_id).ok_or_else(|| {
542 KernelRefusal::internal(
543 KernelStage::Sew,
544 "loop_connectivity_from",
545 "delete_face_and_heal: could not resolve loop connectivity",
546 )
547 })?;
548 let required_to = coedge_from_vertex(next, &edges_by_id).ok_or_else(|| {
549 KernelRefusal::internal(
550 KernelStage::Sew,
551 "loop_connectivity_to",
552 "delete_face_and_heal: could not resolve loop connectivity",
553 )
554 })?;
555 let bridge_in = bridges.get(&required_from).copied();
558 let bridge_out = bridges.get(&required_to).copied();
559 let sharp_from = bridge_in.map_or(required_from, |(_, target)| target);
560 let sharp_to = bridge_out.map_or(required_to, |(_, target)| target);
561 let forward = if sharp_from == vertex_a && sharp_to == vertex_b {
562 true
563 } else if sharp_from == vertex_b && sharp_to == vertex_a {
564 false
565 } else {
566 return Err(KernelRefusal::internal(
567 KernelStage::Sew,
568 "primary_loop_closure",
569 "delete_face_and_heal: new edge does not close the primary loop \
570 (unexpected connectivity)",
571 ));
572 };
573 let new_coedge = CoedgeRecord {
574 id: alloc(),
575 edge_id: sharp_edge_id,
576 forward,
577 pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0))
579 .or_refuse(KernelStage::Sew, "make_line")?,
580 };
581 let placeholder = new_coedge.pcurve.clone();
583 let mut spliced = Vec::with_capacity(3);
584 if let Some((edge_id, _)) = bridge_in {
585 spliced.push(CoedgeRecord { id: alloc(), edge_id, forward: true, pcurve: placeholder.clone() });
586 }
587 spliced.push(new_coedge);
588 if let Some((edge_id, _)) = bridge_out {
589 spliced.push(CoedgeRecord { id: alloc(), edge_id, forward: false, pcurve: placeholder.clone() });
590 }
591 face.loops[loop_index].coedges.splice(coedge_index..=coedge_index, spliced);
592 }
593
594 for &lateral_index in &plan.lateral {
597 let lateral_edge = boundary[lateral_index].0;
598 let neighbour_id = neighbour_ids[lateral_index];
599 let (ns, nf) = neighbour_position(&solid, neighbour_id)?;
600 let face = &mut solid.shells[ns].faces[nf];
601 let (loop_index, coedge_index) = locate_coedge(face, lateral_edge).ok_or_else(|| {
602 KernelRefusal::internal(
603 KernelStage::Sew,
604 "lateral_coedge",
605 format!(
606 "delete_face_and_heal: neighbour {neighbour_id} does not use edge {lateral_edge}"
607 ),
608 )
609 })?;
610 if let Some(&(edge_id, corner)) = bridge_of_lateral.get(&lateral_index) {
611 let coedge = &mut face.loops[loop_index].coedges[coedge_index];
613 let (start, end) = old_boundary[&lateral_edge];
614 coedge.forward = (if coedge.forward { start } else { end }) == corner;
615 coedge.edge_id = edge_id;
616 coedge.id = alloc();
617 continue;
618 }
619 face.loops[loop_index].coedges.remove(coedge_index);
620 if face.loops[loop_index].coedges.is_empty() {
621 return Err(KernelRefusal::internal(
622 KernelStage::Sew,
623 "emptied_loop",
624 "delete_face_and_heal: healing emptied a lateral face loop",
625 ));
626 }
627 }
628
629 solid.shells[shell_index]
631 .faces
632 .retain(|face| face.id != face_id);
633 solid
634 .edges
635 .retain(|edge| !boundary_edge_ids.contains(&edge.id));
636 let removed_vertices: HashSet<u64> =
637 transition_vertices.iter().copied().filter(|vertex| !bridges.contains_key(vertex)).collect();
638 solid.edges.push(sharp_edge);
639 solid.edges.extend(bridge_edges);
640 solid
641 .vertices
642 .retain(|vertex| !removed_vertices.contains(&vertex.id));
643 solid.vertices.push(VertexRecord {
644 id: vertex_a,
645 point: point_a,
646 });
647 solid.vertices.push(VertexRecord {
648 id: vertex_b,
649 point: point_b,
650 });
651
652 let final_edges: HashMap<u64, EdgeRecord> = solid
654 .edges
655 .iter()
656 .map(|edge| (edge.id, edge.clone()))
657 .collect();
658 for neighbour_id in neighbour_ids {
659 let plane = &retrimmed[&neighbour_id];
660 let (ns, nf) = neighbour_position(&solid, neighbour_id)?;
661 retrim_planar_face(
662 &mut solid.shells[ns].faces[nf],
663 plane,
664 &final_edges,
665 scale,
666 "delete_face_and_heal",
667 )
668 ?;
669 }
670
671 let issues = solid.validate();
674 if !issues.is_empty() {
675 return Err(KernelRefusal::internal(
676 KernelStage::Validate,
677 "validate",
678 format!("delete_face_and_heal: healed solid failed validation: {issues:?}"),
679 ));
680 }
681 Ok(solid)
682}
683
684pub fn resolve_face_by_point(solid: &BrepSolid, point: Vec3) -> Result<u64, KernelRefusal> {
687 let scale = solid_model_scale(&solid);
688 let mut best: Option<(u64, f64)> = None;
689 for shell in &solid.shells {
690 for face in &shell.faces {
691 let Ok(projection) = crate::project_point_to_surface(&face.surface, point) else {
692 continue;
693 };
694 if best
695 .map(|(_, known)| projection.distance < known)
696 .unwrap_or(true)
697 {
698 best = Some((face.id, projection.distance));
699 }
700 }
701 }
702 match best {
703 Some((face_id, distance)) if distance <= (scale * 1e-3).max(1e-4) => Ok(face_id),
704 Some((_, distance)) => Err(KernelRefusal::input(
705 KernelStage::Collect,
706 "point_off_faces",
707 format!(
708 "delete_face_and_heal: no face within tolerance of the point (nearest {distance:.6})"
709 ),
710 )),
711 None => Err(KernelRefusal::input(
712 KernelStage::Collect,
713 "no_faces",
714 "delete_face_and_heal: solid has no faces",
715 )),
716 }
717}
718