Skip to main content

geop_ops_inspect/
scope.rs

1//! [`Scope`]: which of the parts placed a question about a whole assembly
2//! is asked of, so that one changed leg is checked without the rest.
3
4use geop_core_math::geop_error::{GeopError, GeopResult};
5use geop_core_program::placed::INSTANCE_SEPARATOR;
6use geop_ops_model::placed::solids::PlacedSolid;
7use serde::{Deserialize, Serialize};
8
9/// Which solids a question is asked of: those of the parts placed `parts`
10/// (`left_leg`, `left_leg/shin`), with the parts placed in them — all
11/// solids, if it names none.
12#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
13#[serde(default)]
14pub struct Scope {
15    pub parts: Vec<String>,
16    /// For a question about pairs: only those with both solids in `parts`,
17    /// not each of them against everything.
18    pub only_among: bool,
19}
20
21/// How far a question was asked before it was stopped (see
22/// [`geop_core_program::watch`]): what was not looked at is unknown.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
24pub struct Stopped {
25    pub checked: usize,
26    pub of: usize,
27}
28
29impl Scope {
30    /// Whether the part placed at `instance` is named, or placed in one
31    /// that is.
32    fn names(&self, instance: &str) -> bool {
33        self.parts.iter().any(|part| {
34            (instance.strip_prefix(part.as_str()))
35                .is_some_and(|rest| rest.is_empty() || rest.starts_with(INSTANCE_SEPARATOR))
36        })
37    }
38
39    /// Whether it asks about the solid of the part placed at `instance`.
40    pub fn holds(&self, instance: &str) -> bool {
41        self.parts.is_empty() || self.names(instance)
42    }
43
44    /// Whether it asks about the pair of solids of the parts placed at `a`
45    /// and `b`.
46    pub fn holds_pair(&self, a: &str, b: &str) -> bool {
47        match self.only_among {
48            true => self.holds(a) && self.holds(b),
49            false => self.holds(a) || self.holds(b),
50        }
51    }
52
53    /// Refuses a part that none of `solids` is of, saying which there are:
54    /// a misspelt part would otherwise answer that nothing overlaps.
55    pub fn check(&self, solids: &[PlacedSolid]) -> GeopResult<()> {
56        for part in &self.parts {
57            let one = Scope {
58                parts: vec![part.clone()],
59                only_among: false,
60            };
61            if !solids.iter().any(|solid| one.names(&solid.instance)) {
62                let mut there: Vec<&str> = (solids.iter())
63                    .map(|solid| solid.instance.as_str())
64                    .filter(|instance| !instance.is_empty())
65                    .collect();
66                there.sort_unstable();
67                there.dedup();
68                return Err(GeopError::new(format!(
69                    "no solid is of a part placed as {part:?}; the parts with solids are: {}",
70                    there.join(", ")
71                )));
72            }
73        }
74        Ok(())
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    fn of(parts: &[&str], only_among: bool) -> Scope {
83        Scope {
84            parts: parts.iter().map(|p| p.to_string()).collect(),
85            only_among,
86        }
87    }
88
89    #[test]
90    fn a_part_holds_what_is_placed_in_it_and_no_part_named_like_it() {
91        let scope = of(&["leg"], false);
92        assert!(scope.holds("leg") && scope.holds("leg/shin"));
93        assert!(!scope.holds("leg2") && !scope.holds("arm") && !scope.holds(""));
94        assert!(Scope::default().holds("") && Scope::default().holds("arm"));
95    }
96
97    #[test]
98    fn a_pair_is_held_by_either_or_only_by_both() {
99        assert!(of(&["leg"], false).holds_pair("leg/shin", "pelvis"));
100        assert!(!of(&["leg"], false).holds_pair("arm", "pelvis"));
101        assert!(!of(&["leg"], true).holds_pair("leg/shin", "pelvis"));
102        assert!(of(&["leg"], true).holds_pair("leg/shin", "leg/foot"));
103    }
104}