1use super::{HolderId, ToolId};
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct Tool {
12 pub id: ToolId,
13 #[serde(default)]
15 pub number: u32,
16 #[serde(default)]
17 pub name: String,
18 #[serde(default)]
19 pub description: String,
20 #[serde(default)]
21 pub kind: ToolKind,
22 #[serde(default)]
23 pub geometry: ToolGeometry,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub turning_geometry: Option<crate::turning::TurningGeometry>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub holder: Option<Holder>,
29 #[serde(default)]
30 pub coolant: Coolant,
31 #[serde(default)]
32 pub feeds: FeedsSpeeds,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub length_offset: Option<u32>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub diameter_offset: Option<u32>,
39 #[serde(default, skip_serializing_if = "String::is_empty")]
41 pub material: String,
42}
43
44impl Tool {
45 pub fn new(id: impl Into<ToolId>, number: u32, kind: ToolKind, geometry: ToolGeometry) -> Self {
46 Self { id: id.into(), number, kind, geometry, ..Default::default() }
47 }
48
49 pub fn radius(&self) -> f64 {
50 self.geometry.diameter * 0.5
51 }
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[serde(rename_all = "kebab-case")]
57#[non_exhaustive]
58pub enum ToolKind {
59 #[default]
61 Flat,
62 Ball,
64 Bull,
66 Chamfer,
68 VBit,
70 TaperedBall,
72 TaperedFlat,
74 Lollipop,
76 FaceMill,
78 Slot,
80 TSlot,
82 Dovetail,
84 Engraver,
86 ThreadMill,
88 Drill,
90 SpotDrill,
92 CenterDrill,
94 Tap,
96 Reamer,
98 BoringBar,
100 LatheInsert,
102}
103
104impl ToolKind {
105 pub fn as_str(&self) -> &'static str {
107 match self {
108 ToolKind::Flat => "flat",
109 ToolKind::Ball => "ball",
110 ToolKind::Bull => "bull",
111 ToolKind::Chamfer => "chamfer",
112 ToolKind::VBit => "v-bit",
113 ToolKind::TaperedBall => "tapered-ball",
114 ToolKind::TaperedFlat => "tapered-flat",
115 ToolKind::Lollipop => "lollipop",
116 ToolKind::FaceMill => "face-mill",
117 ToolKind::Slot => "slot",
118 ToolKind::TSlot => "t-slot",
119 ToolKind::Dovetail => "dovetail",
120 ToolKind::Engraver => "engraver",
121 ToolKind::ThreadMill => "thread-mill",
122 ToolKind::Drill => "drill",
123 ToolKind::SpotDrill => "spot-drill",
124 ToolKind::CenterDrill => "center-drill",
125 ToolKind::Tap => "tap",
126 ToolKind::Reamer => "reamer",
127 ToolKind::BoringBar => "boring-bar",
128 ToolKind::LatheInsert => "lathe-insert",
129 }
130 }
131
132 pub fn is_hole_tool(&self) -> bool {
134 matches!(
135 self,
136 ToolKind::Drill
137 | ToolKind::SpotDrill
138 | ToolKind::CenterDrill
139 | ToolKind::Tap
140 | ToolKind::Reamer
141 | ToolKind::BoringBar
142 )
143 }
144}
145
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct ToolGeometry {
150 pub diameter: f64,
152 pub cutting_length: f64,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub shaft_diameter: Option<f64>,
157 #[serde(default = "default_shaft_length")]
159 pub shaft_length: f64,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub overall_length: Option<f64>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub corner_radius: Option<f64>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub included_angle_deg: Option<f64>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub ball_diameter: Option<f64>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub tip_diameter: Option<f64>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub thread_pitch: Option<f64>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub flute_count: Option<u32>,
181 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
183 pub left_hand: bool,
184}
185
186fn default_shaft_length() -> f64 {
187 30.0
188}
189
190impl Default for ToolGeometry {
191 fn default() -> Self {
192 Self {
193 diameter: 6.0,
194 cutting_length: 20.0,
195 shaft_diameter: None,
196 shaft_length: default_shaft_length(),
197 overall_length: None,
198 corner_radius: None,
199 included_angle_deg: None,
200 ball_diameter: None,
201 tip_diameter: None,
202 thread_pitch: None,
203 flute_count: None,
204 left_hand: false,
205 }
206 }
207}
208
209impl ToolGeometry {
210 pub fn flat(diameter: f64, cutting_length: f64) -> Self {
211 Self { diameter, cutting_length, ..Default::default() }
212 }
213
214 pub fn shaft_diameter(&self) -> f64 {
215 self.shaft_diameter.unwrap_or(self.diameter)
216 }
217
218 pub fn overall_length(&self) -> f64 {
219 self.overall_length.unwrap_or(self.cutting_length + self.shaft_length)
220 }
221}
222
223#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct Holder {
227 #[serde(default)]
228 pub id: HolderId,
229 #[serde(default)]
230 pub name: String,
231 #[serde(default)]
233 pub segments: Vec<HolderSegment>,
234}
235
236#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct HolderSegment {
240 pub height: f64,
241 pub lower_diameter: f64,
243 pub upper_diameter: f64,
245}
246
247impl Holder {
248 pub fn height(&self) -> f64 {
250 self.segments.iter().map(|s| s.height).sum()
251 }
252
253 pub fn radius_at_height(&self, h: f64) -> Option<f64> {
255 if h < 0.0 {
256 return None;
257 }
258 let mut base = 0.0;
259 for s in &self.segments {
260 if h <= base + s.height {
261 let t = if s.height > 0.0 { (h - base) / s.height } else { 0.0 };
262 return Some(0.5 * (s.lower_diameter + t * (s.upper_diameter - s.lower_diameter)));
263 }
264 base += s.height;
265 }
266 None
267 }
268
269 pub fn validate(&self) -> Result<(), String> {
270 for (i, s) in self.segments.iter().enumerate() {
271 let finite = s.height.is_finite() && s.lower_diameter.is_finite() && s.upper_diameter.is_finite();
272 if !finite {
273 return Err(format!("holder segment {i} has a non-finite dimension"));
274 }
275 if s.height <= 0.0 || s.lower_diameter <= 0.0 || s.upper_diameter <= 0.0 {
276 return Err(format!("holder segment {i} must have positive dimensions"));
277 }
278 }
279 Ok(())
280 }
281}
282
283#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "kebab-case")]
286pub enum Coolant {
287 #[default]
288 Off,
289 Flood,
290 Mist,
291 ThroughTool,
292 Air,
293 FloodAndThroughTool,
294 FloodAndMist,
295}
296
297#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
299#[serde(rename_all = "kebab-case")]
300pub enum SpindleDirection {
301 #[default]
302 Cw,
303 Ccw,
304}
305
306impl SpindleDirection {
307 pub const VARIANTS: &'static [&'static str] = &["cw", "ccw"];
309}
310
311#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct FeedsSpeeds {
315 pub spindle_rpm: f64,
316 #[serde(default)]
317 pub spindle_direction: SpindleDirection,
318 pub cutting_feed: f64,
319 pub plunge_feed: f64,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub lead_feed: Option<f64>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub ramp_feed: Option<f64>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub retract_feed: Option<f64>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub surface_speed: Option<f64>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub feed_per_tooth: Option<f64>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub surface_speed_limit_rpm: Option<f64>,
339 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
343 pub css: bool,
344}
345
346impl Default for FeedsSpeeds {
347 fn default() -> Self {
348 Self {
349 spindle_rpm: 10_000.0,
350 spindle_direction: SpindleDirection::Cw,
351 cutting_feed: 1_000.0,
352 plunge_feed: 300.0,
353 lead_feed: None,
354 ramp_feed: None,
355 retract_feed: None,
356 surface_speed: None,
357 feed_per_tooth: None,
358 surface_speed_limit_rpm: None,
359 css: false,
360 }
361 }
362}
363
364impl FeedsSpeeds {
365 pub fn lead_feed(&self) -> f64 {
366 self.lead_feed.unwrap_or(self.cutting_feed)
367 }
368
369 pub fn ramp_feed(&self) -> f64 {
370 self.ramp_feed.unwrap_or(self.cutting_feed)
371 }
372
373 pub fn validate(&self) -> Result<(), String> {
374 let fields = [
375 ("spindle rpm", self.spindle_rpm, false),
376 ("cutting feed", self.cutting_feed, true),
377 ("plunge feed", self.plunge_feed, true),
378 ];
379 for (name, v, positive) in fields {
380 if !v.is_finite() || v < 0.0 || (positive && v == 0.0) {
381 return Err(format!("{name} must be finite and positive"));
382 }
383 }
384 for (name, v) in [
385 ("lead feed", self.lead_feed),
386 ("ramp feed", self.ramp_feed),
387 ("retract feed", self.retract_feed),
388 ] {
389 if let Some(v) = v {
390 if !v.is_finite() || v <= 0.0 {
391 return Err(format!("{name} must be finite and positive"));
392 }
393 }
394 }
395 if let Some(v) = self.surface_speed {
396 if !v.is_finite() || v <= 0.0 {
397 return Err("surface speed must be finite and positive".into());
398 }
399 }
400 if let Some(max) = self.surface_speed_limit_rpm {
401 if !max.is_finite() || max <= 0.0 {
402 return Err("surface speed limit rpm must be finite and positive".into());
403 }
404 }
405 if self.css {
406 if self.surface_speed.is_none() {
407 return Err("constant surface speed needs a surface speed".into());
408 }
409 if self.surface_speed_limit_rpm.is_none() {
410 return Err("constant surface speed needs a finite positive maximum rpm".into());
411 }
412 }
413 Ok(())
414 }
415}