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