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