1use 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 SetupId
52);
53string_id!(
54 ToolId
56);
57string_id!(
58 OperationId
60);
61string_id!(
62 MachineId
64);
65string_id!(
66 HolderId
68);
69
70#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct TargetRef {
78 pub solid: String,
80 #[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#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct TargetSelection {
108 #[serde(default)]
110 pub target_solids: Vec<String>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub drive_faces: Vec<TargetRef>,
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub avoid_faces: Vec<TargetRef>,
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub protected_solids: Vec<String>,
120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
123 pub chains: Vec<ChainRef>,
124}
125
126impl TargetSelection {
127 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 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#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct EdgeRef {
158 pub solid: String,
160 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173#[serde(default)]
174pub struct ChainRef {
175 pub source: ChainSource,
176 pub closed: bool,
178 pub reversed: bool,
180 pub side: ChainSide,
182 pub offset: f64,
184 #[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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
215pub enum ChainSource {
216 Edges { edges: Vec<EdgeRef> },
218 SketchCurves { sketch: String, curves: Vec<String> },
220 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 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
283#[serde(rename_all = "kebab-case")]
284pub enum ChainSide {
285 #[default]
287 Auto,
288 Left,
289 Right,
290 Inside,
291 Outside,
292 On,
294}
295
296impl ChainSide {
297 pub const VARIANTS: &'static [&'static str] = &["auto", "left", "right", "inside", "outside", "on"];
299}