Skip to main content

geop_ops_pattern/
common.rs

1//! What the operations of this crate share: the bodies they act on, copies
2//! of them moved by a [`Motion`] and named after what they copy, combining
3//! the result as an extrude would, and the directions they move along.
4
5use 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
32/// A body of the part: a solid by its name, or a sheet by one of its
33/// faces'.
34pub type BodyRef = ElementReference<NamedModel, geop_ops_model::Body>;
35
36/// The body named `name`: a solid, or the sheet of the face of that name.
37pub fn body(name: impl Into<String>) -> BodyRef {
38    ElementReference::new(geop_ops_model::model(), name)
39}
40
41/// The field picking the bodies a step acts on, keyed `bodies`, in the part
42/// `before` reads: solids, and sheets by one of their faces. `set` gets the
43/// bodies picked.
44pub(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/// What a pattern repeats: `bodies` — kept as new bodies, or combined as
54/// `combine` says — or `features`, each copy combined as its feature was
55/// (see [`crate::features`]). Its fields are among those of the step's
56/// arguments.
57#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
58pub struct Seeds {
59    /// The bodies to copy: solids, or a face of each sheet.
60    pub bodies: Vec<BodyRef>,
61    /// Or the features to do again (see [`crate::Feature`]).
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub features: Vec<FeatureRef>,
64    /// Keep the copies of bodies as new bodies, or combine them with a
65    /// solid.
66    #[serde(default)]
67    pub combine: Combine,
68}
69
70impl Seeds {
71    /// The bodies `bodies`, the copies combined as `combine` says.
72    pub fn bodies(bodies: Vec<BodyRef>, combine: Combine) -> Self {
73        Seeds {
74            bodies,
75            features: Vec::new(),
76            combine,
77        }
78    }
79
80    /// The features `features`.
81    pub fn features(features: Vec<FeatureRef>) -> Self {
82        Seeds {
83            features,
84            ..Seeds::default()
85        }
86    }
87
88    /// Its fields in `f`, around those `own` adds: first the bodies (see
89    /// [`bodies_field`]) or the features, keyed `features`, each by a face
90    /// it made — a hole by its wall — and last, of bodies, how to combine
91    /// them (see [`Combine::fields`], a mode defaulting to the newest solid
92    /// `before` reads). A step repeats one or the other, so only the one in
93    /// use waits for picks, and picking bodies empties the features, and
94    /// the other way round. `set` puts the seeds set where the arguments
95    /// keep them.
96    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        // Only the one in use is needed; the other is picked for when asked.
129        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    /// Copies what it picks in `part` — the model `reader` reads before the
150    /// step — to every placement — a label and the motion from where the
151    /// seeds are — as the step `operation_id`, whose names `names` gives
152    /// (see [`Names::copy`]). Bodies are then kept, or combined as
153    /// [`combine_instances`] does, the bodies themselves labelled
154    /// `seed_label`; features are done again (see
155    /// [`crate::features::repeat_features`]).
156    ///
157    /// Bodies and features both, or neither, are refused. The features'
158    /// cosmetic threads are copied by [`copy_threads`].
159    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    /// Where it is, to lay instances out from — a face filled through it:
194    /// the middle of the box round the edges of the bodies, or of the
195    /// tools of the features, in the part `reader` reads.
196    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    /// Where the handles of a step repeating it sit, in the part `reader`
219    /// reads: the middle of the corners of the bodies, or of the tools of
220    /// the features. None, if it picks none.
221    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/// How a pattern names what it makes, after the kind of its operation:
231/// the copy of each entity named `X` in the instance labelled `i` is named
232/// `kind(P,i,X)` for the step `P` — or, not `labelled`, `kind(P,X)`: of a
233/// step of one instance.
234#[derive(Clone, Copy, Debug, PartialEq)]
235pub(crate) struct Naming {
236    pub kind: &'static str,
237    pub labelled: bool,
238}
239
240impl Naming {
241    /// The names it gives as the step `step`.
242    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
250/// The names a pattern's step gives (see [`Naming`]).
251pub(crate) struct Names {
252    pub namer: Namer,
253    labelled: bool,
254}
255
256impl Names {
257    /// The name of the copy of the entity named `name` in the instance
258    /// labelled `label`.
259    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
267/// The arguments of a step that repeats bodies or features: a pattern, or
268/// a mirror.
269pub(crate) trait Pattern: std::fmt::Debug {
270    /// How it names what it makes.
271    const NAMING: Naming;
272
273    /// What it repeats.
274    fn seeds(&self) -> &Seeds;
275
276    /// The label of the seeds themselves among the instances.
277    fn seed_label(&self) -> &'static str;
278
279    /// Where its instances go, as `reader` reads the part: each a label and
280    /// the motion from the seeds, the seeds themselves left out.
281    fn placements(&self, reader: &Reader) -> GeopResult<Vec<(String, Motion)>>;
282}
283
284/// Builds the pattern `args` as the step `step` into `model`, reading the
285/// part before it through `reader`: its [`ModelOperation::build`].
286pub(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
297/// One body a step leaves behind as one of its instances — a seed, or a
298/// copy of one — and the scope that tells its combination's names apart
299/// from the other instances' (see [`Tool::scope`]).
300pub(crate) struct Instance {
301    pub body: Body,
302    pub scope: String,
303}
304
305/// The scope of the `k`-th of `n` seeds' instance labelled `label`: the
306/// label itself, if there is one seed.
307fn 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
315/// The seeds as instances labelled `label`.
316pub(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
327/// Copies every seed and moves the copy by `motion`, as the instance
328/// labelled `label`: the copy of each entity named `X` — the solid
329/// included — named `rename(X)`.
330pub(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
349/// A body as an error names it: a solid by its name, a sheet by one of its
350/// faces'.
351fn 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
367/// Combines the `instances` a step left as `combine` says: kept as new
368/// bodies, each as it is; or each joined to, cut from or intersected with
369/// the target, one after the other — the target itself, if it is one of
370/// them, excepted. Combined, the result is named `namer`'s root, and what
371/// each combination creates `combine(Scalar,scope,...)` for the step `Scalar` (see
372/// [`Combine::apply`]).
373///
374/// Only solids are combined: a sheet among the instances is refused.
375pub(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
416/// Where the handles of a step acting on `bodies` sit, in the part
417/// `reader` reads: the middle of the corners of the bodies. None, if it
418/// refers to none.
419pub(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
431/// The middle of `points`, where a handle sits: their average, read off
432/// midpoints — a free choice. None, of none.
433pub(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
438/// Where the instances of a pattern of `seeds` laid out by `frames` go,
439/// as `reader` reads the part: each a label and the motion from the seeds.
440pub(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
451/// The middle of the box around `curves`' control points: on the axis of
452/// a round one, where the middle of its corners is not.
453fn 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    // Any point of the box would do: its middle, sharp.
466    Ok(Vector3::from_array(std::array::from_fn(|k| {
467        Scalar::from_f64(hull[k].to_f64())
468    })))
469}
470
471/// The part's cosmetic threads with those each feature the pattern `O`'s
472/// step `step` copies recorded copied to every placement of it, named
473/// alike — the features and the placements as the part is before the
474/// step, which is produced before the step's model.
475fn 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
504/// The part's holes with those each feature the pattern `O`'s step
505/// `step` copies drilled copied to every placement of it, named alike (see
506/// [`copy_threads`]).
507fn 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
532/// Runs the pattern `O` of features or bodies as the step being run:
533/// copies of its seeds in the model, at its placements — of the part as it
534/// is before the step — and, of features, copies of their cosmetic
535/// threads, drawn as the step's annotations.
536pub(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}