geop_ops_drawing/
section.rs1use geop_core_geometry::aabb::union_aabb;
6use geop_core_math::{
7 geop_error::{GeopError, GeopResult, WithContext},
8 plain,
9 scalars::Scalar,
10 vector::{Vector2, Vector3},
11};
12use geop_core_topology::{FaceId, Model, SolidId};
13use geop_lib_booleans::{RemeshParams, boolean};
14use geop_lib_sweep::{SweepLoop, extrude, polygon};
15use geop_ops_model::{BooleanOp, Profile};
16use geop_ops_model::{NamedModel, Namer, frame_along};
17
18fn bounds(model: &Model) -> GeopResult<[[f64; 2]; 3]> {
20 let b = union_aabb(model.faces.values().map(|f| f.surface.aabb()))
21 .ok_or_else(|| GeopError::new("the part has no face"))?;
22 Ok(b.map(|x| [x.lower().to_f64(), x.upper().to_f64()]))
23}
24
25pub fn section_part(
29 model: &NamedModel,
30 origin: &Vector3,
31 normal: &Vector3,
32) -> GeopResult<NamedModel> {
33 let ctx = |e: GeopError| {
34 e.with_context(format!(
35 "section_part(origin={origin:?}, normal={normal:?})"
36 ))
37 };
38 let mut cut = model.clone();
39 let solids: Vec<SolidId> = cut.topology().solids.keys().copied().collect();
41 if solids.is_empty() {
42 return Err(GeopError::new("the part has no solid to cut")).with_context(&ctx);
43 }
44 let b = bounds(cut.topology()).with_context(&ctx)?;
46 let center: Vec<f64> = b.iter().map(|[lo, hi]| (lo + hi) / 2.0).collect();
47 let diagonal = plain::diagonal(b.map(|[lo, _]| lo), b.map(|[_, hi]| hi));
48 let offset: f64 = (0..3)
49 .map(|k| (origin[k].to_f64() - center[k]).powi(2))
50 .sum::<f64>()
51 .sqrt();
52 let reach = Scalar::from_f64(2.0 * (diagonal + offset) + 1.0);
53 let plane = frame_along(*origin, normal).with_context(&ctx)?;
54 let square = [
55 Vector2::from_array([reach.neg(), reach.neg()]),
56 Vector2::from_array([reach, reach.neg()]),
57 Vector2::from_array([reach, reach]),
58 Vector2::from_array([reach.neg(), reach]),
59 ];
60 for (k, solid) in solids.into_iter().enumerate() {
61 let tool_namer = Namer::new("section", &format!("tool{k}"))?;
62 let built = extrude(
63 &mut cut,
64 &tool_namer,
65 Some(&tool_namer.root()),
66 &plane,
67 &[Scalar::ZERO, reach],
68 &[SweepLoop::plain(Profile::closed(
69 polygon(&square).with_context(&ctx)?,
70 ))],
71 )
72 .with_context(&ctx)?;
73 let tool = built
74 .solid
75 .ok_or_else(|| GeopError::new("the cutting box is no solid"))
76 .with_context(&ctx)?;
77 let namer = Namer::new("section", &format!("cut{k}"))?;
78 boolean(
79 &mut cut,
80 &namer,
81 solid,
82 tool,
83 BooleanOp::Difference,
84 RemeshParams::default(),
85 )
86 .map_err(|e| ctx(e.with_context(format!("cutting solid {solid}"))))?;
87 }
88 Ok(cut)
89}
90
91pub fn cut_faces(
94 model: &Model,
95 faces: &[FaceId],
96 origin: &Vector3,
97 normal: &Vector3,
98) -> GeopResult<Vec<FaceId>> {
99 let mut cut = Vec::new();
100 for &face in faces {
101 let Some(plane) = model.get_face(face)?.surface.as_plane()? else {
102 continue;
103 };
104 if plane
105 .normal
106 .prod_cross(normal)
107 .could_be_equal(&Vector3::zero())
108 && plane
109 .point
110 .sub(origin)
111 .prod_dot(normal)
112 .could_be_equal(Scalar::ZERO)
113 {
114 cut.push(face);
115 }
116 }
117 Ok(cut)
118}