Skip to main content

geop_ops_model/
feature.rs

1//! A [`super::NamedModel`]'s features: what each step that combined a tool with a solid
2//! did — a cut, a boss, a hole — kept so that a later step can do it again
3//! elsewhere (a pattern of features), and so that a face can be traced back
4//! to the step that made it.
5//!
6//! A feature is kept as its tools, as they were built before they were
7//! combined, and how each was combined: not as the arguments of its step,
8//! which a part never sees. Doing a feature again is copying its tools,
9//! moving them, and combining them the same way — one boolean each, with
10//! a tool going up to the next face stopping at the next face *where the
11//! copy is* (see `geop_ops_model::Combine`).
12//!
13//! How a tool is combined — [`BooleanOp`] and [`ToolReach`] — is defined
14//! here rather than with the booleans: it is the vocabulary a feature is
15//! recorded in, which every step that combines tools (extrude, hole, rib,
16//! ...) writes, and which a pattern or mirror reads to do the feature
17//! again. The booleans take their arguments in the same terms.
18
19use std::{collections::HashMap, sync::Arc};
20
21use geop_core_editor::selectable::SelectableElement;
22use geop_core_math::geop_error::{GeopError, GeopResult};
23use geop_core_program::{
24    element::{ElementReference, HasElements, Kind, PickedElement},
25    reader::Reader,
26};
27use geop_core_topology::build::BodySpec;
28use serde::{Deserialize, Serialize};
29
30use super::{
31    BodyNames, NamedModel,
32    elements::{Face, read_model},
33    names::{digest, origin_of},
34};
35
36/// Which boolean to perform.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum BooleanOp {
40    /// Everything in either solid.
41    Union,
42    /// Only what is in both.
43    Intersection,
44    /// `solid_a` with `solid_b` removed.
45    Difference,
46    /// What is in either solid but not in both.
47    SymmetricDifference,
48}
49
50/// What one step did by combining tools with a solid: each tool, in the
51/// order it was combined.
52#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
53pub struct Feature {
54    pub tools: Vec<FeatureTool>,
55}
56
57/// One tool of a [`Feature`]: the solid as its step built it, before it was
58/// combined — its geometry and the names of its entities — and how it was
59/// combined with the solid it was combined with.
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61pub struct FeatureTool {
62    pub spec: BodySpec,
63    pub names: BodyNames,
64    pub op: BooleanOp,
65    pub reach: ToolReach,
66}
67
68/// How far a tool reaches when it is combined, and how what its
69/// combination creates is named: what, besides the [`BooleanOp`], a step
70/// combines a tool by, and a [`Feature`] records.
71#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
72pub struct ToolReach {
73    /// For a tool going only up to the next face of the solid: the names
74    /// of its start and end faces. None for all of the tool.
75    pub up_to_next: Option<(String, String)>,
76    /// What tells its combination's names apart from the step's other
77    /// tools' (see [`super::Namer::scoped`]). None for the one tool of its
78    /// step.
79    pub scope: Option<String>,
80}
81
82impl NamedModel {
83    /// Records `tool` as one more tool of the feature of the step `step`.
84    pub fn record_feature_tool(&mut self, step: &str, tool: FeatureTool) {
85        let features = &mut self.features;
86        match features.iter_mut().find(|(s, _)| s == step) {
87            Some((_, feature)) => Arc::make_mut(feature).tools.push(tool),
88            None => features.push((step.to_string(), Arc::new(Feature { tools: vec![tool] }))),
89        }
90    }
91
92    /// The feature of the step `step`. Fails, saying which steps are
93    /// features, for a step that combined nothing.
94    pub fn feature(&self, step: &str) -> GeopResult<&Feature> {
95        self.features
96            .iter()
97            .find(|(s, _)| s == step)
98            .map(|(_, f)| f.as_ref())
99            .ok_or_else(|| {
100                let steps: Vec<&str> = self.features.iter().map(|(s, _)| s.as_str()).collect();
101                GeopError::new(format!(
102                    "step {step:?} is no feature: only a step that joins, cuts or intersects a tool with a solid is (the features: {steps:?})"
103                ))
104            })
105    }
106
107    /// Every feature, by its step, in the order the steps ran.
108    pub fn features(&self) -> impl Iterator<Item = (&str, &Feature)> {
109        self.features.iter().map(|(s, f)| (s.as_str(), f.as_ref()))
110    }
111
112    /// Which feature made each face, as [`FeatureFaces::of`] tells.
113    pub fn feature_faces(&self) -> FeatureFaces<'_> {
114        let mut steps = HashMap::new();
115        for (step, feature) in &self.features {
116            for tool in &feature.tools {
117                for face in &tool.names.faces {
118                    steps.insert(face.clone(), step.as_str());
119                    steps.insert(digest(face), step.as_str());
120                }
121            }
122        }
123        FeatureFaces { steps }
124    }
125}
126
127/// Which feature made a face (see [`NamedModel::feature_faces`]).
128pub struct FeatureFaces<'a> {
129    /// The step of each face of a tool, by the face's name and its digest.
130    steps: HashMap<String, &'a str>,
131}
132
133impl FeatureFaces<'_> {
134    /// The step whose feature made the face named `face`: the face of one
135    /// of its tools, or a piece of one that a boolean split off (see
136    /// [`super::Namer::piece`]) however often it was split since.
137    /// None for a face no feature made: one of a solid built on its own,
138    /// or what a later step made of it.
139    pub fn of(&self, face: &str) -> Option<&str> {
140        // Each origin a shorter name than the piece's: the chain ends.
141        std::iter::successors(Some(face), |&name| origin_of(name))
142            .find_map(|name| self.steps.get(name).copied())
143    }
144}
145
146/// What a step did by combining tools with a solid — a cut, a boss, a hole
147/// — as an element of the model: named by the step, and picked by a face
148/// it made (see [`FeatureFaces`]), lit as every face of it that is left. A
149/// feature of a part placed in the part is picked as one of that part,
150/// behind its instance.
151pub struct FeatureElement;
152
153/// A feature of the part, or of a part placed in it, by the step that
154/// made it.
155pub type FeatureRef = ElementReference<NamedModel, FeatureElement>;
156
157/// The feature of the step `step`.
158pub fn feature(step: impl Into<String>) -> FeatureRef {
159    ElementReference::new(super::model(), step)
160}
161
162impl Kind for FeatureElement {
163    const NAME: &'static str = "feature";
164}
165
166impl HasElements<FeatureElement> for NamedModel {
167    type Element<'a> = &'a Feature;
168    const SINGLETON: Option<&'static str> = <Self as HasElements<Face>>::SINGLETON;
169
170    fn element(&self, name: &str) -> GeopResult<Self::Element<'_>> {
171        self.feature(name)
172    }
173}
174
175/// The part a pick of `instance` is of: the part `reader` reads, or the
176/// part placed there.
177fn part_of<'a>(reader: &Reader<'a>, instance: &str) -> Option<Reader<'a>> {
178    match instance.is_empty() {
179        true => Some(reader.clone()),
180        false => reader.placed_part(instance).ok(),
181    }
182}
183
184impl SelectableElement<NamedModel> for FeatureElement {
185    const PICKED_FROM: &'static [&'static str] = &[Face::NAME];
186
187    fn select(reader: &Reader, hit: &PickedElement) -> Option<FeatureRef> {
188        let face = ElementReference::<NamedModel, Face>::from_picked(hit)?;
189        let instance = face.instance().to_string();
190        let part = read_model(&part_of(reader, &instance)?).ok()?;
191        part.face_id(face.name()).ok()?;
192        let step = part.feature_faces().of(face.name())?.to_string();
193        Some(match instance.is_empty() {
194            true => feature(step),
195            false => feature(step).behind(&instance),
196        })
197    }
198
199    fn drawn(reader: &Reader, feature: &FeatureRef) -> Vec<PickedElement> {
200        let instance = feature.instance();
201        let Some(part) = part_of(reader, instance).and_then(|r| read_model(&r).ok()) else {
202            return Vec::new();
203        };
204        let made = part.feature_faces();
205        let mut faces: Vec<_> = part.topology().faces.keys().copied().collect();
206        faces.sort_by_key(|face| face.0);
207        (faces.into_iter())
208            .filter_map(|face| part.name_of(face))
209            .filter(|&face| made.of(face) == Some(feature.name()))
210            .map(|face| {
211                let face = ElementReference::<NamedModel, Face>::new(super::model(), face);
212                match instance.is_empty() {
213                    true => face.picked(),
214                    false => face.behind(instance).picked(),
215                }
216            })
217            .collect()
218    }
219}