brep_kernel/edit/direct_edit/
delete_face.rs1use super::*;
2
3struct HealPlan {
4 primary: [usize; 2],
6 lateral: [usize; 2],
8 lateral_point: HashMap<usize, Vec3>,
10}
11
12fn plan_heal(planes: &[Plane; 4], f_center: Vec3, f_reach: f64) -> Result<HealPlan, String> {
15 let mut candidates: Vec<HealPlan> = Vec::new();
16 for start in 0..2usize {
17 let primary = [start, start + 2];
18 let lateral = [(start + 1) % 4, (start + 3) % 4];
19 let Some(line) = intersect_planes(&planes[primary[0]], &planes[primary[1]]) else {
20 continue;
21 };
22 let mut lateral_point = HashMap::default();
23 let mut ok = true;
24 for &lat in &lateral {
25 match intersect_line_plane(&line, &planes[lat]) {
26 Some(point) if point.sub(f_center).length() <= f_reach => {
27 lateral_point.insert(lat, point);
28 }
29 _ => {
30 ok = false;
31 break;
32 }
33 }
34 }
35 if !ok {
36 continue;
37 }
38 candidates.push(HealPlan {
39 primary,
40 lateral,
41 lateral_point,
42 });
43 }
44 match candidates.len() {
45 1 => Ok(candidates.pop().unwrap()),
46 0 => Err(
47 "delete_face_and_heal: the neighbours do not re-intersect cleanly \
48 (parallel planes, non-adjacent healing, or a multi-face gap) — refusing \
49 rather than emitting an invalid solid"
50 .into(),
51 ),
52 _ => Err(
53 "delete_face_and_heal: healing is ambiguous — both opposite neighbour \
54 pairs re-intersect through the deleted face"
55 .into(),
56 ),
57 }
58}
59
60pub(super) use crate::offset_retrim::retrim_planar_face;
65
66pub(super) fn locate_coedge(face: &FaceRecord, edge_id: u64) -> Option<(usize, usize)> {
69 for (loop_index, loop_record) in face.loops.iter().enumerate() {
70 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
71 if coedge.edge_id == edge_id {
72 return Some((loop_index, coedge_index));
73 }
74 }
75 }
76 None
77}
78
79pub(super) fn coedge_from_vertex(coedge: &CoedgeRecord, edges: &HashMap<u64, EdgeRecord>) -> Option<u64> {
80 let edge = edges.get(&coedge.edge_id)?;
81 Some(if coedge.forward {
82 edge.start_vertex_id
83 } else {
84 edge.end_vertex_id
85 })
86}
87
88pub(super) fn coedge_to_vertex(coedge: &CoedgeRecord, edges: &HashMap<u64, EdgeRecord>) -> Option<u64> {
89 let edge = edges.get(&coedge.edge_id)?;
90 Some(if coedge.forward {
91 edge.end_vertex_id
92 } else {
93 edge.start_vertex_id
94 })
95}
96
97pub fn delete_face_and_heal(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, String> {
111 crate::accept_sound(delete_face_and_heal_impl(solid, face_id)?, "deleteFace")
112}
113
114pub(super) fn delete_face_and_heal_impl(
116 solid: &BrepSolid,
117 face_id: u64,
118) -> Result<BrepSolid, String> {
119 let mut solid = solid.clone();
120 let scale = solid_model_scale(&solid);
121 let tolerance = (scale * 1e-7).max(1e-9);
122
123 let (shell_index, face_index) = find_face(&solid, face_id)
124 .ok_or_else(|| format!("delete_face_and_heal: no face with id {face_id}"))?;
125
126 let boundary: Vec<(u64, bool)> = {
128 let face = &solid.shells[shell_index].faces[face_index];
129 if face.loops.len() != 1 {
130 return Err(format!(
131 "delete_face_and_heal: face {face_id} has {} loops; only a simple \
132 single-loop transition face is supported",
133 face.loops.len()
134 ));
135 }
136 face.loops[0]
137 .coedges
138 .iter()
139 .map(|coedge| (coedge.edge_id, coedge.forward))
140 .collect()
141 };
142 if boundary.len() != 4 {
143 return Err(format!(
144 "delete_face_and_heal: face {face_id} has {} boundary edges; only 4-sided \
145 transition faces (a single chamfer/fillet edge) are supported \
146 (deferred: multi-face gaps)",
147 boundary.len()
148 ));
149 }
150 let boundary_edge_ids: HashSet<u64> = boundary.iter().map(|(edge_id, _)| *edge_id).collect();
151 if boundary_edge_ids.len() == 3 {
152 return heal_closed_transition(&solid, shell_index, face_index, &boundary);
156 }
157 if boundary_edge_ids.len() != 4 {
158 return Err(
159 "delete_face_and_heal: transition face uses an edge more than once \
160 (deferred: periodic/closed transition)"
161 .into(),
162 );
163 }
164
165 let mut neighbour_ids = [0u64; 4];
167 for (index, (edge_id, _)) in boundary.iter().enumerate() {
168 neighbour_ids[index] = other_face_of_edge(&solid, *edge_id, face_id)?;
169 }
170 let unique: HashSet<u64> = neighbour_ids.iter().copied().collect();
171 if unique.len() != 4 {
172 return Err(
173 "delete_face_and_heal: the transition face touches a neighbour more \
174 than once (deferred: periodic/closed transition)"
175 .into(),
176 );
177 }
178
179 let neighbour_planes: [Plane; 4] = {
183 let mut planes: Vec<Option<Plane>> = Vec::with_capacity(4);
184 for &neighbour_id in &neighbour_ids {
185 let (ns, nf) = find_face(&solid, neighbour_id)
186 .ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
187 planes.push(
188 plane_of_surface(
189 &solid.shells[ns].faces[nf].surface,
190 (scale * 1e-6).max(1e-7),
191 "delete_face_and_heal",
192 )
193 .ok(),
194 );
195 }
196 if planes.iter().any(Option::is_none) {
197 return heal_open_transition_mixed(
198 &solid,
199 shell_index,
200 face_index,
201 &boundary,
202 &neighbour_ids,
203 );
204 }
205 [
206 planes[0].unwrap(),
207 planes[1].unwrap(),
208 planes[2].unwrap(),
209 planes[3].unwrap(),
210 ]
211 };
212
213 let transition_vertices: Vec<u64> = boundary
215 .iter()
216 .map(|(edge_id, forward)| {
217 let edge = solid
218 .edges
219 .iter()
220 .find(|edge| edge.id == *edge_id)
221 .ok_or_else(|| format!("delete_face_and_heal: missing edge {edge_id}"))?;
222 Ok(if *forward {
223 edge.start_vertex_id
224 } else {
225 edge.end_vertex_id
226 })
227 })
228 .collect::<Result<Vec<u64>, String>>()?;
229 if transition_vertices.iter().collect::<HashSet<_>>().len() != 4 {
230 return Err(
231 "delete_face_and_heal: transition face has repeated corner vertices \
232 (deferred: degenerate transition)"
233 .into(),
234 );
235 }
236 let mut f_center = Vec3::default();
237 for &vertex_id in &transition_vertices {
238 f_center = f_center.add(edge_point(&solid, vertex_id)?);
239 }
240 f_center = f_center.scale(0.25);
241 let mut f_reach = 0.0f64;
242 for &vertex_id in &transition_vertices {
243 f_reach = f_reach.max(edge_point(&solid, vertex_id)?.sub(f_center).length());
244 }
245 let f_reach = f_reach * 3.0 + tolerance;
246
247 if let Some(directory) = census_directory() {
248 record_planar_open_heal(&directory, &solid, face_id);
249 }
250 let plan = plan_heal(&neighbour_planes, f_center, f_reach)?;
251
252 let mut next_id = max_topology_id(&solid) + 1;
254 let mut alloc = || {
255 let value = next_id;
256 next_id += 1;
257 value
258 };
259
260 let lateral_a = plan.lateral[0];
261 let lateral_b = plan.lateral[1];
262 let point_a = plan.lateral_point[&lateral_a];
263 let point_b = plan.lateral_point[&lateral_b];
264 if point_a.sub(point_b).length() <= tolerance {
265 return Err(
266 "delete_face_and_heal: recovered corners coincide — the neighbours \
267 do not bound a clean edge"
268 .into(),
269 );
270 }
271 let vertex_a = alloc();
272 let vertex_b = alloc();
273 let mut lateral_vertex: HashMap<usize, u64> = HashMap::default();
274 lateral_vertex.insert(lateral_a, vertex_a);
275 lateral_vertex.insert(lateral_b, vertex_b);
276
277 let sharp_edge_id = alloc();
279 let sharp_edge = EdgeRecord {
280 id: sharp_edge_id,
281 curve: make_line(point_a, point_b)?,
282 t0: 0.0,
283 t1: 1.0,
284 start_vertex_id: vertex_a,
285 end_vertex_id: vertex_b,
286 degenerate: false,
287 name: None,
288 };
289
290 let lateral_set: HashSet<usize> = plan.lateral.iter().copied().collect();
295 let mut collapse: HashMap<u64, u64> = HashMap::default();
296 for index in 0..4usize {
297 let previous = (index + 3) % 4;
298 let lateral_index = if lateral_set.contains(&previous) {
299 previous
300 } else if lateral_set.contains(&index) {
301 index
302 } else {
303 return Err(
304 "delete_face_and_heal: transition corner is not flanked by a \
305 lateral face (unexpected neighbour ordering)"
306 .into(),
307 );
308 };
309 let target = lateral_vertex[&lateral_index];
310 collapse.insert(transition_vertices[index], target);
311 }
312 let new_vertex_points: HashMap<u64, Vec3> = [(vertex_a, point_a), (vertex_b, point_b)]
313 .into_iter()
314 .collect();
315
316 let unmoved = solid.edges.clone();
318 let mut moved: HashSet<u64> = HashSet::default();
319 for edge in &mut solid.edges {
320 if boundary_edge_ids.contains(&edge.id) {
321 continue;
322 }
323 let start_target = collapse.get(&edge.start_vertex_id).copied();
324 let end_target = collapse.get(&edge.end_vertex_id).copied();
325 if start_target.is_none() && end_target.is_none() {
326 continue;
327 }
328 moved.insert(edge.id);
329 if edge.curve.straight_segment(tolerance).is_none() {
330 return Err(
331 "delete_face_and_heal: a side edge meeting the transition face is \
332 not a straight line (deferred: curved neighbour edges)"
333 .into(),
334 );
335 }
336 let mut start_point = edge.curve.evaluate(edge.t0)?;
344 let mut end_point = edge.curve.evaluate(edge.t1)?;
345 if let Some(target) = start_target {
346 edge.start_vertex_id = target;
347 start_point = new_vertex_points[&target];
348 }
349 if let Some(target) = end_target {
350 edge.end_vertex_id = target;
351 end_point = new_vertex_points[&target];
352 }
353 if start_point.sub(end_point).length() <= tolerance {
354 return Err(
355 "delete_face_and_heal: healing would collapse a side edge to zero \
356 length (deferred: degenerate transition)"
357 .into(),
358 );
359 }
360 edge.curve = make_line(start_point, end_point)?;
361 edge.t0 = 0.0;
362 edge.t1 = 1.0;
363 }
364 let retrimmed: HashMap<u64, Plane> =
366 neighbour_ids.iter().copied().zip(neighbour_planes.iter().copied()).collect();
367 moved_edges_lie_on_their_faces(
368 &unmoved,
369 &solid,
370 &moved,
371 &retrimmed,
372 (scale * 1e-6).max(1e-7),
373 "delete_face_and_heal",
374 )?;
375
376 let mut edges_by_id: HashMap<u64, EdgeRecord> = solid
378 .edges
379 .iter()
380 .map(|edge| (edge.id, edge.clone()))
381 .collect();
382 edges_by_id.insert(sharp_edge_id, sharp_edge.clone());
383
384 for &primary_index in &plan.primary {
388 let primary_edge = boundary[primary_index].0;
389 let neighbour_id = neighbour_ids[primary_index];
390 let (ns, nf) = find_face(&solid, neighbour_id)
391 .ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
392 let face = &mut solid.shells[ns].faces[nf];
393 let (loop_index, coedge_index) = locate_coedge(face, primary_edge).ok_or_else(|| {
394 format!(
395 "delete_face_and_heal: neighbour {neighbour_id} does not use edge {primary_edge}"
396 )
397 })?;
398 let coedges = &face.loops[loop_index].coedges;
399 let count = coedges.len();
400 let previous = &coedges[(coedge_index + count - 1) % count];
401 let next = &coedges[(coedge_index + 1) % count];
402 let required_from = coedge_to_vertex(previous, &edges_by_id).ok_or_else(|| {
403 "delete_face_and_heal: could not resolve loop connectivity".to_string()
404 })?;
405 let required_to = coedge_from_vertex(next, &edges_by_id).ok_or_else(|| {
406 "delete_face_and_heal: could not resolve loop connectivity".to_string()
407 })?;
408 let forward = if required_from == vertex_a && required_to == vertex_b {
409 true
410 } else if required_from == vertex_b && required_to == vertex_a {
411 false
412 } else {
413 return Err(
414 "delete_face_and_heal: new edge does not close the primary loop \
415 (unexpected connectivity)"
416 .into(),
417 );
418 };
419 let new_coedge = CoedgeRecord {
420 id: alloc(),
421 edge_id: sharp_edge_id,
422 forward,
423 pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0))?,
425 };
426 face.loops[loop_index].coedges[coedge_index] = new_coedge;
427 }
428
429 for &lateral_index in &plan.lateral {
432 let lateral_edge = boundary[lateral_index].0;
433 let neighbour_id = neighbour_ids[lateral_index];
434 let (ns, nf) = find_face(&solid, neighbour_id)
435 .ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
436 let face = &mut solid.shells[ns].faces[nf];
437 let (loop_index, coedge_index) = locate_coedge(face, lateral_edge).ok_or_else(|| {
438 format!(
439 "delete_face_and_heal: neighbour {neighbour_id} does not use edge {lateral_edge}"
440 )
441 })?;
442 face.loops[loop_index].coedges.remove(coedge_index);
443 if face.loops[loop_index].coedges.is_empty() {
444 return Err("delete_face_and_heal: healing emptied a lateral face loop".into());
445 }
446 }
447
448 solid.shells[shell_index]
450 .faces
451 .retain(|face| face.id != face_id);
452 solid
453 .edges
454 .retain(|edge| !boundary_edge_ids.contains(&edge.id));
455 let removed_vertices: HashSet<u64> = transition_vertices.iter().copied().collect();
456 solid.edges.push(sharp_edge);
457 solid
458 .vertices
459 .retain(|vertex| !removed_vertices.contains(&vertex.id));
460 solid.vertices.push(VertexRecord {
461 id: vertex_a,
462 point: point_a,
463 });
464 solid.vertices.push(VertexRecord {
465 id: vertex_b,
466 point: point_b,
467 });
468
469 let final_edges: HashMap<u64, EdgeRecord> = solid
471 .edges
472 .iter()
473 .map(|edge| (edge.id, edge.clone()))
474 .collect();
475 for neighbour_id in neighbour_ids {
476 let plane = &retrimmed[&neighbour_id];
477 let (ns, nf) = find_face(&solid, neighbour_id)
478 .ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
479 retrim_planar_face(
480 &mut solid.shells[ns].faces[nf],
481 plane,
482 &final_edges,
483 scale,
484 "delete_face_and_heal",
485 )?;
486 }
487
488 let issues = solid.validate();
491 if !issues.is_empty() {
492 return Err(format!(
493 "delete_face_and_heal: healed solid failed validation: {issues:?}"
494 ));
495 }
496 Ok(solid)
497}
498
499pub fn resolve_face_by_point(solid: &BrepSolid, point: Vec3) -> Result<u64, String> {
502 let scale = solid_model_scale(&solid);
503 let mut best: Option<(u64, f64)> = None;
504 for shell in &solid.shells {
505 for face in &shell.faces {
506 let Ok(projection) = crate::project_point_to_surface(&face.surface, point) else {
507 continue;
508 };
509 if best
510 .map(|(_, known)| projection.distance < known)
511 .unwrap_or(true)
512 {
513 best = Some((face.id, projection.distance));
514 }
515 }
516 }
517 match best {
518 Some((face_id, distance)) if distance <= (scale * 1e-3).max(1e-4) => Ok(face_id),
519 Some((_, distance)) => Err(format!(
520 "delete_face_and_heal: no face within tolerance of the point (nearest {distance:.6})"
521 )),
522 None => Err("delete_face_and_heal: solid has no faces".into()),
523 }
524}
525