1use std::collections::HashMap;
11
12use geop_core_geometry::{aabb::union_aabb, nurb_surface::NurbSurface3D};
13use geop_core_math::{
14 geop_error::{GeopError, GeopResult, WithContext},
15 primitives::CoordinateSystem,
16 scalars::Scalar,
17 vector::Vector2,
18 with_context,
19};
20use geop_core_topology::{
21 Body, VertexId, WireId,
22 build::{BodySpec, EdgeSpec},
23};
24use geop_ops_model::{BodyNames, NamedModel, Namer};
25
26use crate::{
27 imprint::shared_edges,
28 remesh::{RemeshParams, remesh},
29};
30
31pub fn section(
43 model: &mut NamedModel,
44 namer: &Namer,
45 a: Body,
46 b: Body,
47 params: RemeshParams,
48) -> GeopResult<WireId> {
49 let before = model.clone();
50 section_of(model, namer, a, b, params).inspect_err(|_| *model = before)
51}
52
53fn section_of(
55 model: &mut NamedModel,
56 namer: &Namer,
57 a: Body,
58 b: Body,
59 params: RemeshParams,
60) -> GeopResult<WireId> {
61 let ctx = with_context!("section(name={}, a={a}, b={b})", namer.root());
62 let copies = [a, b]
63 .map(|body| {
64 model
65 .copy_body(body, |name| namer.name(&[name]))
66 .and_then(|built| built.body())
67 })
68 .into_iter()
69 .collect::<GeopResult<Vec<Body>>>()
70 .with_context(&ctx)?;
71 let [copy_a, copy_b] = copies[..] else {
72 unreachable!("two bodies copied")
73 };
74 remesh(model, namer, copy_a, copy_b, params).with_context(&ctx)?;
75 let curves = shared_edges(model, copy_a, copy_b).with_context(&ctx)?;
76 if curves.is_empty() {
77 return Err(ctx(GeopError::new(
78 "section: the bodies' boundaries do not meet",
79 )));
80 }
81
82 let topology = model.topology();
84 let mut spec = BodySpec::new(false);
85 let mut names = BodyNames::default();
86 let mut index: HashMap<VertexId, usize> = HashMap::new();
87 let mut vertex = |spec: &mut BodySpec, names: &mut BodyNames, v: VertexId| {
88 Ok::<_, GeopError>(match index.get(&v) {
89 Some(&i) => i,
90 None => {
91 spec.vertices.push(topology.get_vertex(v)?.point);
92 names.vertices.push(model.name(v)?.to_string());
93 index.insert(v, spec.vertices.len() - 1);
94 spec.vertices.len() - 1
95 }
96 })
97 };
98 for &edge in &curves {
99 let e = topology.get_edge(edge)?;
100 let start = vertex(&mut spec, &mut names, e.start_vertex)?;
101 let end = vertex(&mut spec, &mut names, e.end_vertex)?;
102 spec.edges.push(EdgeSpec {
103 curve: e.curve.clone(),
104 start,
105 end,
106 });
107 names.edges.push(model.name(edge)?.to_string());
108 }
109 model.delete_bodies(&[copy_a, copy_b]).with_context(&ctx)?;
110 let built = model.build_body(spec, names).with_context(&ctx)?;
111 built
112 .wire
113 .ok_or_else(|| ctx(GeopError::new("section: the curves built no wire")))
114}
115
116pub fn section_by_plane(
123 model: &mut NamedModel,
124 namer: &Namer,
125 body: Body,
126 plane: &CoordinateSystem,
127 params: RemeshParams,
128) -> GeopResult<WireId> {
129 let ctx = with_context!("section_by_plane(name={}, body={body})", namer.root());
130 let topology = model.topology();
131 let faces = topology.body_faces(body).with_context(&ctx)?;
132 let bounds = union_aabb(faces.iter().map(|&f| topology.faces[&f].surface.aabb()))
133 .ok_or_else(|| ctx(GeopError::new("section: the body has no face")))?;
134 let center = geop_core_math::vector::Vector3::from_array(bounds.map(|x| x.midpoint()));
138 let diagonal = bounds
139 .iter()
140 .map(|x| x.width().mul(x.width()))
141 .fold(Scalar::ZERO, |sum, w| sum.add(w))
142 .sqrt()?;
143 let reach = Scalar::from_f64((diagonal.upper().to_f64() + 1.0).ceil());
144 let middle = plane.to_uv(¢er);
145 let (ru, rv) = (
146 reach.div(plane.u().norm())?.sharpen(),
147 reach.div(plane.v().norm())?.sharpen(),
148 );
149 let corner = |su: Scalar, sv: Scalar| {
150 plane.uv_to_xyz(&Vector2::from_array([
151 middle[0].sharpen().add(su.mul(ru)),
152 middle[1].sharpen().add(sv.mul(rv)),
153 ]))
154 };
155 let (minus, plus) = (Scalar::ONE.neg(), Scalar::ONE);
156 let square = NurbSurface3D::bilinear(
157 corner(minus, minus),
158 corner(plus, minus),
159 corner(plus, plus),
160 corner(minus, plus),
161 )
162 .with_context(&ctx)?;
163 let spec = BodySpec::sheet_on(square).with_context(&ctx)?;
164 let names = BodyNames {
165 vertices: (0..4)
166 .map(|k| namer.name(&["plane", &format!("v{k}")]))
167 .collect(),
168 edges: (0..4)
169 .map(|k| namer.name(&["plane", &format!("e{k}")]))
170 .collect(),
171 faces: vec![namer.name(&["plane"])],
172 solid: None,
173 };
174 let sheet = model
175 .build_body(spec, names)
176 .and_then(|built| built.body())
177 .with_context(&ctx)?;
178 let wire = section(model, namer, body, sheet, params).with_context(&ctx);
179 model.delete_bodies(&[sheet]).with_context(&ctx)?;
180 wire
181}
182
183#[cfg(test)]
184mod tests {
185 use super::{section, section_by_plane};
186 use crate::remesh::RemeshParams;
187 use geop_core_math::{primitives::CoordinateSystem, scalars::Scalar};
188 use geop_core_topology::{Body, Sense};
189 use geop_lib_sweep::shapes::{Axis, block, revolved_cylinder_along_axis, v3};
190 use geop_ops_model::{NamedModel, Namer, testing::assert_valid};
191
192 #[test]
197 fn two_overlapping_cubes_cross_in_one_loop() {
198 let mut model = NamedModel::new();
199 let a = block(&mut model, "a", [0.0; 3], [1.0; 3]);
200 let b = block(&mut model, "b", [0.5; 3], [1.5; 3]);
201 let (faces, edges) = (model.topology().faces.len(), model.topology().edges.len());
202 let namer = Namer::new("section", "s").unwrap();
203 let wire = section(
204 &mut model,
205 &namer,
206 a.into(),
207 b.into(),
208 RemeshParams::default(),
209 )
210 .unwrap();
211 assert_valid(&model);
212 assert_eq!(
213 model.topology().faces.len(),
214 faces,
215 "the cubes as they were"
216 );
217 assert_eq!(model.topology().edges.len(), edges + 6);
218 let chains = model.topology().wire_chains(wire).unwrap();
219 assert_eq!(chains.len(), 1);
220 assert_eq!(chains[0].len(), 6);
221 let topology = model.topology();
222 let end = |&(e, sense): &(_, Sense)| {
223 let e = topology.get_edge(e).unwrap();
224 match sense {
225 Sense::Forward => (e.start_vertex, e.end_vertex),
226 Sense::Reversed => (e.end_vertex, e.start_vertex),
227 }
228 };
229 let ends: Vec<_> = chains[0].iter().map(end).collect();
230 for k in 0..6 {
231 assert_eq!(ends[k].1, ends[(k + 1) % 6].0, "a closed chain");
232 }
233 }
234
235 #[test]
238 fn a_plane_across_a_cylinder_cuts_a_circle() {
239 let mut model = NamedModel::new();
240 let cylinder = revolved_cylinder_along_axis(
241 &mut model,
242 "c",
243 v3(0.0, 0.0, 0.0),
244 Scalar::from_f64(0.5),
245 Scalar::from_f64(2.0),
246 Axis::Z,
247 )
248 .unwrap();
249 let namer = Namer::new("section", "s").unwrap();
250 let plane = CoordinateSystem::world_at(v3(0.0, 0.0, 0.7));
251 let wire = section_by_plane(
252 &mut model,
253 &namer,
254 Body::Solid(cylinder),
255 &plane,
256 RemeshParams::default(),
257 )
258 .unwrap();
259 assert_valid(&model);
260 assert!(model.sheet_face_names().is_empty(), "the plane is gone");
261 let chains = model.topology().wire_chains(wire).unwrap();
262 assert_eq!(chains.len(), 1);
263 assert_eq!(chains[0].len(), 4);
264 for &(edge, _) in &chains[0] {
265 let curve = &model.topology().get_edge(edge).unwrap().curve;
266 let mid = curve.evaluate(curve.mid_parameter().unwrap()).unwrap();
267 assert!(mid[2].could_be_equal(Scalar::from_f64(0.7)), "{mid:?}");
268 let r = mid[0].mul(mid[0]).add(mid[1].mul(mid[1])).sqrt().unwrap();
269 assert!((r.to_f64() - 0.5).abs() < 1e-9, "{r:?}");
270 }
271 }
272
273 #[test]
275 fn a_plane_beside_the_body_is_refused() {
276 let mut model = NamedModel::new();
277 let cube = block(&mut model, "a", [0.0; 3], [1.0; 3]);
278 let namer = Namer::new("section", "s").unwrap();
279 let plane = CoordinateSystem::world_at(v3(0.0, 0.0, 3.0));
280 let error = section_by_plane(
281 &mut model,
282 &namer,
283 Body::Solid(cube),
284 &plane,
285 RemeshParams::default(),
286 )
287 .unwrap_err();
288 assert!(error.root_message().contains("do not meet"), "{error:?}");
289 assert!(model.sheet_face_names().is_empty(), "the plane is gone");
290 }
291}