1use geop_core_editor::{
6 edit_form::{Edit, EditForm},
7 part_operation::PartOperation,
8};
9use geop_core_geometry::shape::Axis;
10use geop_core_math::{
11 geop_error::{GeopError, GeopResult, WithContext},
12 plain,
13 primitives::Motion,
14 scalars::Scalar,
15 vector::Vector3,
16 with_context,
17};
18use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
19use geop_core_topology::Body;
20use geop_lib_booleans::{Reach, combine_tools};
21use geop_ops_model::{Combine, Tool};
22use geop_ops_model::{
23 ModelOperation, NamedModel, Namer, Threads, ToolReach, build_model, draw_threads, read_model,
24 resolve_bodies, threads,
25};
26use serde::{Deserialize, Serialize};
27
28use crate::features::FeatureRef;
29use geop_core_geometry::nurb_curve::NurbCurve3D;
30use geop_ops_model::spacing::{Frames, HoleSites, hole_sites, read_hole_sites};
31
32pub type BodyRef = ElementReference<NamedModel, geop_ops_model::Body>;
35
36pub fn body(name: impl Into<String>) -> BodyRef {
38 ElementReference::new(geop_ops_model::model(), name)
39}
40
41pub(crate) fn bodies_field<'a, A>(
45 f: &mut EditForm<'a, A>,
46 before: &Reader<'a>,
47 bodies: &[BodyRef],
48 set: impl Fn(Edit<'_, A, ()>, Vec<BodyRef>) + 'a,
49) {
50 f.pick("bodies", "bodies", before, bodies, true, set);
51}
52
53#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
58pub struct Seeds {
59 pub bodies: Vec<BodyRef>,
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub features: Vec<FeatureRef>,
64 #[serde(default)]
67 pub combine: Combine,
68}
69
70impl Seeds {
71 pub fn bodies(bodies: Vec<BodyRef>, combine: Combine) -> Self {
73 Seeds {
74 bodies,
75 features: Vec::new(),
76 combine,
77 }
78 }
79
80 pub fn features(features: Vec<FeatureRef>) -> Self {
82 Seeds {
83 features,
84 ..Seeds::default()
85 }
86 }
87
88 pub(crate) fn fields<'a, A>(
97 &self,
98 f: &mut EditForm<'a, A>,
99 before: &Reader<'a>,
100 set: impl Fn(&mut A, Seeds) + Clone + 'a,
101 own: impl FnOnce(&mut EditForm<'a, A>),
102 ) {
103 let (seeds, set_bodies) = (self.clone(), set.clone());
104 bodies_field(f, before, &self.bodies, move |edit, picked| {
105 let mut seeds = seeds.clone();
106 if !picked.is_empty() {
107 seeds.features.clear();
108 }
109 seeds.bodies = picked;
110 set_bodies(edit.args, seeds);
111 });
112 let (seeds, set_features) = (self.clone(), set.clone());
113 f.pick(
114 "features",
115 "features",
116 before,
117 &self.features,
118 true,
119 move |edit, picked| {
120 let mut seeds = seeds.clone();
121 if !picked.is_empty() {
122 seeds.bodies.clear();
123 }
124 seeds.features = picked;
125 set_features(edit.args, seeds);
126 },
127 );
128 f.form.dialog.optional(if self.features.is_empty() {
130 "features"
131 } else {
132 "bodies"
133 });
134 own(f);
135 if self.features.is_empty() {
136 let seeds = self.clone();
137 self.combine.fields(f, before, move |args, combine| {
138 set(
139 args,
140 Seeds {
141 combine,
142 ..seeds.clone()
143 },
144 )
145 });
146 }
147 }
148
149 pub(crate) fn repeat(
160 &self,
161 part: &mut NamedModel,
162 reader: &Reader,
163 names: &Names,
164 operation_id: &str,
165 seed_label: &str,
166 placements: &[(String, Motion)],
167 ) -> GeopResult<()> {
168 match (self.bodies.is_empty(), self.features.is_empty()) {
169 (false, false) => Err(GeopError::new(
170 "both bodies and features are picked: a step patterns one or the other",
171 )),
172 (true, true) => Err(GeopError::new("pick the bodies or the features first")),
173 (true, false) => crate::features::repeat_features(
174 part,
175 names,
176 operation_id,
177 &self.features,
178 placements,
179 ),
180 (false, true) => {
181 let seeds = resolve_bodies(reader, &self.bodies)?;
182 let mut instances = seed_instances(&seeds, seed_label);
183 for (label, motion) in placements {
184 instances.extend(copy_seeds(part, &seeds, motion, label, |name| {
185 names.copy(label, name)
186 })?);
187 }
188 combine_instances(part, &names.namer, operation_id, &self.combine, &instances)
189 }
190 }
191 }
192
193 pub(crate) fn anchor(&self, reader: &Reader) -> GeopResult<Vector3> {
197 let part = read_model(reader)?;
198 if self.features.is_empty() {
199 let model = part.topology();
200 let mut curves = Vec::new();
201 for body in resolve_bodies(reader, &self.bodies)? {
202 for edge in model.iter_body_edges(body)? {
203 curves.push(&model.get_edge(edge)?.curve);
204 }
205 }
206 return box_middle(curves);
207 }
208 let steps = crate::features::feature_steps(&part, &self.features)?;
209 let mut curves = Vec::new();
210 for step in &steps {
211 for tool in &part.feature(step)?.tools {
212 curves.extend(tool.spec.edges.iter().map(|e| &e.curve));
213 }
214 }
215 box_middle(curves)
216 }
217
218 pub(crate) fn center(&self, reader: &Reader) -> Option<Vector3> {
222 if self.features.is_empty() {
223 center(reader, &self.bodies)
224 } else {
225 crate::features::features_center(&*read_model(reader).ok()?, &self.features)
226 }
227 }
228}
229
230#[derive(Clone, Copy, Debug, PartialEq)]
235pub(crate) struct Naming {
236 pub kind: &'static str,
237 pub labelled: bool,
238}
239
240impl Naming {
241 pub(crate) fn of(self, step: &str) -> GeopResult<Names> {
243 Ok(Names {
244 namer: Namer::new(self.kind, step)?,
245 labelled: self.labelled,
246 })
247 }
248}
249
250pub(crate) struct Names {
252 pub namer: Namer,
253 labelled: bool,
254}
255
256impl Names {
257 pub(crate) fn copy(&self, label: &str, name: &str) -> String {
260 match self.labelled {
261 true => self.namer.name(&[label, name]),
262 false => self.namer.name(&[name]),
263 }
264 }
265}
266
267pub(crate) trait Pattern: std::fmt::Debug {
270 const NAMING: Naming;
272
273 fn seeds(&self) -> &Seeds;
275
276 fn seed_label(&self) -> &'static str;
278
279 fn placements(&self, reader: &Reader) -> GeopResult<Vec<(String, Motion)>>;
282}
283
284pub(crate) fn build_pattern<A: Pattern>(
287 model: &mut NamedModel,
288 reader: &Reader,
289 step: &str,
290 args: &A,
291) -> GeopResult<()> {
292 let names = A::NAMING.of(step)?;
293 let placements = args.placements(reader)?;
294 (args.seeds()).repeat(model, reader, &names, step, args.seed_label(), &placements)
295}
296
297pub(crate) struct Instance {
301 pub body: Body,
302 pub scope: String,
303}
304
305fn scope(label: &str, k: usize, n: usize) -> String {
308 if n == 1 {
309 label.to_string()
310 } else {
311 format!("{label}.{k}")
312 }
313}
314
315pub(crate) fn seed_instances(seeds: &[Body], label: &str) -> Vec<Instance> {
317 seeds
318 .iter()
319 .enumerate()
320 .map(|(k, &body)| Instance {
321 body,
322 scope: scope(label, k, seeds.len()),
323 })
324 .collect()
325}
326
327pub(crate) fn copy_seeds(
331 part: &mut NamedModel,
332 seeds: &[Body],
333 motion: &Motion,
334 label: &str,
335 rename: impl Fn(&str) -> String,
336) -> GeopResult<Vec<Instance>> {
337 let mut copies = Vec::with_capacity(seeds.len());
338 for (k, &seed) in seeds.iter().enumerate() {
339 let body = part.copy_body(seed, &rename)?.body()?;
340 part.transform_body(body, motion)?;
341 copies.push(Instance {
342 body,
343 scope: scope(label, k, seeds.len()),
344 });
345 }
346 Ok(copies)
347}
348
349fn describe(part: &NamedModel, body: Body) -> String {
352 let name = match body {
353 Body::Solid(solid) => part.name_of(solid),
354 Body::Sheet(_) => part
355 .topology()
356 .body_faces(body)
357 .ok()
358 .and_then(|faces| faces.first().and_then(|&f| part.name_of(f))),
359 };
360 match (body, name) {
361 (Body::Solid(_), Some(name)) => format!("solid {name:?}"),
362 (Body::Sheet(_), Some(name)) => format!("the face {name:?} standing on its own"),
363 (_, None) => body.to_string(),
364 }
365}
366
367pub(crate) fn combine_instances(
376 part: &mut NamedModel,
377 namer: &Namer,
378 operation_id: &str,
379 combine: &Combine,
380 instances: &[Instance],
381) -> GeopResult<()> {
382 let Some(target) = combine.target() else {
383 return Ok(());
384 };
385 let ctx = with_context!("combining with {target:?}");
386 let target = part.solid_id(target).with_context(ctx)?;
387 let mut tools = Vec::new();
388 for instance in instances {
389 match instance.body {
390 Body::Solid(solid) if solid == target => {}
391 Body::Solid(solid) => tools.push(Tool {
392 solid,
393 reach: ToolReach {
394 up_to_next: None,
395 scope: Some(instance.scope.clone()),
396 },
397 }),
398 Body::Sheet(_) => {
399 return Err(GeopError::new(format!(
400 "{} is no solid: only solids are joined, cut or intersected — keep the copies as new bodies",
401 describe(part, instance.body)
402 )))
403 .with_context(ctx);
404 }
405 }
406 }
407 if tools.is_empty() {
408 part.rename(target, namer.root()).with_context(ctx)?;
409 return Ok(());
410 }
411 combine_tools(part, namer, operation_id, &tools, combine)
412 .and_then(Reach::stopped)
413 .with_context(ctx)
414}
415
416pub(crate) fn center(reader: &Reader, bodies: &[BodyRef]) -> Option<Vector3> {
420 let part = read_model(reader).ok()?;
421 let model = part.topology();
422 let mut corners = Vec::new();
423 for body in resolve_bodies(reader, bodies).ok()? {
424 for vertex in model.iter_body_vertices(body).ok()? {
425 corners.push(model.get_vertex(vertex).ok()?.point);
426 }
427 }
428 middle(corners)
429}
430
431pub(crate) fn middle(points: impl IntoIterator<Item = Vector3>) -> Option<Vector3> {
434 let c = plain::centroid(points.into_iter().map(|p| p.to_f64_array()))?;
435 Some(Vector3::from_array(c.map(Scalar::from_f64)))
436}
437
438pub(crate) fn placed_by(
441 frames: &Frames,
442 reader: &Reader,
443 seeds: &Seeds,
444) -> GeopResult<Vec<(String, Motion)>> {
445 let anchor = || seeds.anchor(reader);
446 Ok((frames.motions(reader, &anchor)?.into_iter())
447 .map(|(label, pose)| (label, pose.motion()))
448 .collect())
449}
450
451fn box_middle<'c>(curves: impl IntoIterator<Item = &'c NurbCurve3D>) -> GeopResult<Vector3> {
454 let mut points = Vec::new();
455 for curve in curves {
456 for cp in &curve.control_points {
457 points.push(cp.dehomogenized()?);
458 }
459 }
460 let Some(hull) = points.into_iter().reduce(|a, b| a.union(&b)) else {
461 return Err(GeopError::new(
462 "the seeds have no edges to lay them out from",
463 ));
464 };
465 Ok(Vector3::from_array(std::array::from_fn(|k| {
467 Scalar::from_f64(hull[k].to_f64())
468 })))
469}
470
471fn copy_threads<O>(reader: &Reader, (step, args): &(String, O::Args)) -> GeopResult<Threads>
476where
477 O: ModelOperation + PartOperation,
478 O::Args: Pattern,
479{
480 let mut threads: Threads = reader.previous()?;
481 let features = crate::features::feature_steps(&*read_model(reader)?, &args.seeds().features)?;
482 let placements = args.placements(reader)?;
483 let names = O::Args::NAMING.of(step)?;
484 for step in &features {
485 let ctx = with_context!("copying the threads of feature {step:?}");
486 for name in threads.of_step(step) {
487 let thread = threads.get(&name)?.clone();
488 for (label, motion) in &placements {
489 let mut copy = thread.clone();
490 copy.axis = Axis {
491 point: motion.apply(&thread.axis.point),
492 direction: motion.rotate(&thread.axis.direction),
493 };
494 copy.face = names.copy(label, &thread.face);
495 threads
496 .add(names.copy(label, &name), copy)
497 .with_context(ctx)?;
498 }
499 }
500 }
501 Ok(threads)
502}
503
504fn copy_holes<O>(reader: &Reader, (step, args): &(String, O::Args)) -> GeopResult<HoleSites>
508where
509 O: ModelOperation + PartOperation,
510 O::Args: Pattern,
511{
512 let mut holes = read_hole_sites(reader)?.clone();
513 let features = crate::features::feature_steps(&*read_model(reader)?, &args.seeds().features)?;
514 let placements = args.placements(reader)?;
515 let names = O::Args::NAMING.of(step)?;
516 for step in &features {
517 let ctx = with_context!("copying the holes of feature {step:?}");
518 let drilled: Vec<_> = (holes.of_step(step).into_iter())
519 .map(|(name, site)| (name.to_string(), site.clone()))
520 .collect();
521 for (name, site) in drilled {
522 for (label, motion) in &placements {
523 holes
524 .add(names.copy(label, &name), site.moved(motion))
525 .with_context(ctx)?;
526 }
527 }
528 }
529 Ok(holes)
530}
531
532pub(crate) fn run_pattern<O>(ctx: &mut StepContext, args: &O::Args) -> GeopResult<()>
537where
538 O: ModelOperation + PartOperation,
539 O::Args: Pattern,
540{
541 let features = !args.seeds().features.is_empty();
542 if features {
543 let copies = (ctx.step().to_string(), args.clone());
544 ctx.produce(&threads(), &copies, copy_threads::<O>)?;
545 ctx.produce(&hole_sites(), &copies, copy_holes::<O>)?;
546 }
547 build_model::<O>(ctx, args)?;
548 match features {
549 true => draw_threads(ctx),
550 false => Ok(()),
551 }
552}