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
112pub fn delete_face_and_heal(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, KernelRefusal> {
126 crate::accept_sound(delete_face_and_heal_impl(solid, face_id)?, "deleteFace")
127}
128
129pub(super) fn delete_face_and_heal_impl(
131 solid: &BrepSolid,
132 face_id: u64,
133) -> Result<BrepSolid, KernelRefusal> {
134 let mut solid = solid.clone();
135 let scale = solid_model_scale(&solid);
136 let tolerance = (scale * 1e-7).max(1e-9);
137
138 let (shell_index, face_index) = find_face(&solid, face_id).ok_or_else(|| {
139 KernelRefusal::input(
140 KernelStage::Collect,
141 "face_id",
142 format!("delete_face_and_heal: no face with id {face_id}"),
143 )
144 })?;
145
146 let boundary: Vec<(u64, bool)> = {
148 let face = &solid.shells[shell_index].faces[face_index];
149 if face.loops.len() != 1 {
150 return Err(KernelRefusal::unsupported(
151 KernelStage::Classify,
152 "multi_loop",
153 format!(
154 "delete_face_and_heal: face {face_id} has {} loops; only a simple \
155 single-loop transition face is supported",
156 face.loops.len()
157 ),
158 ));
159 }
160 face.loops[0]
161 .coedges
162 .iter()
163 .map(|coedge| (coedge.edge_id, coedge.forward))
164 .collect()
165 };
166 if boundary.len() != 4 {
167 return Err(KernelRefusal::unsupported(
168 KernelStage::Classify,
169 "side_count",
170 format!(
171 "delete_face_and_heal: face {face_id} has {} boundary edges; only 4-sided \
172 transition faces (a single chamfer/fillet edge) are supported \
173 (deferred: multi-face gaps)",
174 boundary.len()
175 ),
176 ));
177 }
178 let boundary_edge_ids: HashSet<u64> = boundary.iter().map(|(edge_id, _)| *edge_id).collect();
179 if boundary_edge_ids.len() == 3 {
180 return heal_closed_transition(&solid, shell_index, face_index, &boundary)
184 ;
185 }
186 if boundary_edge_ids.len() != 4 {
187 return Err(KernelRefusal::unsupported(
188 KernelStage::Classify,
189 "repeated_edge",
190 "delete_face_and_heal: transition face uses an edge more than once \
191 (deferred: periodic/closed transition)",
192 ));
193 }
194
195 let mut neighbour_ids = [0u64; 4];
197 for (index, (edge_id, _)) in boundary.iter().enumerate() {
198 neighbour_ids[index] =
199 other_face_of_edge(&solid, *edge_id, face_id)?;
200 }
201 let unique: HashSet<u64> = neighbour_ids.iter().copied().collect();
202 if unique.len() != 4 {
203 return Err(KernelRefusal::unsupported(
204 KernelStage::Classify,
205 "repeated_neighbour",
206 "delete_face_and_heal: the transition face touches a neighbour more \
207 than once (deferred: periodic/closed transition)",
208 ));
209 }
210
211 let neighbour_planes: [Plane; 4] = {
215 let mut planes: Vec<Option<Plane>> = Vec::with_capacity(4);
216 for &neighbour_id in &neighbour_ids {
217 let (ns, nf) = neighbour_position(&solid, neighbour_id)?;
218 planes.push(
219 plane_of_surface(
220 &solid.shells[ns].faces[nf].surface,
221 (scale * 1e-6).max(1e-7),
222 "delete_face_and_heal",
223 )
224 .ok(),
225 );
226 }
227 if planes.iter().any(Option::is_none) {
228 return heal_open_transition_mixed(
229 &solid,
230 shell_index,
231 face_index,
232 &boundary,
233 &neighbour_ids,
234 )
235 ;
236 }
237 [
238 planes[0].unwrap(),
239 planes[1].unwrap(),
240 planes[2].unwrap(),
241 planes[3].unwrap(),
242 ]
243 };
244
245 let transition_vertices: Vec<u64> = boundary
247 .iter()
248 .map(|(edge_id, forward)| {
249 let edge = solid
250 .edges
251 .iter()
252 .find(|edge| edge.id == *edge_id)
253 .ok_or_else(|| {
254 KernelRefusal::internal(
255 KernelStage::Collect,
256 "missing_edge",
257 format!("delete_face_and_heal: missing edge {edge_id}"),
258 )
259 })?;
260 Ok(if *forward {
261 edge.start_vertex_id
262 } else {
263 edge.end_vertex_id
264 })
265 })
266 .collect::<Result<Vec<u64>, KernelRefusal>>()?;
267 if transition_vertices.iter().collect::<HashSet<_>>().len() != 4 {
268 return Err(KernelRefusal::unsupported(
269 KernelStage::Classify,
270 "repeated_corner",
271 "delete_face_and_heal: transition face has repeated corner vertices \
272 (deferred: degenerate transition)",
273 ));
274 }
275 let mut f_center = Vec3::default();
276 for &vertex_id in &transition_vertices {
277 f_center = f_center.add(edge_point(&solid, vertex_id)?);
278 }
279 f_center = f_center.scale(0.25);
280 let mut f_reach = 0.0f64;
281 for &vertex_id in &transition_vertices {
282 f_reach = f_reach.max(
283 edge_point(&solid, vertex_id)
284 ?
285 .sub(f_center)
286 .length(),
287 );
288 }
289 let f_reach = f_reach * 3.0 + tolerance;
290
291 if let Some(directory) = census_directory() {
292 record_planar_open_heal(&directory, &solid, face_id);
293 }
294 let plan = plan_heal(&neighbour_planes, f_center, f_reach)?;
295
296 let mut next_id = max_topology_id(&solid) + 1;
298 let mut alloc = || {
299 let value = next_id;
300 next_id += 1;
301 value
302 };
303
304 let lateral_a = plan.lateral[0];
305 let lateral_b = plan.lateral[1];
306 let point_a = plan.lateral_point[&lateral_a];
307 let point_b = plan.lateral_point[&lateral_b];
308 if point_a.sub(point_b).length() <= tolerance {
309 return Err(KernelRefusal::unsupported(
310 KernelStage::Classify,
311 "coincident_corners",
312 "delete_face_and_heal: recovered corners coincide — the neighbours \
313 do not bound a clean edge",
314 ));
315 }
316 let vertex_a = alloc();
317 let vertex_b = alloc();
318 let mut lateral_vertex: HashMap<usize, u64> = HashMap::default();
319 lateral_vertex.insert(lateral_a, vertex_a);
320 lateral_vertex.insert(lateral_b, vertex_b);
321
322 let sharp_edge_id = alloc();
324 let sharp_edge = EdgeRecord {
325 id: sharp_edge_id,
326 curve: make_line(point_a, point_b).or_refuse(KernelStage::Fragment, "make_line")?,
327 t0: 0.0,
328 t1: 1.0,
329 start_vertex_id: vertex_a,
330 end_vertex_id: vertex_b,
331 degenerate: false,
332 name: None,
333 };
334
335 let lateral_set: HashSet<usize> = plan.lateral.iter().copied().collect();
340 let mut collapse: HashMap<u64, u64> = HashMap::default();
341 for index in 0..4usize {
342 let previous = (index + 3) % 4;
343 let lateral_index = if lateral_set.contains(&previous) {
344 previous
345 } else if lateral_set.contains(&index) {
346 index
347 } else {
348 return Err(KernelRefusal::internal(
349 KernelStage::Classify,
350 "corner_flank",
351 "delete_face_and_heal: transition corner is not flanked by a \
352 lateral face (unexpected neighbour ordering)",
353 ));
354 };
355 let target = lateral_vertex[&lateral_index];
356 collapse.insert(transition_vertices[index], target);
357 }
358 let new_vertex_points: HashMap<u64, Vec3> = [(vertex_a, point_a), (vertex_b, point_b)]
359 .into_iter()
360 .collect();
361
362 let unmoved = solid.edges.clone();
364 let mut moved: HashSet<u64> = HashSet::default();
365 for edge in &mut solid.edges {
366 if boundary_edge_ids.contains(&edge.id) {
367 continue;
368 }
369 let start_target = collapse.get(&edge.start_vertex_id).copied();
370 let end_target = collapse.get(&edge.end_vertex_id).copied();
371 if start_target.is_none() && end_target.is_none() {
372 continue;
373 }
374 moved.insert(edge.id);
375 if edge.curve.straight_segment(tolerance).is_none() {
376 return Err(KernelRefusal::unsupported(
377 KernelStage::Classify,
378 "curved_side_edge",
379 "delete_face_and_heal: a side edge meeting the transition face is \
380 not a straight line (deferred: curved neighbour edges)",
381 ));
382 }
383 let mut start_point = edge
391 .curve
392 .evaluate(edge.t0)
393 .or_refuse(KernelStage::Fragment, "evaluate")?;
394 let mut end_point = edge
395 .curve
396 .evaluate(edge.t1)
397 .or_refuse(KernelStage::Fragment, "evaluate")?;
398 if let Some(target) = start_target {
399 edge.start_vertex_id = target;
400 start_point = new_vertex_points[&target];
401 }
402 if let Some(target) = end_target {
403 edge.end_vertex_id = target;
404 end_point = new_vertex_points[&target];
405 }
406 if start_point.sub(end_point).length() <= tolerance {
407 return Err(KernelRefusal::unsupported(
408 KernelStage::Fragment,
409 "collapsed_side_edge",
410 "delete_face_and_heal: healing would collapse a side edge to zero \
411 length (deferred: degenerate transition)",
412 ));
413 }
414 edge.curve = make_line(start_point, end_point).or_refuse(KernelStage::Fragment, "make_line")?;
415 edge.t0 = 0.0;
416 edge.t1 = 1.0;
417 }
418 let retrimmed: HashMap<u64, Plane> =
420 neighbour_ids.iter().copied().zip(neighbour_planes.iter().copied()).collect();
421 moved_edges_lie_on_their_faces(
422 &unmoved,
423 &solid,
424 &moved,
425 &retrimmed,
426 (scale * 1e-6).max(1e-7),
427 "delete_face_and_heal",
428 )
429 ?;
430
431 let mut edges_by_id: HashMap<u64, EdgeRecord> = solid
433 .edges
434 .iter()
435 .map(|edge| (edge.id, edge.clone()))
436 .collect();
437 edges_by_id.insert(sharp_edge_id, sharp_edge.clone());
438
439 for &primary_index in &plan.primary {
443 let primary_edge = boundary[primary_index].0;
444 let neighbour_id = neighbour_ids[primary_index];
445 let (ns, nf) = neighbour_position(&solid, neighbour_id)?;
446 let face = &mut solid.shells[ns].faces[nf];
447 let (loop_index, coedge_index) = locate_coedge(face, primary_edge).ok_or_else(|| {
448 KernelRefusal::internal(
449 KernelStage::Sew,
450 "primary_coedge",
451 format!(
452 "delete_face_and_heal: neighbour {neighbour_id} does not use edge {primary_edge}"
453 ),
454 )
455 })?;
456 let coedges = &face.loops[loop_index].coedges;
457 let count = coedges.len();
458 let previous = &coedges[(coedge_index + count - 1) % count];
459 let next = &coedges[(coedge_index + 1) % count];
460 let required_from = coedge_to_vertex(previous, &edges_by_id).ok_or_else(|| {
461 KernelRefusal::internal(
462 KernelStage::Sew,
463 "loop_connectivity_from",
464 "delete_face_and_heal: could not resolve loop connectivity",
465 )
466 })?;
467 let required_to = coedge_from_vertex(next, &edges_by_id).ok_or_else(|| {
468 KernelRefusal::internal(
469 KernelStage::Sew,
470 "loop_connectivity_to",
471 "delete_face_and_heal: could not resolve loop connectivity",
472 )
473 })?;
474 let forward = if required_from == vertex_a && required_to == vertex_b {
475 true
476 } else if required_from == vertex_b && required_to == vertex_a {
477 false
478 } else {
479 return Err(KernelRefusal::internal(
480 KernelStage::Sew,
481 "primary_loop_closure",
482 "delete_face_and_heal: new edge does not close the primary loop \
483 (unexpected connectivity)",
484 ));
485 };
486 let new_coedge = CoedgeRecord {
487 id: alloc(),
488 edge_id: sharp_edge_id,
489 forward,
490 pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0))
492 .or_refuse(KernelStage::Sew, "make_line")?,
493 };
494 face.loops[loop_index].coedges[coedge_index] = new_coedge;
495 }
496
497 for &lateral_index in &plan.lateral {
500 let lateral_edge = boundary[lateral_index].0;
501 let neighbour_id = neighbour_ids[lateral_index];
502 let (ns, nf) = neighbour_position(&solid, neighbour_id)?;
503 let face = &mut solid.shells[ns].faces[nf];
504 let (loop_index, coedge_index) = locate_coedge(face, lateral_edge).ok_or_else(|| {
505 KernelRefusal::internal(
506 KernelStage::Sew,
507 "lateral_coedge",
508 format!(
509 "delete_face_and_heal: neighbour {neighbour_id} does not use edge {lateral_edge}"
510 ),
511 )
512 })?;
513 face.loops[loop_index].coedges.remove(coedge_index);
514 if face.loops[loop_index].coedges.is_empty() {
515 return Err(KernelRefusal::internal(
516 KernelStage::Sew,
517 "emptied_loop",
518 "delete_face_and_heal: healing emptied a lateral face loop",
519 ));
520 }
521 }
522
523 solid.shells[shell_index]
525 .faces
526 .retain(|face| face.id != face_id);
527 solid
528 .edges
529 .retain(|edge| !boundary_edge_ids.contains(&edge.id));
530 let removed_vertices: HashSet<u64> = transition_vertices.iter().copied().collect();
531 solid.edges.push(sharp_edge);
532 solid
533 .vertices
534 .retain(|vertex| !removed_vertices.contains(&vertex.id));
535 solid.vertices.push(VertexRecord {
536 id: vertex_a,
537 point: point_a,
538 });
539 solid.vertices.push(VertexRecord {
540 id: vertex_b,
541 point: point_b,
542 });
543
544 let final_edges: HashMap<u64, EdgeRecord> = solid
546 .edges
547 .iter()
548 .map(|edge| (edge.id, edge.clone()))
549 .collect();
550 for neighbour_id in neighbour_ids {
551 let plane = &retrimmed[&neighbour_id];
552 let (ns, nf) = neighbour_position(&solid, neighbour_id)?;
553 retrim_planar_face(
554 &mut solid.shells[ns].faces[nf],
555 plane,
556 &final_edges,
557 scale,
558 "delete_face_and_heal",
559 )
560 ?;
561 }
562
563 let issues = solid.validate();
566 if !issues.is_empty() {
567 return Err(KernelRefusal::internal(
568 KernelStage::Validate,
569 "validate",
570 format!("delete_face_and_heal: healed solid failed validation: {issues:?}"),
571 ));
572 }
573 Ok(solid)
574}
575
576pub fn resolve_face_by_point(solid: &BrepSolid, point: Vec3) -> Result<u64, KernelRefusal> {
579 let scale = solid_model_scale(&solid);
580 let mut best: Option<(u64, f64)> = None;
581 for shell in &solid.shells {
582 for face in &shell.faces {
583 let Ok(projection) = crate::project_point_to_surface(&face.surface, point) else {
584 continue;
585 };
586 if best
587 .map(|(_, known)| projection.distance < known)
588 .unwrap_or(true)
589 {
590 best = Some((face.id, projection.distance));
591 }
592 }
593 }
594 match best {
595 Some((face_id, distance)) if distance <= (scale * 1e-3).max(1e-4) => Ok(face_id),
596 Some((_, distance)) => Err(KernelRefusal::input(
597 KernelStage::Collect,
598 "point_off_faces",
599 format!(
600 "delete_face_and_heal: no face within tolerance of the point (nearest {distance:.6})"
601 ),
602 )),
603 None => Err(KernelRefusal::input(
604 KernelStage::Collect,
605 "no_faces",
606 "delete_face_and_heal: solid has no faces",
607 )),
608 }
609}
610