1use super::{Coolant, OperationId, Point3, SpindleDirection, ToolId};
10use serde::{Deserialize, Serialize};
11
12#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
14#[serde(untagged)]
15pub enum Feed {
16 Rapid(RapidTag),
18 Rate(f64),
20}
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum RapidTag {
26 #[default]
27 Rapid,
28}
29
30impl Feed {
31 pub const RAPID: Feed = Feed::Rapid(RapidTag::Rapid);
32
33 pub fn rate(rate: f64) -> Feed {
34 Feed::Rate(rate)
35 }
36
37 pub fn is_rapid(&self) -> bool {
38 matches!(self, Feed::Rapid(_))
39 }
40
41 pub fn value(&self) -> Option<f64> {
43 match self {
44 Feed::Rapid(_) => None,
45 Feed::Rate(r) => Some(*r),
46 }
47 }
48}
49
50impl Default for Feed {
51 fn default() -> Self {
52 Feed::RAPID
53 }
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum ArcPlane {
60 #[default]
61 Xy,
62 Xz,
63 Yz,
64}
65
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
71#[non_exhaustive]
72pub enum MotionKind {
73 Probe { cycle: super::ProbeCycle },
75 Rapid { to: Point3 },
77 Plunge { to: Point3 },
79 Cut { to: Point3 },
81 ArcCw {
84 to: Point3,
85 center: Point3,
86 plane: ArcPlane,
87 #[serde(default)]
88 pitch: f64,
89 },
90 ArcCcw {
92 to: Point3,
93 center: Point3,
94 plane: ArcPlane,
95 #[serde(default)]
96 pitch: f64,
97 },
98 Retract { to: Point3 },
100 LeadIn { to: Point3 },
102 LeadOut { to: Point3 },
104 Link { to: Point3 },
106 Dwell { seconds: f64 },
108 ToolChange { tool: ToolId },
110 Cycle { at: Point3, cycle: Cycle },
112 SpindleOn { rpm: f64, direction: SpindleDirection },
114 SpindleOff,
115 SpindleOrient { angle_deg: f64 },
117 SpindleSync { enabled: bool },
119 Coolant { mode: Coolant },
121 Comment { text: String },
123 WorkPlane { origin: Point3, x_axis: Point3, z_axis: Point3 },
129 WorkPlaneOff,
131 RotaryPosition { axes: Vec<RotaryWord> },
134 TcpMode { enabled: bool },
136 LatheMode { diameter_mode: bool },
139 SpindleSurfaceSpeed { surface_speed: f64, max_rpm: f64, direction: SpindleDirection },
142 ThreadCut { to: Point3, pitch: f64 },
146 TurnThreadCycle { cycle: TurnThreadCycle },
149 PolarInterpolation { enabled: bool },
151 CylindricalInterpolation { radius: Option<f64> },
154 LinearRotary { to: Point3, axes: Vec<RotaryWord> },
157 Stop { optional: bool },
159 PassThrough { text: String },
162}
163
164#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct TurnThreadCycle {
168 pub start: Point3,
169 pub end: Point3,
170 pub pitch: f64,
171 pub depths: Vec<f64>,
172 pub infeed_offsets: Vec<f64>,
173 pub clearance_x: f64,
174 pub internal: bool,
175 pub spring_passes: u32,
176}
177
178impl TurnThreadCycle {
179 pub fn is_finite(&self) -> bool {
180 self.start.is_finite()
181 && self.end.is_finite()
182 && self.pitch.is_finite()
183 && self.clearance_x.is_finite()
184 && self.depths.iter().all(|d| d.is_finite())
185 && self.infeed_offsets.iter().all(|d| d.is_finite())
186 }
187}
188
189#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct RotaryWord {
193 pub name: String,
195 pub angle_deg: f64,
196}
197
198impl MotionKind {
199 pub fn end(&self) -> Option<Point3> {
201 match self {
202 MotionKind::Rapid { to }
203 | MotionKind::Plunge { to }
204 | MotionKind::Cut { to }
205 | MotionKind::ArcCw { to, .. }
206 | MotionKind::ArcCcw { to, .. }
207 | MotionKind::Retract { to }
208 | MotionKind::LeadIn { to }
209 | MotionKind::LeadOut { to }
210 | MotionKind::Link { to }
211 | MotionKind::ThreadCut { to, .. }
212 | MotionKind::LinearRotary { to, .. } => Some(*to),
213 MotionKind::Probe { cycle } => Some(cycle.target),
214 MotionKind::Cycle { at, .. } => Some(*at),
215 MotionKind::TurnThreadCycle { cycle } => Some(Point3::new(cycle.clearance_x, 0.0, cycle.start.z)),
217 MotionKind::Dwell { .. }
218 | MotionKind::ToolChange { .. }
219 | MotionKind::SpindleOn { .. }
220 | MotionKind::SpindleOff
221 | MotionKind::SpindleOrient { .. }
222 | MotionKind::SpindleSync { .. }
223 | MotionKind::Coolant { .. }
224 | MotionKind::Comment { .. }
225 | MotionKind::WorkPlane { .. }
226 | MotionKind::WorkPlaneOff
227 | MotionKind::RotaryPosition { .. }
228 | MotionKind::TcpMode { .. }
229 | MotionKind::LatheMode { .. }
230 | MotionKind::SpindleSurfaceSpeed { .. }
231 | MotionKind::PolarInterpolation { .. }
232 | MotionKind::CylindricalInterpolation { .. }
233 | MotionKind::Stop { .. }
234 | MotionKind::PassThrough { .. } => None,
235 }
236 }
237
238 pub fn is_cutting(&self) -> bool {
240 matches!(
241 self,
242 MotionKind::Plunge { .. }
243 | MotionKind::Cut { .. }
244 | MotionKind::ArcCw { .. }
245 | MotionKind::ArcCcw { .. }
246 | MotionKind::LeadIn { .. }
247 | MotionKind::LeadOut { .. }
248 | MotionKind::Cycle { .. }
249 | MotionKind::ThreadCut { .. }
250 | MotionKind::TurnThreadCycle { .. }
251 | MotionKind::LinearRotary { .. }
252 )
253 }
254
255 pub fn kind_name(&self) -> &'static str {
257 match self {
258 MotionKind::Probe { .. } => "probe",
259 MotionKind::Rapid { .. } => "rapid",
260 MotionKind::Plunge { .. } => "plunge",
261 MotionKind::Cut { .. } => "cut",
262 MotionKind::ArcCw { .. } => "arcCw",
263 MotionKind::ArcCcw { .. } => "arcCcw",
264 MotionKind::Retract { .. } => "retract",
265 MotionKind::LeadIn { .. } => "leadIn",
266 MotionKind::LeadOut { .. } => "leadOut",
267 MotionKind::Link { .. } => "link",
268 MotionKind::Dwell { .. } => "dwell",
269 MotionKind::ToolChange { .. } => "toolChange",
270 MotionKind::Cycle { .. } => "cycle",
271 MotionKind::SpindleOn { .. } => "spindleOn",
272 MotionKind::SpindleOff => "spindleOff",
273 MotionKind::SpindleOrient { .. } => "spindleOrient",
274 MotionKind::SpindleSync { .. } => "spindleSync",
275 MotionKind::Coolant { .. } => "coolant",
276 MotionKind::Comment { .. } => "comment",
277 MotionKind::WorkPlane { .. } => "workPlane",
278 MotionKind::WorkPlaneOff => "workPlaneOff",
279 MotionKind::RotaryPosition { .. } => "rotaryPosition",
280 MotionKind::TcpMode { .. } => "tcpMode",
281 MotionKind::LatheMode { .. } => "latheMode",
282 MotionKind::SpindleSurfaceSpeed { .. } => "spindleSurfaceSpeed",
283 MotionKind::ThreadCut { .. } => "threadCut",
284 MotionKind::TurnThreadCycle { .. } => "turnThreadCycle",
285 MotionKind::PolarInterpolation { .. } => "polarInterpolation",
286 MotionKind::CylindricalInterpolation { .. } => "cylindricalInterpolation",
287 MotionKind::LinearRotary { .. } => "linearRotary",
288 MotionKind::Stop { .. } => "stop",
289 MotionKind::PassThrough { .. } => "passThrough",
290 }
291 }
292
293 pub fn is_finite(&self) -> bool {
295 match self {
296 MotionKind::ArcCw { to, center, pitch, .. } | MotionKind::ArcCcw { to, center, pitch, .. } => {
297 to.is_finite() && center.is_finite() && pitch.is_finite()
298 }
299 MotionKind::Probe { cycle } => cycle.is_finite(),
300 MotionKind::Dwell { seconds } => seconds.is_finite(),
301 MotionKind::SpindleOn { rpm, .. } => rpm.is_finite(),
302 MotionKind::SpindleOrient { angle_deg } => angle_deg.is_finite(),
303 MotionKind::Cycle { at, cycle } => at.is_finite() && cycle.is_finite(),
304 MotionKind::WorkPlane { origin, x_axis, z_axis } => {
305 origin.is_finite() && x_axis.is_finite() && z_axis.is_finite()
306 }
307 MotionKind::RotaryPosition { axes } => axes.iter().all(|a| a.angle_deg.is_finite()),
308 MotionKind::LatheMode { .. } => true,
309 MotionKind::SpindleSurfaceSpeed { surface_speed, max_rpm, .. } => surface_speed.is_finite() && max_rpm.is_finite(),
310 MotionKind::ThreadCut { to, pitch } => to.is_finite() && pitch.is_finite(),
311 MotionKind::TurnThreadCycle { cycle } => cycle.is_finite(),
312 MotionKind::CylindricalInterpolation { radius } => radius.map_or(true, |r| r.is_finite() && r > 0.0),
313 MotionKind::LinearRotary { to, axes } => to.is_finite() && axes.iter().all(|a| a.angle_deg.is_finite()),
314 other => other.end().map_or(true, |p| p.is_finite()),
315 }
316 }
317}
318
319#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
322#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
323#[non_exhaustive]
324pub enum Cycle {
325 Drill {
327 depth_z: f64,
328 retract_z: f64,
329 #[serde(default)]
330 dwell_seconds: f64,
331 },
332 Peck { depth_z: f64, retract_z: f64, peck: f64 },
334 ChipBreak {
336 depth_z: f64,
337 retract_z: f64,
338 peck: f64,
339 #[serde(default)]
340 back_off: f64,
341 },
342 Tap {
344 depth_z: f64,
345 retract_z: f64,
346 pitch: f64,
347 rigid: bool,
348 #[serde(default)]
349 left_hand: bool,
350 },
351 Ream { depth_z: f64, retract_z: f64 },
353 Bore {
355 depth_z: f64,
356 retract_z: f64,
357 #[serde(default)]
358 dwell_seconds: f64,
359 #[serde(default)]
360 shift: f64,
361 #[serde(default)]
362 feed_out: bool,
363 },
364 BackBore {
367 depth_z: f64,
368 retract_z: f64,
369 shift: f64,
370 #[serde(default)]
371 entry_z: f64,
372 },
373}
374
375impl Cycle {
376 pub fn depth_z(&self) -> f64 {
377 match self {
378 Cycle::Drill { depth_z, .. }
379 | Cycle::Peck { depth_z, .. }
380 | Cycle::ChipBreak { depth_z, .. }
381 | Cycle::Tap { depth_z, .. }
382 | Cycle::Ream { depth_z, .. }
383 | Cycle::Bore { depth_z, .. }
384 | Cycle::BackBore { depth_z, .. } => *depth_z,
385 }
386 }
387
388 pub fn retract_z(&self) -> f64 {
389 match self {
390 Cycle::Drill { retract_z, .. }
391 | Cycle::Peck { retract_z, .. }
392 | Cycle::ChipBreak { retract_z, .. }
393 | Cycle::Tap { retract_z, .. }
394 | Cycle::Ream { retract_z, .. }
395 | Cycle::Bore { retract_z, .. }
396 | Cycle::BackBore { retract_z, .. } => *retract_z,
397 }
398 }
399
400 pub fn is_finite(&self) -> bool {
401 let extra = match self {
402 Cycle::Drill { dwell_seconds, .. } => dwell_seconds.is_finite(),
403 Cycle::Peck { peck, .. } => peck.is_finite(),
404 Cycle::ChipBreak { peck, back_off, .. } => peck.is_finite() && back_off.is_finite(),
405 Cycle::Tap { pitch, .. } => pitch.is_finite(),
406 Cycle::Ream { .. } => true,
407 Cycle::Bore { dwell_seconds, shift, .. } => dwell_seconds.is_finite() && shift.is_finite(),
408 Cycle::BackBore { shift, entry_z, .. } => shift.is_finite() && entry_z.is_finite(),
409 };
410 extra && self.depth_z().is_finite() && self.retract_z().is_finite()
411 }
412}
413
414#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub struct Motion {
418 #[serde(flatten)]
419 pub kind: MotionKind,
420 #[serde(default)]
421 pub feed: Feed,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub tool_axis: Option<Point3>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub path_id: Option<u32>,
429}
430
431impl Motion {
432 pub fn new(kind: MotionKind, feed: Feed) -> Self {
433 Self { kind, feed, tool_axis: None, path_id: None }
434 }
435
436 pub fn rapid(to: Point3) -> Self {
437 Self::new(MotionKind::Rapid { to }, Feed::RAPID)
438 }
439
440 pub fn plunge(to: Point3, feed: f64) -> Self {
441 Self::new(MotionKind::Plunge { to }, Feed::Rate(feed))
442 }
443
444 pub fn cut(to: Point3, feed: f64) -> Self {
445 Self::new(MotionKind::Cut { to }, Feed::Rate(feed))
446 }
447
448 pub fn retract(to: Point3, feed: Feed) -> Self {
449 Self::new(MotionKind::Retract { to }, feed)
450 }
451
452 pub fn link(to: Point3, feed: f64) -> Self {
453 Self::new(MotionKind::Link { to }, Feed::Rate(feed))
454 }
455
456 pub fn with_path(mut self, path_id: u32) -> Self {
457 self.path_id = Some(path_id);
458 self
459 }
460
461 pub fn end(&self) -> Option<Point3> {
462 self.kind.end()
463 }
464}
465
466#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
468#[serde(rename_all = "camelCase")]
469pub struct Toolpath {
470 pub operation: OperationId,
471 pub tool: ToolId,
472 #[serde(default)]
473 pub motions: Vec<Motion>,
474 #[serde(default)]
475 pub report: OperationReport,
476}
477
478impl Toolpath {
479 pub fn new(operation: impl Into<OperationId>, tool: impl Into<ToolId>) -> Self {
480 Self { operation: operation.into(), tool: tool.into(), motions: Vec::new(), report: OperationReport::default() }
481 }
482
483 pub fn cutting_length(&self) -> f64 {
485 let mut total = 0.0;
486 let mut at: Option<Point3> = None;
487 for m in &self.motions {
488 if let Some(end) = m.end() {
489 if let Some(start) = at {
490 if m.kind.is_cutting() {
491 total += start.distance(end);
492 }
493 }
494 at = Some(end);
495 }
496 }
497 total
498 }
499
500 pub fn first_non_finite(&self) -> Option<usize> {
502 self.motions.iter().position(|m| {
503 !m.kind.is_finite()
504 || m.feed.value().map_or(false, |f| !f.is_finite())
505 || m.tool_axis.map_or(false, |a| !a.is_finite())
506 })
507 }
508}
509
510#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
512#[serde(rename_all = "camelCase")]
513pub struct ProgramMotion {
514 pub operation: OperationId,
515 pub motion: Motion,
516}
517
518#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
520#[serde(rename_all = "kebab-case")]
521pub enum OperationStatus {
522 #[default]
524 Pending,
525 Ok,
527 Warning,
529 Error,
531 Cancelled,
533}
534
535#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
537#[serde(rename_all = "camelCase")]
538pub struct Diagnostic {
539 pub code: String,
541 pub message: String,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub detail: Option<String>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub location: Option<Point3>,
548}
549
550impl Diagnostic {
551 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
552 Self { code: code.into(), message: message.into(), detail: None, location: None }
553 }
554
555 pub fn at(mut self, location: Point3) -> Self {
556 self.location = Some(location);
557 self
558 }
559
560 pub fn detail(mut self, detail: impl Into<String>) -> Self {
561 self.detail = Some(detail.into());
562 self
563 }
564}
565
566#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
568#[serde(rename_all = "camelCase")]
569pub struct EmptyRegion {
570 pub reason: String,
572 #[serde(default, skip_serializing_if = "Option::is_none")]
574 pub target: Option<super::TargetRef>,
575}
576
577#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct ApproximationLimit {
581 pub kind: String,
583 #[serde(default, skip_serializing_if = "Option::is_none")]
585 pub bound: Option<f64>,
586 #[serde(default, skip_serializing_if = "Option::is_none")]
587 pub detail: Option<String>,
588}
589
590#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593#[serde(default)]
594pub struct WorkCounts {
595 pub triangles: u64,
596 pub candidates: u64,
597 pub samples: u64,
598 pub contacts: u64,
599 pub passes: u64,
600 pub motions: u64,
601}
602
603#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
605#[serde(rename_all = "camelCase")]
606pub struct OperationReport {
607 #[serde(default)]
608 pub status: OperationStatus,
609 #[serde(default, skip_serializing_if = "Vec::is_empty")]
610 pub errors: Vec<Diagnostic>,
611 #[serde(default, skip_serializing_if = "Vec::is_empty")]
612 pub warnings: Vec<Diagnostic>,
613 #[serde(default, skip_serializing_if = "Vec::is_empty")]
614 pub empty_regions: Vec<EmptyRegion>,
615 #[serde(default, skip_serializing_if = "Vec::is_empty")]
616 pub approximations: Vec<ApproximationLimit>,
617 #[serde(default)]
618 pub counts: WorkCounts,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub estimated_seconds: Option<f64>,
622}
623
624impl OperationReport {
625 pub fn warn(&mut self, d: Diagnostic) {
626 self.warnings.push(d);
627 if self.status == OperationStatus::Ok || self.status == OperationStatus::Pending {
628 self.status = OperationStatus::Warning;
629 }
630 }
631
632 pub fn fail(&mut self, d: Diagnostic) {
633 self.errors.push(d);
634 self.status = OperationStatus::Error;
635 }
636
637 pub fn finish(&mut self) {
639 self.status = if !self.errors.is_empty() {
640 OperationStatus::Error
641 } else if !self.warnings.is_empty() {
642 OperationStatus::Warning
643 } else {
644 OperationStatus::Ok
645 };
646 }
647
648 pub fn is_usable(&self) -> bool {
649 matches!(self.status, OperationStatus::Ok | OperationStatus::Warning)
650 }
651}