1use 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 SetupId
53);
54string_id!(
55 ToolId
57);
58string_id!(
59 OperationId
61);
62string_id!(
63 MachineId
65);
66string_id!(
67 HolderId
69);
70
71#[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 pub solid: String,
82 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
108#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct TargetSelection {
111 #[serde(default)]
113 pub target_solids: Vec<String>,
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub drive_faces: Vec<TargetRef>,
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub avoid_faces: Vec<TargetRef>,
120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
122 pub protected_solids: Vec<String>,
123 #[serde(default, skip_serializing_if = "Vec::is_empty")]
126 pub chains: Vec<ChainRef>,
127}
128
129impl TargetSelection {
130 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 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#[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 pub solid: String,
164 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#[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 pub closed: bool,
183 pub reversed: bool,
185 pub side: ChainSide,
187 pub offset: f64,
189 #[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#[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 Edges { edges: Vec<EdgeRef> },
224 SketchCurves { sketch: String, curves: Vec<String> },
226 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 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#[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 #[default]
294 Auto,
295 Left,
296 Right,
297 Inside,
298 Outside,
299 On,
301}
302
303impl ChainSide {
304 pub const VARIANTS: &'static [&'static str] = &["auto", "left", "right", "inside", "outside", "on"];
306}