1use 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) fn retrim_planar_face(
63 face: &mut FaceRecord,
64 plane: &Plane,
65 edges: &HashMap<u64, EdgeRecord>,
66 scale: f64,
67 op: &str,
68) -> Result<(), String> {
69 let mut u_min = f64::INFINITY;
70 let mut u_max = f64::NEG_INFINITY;
71 let mut v_min = f64::INFINITY;
72 let mut v_max = f64::NEG_INFINITY;
73 let record =
74 |point: Vec3, u_min: &mut f64, u_max: &mut f64, v_min: &mut f64, v_max: &mut f64| {
75 let delta = point.sub(plane.origin);
76 let u = delta.dot(plane.u_dir);
77 let v = delta.dot(plane.v_dir);
78 *u_min = u_min.min(u);
79 *u_max = u_max.max(u);
80 *v_min = v_min.min(v);
81 *v_max = v_max.max(v);
82 };
83 for loop_record in &face.loops {
84 for coedge in &loop_record.coedges {
85 let edge = edges
86 .get(&coedge.edge_id)
87 .ok_or_else(|| format!("{op}: missing edge {}", coedge.edge_id))?;
88 for step in 0..=4 {
89 let fraction = step as f64 / 4.0;
90 let t = edge.t0 + (edge.t1 - edge.t0) * fraction;
91 let point = edge.curve.evaluate(t)?;
92 record(point, &mut u_min, &mut u_max, &mut v_min, &mut v_max);
93 }
94 }
95 }
96 if !(u_min.is_finite() && u_max.is_finite() && v_min.is_finite() && v_max.is_finite()) {
97 return Err(format!("{op}: empty face boundary"));
98 }
99 let margin = ((u_max - u_min).max(v_max - v_min) * 0.25).max(scale * 1e-3);
100 let new_origin = plane
101 .origin
102 .add(plane.u_dir.scale(u_min - margin))
103 .add(plane.v_dir.scale(v_min - margin));
104 let width = (u_max - u_min) + 2.0 * margin;
105 let height = (v_max - v_min) + 2.0 * margin;
106 let surface = make_plane(new_origin, plane.u_dir, plane.v_dir, width, height)?;
107 for loop_record in &mut face.loops {
108 for coedge in &mut loop_record.coedges {
109 let edge = edges
110 .get(&coedge.edge_id)
111 .ok_or_else(|| format!("{op}: missing edge {}", coedge.edge_id))?;
112 let mut pcurve = build_pcurve_on_surface(&surface, &edge.curve)?;
113 if !coedge.forward {
114 pcurve = pcurve.reversed()?;
115 }
116 coedge.pcurve = pcurve;
117 }
118 }
119 face.surface = surface;
120 Ok(())
121}
122
123pub(super) fn locate_coedge(face: &FaceRecord, edge_id: u64) -> Option<(usize, usize)> {
126 for (loop_index, loop_record) in face.loops.iter().enumerate() {
127 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
128 if coedge.edge_id == edge_id {
129 return Some((loop_index, coedge_index));
130 }
131 }
132 }
133 None
134}
135
136pub(super) fn coedge_from_vertex(coedge: &CoedgeRecord, edges: &HashMap<u64, EdgeRecord>) -> Option<u64> {
137 let edge = edges.get(&coedge.edge_id)?;
138 Some(if coedge.forward {
139 edge.start_vertex_id
140 } else {
141 edge.end_vertex_id
142 })
143}
144
145pub(super) fn coedge_to_vertex(coedge: &CoedgeRecord, edges: &HashMap<u64, EdgeRecord>) -> Option<u64> {
146 let edge = edges.get(&coedge.edge_id)?;
147 Some(if coedge.forward {
148 edge.end_vertex_id
149 } else {
150 edge.start_vertex_id
151 })
152}
153
154pub fn delete_face_and_heal(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, String> {
159 let mut solid = solid.clone();
160 let scale = solid_scale(&solid);
161 let tolerance = (scale * 1e-7).max(1e-9);
162
163 let (shell_index, face_index) = find_face(&solid, face_id)
164 .ok_or_else(|| format!("delete_face_and_heal: no face with id {face_id}"))?;
165
166 let boundary: Vec<(u64, bool)> = {
168 let face = &solid.shells[shell_index].faces[face_index];
169 if face.loops.len() != 1 {
170 return Err(format!(
171 "delete_face_and_heal: face {face_id} has {} loops; only a simple \
172 single-loop transition face is supported",
173 face.loops.len()
174 ));
175 }
176 face.loops[0]
177 .coedges
178 .iter()
179 .map(|coedge| (coedge.edge_id, coedge.forward))
180 .collect()
181 };
182 if boundary.len() != 4 {
183 return Err(format!(
184 "delete_face_and_heal: face {face_id} has {} boundary edges; only 4-sided \
185 transition faces (a single chamfer/fillet edge) are supported \
186 (deferred: multi-face gaps)",
187 boundary.len()
188 ));
189 }
190 let boundary_edge_ids: HashSet<u64> = boundary.iter().map(|(edge_id, _)| *edge_id).collect();
191 if boundary_edge_ids.len() == 3 {
192 return heal_closed_transition(&solid, shell_index, face_index, &boundary);
196 }
197 if boundary_edge_ids.len() != 4 {
198 return Err(
199 "delete_face_and_heal: transition face uses an edge more than once \
200 (deferred: periodic/closed transition)"
201 .into(),
202 );
203 }
204
205 let mut neighbour_ids = [0u64; 4];
207 for (index, (edge_id, _)) in boundary.iter().enumerate() {
208 neighbour_ids[index] = other_face_of_edge(&solid, *edge_id, face_id)?;
209 }
210 let unique: HashSet<u64> = neighbour_ids.iter().copied().collect();
211 if unique.len() != 4 {
212 return Err(
213 "delete_face_and_heal: the transition face touches a neighbour more \
214 than once (deferred: periodic/closed transition)"
215 .into(),
216 );
217 }
218
219 let neighbour_planes: [Plane; 4] = {
223 let mut planes: Vec<Option<Plane>> = Vec::with_capacity(4);
224 for &neighbour_id in &neighbour_ids {
225 let (ns, nf) = find_face(&solid, neighbour_id)
226 .ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
227 planes.push(
228 plane_of_surface(
229 &solid.shells[ns].faces[nf].surface,
230 (scale * 1e-6).max(1e-7),
231 "delete_face_and_heal",
232 )
233 .ok(),
234 );
235 }
236 if planes.iter().any(Option::is_none) {
237 return heal_open_transition_mixed(
238 &solid,
239 shell_index,
240 face_index,
241 &boundary,
242 &neighbour_ids,
243 );
244 }
245 [
246 planes[0].unwrap(),
247 planes[1].unwrap(),
248 planes[2].unwrap(),
249 planes[3].unwrap(),
250 ]
251 };
252
253 let transition_vertices: Vec<u64> = boundary
255 .iter()
256 .map(|(edge_id, forward)| {
257 let edge = solid
258 .edges
259 .iter()
260 .find(|edge| edge.id == *edge_id)
261 .ok_or_else(|| format!("delete_face_and_heal: missing edge {edge_id}"))?;
262 Ok(if *forward {
263 edge.start_vertex_id
264 } else {
265 edge.end_vertex_id
266 })
267 })
268 .collect::<Result<Vec<u64>, String>>()?;
269 if transition_vertices.iter().collect::<HashSet<_>>().len() != 4 {
270 return Err(
271 "delete_face_and_heal: transition face has repeated corner vertices \
272 (deferred: degenerate transition)"
273 .into(),
274 );
275 }
276 let mut f_center = Vec3::default();
277 for &vertex_id in &transition_vertices {
278 f_center = f_center.add(edge_point(&solid, vertex_id)?);
279 }
280 f_center = f_center.scale(0.25);
281 let mut f_reach = 0.0f64;
282 for &vertex_id in &transition_vertices {
283 f_reach = f_reach.max(edge_point(&solid, vertex_id)?.sub(f_center).length());
284 }
285 let f_reach = f_reach * 3.0 + tolerance;
286
287 let plan = plan_heal(&neighbour_planes, f_center, f_reach)?;
288
289 let mut next_id = max_topology_id(&solid) + 1;
291 let mut alloc = || {
292 let value = next_id;
293 next_id += 1;
294 value
295 };
296
297 let lateral_a = plan.lateral[0];
298 let lateral_b = plan.lateral[1];
299 let point_a = plan.lateral_point[&lateral_a];
300 let point_b = plan.lateral_point[&lateral_b];
301 if point_a.sub(point_b).length() <= tolerance {
302 return Err(
303 "delete_face_and_heal: recovered corners coincide — the neighbours \
304 do not bound a clean edge"
305 .into(),
306 );
307 }
308 let vertex_a = alloc();
309 let vertex_b = alloc();
310 let mut lateral_vertex: HashMap<usize, u64> = HashMap::default();
311 lateral_vertex.insert(lateral_a, vertex_a);
312 lateral_vertex.insert(lateral_b, vertex_b);
313
314 let sharp_edge_id = alloc();
316 let sharp_edge = EdgeRecord {
317 id: sharp_edge_id,
318 curve: make_line(point_a, point_b)?,
319 t0: 0.0,
320 t1: 1.0,
321 start_vertex_id: vertex_a,
322 end_vertex_id: vertex_b,
323 degenerate: false,
324 name: None,
325 };
326
327 let lateral_set: HashSet<usize> = plan.lateral.iter().copied().collect();
332 let mut collapse: HashMap<u64, u64> = HashMap::default();
333 for index in 0..4usize {
334 let previous = (index + 3) % 4;
335 let lateral_index = if lateral_set.contains(&previous) {
336 previous
337 } else if lateral_set.contains(&index) {
338 index
339 } else {
340 return Err(
341 "delete_face_and_heal: transition corner is not flanked by a \
342 lateral face (unexpected neighbour ordering)"
343 .into(),
344 );
345 };
346 let target = lateral_vertex[&lateral_index];
347 collapse.insert(transition_vertices[index], target);
348 }
349 let new_vertex_points: HashMap<u64, Vec3> = [(vertex_a, point_a), (vertex_b, point_b)]
350 .into_iter()
351 .collect();
352
353 for edge in &mut solid.edges {
355 if boundary_edge_ids.contains(&edge.id) {
356 continue;
357 }
358 let start_target = collapse.get(&edge.start_vertex_id).copied();
359 let end_target = collapse.get(&edge.end_vertex_id).copied();
360 if start_target.is_none() && end_target.is_none() {
361 continue;
362 }
363 if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
364 return Err(
365 "delete_face_and_heal: a side edge meeting the transition face is \
366 not a straight line (deferred: curved neighbour edges)"
367 .into(),
368 );
369 }
370 let mut start_point = edge.curve.control_points[0].point()?;
371 let mut end_point = edge.curve.control_points[1].point()?;
372 if let Some(target) = start_target {
373 edge.start_vertex_id = target;
374 start_point = new_vertex_points[&target];
375 }
376 if let Some(target) = end_target {
377 edge.end_vertex_id = target;
378 end_point = new_vertex_points[&target];
379 }
380 if start_point.sub(end_point).length() <= tolerance {
381 return Err(
382 "delete_face_and_heal: healing would collapse a side edge to zero \
383 length (deferred: degenerate transition)"
384 .into(),
385 );
386 }
387 edge.curve = make_line(start_point, end_point)?;
388 edge.t0 = 0.0;
389 edge.t1 = 1.0;
390 }
391
392 let mut edges_by_id: HashMap<u64, EdgeRecord> = solid
394 .edges
395 .iter()
396 .map(|edge| (edge.id, edge.clone()))
397 .collect();
398 edges_by_id.insert(sharp_edge_id, sharp_edge.clone());
399
400 for &primary_index in &plan.primary {
404 let primary_edge = boundary[primary_index].0;
405 let neighbour_id = neighbour_ids[primary_index];
406 let (ns, nf) = find_face(&solid, neighbour_id)
407 .ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
408 let face = &mut solid.shells[ns].faces[nf];
409 let (loop_index, coedge_index) = locate_coedge(face, primary_edge).ok_or_else(|| {
410 format!(
411 "delete_face_and_heal: neighbour {neighbour_id} does not use edge {primary_edge}"
412 )
413 })?;
414 let coedges = &face.loops[loop_index].coedges;
415 let count = coedges.len();
416 let previous = &coedges[(coedge_index + count - 1) % count];
417 let next = &coedges[(coedge_index + 1) % count];
418 let required_from = coedge_to_vertex(previous, &edges_by_id).ok_or_else(|| {
419 "delete_face_and_heal: could not resolve loop connectivity".to_string()
420 })?;
421 let required_to = coedge_from_vertex(next, &edges_by_id).ok_or_else(|| {
422 "delete_face_and_heal: could not resolve loop connectivity".to_string()
423 })?;
424 let forward = if required_from == vertex_a && required_to == vertex_b {
425 true
426 } else if required_from == vertex_b && required_to == vertex_a {
427 false
428 } else {
429 return Err(
430 "delete_face_and_heal: new edge does not close the primary loop \
431 (unexpected connectivity)"
432 .into(),
433 );
434 };
435 let new_coedge = CoedgeRecord {
436 id: alloc(),
437 edge_id: sharp_edge_id,
438 forward,
439 pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0))?,
441 };
442 face.loops[loop_index].coedges[coedge_index] = new_coedge;
443 }
444
445 for &lateral_index in &plan.lateral {
448 let lateral_edge = boundary[lateral_index].0;
449 let neighbour_id = neighbour_ids[lateral_index];
450 let (ns, nf) = find_face(&solid, neighbour_id)
451 .ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
452 let face = &mut solid.shells[ns].faces[nf];
453 let (loop_index, coedge_index) = locate_coedge(face, lateral_edge).ok_or_else(|| {
454 format!(
455 "delete_face_and_heal: neighbour {neighbour_id} does not use edge {lateral_edge}"
456 )
457 })?;
458 face.loops[loop_index].coedges.remove(coedge_index);
459 if face.loops[loop_index].coedges.is_empty() {
460 return Err("delete_face_and_heal: healing emptied a lateral face loop".into());
461 }
462 }
463
464 solid.shells[shell_index]
466 .faces
467 .retain(|face| face.id != face_id);
468 solid
469 .edges
470 .retain(|edge| !boundary_edge_ids.contains(&edge.id));
471 let removed_vertices: HashSet<u64> = transition_vertices.iter().copied().collect();
472 solid.edges.push(sharp_edge);
473 solid
474 .vertices
475 .retain(|vertex| !removed_vertices.contains(&vertex.id));
476 solid.vertices.push(VertexRecord {
477 id: vertex_a,
478 point: point_a,
479 });
480 solid.vertices.push(VertexRecord {
481 id: vertex_b,
482 point: point_b,
483 });
484
485 let final_edges: HashMap<u64, EdgeRecord> = solid
487 .edges
488 .iter()
489 .map(|edge| (edge.id, edge.clone()))
490 .collect();
491 for (index, &neighbour_id) in neighbour_ids.iter().enumerate() {
492 let plane = neighbour_planes[index];
493 let (ns, nf) = find_face(&solid, neighbour_id)
494 .ok_or_else(|| format!("delete_face_and_heal: missing neighbour {neighbour_id}"))?;
495 retrim_planar_face(
496 &mut solid.shells[ns].faces[nf],
497 &plane,
498 &final_edges,
499 scale,
500 "delete_face_and_heal",
501 )?;
502 }
503
504 let issues = solid.validate();
507 if !issues.is_empty() {
508 return Err(format!(
509 "delete_face_and_heal: healed solid failed validation: {issues:?}"
510 ));
511 }
512 Ok(solid)
513}
514
515pub fn resolve_face_by_point(solid: &BrepSolid, point: Vec3) -> Result<u64, String> {
518 let scale = solid_scale(&solid);
519 let mut best: Option<(u64, f64)> = None;
520 for shell in &solid.shells {
521 for face in &shell.faces {
522 let Ok(projection) = crate::project_point_to_surface(&face.surface, point) else {
523 continue;
524 };
525 if best
526 .map(|(_, known)| projection.distance < known)
527 .unwrap_or(true)
528 {
529 best = Some((face.id, projection.distance));
530 }
531 }
532 }
533 match best {
534 Some((face_id, distance)) if distance <= (scale * 1e-3).max(1e-4) => Ok(face_id),
535 Some((_, distance)) => Err(format!(
536 "delete_face_and_heal: no face within tolerance of the point (nearest {distance:.6})"
537 )),
538 None => Err("delete_face_and_heal: solid has no faces".into()),
539 }
540}