1use geop_core_geometry::{
27 nurb_curve::{NurbCurve2D, NurbCurve3D},
28 search::PROJECT_ITERATIONS,
29};
30use geop_core_math::{
31 geop_error::{GeopError, GeopResult, WithContext},
32 scalars::Scalar,
33 vector::{Vector2, Vector3},
34 with_context,
35};
36use geop_core_topology::{
37 Body, FaceId, Sense,
38 build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
39};
40use geop_ops_model::{BodyNames, NamedModel, Namer};
41
42use crate::rebuild::rebuilt;
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum Heal {
47 Open,
49 Extend,
52}
53
54pub fn delete_faces(
60 part: &mut NamedModel,
61 namer: &Namer,
62 faces: &[FaceId],
63 whole_feature: bool,
64 heal: Heal,
65) -> GeopResult<Body> {
66 let ctx = with_context!(
67 "delete_faces({}, faces={faces:?}, heal={heal:?})",
68 namer.root()
69 );
70 let Some(&first) = faces.first() else {
71 return Err(GeopError::new("pick the faces to delete")).with_context(ctx);
72 };
73 let solid = part
74 .solid_of_face(first, "only faces of a solid can be deleted")
75 .with_context(ctx)?;
76 let model = part.topology();
77 let all = model.solid_faces(solid).with_context(ctx)?;
78 if let Some(&f) = faces.iter().find(|f| !all.contains(f)) {
79 return Err(GeopError::new(format!(
80 "face {} is not of the same solid as face {}: delete the faces of one solid at a time",
81 part.name(f)?,
82 part.name(first)?
83 )))
84 .with_context(ctx);
85 }
86 if model.get_solid(solid).with_context(ctx)?.shells.len() != 1 {
87 return Err(GeopError::new(
88 "the solid has a void: deleting its faces is not supported",
89 ))
90 .with_context(ctx);
91 }
92 let (spec, sources) = model.body_spec(&all, true).with_context(ctx)?;
93 let names = BodyNames {
94 solid: Some(namer.root()),
95 ..part.source_names(&sources)?
96 };
97 let mut deleted: Vec<bool> = sources.faces.iter().map(|f| faces.contains(f)).collect();
98 if whole_feature {
99 deleted = feature_of(&spec, &names, &deleted).with_context(ctx)?;
100 }
101 if deleted.iter().all(|&d| d) {
102 return Err(GeopError::new(
103 "every face of the solid is picked: delete the body instead",
104 ))
105 .with_context(ctx);
106 }
107 match heal {
108 Heal::Open => {
109 let mut edited = Edited::new(&spec, &names);
110 edited.delete = deleted;
111 let (sheet, sheet_names, _) = edited.compacted(false)?;
112 part.delete_bodies(&[solid.into()]).with_context(ctx)?;
113 part.build_body(sheet, sheet_names)
114 .with_context(ctx)?
115 .body()
116 .with_context(ctx)
117 }
118 Heal::Extend => {
119 let (healed, healed_names) =
120 healed(&spec, &names, namer, &deleted).with_context(ctx)?;
121 part.replace_with_solid(&[solid.into()], healed, healed_names)
122 .map(Body::Solid)
123 .with_context(ctx)
124 }
125 }
126}
127
128fn feature_of(spec: &BodySpec, names: &BodyNames, picked: &[bool]) -> GeopResult<Vec<bool>> {
136 let mut on_hole = vec![false; spec.edges.len()];
137 for face in &spec.faces {
138 for c in face.holes.iter().flatten() {
139 if let CoedgeOn::Edge(e, _) = c.on {
140 on_hole[e] = true;
141 }
142 }
143 }
144 let uses = spec.edge_uses();
145 let mut feature = picked.to_vec();
146 let mut todo: Vec<usize> = (0..picked.len()).filter(|&f| picked[f]).collect();
147 while let Some(f) = todo.pop() {
148 for c in spec.faces[f].loops().into_iter().flatten() {
149 let CoedgeOn::Edge(e, _) = c.on else { continue };
150 if on_hole[e] {
151 continue;
152 }
153 for &(g, ..) in &uses[e] {
154 if !feature[g] {
155 feature[g] = true;
156 todo.push(g);
157 }
158 }
159 }
160 }
161 if let Some(f) = (0..picked.len()).find(|&f| picked[f])
162 && feature.iter().all(|&inside| inside)
163 {
164 return Err(GeopError::new(format!(
165 "face {} is part of no feature filling a hole of the faces around it: every face of the solid is reached from it",
166 names.faces[f]
167 )));
168 }
169 for (e, at) in uses.iter().enumerate() {
171 let [(f, ..), (g, ..)] = at.as_slice() else {
172 continue;
173 };
174 if feature[*f] == feature[*g] {
175 continue;
176 }
177 let (inside, outside) = if feature[*f] { (*f, *g) } else { (*g, *f) };
178 let on_outside_hole = (spec.faces[outside].holes.iter().flatten())
179 .any(|c| matches!(c.on, CoedgeOn::Edge(x, _) if x == e));
180 if !on_outside_hole {
181 return Err(GeopError::new(format!(
182 "face {} is part of no feature filling a hole of the faces around it: edge {} between it and face {} lies on no hole of that face",
183 names.faces[inside], names.edges[e], names.faces[outside]
184 )));
185 }
186 }
187 Ok(feature)
188}
189
190fn healed(
194 spec: &BodySpec,
195 names: &BodyNames,
196 namer: &Namer,
197 deleted: &[bool],
198) -> GeopResult<(BodySpec, BodyNames)> {
199 let uses = spec.edge_uses();
202 let across = |e: usize| uses[e].iter().any(|&(f, ..)| deleted[f]);
203 let mut closed_holes: Vec<(usize, usize)> = Vec::new();
204 let mut crossing = false;
205 for (f, face) in spec.faces.iter().enumerate() {
206 if deleted[f] {
207 continue;
208 }
209 for (l, lp) in face.loops().into_iter().enumerate() {
210 let along = (lp.iter())
211 .filter(|c| matches!(c.on, CoedgeOn::Edge(e, _) if across(e)))
212 .count();
213 if along == 0 {
214 continue;
215 }
216 if l > 0 && along == lp.len() {
217 closed_holes.push((f, l - 1));
218 } else {
219 crossing = true;
220 }
221 }
222 }
223 let listed = || -> Vec<&str> {
224 (0..spec.faces.len())
225 .filter(|&f| deleted[f])
226 .map(|f| names.faces[f].as_str())
227 .collect()
228 };
229 if !crossing {
230 let mut edited = Edited::new(spec, names);
231 edited.delete = deleted.to_vec();
232 edited.closed = closed_holes;
233 return edited.compacted(true).map(|(spec, names, _)| (spec, names));
234 }
235 if !closed_holes.is_empty() {
236 return Err(GeopError::new(format!(
237 "the faces {:?} both fill holes and cross their neighbours' outer loops: healing over them at once is not supported",
238 listed()
239 )));
240 }
241 let sides = gap_sides(spec, names, deleted)?;
242 let n = sides.len();
243 if !(3..=MOST_SIDES).contains(&n) {
244 return Err(GeopError::new(format!(
245 "the gap the faces {:?} leave has {n} sides: healing over a gap of fewer than 3 or more than {MOST_SIDES} sides is not supported",
246 listed()
247 )));
248 }
249 let mut failures = Vec::new();
250 for triangles in triangulations(n) {
251 let tried = Grown::new(spec, names, namer, deleted, &sides, &triangles)
252 .and_then(|grown| grown.compacted())
253 .and_then(|(grown, grown_names, reseat)| {
254 let replaced = vec![false; grown.faces.len()];
255 Ok((
256 rebuilt(&grown, &grown_names, &replaced, &reseat)?,
257 grown_names,
258 ))
259 });
260 match tried {
261 Ok(done) => return Ok(done),
262 Err(e) => failures.push(format!("{triangles:?}: {}", e.root_message())),
263 }
264 }
265 let around: Vec<&str> = sides.iter().map(|s| names.faces[s.face].as_str()).collect();
266 Err(GeopError::new(format!(
267 "the faces around the gap, {around:?} in order, meet in none of the {} ways they could close it, each three of them at a corner — {}",
268 failures.len(),
269 failures.join("; ")
270 )))
271 .map_err(|e| e.with_context(format!("healing over the faces {:?}", listed())))
272}
273
274const MOST_SIDES: usize = 8;
278
279struct Side {
284 face: usize,
285 lp: usize,
286 run: Vec<usize>,
287 start: usize,
288 end: usize,
289}
290
291fn gap_sides(spec: &BodySpec, names: &BodyNames, deleted: &[bool]) -> GeopResult<Vec<Side>> {
297 let uses = spec.edge_uses();
298 let across = |e: usize| uses[e].iter().any(|&(f, ..)| deleted[f]);
299 let mut sides = Vec::new();
300 for (f, face) in spec.faces.iter().enumerate() {
301 if deleted[f] {
302 continue;
303 }
304 for (l, lp) in face.loops().into_iter().enumerate() {
305 let along: Vec<bool> = (lp.iter())
306 .map(|c| matches!(c.on, CoedgeOn::Edge(e, _) if across(e)))
307 .collect();
308 let n = lp.len();
309 for k in 0..n {
310 if !along[k] || along[(k + n - 1) % n] {
311 continue;
312 }
313 let mut run = Vec::new();
314 let mut j = k;
315 while along[j] && run.len() < n {
316 run.push(j);
317 j = (j + 1) % n;
318 }
319 let start = spec.ends(lp[run[0]].on)?.0;
320 let end = spec.ends(lp[run[run.len() - 1]].on)?.1;
321 sides.push(Side {
322 face: f,
323 lp: l,
324 run,
325 start,
326 end,
327 });
328 }
329 }
330 }
331 let mut ordered: Vec<Side> = Vec::with_capacity(sides.len());
333 let total = sides.len();
334 let Some(first) = sides.pop() else {
335 return Err(GeopError::new(
336 "the faces deleted border no face that stays",
337 ));
338 };
339 ordered.push(first);
340 loop {
341 let end = ordered[ordered.len() - 1].end;
342 let next: Vec<usize> = (0..sides.len())
343 .filter(|&k| sides[k].start == end)
344 .collect();
345 match next.as_slice() {
346 [] if end == ordered[0].start => break,
347 [k] => ordered.push(sides.swap_remove(*k)),
348 _ => {
349 return Err(GeopError::new(format!(
350 "the gap the faces deleted leave touches itself at vertex {}: healing over it is not supported",
351 names.vertices[end]
352 )));
353 }
354 }
355 }
356 if ordered.len() != total {
357 return Err(GeopError::new(format!(
358 "the gap the faces deleted leave has more than one boundary — faces {:?} lie along another: healing over it is not supported",
359 sides
360 .iter()
361 .map(|s| names.faces[s.face].as_str())
362 .collect::<Vec<_>>()
363 )));
364 }
365 Ok(ordered)
366}
367
368fn triangulations(n: usize) -> Vec<Vec<[usize; 3]>> {
372 fn of(corners: &[usize]) -> Vec<Vec<[usize; 3]>> {
373 if corners.len() < 3 {
374 return vec![Vec::new()];
375 }
376 let (first, last) = (corners[0], corners[corners.len() - 1]);
377 let mut out = Vec::new();
378 for m in 1..corners.len() - 1 {
379 for left in of(&corners[..=m]) {
380 for right in of(&corners[m..]) {
381 let mut triangles = vec![[first, corners[m], last]];
382 triangles.extend(left.iter().copied());
383 triangles.extend(right.iter().copied());
384 out.push(triangles);
385 }
386 }
387 }
388 out
389 }
390 of(&(0..n).collect::<Vec<_>>())
391}
392
393struct Grown<'a> {
406 spec: &'a BodySpec,
407 names: &'a BodyNames,
408 vertices: Vec<(Vector3, String, bool)>,
411 vertex: Vec<usize>,
413 edges: Vec<(EdgeSpec, String)>,
415 faces: Vec<(usize, Vec<Vec<CoedgeSpec>>)>,
417}
418
419impl<'a> Grown<'a> {
420 fn new(
421 spec: &'a BodySpec,
422 names: &'a BodyNames,
423 namer: &Namer,
424 deleted: &[bool],
425 sides: &[Side],
426 triangles: &[[usize; 3]],
427 ) -> GeopResult<Self> {
428 let n = sides.len();
429 let face_name = |s: usize| names.faces[sides[s].face].as_str();
430 let adjacent = |a: usize, b: usize| (a + 1) % n == b || (b + 1) % n == a;
432 for t in triangles {
433 for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[0], t[2])] {
434 if !adjacent(a, b) && sides[a].face == sides[b].face {
435 return Err(GeopError::new(format!(
436 "face {} would meet itself across the gap",
437 face_name(a)
438 )));
439 }
440 }
441 }
442 let around = spec.vertex_faces()?;
443 let mut grown = Grown {
444 spec,
445 names,
446 vertices: (spec.vertices.iter().zip(&names.vertices))
447 .map(|(p, name)| (*p, name.clone(), false))
448 .collect(),
449 vertex: (0..spec.vertices.len()).collect(),
450 edges: Vec::new(),
451 faces: Vec::new(),
452 };
453 let corner = |s: usize| sides[s].end;
456 let closes = |t: &[usize; 3], s: usize| t.contains(&s) && t.contains(&((s + 1) % n));
457 let mut corners = Vec::with_capacity(triangles.len());
458 for t in triangles {
459 let closed: Vec<usize> = (0..n).filter(|&s| closes(t, s)).map(corner).collect();
460 let near: Vec<usize> = if closed.is_empty() {
461 (t.iter())
462 .flat_map(|&s| [sides[s].start, sides[s].end])
463 .collect()
464 } else {
465 closed.clone()
466 };
467 let mut sum = Vector3::zero();
468 for &v in &near {
469 sum = sum.add(&spec.vertices[v]);
470 }
471 let share = Scalar::from_ratio(1, near.len() as i64)?;
472 let seed = sum.prod_scalar(share).sharpen();
473 let index = match closed.first() {
474 Some(&keep) => {
475 for &v in &closed {
476 grown.vertex[v] = keep;
477 }
478 keep
479 }
480 None => {
481 let faces: Vec<&str> = t.iter().map(|&s| face_name(s)).collect();
482 grown.vertices.push((seed, namer.name(&faces), true));
483 grown.vertices.len() - 1
484 }
485 };
486 grown.vertices[index].0 = seed;
487 grown.vertices[index].2 = true;
488 corners.push(index);
489 }
490 let uv_on = |s: usize, t: usize| -> GeopResult<Vector2> {
494 let f = sides[s].face;
495 for v in (0..spec.vertices.len()).filter(|&v| grown.vertex[v] == corners[t]) {
496 if let Some(&(_, uv)) = around[v].iter().find(|&&(g, _)| g == f) {
497 return Ok(uv);
498 }
499 }
500 let start = (around[sides[s].start].iter())
501 .find(|&&(g, _)| g == f)
502 .map(|&(_, uv)| uv)
503 .ok_or_else(|| GeopError::new("a side's start is not on its face"))?;
504 let (u, v) = spec.faces[f].surface.project_in_domain(
505 grown.vertices[corners[t]].0,
506 start[0].sharpen(),
507 start[1].sharpen(),
508 PROJECT_ITERATIONS,
509 )?;
510 Ok(Vector2::from_array([u.sharpen(), v.sharpen()]))
511 };
512 let fan = |s: usize| -> Vec<usize> {
516 let offset = |x: usize| (x + n - s) % n;
517 let mut round: Vec<usize> = (0..triangles.len())
518 .filter(|&t| triangles[t].contains(&s))
519 .collect();
520 round
521 .sort_by_key(|&t| std::cmp::Reverse(triangles[t].iter().map(|&x| offset(x)).max()));
522 round
523 };
524 let mut diagonal: Vec<((usize, usize), usize)> = Vec::new();
526 let mut used_names: Vec<usize> = Vec::new();
527 for s in 0..n {
528 let round = fan(s);
529 for pair in round.windows(2) {
530 let (t1, t2) = (pair[0], pair[1]);
531 let x = *(triangles[t1].iter())
532 .find(|&&x| x != s && triangles[t2].contains(&x))
533 .expect("two triangles round a side share another");
534 let key = (s.min(x), s.max(x));
535 if diagonal.iter().any(|(k, _)| *k == key) {
536 continue;
537 }
538 let (a, b) = (grown.vertices[corners[t1]].0, grown.vertices[corners[t2]].0);
539 let single = |side: usize| match sides[side].run.as_slice() {
542 [k] => match spec.faces[sides[side].face].loops()[sides[side].lp][*k].on {
543 CoedgeOn::Edge(e, _) if !used_names.contains(&e) => Some(e),
544 _ => None,
545 },
546 _ => None,
547 };
548 let name = match single(s).or_else(|| single(x)) {
549 Some(e) => {
550 used_names.push(e);
551 names.edges[e].clone()
552 }
553 None => namer.name(&[face_name(key.0), face_name(key.1)]),
554 };
555 grown.edges.push((
556 EdgeSpec {
557 curve: NurbCurve3D::line(a, b)?,
558 start: corners[t1],
559 end: corners[t2],
560 },
561 name,
562 ));
563 diagonal.push((key, spec.edges.len() + grown.edges.len() - 1));
564 }
565 }
566 for (f, face) in spec.faces.iter().enumerate() {
569 if deleted[f] {
570 continue;
571 }
572 let mut loops = Vec::new();
573 for (l, lp) in face.loops().into_iter().enumerate() {
574 let mut kept = Vec::with_capacity(lp.len());
575 for (k, c) in lp.iter().enumerate() {
576 let Some(s) = (0..n).find(|&s| {
577 sides[s].face == f && sides[s].lp == l && sides[s].run.contains(&k)
578 }) else {
579 kept.push(c.clone());
580 continue;
581 };
582 if sides[s].run[0] != k {
583 continue;
584 }
585 let round = fan(s);
586 for pair in round.windows(2) {
587 let (t1, t2) = (pair[0], pair[1]);
588 let x = *(triangles[t1].iter())
589 .find(|&&x| x != s && triangles[t2].contains(&x))
590 .expect("two triangles round a side share another");
591 let e = (diagonal.iter())
592 .find(|(key, _)| *key == (s.min(x), s.max(x)))
593 .map(|&(_, e)| e)
594 .expect("every pair round a fan has its edge");
595 let edge = &grown.edges[e - spec.edges.len()].0;
596 let sense = if edge.start == corners[t1] {
597 Sense::Forward
598 } else {
599 Sense::Reversed
600 };
601 let pcurve = NurbCurve2D::line(uv_on(s, t1)?, uv_on(s, t2)?)?;
602 kept.push(CoedgeSpec {
603 on: CoedgeOn::Edge(e, sense),
604 pcurve,
605 });
606 }
607 }
608 loops.push(kept);
609 }
610 grown.faces.push((f, loops));
611 }
612 Ok(grown)
613 }
614
615 fn compacted(&self) -> GeopResult<(BodySpec, BodyNames, Vec<bool>)> {
618 let old = self.spec.edges.len();
619 let edge = |e: usize| -> (EdgeSpec, &str) {
620 if e < old {
621 let edge = &self.spec.edges[e];
622 let spec = EdgeSpec {
623 curve: edge.curve.clone(),
624 start: self.vertex[edge.start],
625 end: self.vertex[edge.end],
626 };
627 (spec, self.names.edges[e].as_str())
628 } else {
629 let (spec, name) = &self.edges[e - old];
630 (spec.clone(), name.as_str())
631 }
632 };
633 let faces = (self.faces.iter())
634 .map(|(f, loops)| {
635 let loops = (loops.iter())
636 .map(|lp| {
637 (lp.iter())
638 .map(|c| CoedgeSpec {
639 on: match c.on {
640 CoedgeOn::Vertex(v) => CoedgeOn::Vertex(self.vertex[v]),
641 on => on,
642 },
643 pcurve: c.pcurve.clone(),
644 })
645 .collect()
646 })
647 .collect();
648 (*f, loops)
649 })
650 .collect();
651 renumbered(
652 self.spec,
653 self.names,
654 faces,
655 edge,
656 |v| {
657 (
658 self.vertices[v].0,
659 self.vertices[v].1.as_str(),
660 self.vertices[v].2,
661 )
662 },
663 true,
664 )
665 }
666}
667
668fn renumbered<'n>(
674 spec: &BodySpec,
675 names: &BodyNames,
676 faces: Vec<(usize, Vec<Vec<CoedgeSpec>>)>,
677 edge: impl Fn(usize) -> (EdgeSpec, &'n str),
678 vertex: impl Fn(usize) -> (Vector3, &'n str, bool),
679 solid: bool,
680) -> GeopResult<(BodySpec, BodyNames, Vec<bool>)> {
681 let mut edge_index: std::collections::HashMap<usize, usize> = Default::default();
682 let mut vertex_index: std::collections::HashMap<usize, usize> = Default::default();
683 let mut out = BodySpec::new(solid);
684 let mut out_names = BodyNames {
685 solid: names.solid.clone().filter(|_| solid),
686 ..Default::default()
687 };
688 let mut reseat = Vec::new();
689 let mut number_vertex = |v: usize, out: &mut BodySpec, out_names: &mut BodyNames| {
690 *vertex_index.entry(v).or_insert_with(|| {
691 let (point, name, moves) = vertex(v);
692 out.vertices.push(point);
693 out_names.vertices.push(name.to_string());
694 reseat.push(moves);
695 out.vertices.len() - 1
696 })
697 };
698 let mut new_faces = Vec::with_capacity(faces.len());
699 for (f, loops) in faces {
700 let mut renumbered = Vec::with_capacity(loops.len());
701 for lp in loops {
702 let mut coedges = Vec::with_capacity(lp.len());
703 for c in lp {
704 let on = match c.on {
705 CoedgeOn::Edge(e, sense) => {
706 let index = match edge_index.get(&e) {
707 Some(&i) => i,
708 None => {
709 let (mut spec_edge, name) = edge(e);
710 spec_edge.start =
711 number_vertex(spec_edge.start, &mut out, &mut out_names);
712 spec_edge.end =
713 number_vertex(spec_edge.end, &mut out, &mut out_names);
714 out.edges.push(spec_edge);
715 out_names.edges.push(name.to_string());
716 edge_index.insert(e, out.edges.len() - 1);
717 out.edges.len() - 1
718 }
719 };
720 CoedgeOn::Edge(index, sense)
721 }
722 CoedgeOn::Vertex(v) => {
723 CoedgeOn::Vertex(number_vertex(v, &mut out, &mut out_names))
724 }
725 };
726 coedges.push(CoedgeSpec {
727 on,
728 pcurve: c.pcurve,
729 });
730 }
731 renumbered.push(coedges);
732 }
733 if renumbered.iter().any(Vec::is_empty) {
734 return Err(GeopError::new(format!(
735 "a loop of face {} would be left with no edge",
736 names.faces[f]
737 )));
738 }
739 let outer = renumbered.remove(0);
740 new_faces.push(FaceSpec {
741 surface: spec.faces[f].surface.clone(),
742 outer,
743 holes: renumbered,
744 });
745 out_names.faces.push(names.faces[f].clone());
746 }
747 out.faces = new_faces;
748 out.shells = vec![(0..out.faces.len()).collect()];
749 out.check()?;
750 Ok((out, out_names, reseat))
751}
752
753struct Edited<'a> {
756 spec: &'a BodySpec,
757 names: &'a BodyNames,
758 delete: Vec<bool>,
760 closed: Vec<(usize, usize)>,
762}
763
764impl<'a> Edited<'a> {
765 fn new(spec: &'a BodySpec, names: &'a BodyNames) -> Self {
766 Self {
767 spec,
768 names,
769 delete: vec![false; spec.faces.len()],
770 closed: Vec::new(),
771 }
772 }
773
774 fn compacted(&self, solid: bool) -> GeopResult<(BodySpec, BodyNames, Vec<bool>)> {
777 let spec = self.spec;
778 let faces = (spec.faces.iter().enumerate())
779 .filter(|&(f, _)| !self.delete[f])
780 .map(|(f, face)| {
781 let loops = (face.loops().into_iter().enumerate())
782 .filter(|&(l, _)| l == 0 || !self.closed.contains(&(f, l - 1)))
783 .map(|(_, lp)| lp.clone())
784 .collect();
785 (f, loops)
786 })
787 .collect();
788 renumbered(
789 spec,
790 self.names,
791 faces,
792 |e| (spec.edges[e].clone(), self.names.edges[e].as_str()),
793 |v| (spec.vertices[v], self.names.vertices[v].as_str(), false),
794 solid,
795 )
796 }
797}