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