1use super::{Bounds3, MachineId, Point3, RigidTransform, SetupId, TargetRef};
4use serde::{Deserialize, Serialize};
5
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct Setup {
15 pub id: SetupId,
16 #[serde(default)]
17 pub name: String,
18 #[serde(default)]
20 pub model_to_machine: RigidTransform,
21 #[serde(default)]
23 pub wcs: WorkOffset,
24 #[serde(default)]
26 pub stock: Stock,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub turning_stick_out: Option<f64>,
30 #[serde(default)]
32 pub heights: Heights,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub machine: Option<MachineId>,
36 #[serde(default)]
38 pub targets: Vec<TargetRef>,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub protected: Vec<TargetRef>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub program: Option<ProgramInfo>,
45}
46
47impl Setup {
48 pub fn new(id: impl Into<SetupId>, name: impl Into<String>) -> Self {
49 Self { id: id.into(), name: name.into(), ..Default::default() }
50 }
51
52 pub fn machine_to_model(&self) -> RigidTransform {
54 self.model_to_machine.inverse()
55 }
56}
57
58#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62#[serde(default)]
63pub struct WorkOffset {
64 pub number: u32,
66 pub origin: Point3,
68}
69
70impl Default for WorkOffset {
71 fn default() -> Self {
72 Self { number: 1, origin: Point3::ZERO }
73 }
74}
75
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct ProgramInfo {
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub number: Option<u32>,
83 #[serde(default)]
84 pub comment: String,
85}
86
87#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91#[serde(tag = "kind", rename_all = "camelCase")]
92#[non_exhaustive]
93pub enum Stock {
94 BoundsOffset {
96 #[serde(default)]
97 x: f64,
98 #[serde(default)]
99 y: f64,
100 #[serde(default)]
101 top: f64,
102 #[serde(default)]
103 bottom: f64,
104 },
105 Box { min: Point3, max: Point3 },
107 Cylinder { base: Point3, axis: Point3, radius: f64, height: f64 },
109 FromSolid { solid: String },
111 FromRest { setup: SetupId },
113}
114
115impl Default for Stock {
116 fn default() -> Self {
117 Stock::BoundsOffset { x: 0.0, y: 0.0, top: 0.0, bottom: 0.0 }
118 }
119}
120
121impl Stock {
122 pub fn model_bounds(&self, target_bounds: &Bounds3) -> Option<Bounds3> {
126 match self {
127 Stock::BoundsOffset { x, y, top, bottom } => {
128 if target_bounds.is_empty() {
129 return None;
130 }
131 Some(Bounds3::new(
132 Point3::new(target_bounds.min.x - x, target_bounds.min.y - y, target_bounds.min.z - bottom),
133 Point3::new(target_bounds.max.x + x, target_bounds.max.y + y, target_bounds.max.z + top),
134 ))
135 }
136 Stock::Box { min, max } => Some(Bounds3::new(*min, *max)),
137 Stock::Cylinder { base, axis, radius, height } => {
138 let a = axis.normalized()?;
139 let top = base.add(a.scale(*height));
140 let ext = Point3::new(
142 radius * (1.0 - a.x * a.x).max(0.0).sqrt(),
143 radius * (1.0 - a.y * a.y).max(0.0).sqrt(),
144 radius * (1.0 - a.z * a.z).max(0.0).sqrt(),
145 );
146 Some(Bounds3::new(base.min(top).sub(ext), base.max(top).add(ext)))
147 }
148 Stock::FromSolid { .. } | Stock::FromRest { .. } => None,
149 }
150 }
151
152 pub fn validate(&self) -> Result<(), String> {
154 match self {
155 Stock::BoundsOffset { x, y, top, bottom } => {
156 for (name, v) in [("x", x), ("y", y), ("top", top), ("bottom", bottom)] {
157 if !v.is_finite() {
158 return Err(format!("stock offset {name} is not finite"));
159 }
160 }
161 Ok(())
162 }
163 Stock::Box { min, max } => {
164 if !min.is_finite() || !max.is_finite() {
165 return Err("stock box has a non-finite corner".into());
166 }
167 if min.x >= max.x || min.y >= max.y || min.z >= max.z {
168 return Err("stock box must have positive size on every axis".into());
169 }
170 Ok(())
171 }
172 Stock::Cylinder { base, axis, radius, height } => {
173 if !base.is_finite() || !axis.is_finite() || !radius.is_finite() || !height.is_finite() {
174 return Err("stock cylinder has a non-finite parameter".into());
175 }
176 if axis.normalized().is_none() {
177 return Err("stock cylinder axis must be non-zero".into());
178 }
179 if *radius <= 0.0 || *height <= 0.0 {
180 return Err("stock cylinder radius and height must be positive".into());
181 }
182 Ok(())
183 }
184 Stock::FromSolid { solid } => {
185 if solid.is_empty() {
186 Err("stock body name is empty".into())
187 } else {
188 Ok(())
189 }
190 }
191 Stock::FromRest { setup } => {
192 if setup.as_str().is_empty() {
193 Err("rest stock setup id is empty".into())
194 } else {
195 Ok(())
196 }
197 }
198 }
199 }
200}
201
202#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
204#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206#[serde(default)]
207pub struct Heights {
208 pub clearance: f64,
210 pub retract: f64,
212 pub feed: f64,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub top: Option<f64>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub bottom: Option<f64>,
220}
221
222impl Default for Heights {
223 fn default() -> Self {
224 Self { clearance: 15.0, retract: 5.0, feed: 2.0, top: None, bottom: None }
225 }
226}
227
228impl Heights {
229 pub fn validate(&self) -> Result<(), String> {
231 for (name, v) in [("clearance", self.clearance), ("retract", self.retract), ("feed", self.feed)] {
232 if !v.is_finite() {
233 return Err(format!("{name} height is not finite"));
234 }
235 }
236 if self.clearance < self.retract {
237 return Err("clearance height must not be below retract height".into());
238 }
239 if self.retract < self.feed {
240 return Err("retract height must not be below feed height".into());
241 }
242 if let (Some(top), Some(bottom)) = (self.top, self.bottom) {
243 if !top.is_finite() || !bottom.is_finite() {
244 return Err("top/bottom height is not finite".into());
245 }
246 if top < bottom {
247 return Err("top height must not be below bottom height".into());
248 }
249 }
250 Ok(())
251 }
252}