1use std::collections::HashMap;
90
91use geop_core_geometry::{
92 intersection::curve_surface_overlaps,
93 nurb_curve::{NurbCurve2D, NurbCurve3D, loop_signed_area},
94 nurb_surface::NurbSurface3D,
95 search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size},
96};
97use geop_core_math::{
98 geop_error::{GeopError, GeopResult, WithContext},
99 scalars::Scalar,
100 solvers::walk::{Walked, walk},
101 vector::{Vector2, Vector3},
102 with_context,
103};
104use geop_core_topology::{
105 Body, Curve2, Curve3, FaceId, Model, Sense, ShellId, SolidId,
106 build::{BodySpec, BuiltBody, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
107 contains::face::{PointClassification, pcurves_contain},
108 validation::{ValidationParameters, body_crossing},
109};
110use geop_lib_booleans::{RemeshParams, boolean};
111use geop_ops_model::{BodyNames, BooleanOp, NamedModel, Namer};
112
113use crate::rebuild::{Placed, along, room, smooth_along};
114
115const SEED: u64 = 0x5E11_0000_0000_0001;
119
120pub fn shell(
135 part: &mut NamedModel,
136 namer: &Namer,
137 solid: SolidId,
138 open: &[FaceId],
139 thickness: Scalar,
140 thicker: &[(FaceId, Scalar)],
141) -> GeopResult<SolidId> {
142 let ctx = with_context!(
143 "shell({}, {solid}, open={open:?}, {thickness:?}, thicker={thicker:?})",
144 namer.root()
145 );
146 let outward = thickness.definitely_less(Scalar::ZERO);
149 let sign = |t: Scalar| if outward { t.neg() } else { t };
150 for &t in std::iter::once(&thickness).chain(thicker.iter().map(|(_, t)| t)) {
151 if !sign(t).definitely_greater(Scalar::ZERO) {
152 return Err(GeopError::new(format!(
153 "a shell's walls need a thickness other than zero, all inward or all outward, not {t:?}"
154 )))
155 .with_context(ctx);
156 }
157 }
158 let model = part.topology();
159 let faces = model.solid_faces(solid).with_context(ctx)?;
160 if let Some(f) = (open.iter())
161 .chain(thicker.iter().map(|(f, _)| f))
162 .find(|f| !faces.contains(f))
163 {
164 return Err(GeopError::new(format!(
165 "face {f} is not a face of the solid being shelled"
166 )))
167 .with_context(ctx);
168 }
169 let mut shells = Vec::new();
173 for &s in &model.get_solid(solid).with_context(ctx)?.shells {
174 shells.push(model.get_shell(s).with_context(ctx)?.faces.clone());
175 }
176 if let Some(&f) = open.iter().find(|f| !shells[0].contains(f)) {
177 return Err(GeopError::new(format!(
178 "face {} bounds a void of the solid: opening the wall round a void is not supported",
179 part.name(f)?
180 )))
181 .with_context(ctx);
182 }
183 let thick = |f: &FaceId| {
186 sign(
187 (thicker.iter())
188 .find(|(g, _)| g == f)
189 .map_or(thickness, |&(_, t)| t),
190 )
191 };
192 let mut bodies: Vec<(BodySpec, BodyNames)> = Vec::new();
193 let mut inner_of_opening = None;
194 for (k, faces) in shells.iter().enumerate() {
195 let (mut spec, sources) = model.body_spec(faces, true).with_context(ctx)?;
196 if outward {
197 for face in &mut spec.faces {
198 *face = face.reversed();
199 }
200 }
201 let names = part.source_names(&sources).with_context(ctx)?;
202 let removed: Vec<bool> = sources.faces.iter().map(|f| open.contains(f)).collect();
203 let thickness: Vec<Scalar> = sources.faces.iter().map(thick).collect();
204 let corner = inward_corner(&spec, &removed).with_context(ctx)?;
205 if let Some((f, g, e)) = corner.filter(|_| outward) {
206 return Err(GeopError::new(format!(
207 "face {} is taken away where it meets face {} at an inward corner, along edge {}, and the walls grow outward: not supported",
208 names.faces[f], names.faces[g], names.edges[e]
209 )))
210 .with_context(ctx);
211 }
212 let hollow = Hollow::new(&spec, &names, removed, &thickness).with_context(ctx)?;
213 if corner.is_some() {
214 inner_of_opening = Some(hollow.inner_side(namer).with_context(ctx)?);
215 continue;
216 }
217 let (mut result, mut result_names) = hollow.result(namer).with_context(ctx)?;
218 if outward != (k > 0) {
223 result.shells.reverse();
224 }
225 if k > 0 {
226 result_names.solid = Some(namer.name(&["void", &k.to_string()]));
227 }
228 bodies.push((result, result_names));
229 }
230 refuse_crossing(&bodies).with_context(ctx)?;
231 let mut walls = bodies.into_iter();
232 let shelled = match inner_of_opening {
233 Some(inner) => subtracted(part, namer, solid, inner).with_context(ctx)?,
234 None => {
235 let (result, names) = walls.next().expect("the outside's walls");
236 part.replace_with_solid(&[solid.into()], result, names)
237 .with_context(ctx)?
238 }
239 };
240 for (result, names) in walls {
241 part.build_body(result, names).with_context(ctx)?;
242 }
243 Ok(shelled)
244}
245
246pub fn thicken(
256 part: &mut NamedModel,
257 namer: &Namer,
258 sheet: ShellId,
259 thickness: Scalar,
260 along_normal: bool,
261) -> GeopResult<SolidId> {
262 let ctx = with_context!(
263 "thicken({}, sheet {sheet}, {thickness:?}, along_normal={along_normal})",
264 namer.root()
265 );
266 if !thickness.definitely_greater(Scalar::ZERO) {
267 return Err(GeopError::new(format!(
268 "a thickness greater than zero is needed, not {thickness:?}"
269 )))
270 .with_context(ctx);
271 }
272 let model = part.topology();
273 let faces = model.body_faces(Body::Sheet(sheet)).with_context(ctx)?;
274 let (mut spec, sources) = model.body_spec(&faces, false).with_context(ctx)?;
275 if along_normal {
276 for face in &mut spec.faces {
278 *face = face.reversed();
279 }
280 }
281 let names = part.source_names(&sources).with_context(ctx)?;
282 let removed = vec![false; spec.faces.len()];
283 let thickness = vec![thickness; spec.faces.len()];
284 let hollow = Hollow::new(&spec, &names, removed, &thickness).with_context(ctx)?;
285 let (result, result_names) = hollow.result(namer).with_context(ctx)?;
286 let bodies = [(result, result_names)];
287 refuse_crossing(&bodies).with_context(ctx)?;
288 let [(result, result_names)] = bodies;
289 part.replace_with_solid(&[Body::Sheet(sheet)], result, result_names)
290 .with_context(ctx)
291}
292
293pub fn offset_faces(
301 part: &mut NamedModel,
302 namer: &Namer,
303 faces: &[FaceId],
304 distance: Scalar,
305) -> GeopResult<BuiltBody> {
306 let ctx = with_context!("offset_faces({}, {faces:?}, {distance:?})", namer.root());
307 if distance.could_be_equal(Scalar::ZERO) {
308 return Err(GeopError::new(format!(
309 "an offset needs a distance other than zero, not {distance:?}"
310 )))
311 .with_context(ctx);
312 }
313 let (spec, sources) = part.topology().body_spec(faces, false).with_context(ctx)?;
314 let names = part.source_names(&sources).with_context(ctx)?;
315 let removed = vec![false; spec.faces.len()];
316 let thickness = vec![distance.neg(); spec.faces.len()];
317 let hollow = Hollow::new(&spec, &names, removed, &thickness).with_context(ctx)?;
318 let bodies = [hollow.inner_side(namer).with_context(ctx)?];
319 refuse_crossing(&bodies).with_context(ctx)?;
320 let [(result, result_names)] = bodies;
321 part.build_body(result, result_names).with_context(ctx)
322}
323
324fn refuse_crossing(bodies: &[(BodySpec, BodyNames)]) -> GeopResult<()> {
332 let mut model = Model::new();
333 let mut built = Vec::new();
334 for (spec, _) in bodies {
335 built.push(model.build_body(spec.clone())?);
336 }
337 let params = ValidationParameters::default();
338 let all = built
339 .iter()
340 .map(|b| b.body())
341 .collect::<GeopResult<Vec<_>>>()?;
342 let Some((edge, face, point)) = body_crossing(¶ms, &model, &all)? else {
343 return Ok(());
344 };
345 let name = |find: &dyn Fn(&BuiltBody) -> Option<usize>, of: fn(&BodyNames) -> &Vec<String>| {
346 (built.iter().zip(bodies))
347 .find_map(|(b, (_, names))| find(b).map(|i| of(names)[i].clone()))
348 .expect("built from these")
349 };
350 Err(GeopError::new(format!(
351 "the walls run into each other: edge {} crosses face {} at {point:?}: the walls are too thick for this part, which is not supported",
352 name(&|b| b.edges.iter().position(|&e| e == edge), |n| &n.edges),
353 name(&|b| b.faces.iter().position(|&f| f == face), |n| &n.faces),
354 )))
355}
356
357fn subtracted(
363 part: &mut NamedModel,
364 namer: &Namer,
365 solid: SolidId,
366 (mut inner, mut names): (BodySpec, BodyNames),
367) -> GeopResult<SolidId> {
368 inner.solid = true;
369 names.solid = Some(namer.name(&["inner"]));
370 let inner = part
371 .build_body(inner, names)
372 .map_err(|e| e.with_context("closing the inner side into a solid"))?
373 .solid
374 .ok_or_else(|| GeopError::new("the inner side closes into no solid"))?;
375 boolean(
376 part,
377 namer,
378 solid,
379 inner,
380 BooleanOp::Difference,
381 RemeshParams::default(),
382 )
383 .map_err(|e| e.with_context("the solid less its inner side"))?
384 .ok_or_else(|| GeopError::new("the walls leave nothing of the solid"))
385}
386
387fn inward_corner(spec: &BodySpec, removed: &[bool]) -> GeopResult<Option<(usize, usize, usize)>> {
399 for (e, at) in spec.edge_uses().iter().enumerate() {
400 let [(f, f_sense, f_pcurve), (g, _, g_pcurve)] = match *at.as_slice() {
401 [a, b] if removed[a.0] && !removed[b.0] => [a, b],
402 [a, b] if removed[b.0] && !removed[a.0] => [b, a],
403 _ => continue,
404 };
405 let curve = &spec.edges[e].curve;
406 let t = curve.mid_parameter()?;
407 let point = curve.evaluate(t)?;
408 let tangent = curve.tangent(t)?;
409 let along = match f_sense {
410 Sense::Forward => tangent,
411 Sense::Reversed => tangent.neg(),
412 };
413 let normal = |face: usize, pcurve: &Curve2| -> GeopResult<Vector3> {
414 let surface = &spec.faces[face].surface;
415 let seed = pcurve.evaluate(pcurve.mid_parameter()?)?;
416 let (u, v) = surface.project(
417 point,
418 seed[0].sharpen(),
419 seed[1].sharpen(),
420 PROJECT_ITERATIONS,
421 )?;
422 surface.normal(u, v)
423 };
424 let (n_f, n_g) = (normal(f, f_pcurve)?, normal(g, g_pcurve)?);
425 if n_f
428 .prod_cross(&along)
429 .prod_dot(&n_g)
430 .definitely_greater(Scalar::ZERO)
431 {
432 return Ok(Some((f, g, e)));
433 }
434 }
435 Ok(None)
436}
437
438#[derive(Clone, Copy, Debug, PartialEq, Eq)]
440enum Copy {
441 Own,
443 Vanishes,
447 As(usize),
451}
452
453struct Contracted {
458 spec: BodySpec,
459 names: BodyNames,
460 kept: Vec<bool>,
462 vertex: Vec<usize>,
464 edge: Vec<Option<(usize, Sense)>>,
466 face: Vec<Option<usize>>,
468}
469
470fn collapse_edges(
486 spec: &BodySpec,
487 names: &BodyNames,
488 collapsed: &[bool],
489 inverted: &[bool],
490 shrunk: &[bool],
491) -> GeopResult<Vec<Copy>> {
492 let uses = spec.edge_uses();
493 let gone = |f: usize| collapsed[f] || inverted[f];
494 let mut copy = vec![Copy::Own; spec.edges.len()];
495 let mut along: Vec<Vec<usize>> = vec![Vec::new(); spec.faces.len()];
496 for (e, at) in uses.iter().enumerate() {
497 let [(f, ..), (g, ..)] = match *at.as_slice() {
498 [a, b] if a.0 != b.0 => [a, b],
499 _ => {
500 if at.iter().any(|&(f, ..)| gone(f)) || shrunk[e] {
501 return Err(GeopError::new(format!(
502 "the walls are too thick: face {} collapses, or the copy of its edge {} shrinks to nothing, and that edge is free or a seam, which is not supported",
503 names.faces[at[0].0], names.edges[e]
504 )));
505 }
506 continue;
507 }
508 };
509 if shrunk[e] {
510 copy[e] = Copy::Vanishes;
511 continue;
512 }
513 match (gone(f), gone(g)) {
514 (false, false) => {}
515 (true, true) => copy[e] = Copy::Vanishes,
516 _ => {
517 let x = if gone(f) { f } else { g };
518 if inverted[x] || smooth_along(spec, &spec.edges[e], f, g)? {
519 along[x].push(e);
520 } else {
521 copy[e] = Copy::Vanishes;
522 }
523 }
524 }
525 }
526 for (f, edges) in along.iter().enumerate() {
527 match *edges.as_slice() {
528 [] => {}
529 [first, second] => copy[second] = Copy::As(first),
530 _ => {
531 return Err(GeopError::new(format!(
532 "the walls are too thick: face {} vanishes — its inner copy collapses or turns around — and {} of its edges to faces that stay are left, where a blend or a chamfer leaves two: not supported",
533 names.faces[f],
534 edges.len()
535 )));
536 }
537 }
538 }
539 Ok(copy)
540}
541
542fn inverted(spec: &BodySpec, kept: &[bool], shrunk: &[bool]) -> Vec<bool> {
550 (spec.faces.iter().enumerate())
551 .map(|(f, face)| {
552 let edges: Vec<usize> = (face.outer.iter())
553 .filter_map(|c| match c.on {
554 CoedgeOn::Edge(e, _) => Some(e),
555 CoedgeOn::Vertex(_) => None,
556 })
557 .collect();
558 kept[f]
559 && face.holes.is_empty()
560 && edges.iter().any(|&e| shrunk[e])
561 && edges.iter().filter(|&&e| !shrunk[e]).count() <= 2
562 })
563 .collect()
564}
565
566fn along_pole(surface: &NurbSurface3D, pcurve: &Curve2) -> GeopResult<bool> {
571 for p in &pcurve.control_points {
572 let p = p.dehomogenized()?;
573 if surface.pole_at(p[0], p[1]).is_none() {
574 return Ok(false);
575 }
576 }
577 Ok(true)
578}
579
580fn contract(
584 spec: &BodySpec,
585 names: &BodyNames,
586 kept: &[bool],
587 gone: &[bool],
588 copy: &[Copy],
589) -> GeopResult<Contracted> {
590 let mut group: Vec<usize> = (0..spec.vertices.len()).collect();
592 fn root(group: &mut [usize], v: usize) -> usize {
593 let mut r = v;
594 while group[r] != r {
595 r = group[r];
596 }
597 group[v] = r;
598 r
599 }
600 for (e, edge) in spec.edges.iter().enumerate() {
601 if copy[e] == Copy::Vanishes {
602 let (a, b) = (root(&mut group, edge.start), root(&mut group, edge.end));
603 group[a.max(b)] = a.min(b);
604 }
605 }
606 let mut out = BodySpec::new(spec.solid);
607 let mut out_names = BodyNames {
608 solid: names.solid.clone(),
609 ..Default::default()
610 };
611 let mut index = vec![None; spec.vertices.len()];
612 let mut vertex = Vec::with_capacity(spec.vertices.len());
613 for v in 0..spec.vertices.len() {
614 let r = root(&mut group, v);
615 vertex.push(*index[r].get_or_insert_with(|| {
616 out.vertices.push(spec.vertices[r]);
617 out_names.vertices.push(names.vertices[r].clone());
618 out.vertices.len() - 1
619 }));
620 }
621 let mut edge = vec![None; spec.edges.len()];
622 for (e, original) in spec.edges.iter().enumerate() {
623 if copy[e] == Copy::Own {
624 edge[e] = Some((out.edges.len(), Sense::Forward));
625 out.edges.push(EdgeSpec {
626 curve: original.curve.clone(),
627 start: vertex[original.start],
628 end: vertex[original.end],
629 });
630 out_names.edges.push(names.edges[e].clone());
631 }
632 }
633 for (e, original) in spec.edges.iter().enumerate() {
634 if let Copy::As(first) = copy[e] {
635 let (i, _) = edge[first].expect("the first edge along a blend has its own copy");
636 let ends = (vertex[original.start], vertex[original.end]);
637 let sense = if ends == (out.edges[i].start, out.edges[i].end) {
638 Sense::Forward
639 } else if ends == (out.edges[i].end, out.edges[i].start) {
640 Sense::Reversed
641 } else {
642 return Err(GeopError::new(format!(
643 "the walls are too thick: edges {} and {} along a collapsing blend do not end where each other's copies do: not supported",
644 names.edges[first], names.edges[e]
645 )));
646 };
647 edge[e] = Some((i, sense));
648 }
649 }
650 let mut face = vec![None; spec.faces.len()];
651 let mut out_kept = Vec::new();
652 for (f, original) in spec.faces.iter().enumerate() {
653 if gone[f] {
654 continue;
655 }
656 let mut loops = Vec::new();
657 for lp in original.loops() {
658 let mut copied = Vec::new();
659 for c in lp {
660 let on = match c.on {
661 CoedgeOn::Edge(e, sense) => match edge[e] {
666 None => CoedgeOn::Vertex(vertex[spec.ends(c.on)?.0]),
667 Some((i, along)) => CoedgeOn::Edge(
668 i,
669 if along == Sense::Forward {
670 sense
671 } else {
672 sense.opposite()
673 },
674 ),
675 },
676 CoedgeOn::Vertex(v) => CoedgeOn::Vertex(vertex[v]),
677 };
678 copied.push(CoedgeSpec {
679 on,
680 pcurve: c.pcurve.clone(),
681 });
682 }
683 if !copied.iter().any(|c| matches!(c.on, CoedgeOn::Edge(..))) {
684 return Err(GeopError::new(format!(
685 "the walls are too thick: every edge of a loop of face {} vanishes, and its inner copy with it",
686 names.faces[f]
687 )));
688 }
689 loops.push(copied);
690 }
691 face[f] = Some(out.faces.len());
692 let mut loops = loops.into_iter();
693 out.faces.push(FaceSpec {
694 surface: original.surface.clone(),
695 outer: loops.next().expect("a face has an outer loop"),
696 holes: loops.collect(),
697 });
698 out_names.faces.push(names.faces[f].clone());
699 out_kept.push(kept[f]);
700 }
701 out.shells = spec
702 .shells
703 .iter()
704 .map(|shell| shell.iter().filter_map(|&f| face[f]).collect())
705 .collect();
706 Ok(Contracted {
707 spec: out,
708 names: out_names,
709 kept: out_kept,
710 vertex,
711 edge,
712 face,
713 })
714}
715
716fn smoothly_open(spec: &BodySpec, names: &BodyNames, removed: &[bool]) -> GeopResult<Vec<bool>> {
725 let mut tangent = vec![0usize; spec.faces.len()];
726 let mut other = vec![0usize; spec.faces.len()];
727 for (e, at) in spec.edge_uses().iter().enumerate() {
728 let [a, b] = match *at.as_slice() {
729 [a, b] if a.0 != b.0 => [a.0, b.0],
730 _ => continue,
731 };
732 match (removed[a], removed[b]) {
733 (false, false) => {}
734 (true, true) => {
735 other[a] += 1;
736 other[b] += 1;
737 }
738 _ => {
739 let gone = if removed[a] { a } else { b };
740 if smooth_along(spec, &spec.edges[e], a, b)? {
741 tangent[gone] += 1;
742 } else {
743 other[gone] += 1;
744 }
745 }
746 }
747 }
748 (0..spec.faces.len())
749 .map(|f| {
750 if tangent[f] == 0 {
751 return Ok(false);
752 }
753 if other[f] > 0 {
754 return Err(GeopError::new(format!(
755 "face {} is taken away tangent to a face kept along {} of its edges, and not along {}: the walls cannot end on a smooth surface, which is not supported",
756 names.faces[f], tangent[f], other[f]
757 )));
758 }
759 Ok(true)
760 })
761 .collect()
762}
763
764#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
767struct At {
768 f: usize,
769 l: usize,
770 k: usize,
771}
772
773type Pieces<T> = (Option<T>, Option<T>);
776
777struct Numbering {
781 vertex: Vec<Option<usize>>,
782 inner_vertex: Vec<Option<usize>>,
783 edge: Vec<Option<usize>>,
784 inner_edge: Vec<Option<usize>>,
785 pieces: Vec<Pieces<usize>>,
786}
787
788fn in_order<T>(pieces: (T, T), sense: Sense) -> (T, T) {
790 match sense {
791 Sense::Forward => pieces,
792 Sense::Reversed => (pieces.1, pieces.0),
793 }
794}
795
796struct Hollow<'a> {
798 spec: &'a BodySpec,
799 names: &'a BodyNames,
800 removed: Vec<bool>,
802 walled: Vec<bool>,
806 open_edge: Vec<bool>,
808 free_edge: Vec<bool>,
811 smooth: Vec<bool>,
814 inner: BodySpec,
819 copies: Contracted,
821 pieces: Vec<Pieces<Curve3>>,
824}
825
826impl<'a> Hollow<'a> {
827 fn new(
828 spec: &'a BodySpec,
829 names: &'a BodyNames,
830 removed: Vec<bool>,
831 thickness: &[Scalar],
832 ) -> GeopResult<Self> {
833 let mut walled = vec![false; spec.vertices.len()];
834 let mut open_edge = vec![true; spec.edges.len()];
835 let mut uses = vec![0usize; spec.edges.len()];
836 for (f, face) in spec.faces.iter().enumerate() {
837 for lp in face.loops() {
838 for c in lp {
839 let (_, end) = spec.ends(c.on)?;
840 walled[end] |= !removed[f];
841 if let CoedgeOn::Edge(e, _) = c.on {
842 open_edge[e] &= removed[f];
843 uses[e] += 1;
844 }
845 }
846 }
847 }
848 let kept: Vec<bool> = removed.iter().map(|&r| !r).collect();
853 let smooth = smoothly_open(spec, names, &removed)?;
854 let mut offsets = spec.clone();
855 let mut collapsed = vec![false; spec.faces.len()];
856 for (f, face) in offsets.faces.iter_mut().enumerate() {
857 if kept[f] {
858 match room(face.surface.clone())?
862 .offset(thickness[f].neg())
863 .map_err(|e| e.with_context(format!("offsetting face {}", names.faces[f])))?
864 {
865 Some(surface) => face.surface = surface,
866 None => collapsed[f] = true,
867 }
868 }
869 }
870 let mut shrunk = vec![false; spec.edges.len()];
875 for (e, at) in offsets.edge_uses().iter().enumerate() {
876 for &(f, _, pcurve) in at {
877 if kept[f] && !collapsed[f] && along_pole(&offsets.faces[f].surface, pcurve)? {
878 shrunk[e] = true;
879 }
880 }
881 }
882 let (copies, inner) = loop {
883 let inverted = inverted(spec, &kept, &shrunk);
884 let copy = collapse_edges(spec, names, &collapsed, &inverted, &shrunk)?;
885 let gone: Vec<bool> = (0..spec.faces.len())
886 .map(|f| collapsed[f] || inverted[f] || smooth[f])
887 .collect();
888 if (0..spec.faces.len()).all(|f| !kept[f] || gone[f]) {
889 return Err(GeopError::new(
890 "the walls are too thick: the inner copy of every face kept turns around or shrinks to nothing",
891 ));
892 }
893 let copies = contract(&offsets, names, &kept, &gone, ©)?;
894 let reseat = vec![false; copies.spec.vertices.len()];
895 let placed = Placed::new(&copies.spec, &copies.names, &copies.kept, &reseat).map_err(|e| {
896 match (shrunk.iter().position(|&t| t), inverted.iter().position(|&i| i)) {
900 (Some(t), Some(f)) => GeopError::new(format!(
901 "the walls are too thick: edge {} turns around, and the inner copy of face {} with it, but the faces either side of it do not meet in its place ({})",
902 names.edges[t],
903 names.faces[f],
904 e.root_message()
905 )),
906 _ => e,
907 }
908 .with_context("placing the walls' inner side's vertices")
909 })?;
910 let now = placed.turned();
911 if now.is_empty() {
912 let inner = placed
913 .rebuilt()
914 .map_err(|e| e.with_context("building the walls' inner side"))?;
915 break (copies, inner);
916 }
917 for (e, copied) in copies.edge.iter().enumerate() {
918 if copied.is_some_and(|(i, _)| now.contains(&i)) {
919 shrunk[e] = true;
920 }
921 }
922 };
923 let mut hollow = Self {
924 spec,
925 names,
926 removed,
927 walled,
928 open_edge,
929 free_edge: (0..spec.edges.len())
932 .map(|e| uses[e] == 1 || spec.edge_uses()[e].iter().any(|&(f, ..)| smooth[f]))
933 .collect(),
934 smooth,
935 inner,
936 copies,
937 pieces: Vec::new(),
938 };
939 hollow.pieces = (0..spec.edges.len())
940 .map(|e| hollow.pieces_of(e))
941 .collect::<GeopResult<_>>()?;
942 Ok(hollow)
943 }
944
945 fn coedges(&self) -> impl Iterator<Item = (At, &'a CoedgeSpec)> + 'a {
947 let spec = self.spec;
948 spec.faces.iter().enumerate().flat_map(|(f, face)| {
949 face.loops()
950 .into_iter()
951 .enumerate()
952 .flat_map(move |(l, lp)| {
953 lp.iter().enumerate().map(move |(k, c)| (At { f, l, k }, c))
954 })
955 })
956 }
957
958 fn previous(&self, at: At) -> At {
960 let n = self.spec.faces[at.f].loops()[at.l].len();
961 At {
962 k: (at.k + n - 1) % n,
963 ..at
964 }
965 }
966
967 fn inner_point(&self, v: usize) -> Vector3 {
968 self.inner.vertices[self.copies.vertex[v]]
969 }
970
971 fn inner_curve(&self, e: usize) -> Option<Curve3> {
974 self.copies.edge[e].map(|(i, sense)| sense.orient(&self.inner.edges[i].curve))
975 }
976
977 fn pieces_of(&self, e: usize) -> GeopResult<Pieces<Curve3>> {
980 if !self.open_edge[e] {
981 return Ok((None, None));
982 }
983 let edge = &self.spec.edges[e];
984 let piece = |from: Vector3, to: Vector3| along(&edge.curve, from, to);
985 let (s, t) = (edge.start, edge.end);
986 Ok((
987 self.walled[s]
988 .then(|| piece(self.spec.vertices[s], self.inner_point(s)))
989 .transpose()?,
990 self.walled[t]
991 .then(|| piece(self.inner_point(t), self.spec.vertices[t]))
992 .transpose()?,
993 ))
994 }
995
996 fn inner_coedge(&self, at: At) -> Option<CoedgeSpec> {
1002 let face = self.copies.face[at.f]?;
1003 let CoedgeOn::Edge(e, sense) = self.spec.faces[at.f].loops()[at.l][at.k].on else {
1004 return None;
1005 };
1006 let (i, along) = self.copies.edge[e]?;
1007 let sense = if along == Sense::Forward {
1008 sense
1009 } else {
1010 sense.opposite()
1011 };
1012 self.inner.faces[face].loops()[at.l]
1013 .iter()
1014 .find(|c| c.on == CoedgeOn::Edge(i, sense))
1015 .cloned()
1016 }
1017
1018 fn inner_end(&self, at: At) -> GeopResult<Vector2> {
1021 let mut here = at;
1022 loop {
1023 if let Some(c) = self.inner_coedge(here) {
1024 return Ok(c.pcurve.ends()?.1);
1025 }
1026 here = self.previous(here);
1027 if here == at {
1028 return Err(GeopError::new("no coedge of the loop has a copy"));
1029 }
1030 }
1031 }
1032
1033 fn result(&self, namer: &Namer) -> GeopResult<(BodySpec, BodyNames)> {
1036 let spec = self.spec;
1037 let mut out = BodySpec::new(true);
1038 let mut names = BodyNames {
1039 solid: Some(namer.root()),
1040 ..Default::default()
1041 };
1042 let inner_name = |name: &str| namer.name(&[name]);
1043
1044 let mut at = Numbering {
1045 vertex: vec![None; spec.vertices.len()],
1046 inner_vertex: vec![None; self.inner.vertices.len()],
1047 edge: vec![None; spec.edges.len()],
1048 inner_edge: vec![None; self.inner.edges.len()],
1049 pieces: vec![(None, None); spec.edges.len()],
1050 };
1051 for v in 0..spec.vertices.len() {
1052 if self.walled[v] {
1053 at.vertex[v] = Some(out.vertices.len());
1054 out.vertices.push(spec.vertices[v]);
1055 names.vertices.push(self.names.vertices[v].clone());
1056 }
1057 }
1058 for v in 0..spec.vertices.len() {
1061 let i = self.copies.vertex[v];
1062 if self.walled[v] && at.inner_vertex[i].is_none() {
1063 at.inner_vertex[i] = Some(out.vertices.len());
1064 out.vertices.push(self.inner.vertices[i]);
1065 names.vertices.push(inner_name(&self.names.vertices[v]));
1066 }
1067 }
1068 let inner_vertex_of = |v: usize| at.inner_vertex[self.copies.vertex[v]];
1069 let inner_vertex: Vec<Option<usize>> =
1070 (0..spec.vertices.len()).map(inner_vertex_of).collect();
1071 let vertex = &at.vertex;
1072 let mut push_edge = |curve: Curve3, start: usize, end: usize, name: String| {
1073 out.edges.push(EdgeSpec { curve, start, end });
1074 names.edges.push(name);
1075 Some(out.edges.len() - 1)
1076 };
1077 for (e, original) in spec.edges.iter().enumerate() {
1078 let name = &self.names.edges[e];
1079 let (s, t) = (original.start, original.end);
1080 if !self.open_edge[e] {
1081 at.edge[e] = push_edge(
1082 original.curve.clone(),
1083 vertex[s].unwrap(),
1084 vertex[t].unwrap(),
1085 name.clone(),
1086 );
1087 if let Some((i, Sense::Forward)) = self.copies.edge[e]
1089 && at.inner_edge[i].is_none()
1090 {
1091 let copy = &self.inner.edges[i];
1092 at.inner_edge[i] = push_edge(
1093 copy.curve.clone(),
1094 at.inner_vertex[copy.start].unwrap(),
1095 at.inner_vertex[copy.end].unwrap(),
1096 inner_name(name),
1097 );
1098 }
1099 }
1100 let (first, second) = &self.pieces[e];
1101 if let Some(curve) = first {
1102 at.pieces[e].0 = push_edge(
1103 curve.clone(),
1104 vertex[s].unwrap(),
1105 inner_vertex[s].unwrap(),
1106 namer.name(&[name, "start"]),
1107 );
1108 }
1109 if let Some(curve) = second {
1110 at.pieces[e].1 = push_edge(
1111 curve.clone(),
1112 inner_vertex[t].unwrap(),
1113 vertex[t].unwrap(),
1114 namer.name(&[name, "end"]),
1115 );
1116 }
1117 }
1118 let map = |on: CoedgeOn, edges: &[Option<usize>], vertices: &[Option<usize>]| match on {
1119 CoedgeOn::Edge(e, sense) => CoedgeOn::Edge(edges[e].unwrap(), sense),
1120 CoedgeOn::Vertex(v) => CoedgeOn::Vertex(vertices[v].unwrap()),
1121 };
1122 let inner_vertex = &inner_vertex;
1123
1124 let mut outer_faces = Vec::new();
1125 let mut inner_faces = Vec::new();
1126 for (f, face) in spec.faces.iter().enumerate() {
1127 let name = &self.names.faces[f];
1128 if self.smooth[f] {
1129 continue;
1130 }
1131 if self.removed[f] {
1132 let rims = self.rims(f, &at)?;
1133 let several = rims.len() > 1;
1134 for (k, rim) in rims.into_iter().enumerate() {
1135 outer_faces.push(out.faces.len());
1136 out.faces.push(rim);
1137 names.faces.push(if several {
1138 namer.name(&[name, &k.to_string()])
1139 } else {
1140 namer.name(&[name])
1141 });
1142 }
1143 continue;
1144 }
1145 let remap = |lp: &Vec<CoedgeSpec>| -> Vec<CoedgeSpec> {
1146 lp.iter()
1147 .map(|c| CoedgeSpec {
1148 on: map(c.on, &at.edge, &at.vertex),
1149 pcurve: c.pcurve.clone(),
1150 })
1151 .collect()
1152 };
1153 outer_faces.push(out.faces.len());
1154 out.faces.push(FaceSpec {
1155 surface: face.surface.clone(),
1156 outer: remap(&face.outer),
1157 holes: face.holes.iter().map(remap).collect(),
1158 });
1159 names.faces.push(name.clone());
1160
1161 let Some(copy) = self.copies.face[f] else {
1162 continue;
1163 };
1164 let copy = &self.inner.faces[copy];
1165 let inner_loop = |lp: &Vec<CoedgeSpec>| -> Vec<CoedgeSpec> {
1166 lp.iter()
1167 .map(|c| CoedgeSpec {
1168 on: map(c.on, &at.inner_edge, &at.inner_vertex),
1169 pcurve: c.pcurve.clone(),
1170 })
1171 .collect()
1172 };
1173 let inner = FaceSpec {
1174 surface: copy.surface.clone(),
1175 outer: inner_loop(©.outer),
1176 holes: copy.holes.iter().map(inner_loop).collect(),
1177 };
1178 inner_faces.push(out.faces.len());
1179 out.faces.push(inner.reversed());
1180 names.faces.push(inner_name(name));
1181 }
1182 let mut side: Vec<Option<usize>> = vec![None; spec.vertices.len()];
1186 let mut walls = Vec::new();
1187 for (here, c) in self.coedges() {
1188 let CoedgeOn::Edge(e, sense) = c.on else {
1189 continue;
1190 };
1191 if !self.free_edge[e] || self.removed[here.f] {
1192 continue;
1193 }
1194 let (x, y) = spec.ends(c.on)?;
1195 for v in [x, y] {
1196 if side[v].is_none() {
1197 side[v] = Some(out.edges.len());
1198 out.edges.push(EdgeSpec {
1199 curve: NurbCurve3D::line(spec.vertices[v], self.inner_point(v))?,
1200 start: at.vertex[v].unwrap(),
1201 end: inner_vertex[v].unwrap(),
1202 });
1203 names
1204 .edges
1205 .push(namer.name(&[&self.names.vertices[v], "side"]));
1206 }
1207 }
1208 let outer = sense.orient(&spec.edges[e].curve);
1209 let inner = sense.orient(&self.inner_curve(e).expect("a free edge has a copy"));
1210 let (copy, along) = self.copies.edge[e].expect("a free edge has a copy");
1211 let copy_sense = if along == Sense::Forward {
1212 sense
1213 } else {
1214 sense.opposite()
1215 };
1216 let lp = [
1220 CoedgeOn::Edge(at.inner_edge[copy].unwrap(), copy_sense),
1221 CoedgeOn::Edge(side[y].unwrap(), Sense::Reversed),
1222 CoedgeOn::Edge(at.edge[e].unwrap(), sense.opposite()),
1223 CoedgeOn::Edge(side[x].unwrap(), Sense::Forward),
1224 ];
1225 walls.push(out.faces.len());
1226 let surface = NurbSurface3D::ruled(&inner, &outer).map_err(|err| {
1227 err.with_context(format!("the wall along edge {}", self.names.edges[e]))
1228 })?;
1229 out.faces.push(FaceSpec::on_unit_square(surface, lp)?);
1230 names
1231 .faces
1232 .push(namer.name(&[&self.names.edges[e], "side"]));
1233 }
1234 out.shells = if self.removed.iter().any(|&r| r) || !walls.is_empty() {
1235 vec![
1236 outer_faces
1237 .into_iter()
1238 .chain(inner_faces)
1239 .chain(walls)
1240 .collect(),
1241 ]
1242 } else {
1243 vec![outer_faces, inner_faces]
1244 };
1245 Ok((out, names))
1246 }
1247
1248 fn inner_side(&self, namer: &Namer) -> GeopResult<(BodySpec, BodyNames)> {
1253 let mut out = self.inner.clone();
1254 out.solid = false;
1255 out.shells = vec![(0..out.faces.len()).collect()];
1256 let names = BodyNames {
1257 vertices: self
1258 .copies
1259 .names
1260 .vertices
1261 .iter()
1262 .map(|n| namer.name(&[n]))
1263 .collect(),
1264 edges: self
1265 .copies
1266 .names
1267 .edges
1268 .iter()
1269 .map(|n| namer.name(&[n]))
1270 .collect(),
1271 faces: self
1272 .copies
1273 .names
1274 .faces
1275 .iter()
1276 .map(|n| namer.name(&[n]))
1277 .collect(),
1278 solid: None,
1279 };
1280 Ok((out, names))
1281 }
1282
1283 fn rims(&self, f: usize, at: &Numbering) -> GeopResult<Vec<FaceSpec>> {
1287 let spec = self.spec;
1288 let vertex = &at.vertex;
1289 let inner_vertex: Vec<Option<usize>> = (0..spec.vertices.len())
1290 .map(|v| at.inner_vertex[self.copies.vertex[v]])
1291 .collect();
1292 let surface = &spec.faces[f].surface;
1293 let unsupported = |v: usize| {
1294 GeopError::new(format!(
1295 "face {} is taken away at vertex {}, where its surface comes to a point: not supported",
1296 self.names.faces[f], self.names.vertices[v]
1297 ))
1298 };
1299 let mut coedges: Vec<(CoedgeSpec, usize, usize)> = Vec::new();
1302 for (here, c) in self.coedges().filter(|(here, _)| here.f == f) {
1303 let CoedgeOn::Edge(e, sense) = c.on else {
1304 let CoedgeOn::Vertex(v) = c.on else {
1305 unreachable!()
1306 };
1307 if self.walled[v] {
1308 return Err(unsupported(v));
1309 }
1310 continue;
1311 };
1312 let (x, y) = spec.ends(c.on)?;
1313 if !self.open_edge[e] {
1314 coedges.push((
1316 CoedgeSpec {
1317 on: CoedgeOn::Edge(at.edge[e].unwrap(), sense),
1318 pcurve: c.pcurve.clone(),
1319 },
1320 vertex[x].unwrap(),
1321 vertex[y].unwrap(),
1322 ));
1323 if let Some(inner) = self.inner_coedge(here) {
1325 let CoedgeOn::Edge(copy, copy_sense) = inner.on else {
1326 unreachable!("the copy of an edge");
1327 };
1328 let (from, to) = self.inner.ends(inner.on)?;
1329 coedges.push((
1330 CoedgeSpec {
1331 on: CoedgeOn::Edge(at.inner_edge[copy].unwrap(), copy_sense.opposite()),
1332 pcurve: inner.pcurve.reverse(),
1333 },
1334 at.inner_vertex[to].unwrap(),
1335 at.inner_vertex[from].unwrap(),
1336 ));
1337 }
1338 continue;
1339 }
1340 let (orig_start, orig_end) = c.pcurve.ends()?;
1343 let (start_piece, end_piece) = in_order(at.pieces[e], sense);
1344 let curves = &self.pieces[e];
1345 let (start_curve, end_curve) = in_order((&curves.0, &curves.1), sense);
1346 if let (Some(piece), Some(curve)) = (start_piece, start_curve) {
1347 let to = self.inner_end(self.previous(here))?;
1348 let curve = sense.orient(curve);
1349 coedges.push((
1350 CoedgeSpec {
1351 on: CoedgeOn::Edge(piece, sense),
1352 pcurve: pcurve(surface, &c.pcurve, Some(&curve), orig_start, to)?,
1353 },
1354 vertex[x].unwrap(),
1355 inner_vertex[x].unwrap(),
1356 ));
1357 }
1358 if let (Some(piece), Some(curve)) = (end_piece, end_curve) {
1359 let from = self.inner_end(here)?;
1360 let curve = sense.orient(curve);
1361 coedges.push((
1362 CoedgeSpec {
1363 on: CoedgeOn::Edge(piece, sense),
1364 pcurve: pcurve(surface, &c.pcurve, Some(&curve), from, orig_end)?,
1365 },
1366 inner_vertex[y].unwrap(),
1367 vertex[y].unwrap(),
1368 ));
1369 }
1370 }
1371
1372 let mut starting: HashMap<usize, usize> = HashMap::new();
1374 for (i, (_, start, _)) in coedges.iter().enumerate() {
1375 if starting.insert(*start, i).is_some() {
1376 return Err(GeopError::new(format!(
1377 "what is left of face {} touches itself at a vertex: not supported",
1378 self.names.faces[f]
1379 )));
1380 }
1381 }
1382 let mut used = vec![false; coedges.len()];
1383 let mut loops: Vec<Vec<usize>> = Vec::new();
1384 for first in 0..coedges.len() {
1385 if used[first] {
1386 continue;
1387 }
1388 let around = walk(first, |&i| {
1392 let next = starting.get(&coedges[i].2).ok_or_else(|| {
1393 GeopError::new(format!(
1394 "what is left of face {} does not close up",
1395 self.names.faces[f]
1396 ))
1397 })?;
1398 Ok::<_, GeopError>(Some(*next))
1399 })?;
1400 if around.end != Walked::Closed {
1401 return Err(GeopError::new(format!(
1402 "what is left of face {} runs into itself",
1403 self.names.faces[f]
1404 )));
1405 }
1406 for &i in &around.nodes {
1407 used[i] = true;
1408 }
1409 loops.push(around.nodes);
1410 }
1411
1412 let mut outers: Vec<(Vec<usize>, Scalar)> = Vec::new();
1415 let mut holes: Vec<Vec<usize>> = Vec::new();
1416 for lp in loops {
1417 let area = loop_signed_area(lp.iter().map(|&i| &coedges[i].0.pcurve))?;
1418 if area.definitely_greater(Scalar::ZERO) {
1419 outers.push((lp, area));
1420 } else if area.definitely_less(Scalar::ZERO) {
1421 holes.push(lp);
1422 } else {
1423 return Err(GeopError::new(format!(
1424 "cannot tell which way a loop of what is left of face {} runs",
1425 self.names.faces[f]
1426 )));
1427 }
1428 }
1429 let mut faces: Vec<FaceSpec> = outers
1430 .iter()
1431 .map(|(lp, _)| FaceSpec {
1432 surface: surface.clone(),
1433 outer: lp.iter().map(|&i| coedges[i].0.clone()).collect(),
1434 holes: Vec::new(),
1435 })
1436 .collect();
1437 for lp in &holes {
1443 let first = &coedges[lp[0]].0.pcurve;
1444 let point = first.evaluate(first.mid_parameter()?)?;
1445 let mut owner: Option<usize> = None;
1446 for (i, (outer, area)) in outers.iter().enumerate() {
1447 let pcurves: Vec<&Curve2> = outer.iter().map(|&k| &coedges[k].0.pcurve).collect();
1448 let inside = match pcurves_contain(
1449 surface,
1450 &pcurves,
1451 point,
1452 MAX_NODES,
1453 min_subdivision_size(),
1454 SEED,
1455 )? {
1456 PointClassification::Inside => true,
1457 PointClassification::Outside => false,
1458 PointClassification::OnVertex | PointClassification::OnCoedge => {
1459 return Err(GeopError::new(format!(
1460 "a hole of what is left of face {} meets one of its parts' outer loops",
1461 self.names.faces[f]
1462 )));
1463 }
1464 };
1465 if inside && owner.is_none_or(|o| area.definitely_less(outers[o].1)) {
1466 owner = Some(i);
1467 }
1468 }
1469 let owner = owner.ok_or_else(|| {
1470 GeopError::new(format!(
1471 "a hole of what is left of face {} lies in none of its parts",
1472 self.names.faces[f]
1473 ))
1474 })?;
1475 faces[owner]
1476 .holes
1477 .push(lp.iter().map(|&i| coedges[i].0.clone()).collect());
1478 }
1479 Ok(faces)
1480 }
1481}
1482
1483pub fn surface_missed(
1494 curve: &NurbCurve3D,
1495 faces: impl IntoIterator<Item = usize>,
1496 surfaces: &[NurbSurface3D],
1497) -> GeopResult<Option<(usize, Vector3)>> {
1498 let (t0, t1) = curve.domain();
1499 for f in faces {
1500 let overlaps = curve_surface_overlaps(
1501 curve,
1502 &surfaces[f],
1503 Scalar::ZERO,
1504 MAX_NODES,
1505 min_subdivision_size(),
1506 )?;
1507 if overlaps
1508 .iter()
1509 .any(|o| o.start.t.could_be_equal(t0) && o.end.t.could_be_equal(t1))
1510 {
1511 continue;
1512 }
1513 let (from, to) = match overlaps.first() {
1515 Some(o) if o.start.t.could_be_equal(t0) => (
1516 o.end.t.upper(),
1517 overlaps.get(1).map_or(t1, |n| n.start.t.lower()),
1518 ),
1519 Some(o) => (t0, o.start.t.lower()),
1520 None => (t0, t1),
1521 };
1522 let off = curve.evaluate(from.add(to).div(Scalar::TWO)?.sharpen())?;
1523 return Ok(Some((f, off)));
1524 }
1525 Ok(None)
1526}
1527
1528pub fn pcurve(
1538 surface: &NurbSurface3D,
1539 original: &Curve2,
1540 curve: Option<&NurbCurve3D>,
1541 start: Vector2,
1542 end: Vector2,
1543) -> GeopResult<Curve2> {
1544 let Some(curve) = curve else {
1545 return NurbCurve2D::line(start, end);
1546 };
1547 if let Some(mut mapped) = surface.affine_pcurve(curve)? {
1548 let (a, b) = mapped.ends()?;
1551 if a.could_be_equal(&start) && b.could_be_equal(&end) {
1552 let last = mapped.control_points.len() - 1;
1553 for (k, at, given) in [(0, a, start), (last, b, end)] {
1554 let w = mapped.control_points[k][2];
1555 let pinned = |c: usize| {
1556 let x = at[c].intersect(given[c]);
1557 if w.is_exactly(Scalar::ONE) {
1558 x
1559 } else {
1560 x.mul(w)
1561 }
1562 };
1563 mapped.control_points[k] = Vector3::from_array([pinned(0), pinned(1), w]);
1564 }
1565 return Ok(mapped);
1566 }
1567 }
1568 let straight = original.degree == 1 && original.control_points.len() == 2 && {
1569 let (a, b) = original.ends()?;
1570 let keeps = |k: usize| a[k].could_be_equal(b[k]) && start[k].could_be_equal(end[k]);
1571 keeps(0) || keeps(1)
1572 };
1573 if straight {
1574 return NurbCurve2D::line(start, end);
1575 }
1576 surface.fit_pcurve(
1577 curve,
1578 Some(start),
1579 Some(end),
1580 MAX_NODES,
1581 min_subdivision_size(),
1582 )
1583}
1584
1585#[cfg(test)]
1586mod tests;