Skip to main content

brep_cam/model/
ids.rs

1//! Document-level identities and target references.
2//!
3//! Setups, tools, operations and machines are addressed by string ids the
4//! document assigns and keeps stable across saves. Bodies and faces are
5//! addressed by the BREP's persistent NAMES (the kernel propagates face names
6//! through booleans), which is the identity the application's selection and
7//! feature references already use.
8
9use super::Point3;
10use serde::{Deserialize, Serialize};
11
12macro_rules! string_id {
13    ($(#[$doc:meta])* $name:ident) => {
14        $(#[$doc])*
15        #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
16        #[serde(transparent)]
17        pub struct $name(pub String);
18
19        impl $name {
20            pub fn new(id: impl Into<String>) -> Self {
21                Self(id.into())
22            }
23
24            pub fn as_str(&self) -> &str {
25                &self.0
26            }
27        }
28
29        impl From<&str> for $name {
30            fn from(s: &str) -> Self {
31                Self(s.to_string())
32            }
33        }
34
35        impl From<String> for $name {
36            fn from(s: String) -> Self {
37                Self(s)
38            }
39        }
40
41        impl std::fmt::Display for $name {
42            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43                f.write_str(&self.0)
44            }
45        }
46    };
47}
48
49string_id!(
50    /// Identity of a [`crate::model::Setup`] within a document.
51    SetupId
52);
53string_id!(
54    /// Identity of a [`crate::model::Tool`] within a document or library.
55    ToolId
56);
57string_id!(
58    /// Identity of an [`crate::model::Operation`] within a document.
59    OperationId
60);
61string_id!(
62    /// Identity of a [`crate::model::Machine`] definition.
63    MachineId
64);
65string_id!(
66    /// Identity of a [`crate::model::Holder`] definition.
67    HolderId
68);
69
70/// A stable reference to a BREP body or to one face of it.
71///
72/// `face == None` names the whole body. Resolution against a mesh snapshot
73/// happens by name; an unresolvable reference is a reported error, never a
74/// silently empty target.
75#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct TargetRef {
78    /// Persistent body (solid) name.
79    pub solid: String,
80    /// Persistent face name within `solid`, or `None` for the whole body.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub face: Option<String>,
83}
84
85impl TargetRef {
86    pub fn solid(solid: impl Into<String>) -> Self {
87        Self { solid: solid.into(), face: None }
88    }
89
90    pub fn face(solid: impl Into<String>, face: impl Into<String>) -> Self {
91        Self { solid: solid.into(), face: Some(face.into()) }
92    }
93
94    pub fn is_whole_body(&self) -> bool {
95        self.face.is_none()
96    }
97}
98
99/// What an operation machines and what it must not touch.
100///
101/// `drive_faces` restrict where cutting is REQUESTED; collision and gouge
102/// checks always protect every triangle of `target_solids` and
103/// `protected_solids`. An empty `target_solids` list is a reported error for
104/// an enabled operation.
105#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct TargetSelection {
108    /// Bodies to machine.
109    #[serde(default)]
110    pub target_solids: Vec<String>,
111    /// Faces (on the target bodies) that bound the requested cutting region.
112    #[serde(default, skip_serializing_if = "Vec::is_empty")]
113    pub drive_faces: Vec<TargetRef>,
114    /// Faces to avoid (checked, never cut) in addition to the protected bodies.
115    #[serde(default, skip_serializing_if = "Vec::is_empty")]
116    pub avoid_faces: Vec<TargetRef>,
117    /// Fixtures, clamps and other bodies that are protected but not machined.
118    #[serde(default, skip_serializing_if = "Vec::is_empty")]
119    pub protected_solids: Vec<String>,
120    /// Ordered edge chains and sketch curves/profiles to machine along (2D
121    /// contour, slot, chamfer, trace, engrave, ...).
122    #[serde(default, skip_serializing_if = "Vec::is_empty")]
123    pub chains: Vec<ChainRef>,
124}
125
126impl TargetSelection {
127    /// Names and chain shapes are well formed (resolution against the model is
128    /// the dispatcher's).
129    pub fn validate(&self) -> Result<(), String> {
130        for (i, chain) in self.chains.iter().enumerate() {
131            chain.validate().map_err(|m| format!("chain {i}: {m}"))?;
132        }
133        Ok(())
134    }
135
136    pub fn solids<I, S>(solids: I) -> Self
137    where
138        I: IntoIterator<Item = S>,
139        S: Into<String>,
140    {
141        Self { target_solids: solids.into_iter().map(Into::into).collect(), ..Default::default() }
142    }
143
144    /// Every body whose triangles take part in protection checks, de-duplicated and sorted.
145    pub fn protected_bodies(&self) -> Vec<String> {
146        let mut all: Vec<String> =
147            self.target_solids.iter().chain(self.protected_solids.iter()).cloned().collect();
148        all.sort();
149        all.dedup();
150        all
151    }
152}
153
154/// One BREP edge by persistent names, like [`TargetRef`] for faces.
155#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct EdgeRef {
158    /// Persistent body (solid) name.
159    pub solid: String,
160    /// Persistent edge name within `solid`.
161    pub edge: String,
162}
163
164impl EdgeRef {
165    pub fn new(solid: impl Into<String>, edge: impl Into<String>) -> Self {
166        Self { solid: solid.into(), edge: edge.into() }
167    }
168}
169
170/// An ordered chain of curves to machine along.
171#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173#[serde(default)]
174pub struct ChainRef {
175    pub source: ChainSource,
176    /// The chain closes on itself; resolution checks it and reports a mismatch.
177    pub closed: bool,
178    /// Traverse opposite to the stored order.
179    pub reversed: bool,
180    /// Which side of the chain the tool stays on.
181    pub side: ChainSide,
182    /// Extra offset from the chain towards `side`, in model units (may be negative).
183    pub offset: f64,
184    /// Where the tool starts along the chain (nearest chain point to this, in
185    /// model coordinates); `None`: the chain's first point.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub start: Option<Point3>,
188}
189
190impl Default for ChainRef {
191    fn default() -> Self {
192        Self { source: ChainSource::default(), closed: false, reversed: false, side: ChainSide::Auto, offset: 0.0, start: None }
193    }
194}
195
196impl ChainRef {
197    pub fn validate(&self) -> Result<(), String> {
198        self.source.validate()?;
199        if !self.offset.is_finite() {
200            return Err("chain offset is not finite".into());
201        }
202        if self.start.map_or(false, |p| !p.is_finite()) {
203            return Err("chain start point is not finite".into());
204        }
205        if !self.closed && matches!(self.side, ChainSide::Inside | ChainSide::Outside) {
206            return Err("inside/outside needs a closed chain; use left or right on an open chain".into());
207        }
208        Ok(())
209    }
210}
211
212/// Where a chain's curves come from.
213#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
215pub enum ChainSource {
216    /// BREP edges in chain order (consecutive edges share a vertex).
217    Edges { edges: Vec<EdgeRef> },
218    /// Sketch curves by geometry id (the `{id}` of the edge name `{sketch}:G{id}`), in chain order.
219    SketchCurves { sketch: String, curves: Vec<String> },
220    /// A sketch's closed profile (the face `{sketch}:PROFILE`); `regions` picks
221    /// regions by index in the kernel's SketchProfile region order, each region
222    /// an outer loop with its islands (empty: every region).
223    SketchProfile {
224        sketch: String,
225        #[serde(default)]
226        regions: Vec<u32>,
227    },
228}
229
230impl Default for ChainSource {
231    fn default() -> Self {
232        ChainSource::Edges { edges: Vec::new() }
233    }
234}
235
236impl ChainSource {
237    /// Serde names of every variant, in declaration order.
238    pub const VARIANTS: &'static [&'static str] = &["edges", "sketchCurves", "sketchProfile"];
239
240    pub fn validate(&self) -> Result<(), String> {
241        match self {
242            ChainSource::Edges { edges } => {
243                if edges.is_empty() {
244                    return Err("an edge chain needs at least one edge".into());
245                }
246                let mut seen = std::collections::BTreeSet::new();
247                for e in edges {
248                    if e.solid.is_empty() || e.edge.is_empty() {
249                        return Err("an edge reference needs a body and an edge name".into());
250                    }
251                    if !seen.insert(e) {
252                        return Err(format!("edge {:?} of {:?} appears twice in the chain", e.edge, e.solid));
253                    }
254                }
255                Ok(())
256            }
257            ChainSource::SketchCurves { sketch, curves } => {
258                if sketch.is_empty() || curves.is_empty() || curves.iter().any(|c| c.is_empty()) {
259                    return Err("a sketch chain needs a sketch id and at least one curve id".into());
260                }
261                let unique: std::collections::BTreeSet<&String> = curves.iter().collect();
262                if unique.len() != curves.len() {
263                    return Err("a sketch chain lists a curve twice".into());
264                }
265                Ok(())
266            }
267            ChainSource::SketchProfile { sketch, regions } => {
268                if sketch.is_empty() {
269                    return Err("a sketch profile needs a sketch id".into());
270                }
271                let unique: std::collections::BTreeSet<&u32> = regions.iter().collect();
272                if unique.len() != regions.len() {
273                    return Err("a sketch profile lists a region twice".into());
274                }
275                Ok(())
276            }
277        }
278    }
279}
280
281/// Which side of a chain the tool stays on.
282#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
283#[serde(rename_all = "kebab-case")]
284pub enum ChainSide {
285    /// The strategy decides (contour: outside of a closed chain).
286    #[default]
287    Auto,
288    Left,
289    Right,
290    Inside,
291    Outside,
292    /// The tool centre runs on the chain.
293    On,
294}
295
296impl ChainSide {
297    /// Serde names of every variant, in declaration order.
298    pub const VARIANTS: &'static [&'static str] = &["auto", "left", "right", "inside", "outside", "on"];
299}