1use std::collections::{BTreeMap, HashMap, HashSet};
20
21use geop_core_math::{
22 geop_error::{GeopError, GeopResult, WithContext},
23 vector::Vector2,
24 with_context,
25};
26use geop_core_topology::{
27 Body, EdgeId, FaceId, Model, VertexId,
28 contains::face::{PointClassification, face_contains},
29};
30use geop_ops_model::{NamedModel, Namer, origin_of};
31
32use crate::{
33 naming::BooleanNaming,
34 remesh::{
35 RemeshParams, remesh, remesh_vertices::remesh_vertices,
36 remesh_vertices_x_edges::remesh_vertices_x_edges,
37 },
38};
39
40const FACE_CONTAINS_SEED: u64 = 0x5DEE_CE66_D1CE_4E5B;
43
44fn body_edges(model: &Model, body: Body) -> GeopResult<HashSet<EdgeId>> {
46 let mut edges = HashSet::new();
47 for face in model.body_faces(body)? {
48 edges.extend(model.face_edges(face));
49 }
50 Ok(edges)
51}
52
53pub(crate) fn shared_edges(model: &NamedModel, a: Body, b: Body) -> GeopResult<Vec<EdgeId>> {
57 let topology = model.topology();
58 let on_b = body_edges(topology, b)?;
59 let mut shared: Vec<(String, EdgeId)> = body_edges(topology, a)?
60 .into_iter()
61 .filter(|e| on_b.contains(e))
62 .map(|e| Ok((model.name(e)?.to_string(), e)))
63 .collect::<GeopResult<_>>()?;
64 shared.sort();
65 Ok(shared.into_iter().map(|(_, e)| e).collect())
66}
67
68fn tool_face_of(
72 model: &NamedModel,
73 namer: &Namer,
74 edge: EdgeId,
75 tool_faces: &HashSet<FaceId>,
76) -> GeopResult<String> {
77 let topology = model.topology();
78 let mut names = Vec::new();
79 for coedge in topology.coedges_of_edge(edge) {
80 let on = topology.get_coedge(coedge)?.face;
81 if tool_faces.contains(&on) {
82 let name = model.name(on)?;
83 names.push(namer.arguments_of(name).unwrap_or(name).to_string());
84 }
85 }
86 (names.into_iter().min())
87 .ok_or_else(|| GeopError::new(format!("the curve {edge} runs along no face of the tools")))
88}
89
90pub fn imprint(
101 model: &mut NamedModel,
102 namer: &Namer,
103 a: Body,
104 b: Body,
105 params: RemeshParams,
106) -> GeopResult<Vec<EdgeId>> {
107 let ctx = with_context!("imprint(name={}, a={a}, b={b})", namer.root());
108 let before = model.clone();
109 remesh(model, namer, a, b, params).with_context(&ctx)?;
110 let curves = shared_edges(model, a, b).with_context(&ctx)?;
111 if curves.is_empty() {
112 *model = before;
113 return Err(ctx(GeopError::new(
114 "imprint: the bodies' boundaries do not meet",
115 )));
116 }
117 let (spec, names) = model.body_record(b).with_context(&ctx)?;
119 model.delete_bodies(&[b]).with_context(&ctx)?;
120 let names = names.renamed(|name| {
121 if model.names().is_taken(name) {
122 namer.name(&[name])
123 } else {
124 name.to_string()
125 }
126 });
127 model.build_body(spec, names).with_context(&ctx)?;
128 Ok(curves)
129}
130
131pub fn imprint_on_face(
150 model: &mut NamedModel,
151 namer: &Namer,
152 face: FaceId,
153 tools: &[Body],
154 params: RemeshParams,
155) -> GeopResult<Vec<EdgeId>> {
156 let ctx = with_context!(
157 "imprint_on_face(name={}, face={face}, tools={tools:?})",
158 namer.root()
159 );
160 let body = model.topology().body_of_face(face).with_context(&ctx)?;
161
162 let copy = model
164 .copy_faces(&[face], None, |name| namer.name(&["copy", name]))
165 .and_then(|built| built.body())
166 .with_context(&ctx)?;
167 for &tool in tools {
168 remesh(model, namer, copy, tool, params).with_context(&ctx)?;
169 }
170
171 let topology = model.topology();
176 let on_copy = body_edges(topology, copy).with_context(&ctx)?;
177 let mut on_tools = HashSet::new();
178 for &tool in tools {
179 on_tools.extend(body_edges(topology, tool).with_context(&ctx)?);
180 }
181 let mut curves: Vec<(String, EdgeId, Vector2)> = Vec::new();
182 for &edge in on_copy.intersection(&on_tools) {
183 let coedge = topology
184 .coedges_of_edge(edge)
185 .into_iter()
186 .find(|&c| {
187 topology
188 .get_coedge(c)
189 .is_ok_and(|c| topology.body_of_face(c.face).is_ok_and(|b| b == copy))
190 })
191 .ok_or_else(|| {
192 ctx(GeopError::new(format!(
193 "imprint: edge {edge} is on no face of the copy"
194 )))
195 })?;
196 let pcurve = &topology.get_coedge(coedge)?.pcurve;
197 let middle = pcurve.evaluate(pcurve.mid_parameter()?)?;
198 curves.push((model.name(edge).map(str::to_string)?, edge, middle));
199 }
200 curves.sort_by(|a, b| a.0.cmp(&b.0));
201 let face_name = model.name(face).map(str::to_string)?;
202 let tool_faces: HashSet<FaceId> = (tools.iter())
203 .map(|&tool| topology.body_faces(tool))
204 .collect::<GeopResult<Vec<_>>>()
205 .with_context(&ctx)?
206 .into_iter()
207 .flatten()
208 .collect();
209 if curves.is_empty() {
210 return Err(ctx(GeopError::new(format!(
211 "nothing crosses the face {face_name:?}: what divides it has to run across it"
212 ))));
213 }
214
215 let on_boundary = |edge: EdgeId| {
219 let of_copy = (topology.coedges_of_edge(edge).into_iter()).filter(|&c| {
220 (topology.get_coedge(c))
221 .is_ok_and(|c| topology.body_of_face(c.face).is_ok_and(|b| b == copy))
222 });
223 of_copy.count() < 2
224 };
225 let mut along: Vec<&str> = (curves.iter())
226 .filter(|(_, edge, _)| on_boundary(*edge))
227 .map(|(name, _, _)| {
228 let copy = origin_of(name).unwrap_or(name);
231 let arguments = namer.arguments_of(copy).unwrap_or(copy);
232 arguments.strip_prefix("copy,").unwrap_or(arguments)
233 })
234 .collect();
235 if !along.is_empty() {
236 along.sort();
237 along.dedup();
238 return Err(ctx(GeopError::new(format!(
239 "what divides the face {face_name:?} runs along its edge {along:?}, which already bounds it: draw it clear of the edge"
240 ))));
241 }
242
243 let boundary: HashSet<VertexId> = on_copy
247 .difference(&on_tools)
248 .map(|&e| topology.get_edge(e).map(|e| [e.start_vertex, e.end_vertex]))
249 .collect::<GeopResult<Vec<_>>>()?
250 .into_iter()
251 .flatten()
252 .collect();
253 let mut ends: HashMap<VertexId, usize> = HashMap::new();
254 for (_, edge, _) in &curves {
255 let edge = topology.get_edge(*edge)?;
256 *ends.entry(edge.start_vertex).or_default() += 1;
257 *ends.entry(edge.end_vertex).or_default() += 1;
258 }
259 let loose = |v: VertexId| ends[&v] == 1 && !boundary.contains(&v);
260 let mut dangling = Vec::new();
261 for (_, edge, _) in &curves {
262 let at = topology.get_edge(*edge)?;
263 if loose(at.start_vertex) || loose(at.end_vertex) {
264 dangling.push(tool_face_of(model, namer, *edge, &tool_faces)?);
265 }
266 }
267 if !dangling.is_empty() {
268 dangling.sort();
269 dangling.dedup();
270 return Err(ctx(GeopError::new(format!(
271 "{dangling:?} ends inside the face {face_name:?}, and a dangling edge divides nothing: to divide it, a curve has to cross it or close on itself"
272 ))));
273 }
274
275 model.delete_bodies(&[copy]).with_context(&ctx)?;
276
277 let mut bodies = vec![body];
280 bodies.extend(tools);
281 let mut naming = BooleanNaming::new(model, namer, &bodies).with_context(&ctx)?;
282 for &tool in tools {
283 remesh_vertices(model, body, tool).with_context(&ctx)?;
284 remesh_vertices_x_edges(model, &mut naming, body, tool, ¶ms).with_context(&ctx)?;
285 }
286 let mut pieces = vec![face];
287 let mut split_along: Vec<(FaceId, EdgeId)> = Vec::new();
288 for (curve_name, edge, middle) in &curves {
289 let mut target = None;
290 for &piece in &pieces {
291 let class = face_contains(
292 model.topology(),
293 piece,
294 middle[0],
295 middle[1],
296 params.max_nodes,
297 params.min_subdivision_size,
298 FACE_CONTAINS_SEED,
299 )
300 .with_context(&ctx)?;
301 if class == PointClassification::Inside {
302 target = Some(piece);
303 break;
304 }
305 }
306 let target = target.ok_or_else(|| {
307 ctx(GeopError::new(format!(
308 "imprint: the curve {curve_name:?}, at {middle:?} on the face, lies inside none of its pieces"
309 )))
310 })?;
311 let split = model
312 .splice_edge_into_face(
313 *edge,
314 target,
315 params.max_nodes,
316 params.min_subdivision_size,
317 naming.provisional(),
318 )
319 .with_context(&ctx)?;
320 naming.face_split(target, *edge, split)?;
321 pieces.extend(split);
322 split_along.extend(split.map(|piece| (piece, *edge)));
323 }
324 naming.finish(model).with_context(&ctx)?;
325 let topology = model.topology();
328 let mut joining = Vec::new();
329 for (_, edge, _) in &curves {
330 let mut sides = HashSet::new();
331 for coedge in topology.coedges_of_edge(*edge) {
332 let side = topology.get_coedge(coedge)?.face;
333 if pieces.contains(&side) && !sides.insert(side) {
334 joining.push(tool_face_of(model, namer, *edge, &tool_faces)?);
335 break;
336 }
337 }
338 }
339 if !joining.is_empty() {
340 joining.sort();
341 joining.dedup();
342 return Err(ctx(GeopError::new(format!(
343 "{joining:?} only joins two of the boundaries of the face {face_name:?}, as a line from its outside to a hole does, and a piece of it on both sides divides nothing: to divide it, a curve has to have another piece on its other side"
344 ))));
345 }
346
347 let mut stretches: BTreeMap<String, Vec<EdgeId>> = BTreeMap::new();
349 for (_, edge, _) in &curves {
350 let made_by = tool_face_of(model, namer, *edge, &tool_faces).with_context(&ctx)?;
351 stretches.entry(made_by).or_default().push(*edge);
352 }
353 model.delete_bodies(tools).with_context(&ctx)?;
354 for (made_by, edges) in stretches {
355 for (k, &edge) in edges.iter().enumerate() {
356 let at = match edges.len() {
357 1 => made_by.clone(),
358 _ => format!("{made_by},{k}"),
359 };
360 model.rename(edge, namer.name(&[&at])).with_context(&ctx)?;
361 for &(piece, _) in split_along.iter().filter(|&&(_, e)| e == edge) {
362 model
363 .rename(piece, namer.piece(&face_name, &at))
364 .with_context(&ctx)?;
365 }
366 }
367 }
368 Ok(curves.into_iter().map(|(_, edge, _)| edge).collect())
369}
370
371#[cfg(test)]
372mod tests {
373 use super::{imprint, imprint_on_face};
374 use crate::remesh::RemeshParams;
375 use geop_core_math::{
376 geop_error::GeopResult, primitives::CoordinateSystem, scalars::Scalar, vector::Vector2,
377 };
378 use geop_core_topology::{Body, EdgeId, SolidId};
379 use geop_lib_sweep::{
380 SweepLoop, extrude, polygon, polyline,
381 shapes::{Axis, block, revolved_cylinder_along_axis, v3},
382 };
383 use geop_ops_model::Profile;
384 use geop_ops_model::{NamedModel, Namer, testing::assert_valid};
385
386 fn v2s(points: &[[f64; 2]]) -> Vec<Vector2> {
387 points
388 .iter()
389 .map(|p| Vector2::from_array([Scalar::from_f64(p[0]), Scalar::from_f64(p[1])]))
390 .collect()
391 }
392
393 fn imprint_swept(
397 build: impl Fn(&mut NamedModel) -> SolidId,
398 target: &str,
399 profile: Profile,
400 ) -> GeopResult<(NamedModel, Vec<EdgeId>)> {
401 let mut model = NamedModel::new();
402 build(&mut model);
403 let target = model.face_id(target)?;
404 let plane = CoordinateSystem::world_at(v3(0.0, 0.0, 2.0));
405 let namer = Namer::new("project", "p")?;
406 let swept = extrude(
407 &mut model,
408 &namer,
409 None,
410 &plane,
411 &[Scalar::from_f64(-5.0), Scalar::from_f64(1.0)],
412 &[SweepLoop::plain(profile)],
413 )?;
414 let edges = imprint_on_face(
415 &mut model,
416 &namer,
417 target,
418 &[swept.body()?],
419 RemeshParams::default(),
420 )?;
421 Ok((model, edges))
422 }
423
424 fn cube(model: &mut NamedModel) -> SolidId {
425 block(model, "c", [0.0; 3], [1.0; 3])
426 }
427
428 const TOP: &str = "cube(c,start)";
430
431 fn chain(points: &[[f64; 2]]) -> Profile {
432 Profile::open(polyline(&v2s(points)).unwrap())
433 }
434
435 #[test]
439 fn a_line_across_a_face_divides_it_in_two() {
440 let (model, edges) = imprint_swept(cube, TOP, chain(&[[0.3, -1.0], [0.3, 2.0]])).unwrap();
441 assert_valid(&model);
442 assert_eq!(edges.len(), 1);
443 assert_eq!(model.topology().solids.len(), 1);
444 assert!(model.sheet_face_names().is_empty(), "the sweep is gone");
445 assert_eq!(model.topology().faces.len(), 7);
448 assert_eq!(model.topology().edges.len(), 15);
449 assert_eq!(model.topology().vertices.len(), 10);
450 }
451
452 #[test]
455 fn a_bent_chain_divides_a_face_along_both_legs() {
456 let (model, edges) =
457 imprint_swept(cube, TOP, chain(&[[0.3, -1.0], [0.5, 0.5], [2.0, 0.6]])).unwrap();
458 assert_valid(&model);
459 assert_eq!(edges.len(), 2);
460 assert_eq!(model.topology().faces.len(), 7);
461 }
462
463 #[test]
466 fn a_closed_loop_inside_a_face_cuts_out_a_face() {
467 let square = polygon(&v2s(&[[0.2, 0.2], [0.6, 0.2], [0.6, 0.6], [0.2, 0.6]])).unwrap();
468 let (model, edges) = imprint_swept(cube, TOP, Profile::closed(square)).unwrap();
469 assert_valid(&model);
470 assert_eq!(edges.len(), 4);
471 assert_eq!(model.topology().faces.len(), 7);
472 let top = model
473 .topology()
474 .get_face(model.face_id(TOP).unwrap())
475 .unwrap();
476 assert_eq!(top.holes.len(), 1);
477 }
478
479 #[test]
485 fn a_slanted_line_divides_a_round_face_along_an_ellipse() {
486 let cylinder = |model: &mut NamedModel| {
487 revolved_cylinder_along_axis(
488 model,
489 "y",
490 v3(-0.5, 0.0, 0.0),
491 Scalar::from_f64(0.5),
492 Scalar::ONE,
493 Axis::X,
494 )
495 .unwrap()
496 };
497 let (model, edges) = imprint_swept(
498 cylinder,
499 "cylinder(y,c1,q0)",
500 chain(&[[-1.0, -0.3], [1.0, 0.3]]),
501 )
502 .unwrap();
503 assert_valid(&model);
504 assert_eq!(edges.len(), 1);
505 assert_eq!(model.topology().faces.len(), 13);
508 }
509
510 #[test]
512 fn a_line_ending_inside_the_face_is_refused() {
513 let error =
514 imprint_swept(cube, TOP, chain(&[[0.3, -1.0], [0.3, 0.5]])).expect_err("refused");
515 assert!(error.root_message().contains("ends inside"), "{error:?}");
516 }
517
518 #[test]
520 fn a_line_beside_the_face_is_refused() {
521 let error =
522 imprint_swept(cube, TOP, chain(&[[3.0, -1.0], [3.0, 2.0]])).expect_err("refused");
523 assert!(
524 error.root_message().contains("nothing crosses"),
525 "{error:?}"
526 );
527 }
528
529 #[test]
534 fn two_overlapping_cubes_divide_each_other() {
535 let mut model = NamedModel::new();
536 let a = block(&mut model, "a", [0.0; 3], [1.0; 3]);
537 let b = block(&mut model, "b", [0.5; 3], [1.5; 3]);
538 let namer = Namer::new("imprint", "i").unwrap();
539 let edges = imprint(
540 &mut model,
541 &namer,
542 a.into(),
543 b.into(),
544 RemeshParams::default(),
545 )
546 .unwrap();
547 assert_valid(&model);
548 assert_eq!(edges.len(), 6);
549 assert_eq!(model.solid_names(), ["cube(a)", "cube(b)"]);
550 let topology = model.topology();
551 let faces = |name: &str| {
552 let solid = model.solid_id(name).unwrap();
553 topology.solid_faces(solid).unwrap().len()
554 };
555 assert_eq!((faces("cube(a)"), faces("cube(b)")), (9, 9));
556 assert_eq!(topology.edges.len(), 2 * (12 + 3 + 6));
558 }
559
560 #[test]
562 fn bodies_apart_are_refused() {
563 let mut model = NamedModel::new();
564 let a = block(&mut model, "a", [0.0; 3], [1.0; 3]);
565 let b = block(&mut model, "b", [2.0; 3], [3.0; 3]);
566 let before = model.clone();
567 let namer = Namer::new("imprint", "i").unwrap();
568 let error = imprint(
569 &mut model,
570 &namer,
571 a.into(),
572 b.into(),
573 RemeshParams::default(),
574 )
575 .unwrap_err();
576 assert!(error.root_message().contains("do not meet"), "{error:?}");
577 assert_eq!(model.topology().faces.len(), before.topology().faces.len());
578 }
579
580 #[test]
584 fn a_solid_standing_through_a_face_cuts_a_square_out_of_it() {
585 let mut model = NamedModel::new();
586 cube(&mut model);
587 let top = model.face_id(TOP).unwrap();
588 let tool = block(&mut model, "t", [0.2, 0.2, 0.5], [0.6, 0.6, 1.5]);
589 let namer = Namer::new("imprint", "i").unwrap();
590 let edges = imprint_on_face(
591 &mut model,
592 &namer,
593 top,
594 &[Body::Solid(tool)],
595 RemeshParams::default(),
596 )
597 .unwrap();
598 assert_valid(&model);
599 assert_eq!(edges.len(), 4);
600 assert_eq!(model.solid_names(), ["cube(c)"]);
601 assert_eq!(model.topology().faces.len(), 7);
602 }
603}