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