use super::{Coolant, OperationId, Point3, SpindleDirection, ToolId};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Feed {
Rapid(RapidTag),
Rate(f64),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RapidTag {
#[default]
Rapid,
}
impl Feed {
pub const RAPID: Feed = Feed::Rapid(RapidTag::Rapid);
pub fn rate(rate: f64) -> Feed {
Feed::Rate(rate)
}
pub fn is_rapid(&self) -> bool {
matches!(self, Feed::Rapid(_))
}
pub fn value(&self) -> Option<f64> {
match self {
Feed::Rapid(_) => None,
Feed::Rate(r) => Some(*r),
}
}
}
impl Default for Feed {
fn default() -> Self {
Feed::RAPID
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ArcPlane {
#[default]
Xy,
Xz,
Yz,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
#[non_exhaustive]
pub enum MotionKind {
Probe { cycle: super::ProbeCycle },
Rapid { to: Point3 },
Plunge { to: Point3 },
Cut { to: Point3 },
ArcCw {
to: Point3,
center: Point3,
plane: ArcPlane,
#[serde(default)]
pitch: f64,
},
ArcCcw {
to: Point3,
center: Point3,
plane: ArcPlane,
#[serde(default)]
pitch: f64,
},
Retract { to: Point3 },
LeadIn { to: Point3 },
LeadOut { to: Point3 },
Link { to: Point3 },
Dwell { seconds: f64 },
ToolChange { tool: ToolId },
Cycle { at: Point3, cycle: Cycle },
SpindleOn { rpm: f64, direction: SpindleDirection },
SpindleOff,
SpindleOrient { angle_deg: f64 },
SpindleSync { enabled: bool },
Coolant { mode: Coolant },
Comment { text: String },
WorkPlane { origin: Point3, x_axis: Point3, z_axis: Point3 },
WorkPlaneOff,
RotaryPosition { axes: Vec<RotaryWord> },
TcpMode { enabled: bool },
LatheMode { diameter_mode: bool },
SpindleSurfaceSpeed { surface_speed: f64, max_rpm: f64, direction: SpindleDirection },
ThreadCut { to: Point3, pitch: f64 },
TurnThreadCycle { cycle: TurnThreadCycle },
PolarInterpolation { enabled: bool },
CylindricalInterpolation { radius: Option<f64> },
LinearRotary { to: Point3, axes: Vec<RotaryWord> },
Stop { optional: bool },
PassThrough { text: String },
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnThreadCycle {
pub start: Point3,
pub end: Point3,
pub pitch: f64,
pub depths: Vec<f64>,
pub infeed_offsets: Vec<f64>,
pub clearance_x: f64,
pub internal: bool,
pub spring_passes: u32,
}
impl TurnThreadCycle {
pub fn is_finite(&self) -> bool {
self.start.is_finite()
&& self.end.is_finite()
&& self.pitch.is_finite()
&& self.clearance_x.is_finite()
&& self.depths.iter().all(|d| d.is_finite())
&& self.infeed_offsets.iter().all(|d| d.is_finite())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RotaryWord {
pub name: String,
pub angle_deg: f64,
}
impl MotionKind {
pub fn end(&self) -> Option<Point3> {
match self {
MotionKind::Rapid { to }
| MotionKind::Plunge { to }
| MotionKind::Cut { to }
| MotionKind::ArcCw { to, .. }
| MotionKind::ArcCcw { to, .. }
| MotionKind::Retract { to }
| MotionKind::LeadIn { to }
| MotionKind::LeadOut { to }
| MotionKind::Link { to }
| MotionKind::ThreadCut { to, .. }
| MotionKind::LinearRotary { to, .. } => Some(*to),
MotionKind::Probe { cycle } => Some(cycle.target),
MotionKind::Cycle { at, .. } => Some(*at),
MotionKind::TurnThreadCycle { cycle } => Some(Point3::new(cycle.clearance_x, 0.0, cycle.start.z)),
MotionKind::Dwell { .. }
| MotionKind::ToolChange { .. }
| MotionKind::SpindleOn { .. }
| MotionKind::SpindleOff
| MotionKind::SpindleOrient { .. }
| MotionKind::SpindleSync { .. }
| MotionKind::Coolant { .. }
| MotionKind::Comment { .. }
| MotionKind::WorkPlane { .. }
| MotionKind::WorkPlaneOff
| MotionKind::RotaryPosition { .. }
| MotionKind::TcpMode { .. }
| MotionKind::LatheMode { .. }
| MotionKind::SpindleSurfaceSpeed { .. }
| MotionKind::PolarInterpolation { .. }
| MotionKind::CylindricalInterpolation { .. }
| MotionKind::Stop { .. }
| MotionKind::PassThrough { .. } => None,
}
}
pub fn is_cutting(&self) -> bool {
matches!(
self,
MotionKind::Plunge { .. }
| MotionKind::Cut { .. }
| MotionKind::ArcCw { .. }
| MotionKind::ArcCcw { .. }
| MotionKind::LeadIn { .. }
| MotionKind::LeadOut { .. }
| MotionKind::Cycle { .. }
| MotionKind::ThreadCut { .. }
| MotionKind::TurnThreadCycle { .. }
| MotionKind::LinearRotary { .. }
)
}
pub fn kind_name(&self) -> &'static str {
match self {
MotionKind::Probe { .. } => "probe",
MotionKind::Rapid { .. } => "rapid",
MotionKind::Plunge { .. } => "plunge",
MotionKind::Cut { .. } => "cut",
MotionKind::ArcCw { .. } => "arcCw",
MotionKind::ArcCcw { .. } => "arcCcw",
MotionKind::Retract { .. } => "retract",
MotionKind::LeadIn { .. } => "leadIn",
MotionKind::LeadOut { .. } => "leadOut",
MotionKind::Link { .. } => "link",
MotionKind::Dwell { .. } => "dwell",
MotionKind::ToolChange { .. } => "toolChange",
MotionKind::Cycle { .. } => "cycle",
MotionKind::SpindleOn { .. } => "spindleOn",
MotionKind::SpindleOff => "spindleOff",
MotionKind::SpindleOrient { .. } => "spindleOrient",
MotionKind::SpindleSync { .. } => "spindleSync",
MotionKind::Coolant { .. } => "coolant",
MotionKind::Comment { .. } => "comment",
MotionKind::WorkPlane { .. } => "workPlane",
MotionKind::WorkPlaneOff => "workPlaneOff",
MotionKind::RotaryPosition { .. } => "rotaryPosition",
MotionKind::TcpMode { .. } => "tcpMode",
MotionKind::LatheMode { .. } => "latheMode",
MotionKind::SpindleSurfaceSpeed { .. } => "spindleSurfaceSpeed",
MotionKind::ThreadCut { .. } => "threadCut",
MotionKind::TurnThreadCycle { .. } => "turnThreadCycle",
MotionKind::PolarInterpolation { .. } => "polarInterpolation",
MotionKind::CylindricalInterpolation { .. } => "cylindricalInterpolation",
MotionKind::LinearRotary { .. } => "linearRotary",
MotionKind::Stop { .. } => "stop",
MotionKind::PassThrough { .. } => "passThrough",
}
}
pub fn is_finite(&self) -> bool {
match self {
MotionKind::ArcCw { to, center, pitch, .. } | MotionKind::ArcCcw { to, center, pitch, .. } => {
to.is_finite() && center.is_finite() && pitch.is_finite()
}
MotionKind::Probe { cycle } => cycle.is_finite(),
MotionKind::Dwell { seconds } => seconds.is_finite(),
MotionKind::SpindleOn { rpm, .. } => rpm.is_finite(),
MotionKind::SpindleOrient { angle_deg } => angle_deg.is_finite(),
MotionKind::Cycle { at, cycle } => at.is_finite() && cycle.is_finite(),
MotionKind::WorkPlane { origin, x_axis, z_axis } => {
origin.is_finite() && x_axis.is_finite() && z_axis.is_finite()
}
MotionKind::RotaryPosition { axes } => axes.iter().all(|a| a.angle_deg.is_finite()),
MotionKind::LatheMode { .. } => true,
MotionKind::SpindleSurfaceSpeed { surface_speed, max_rpm, .. } => surface_speed.is_finite() && max_rpm.is_finite(),
MotionKind::ThreadCut { to, pitch } => to.is_finite() && pitch.is_finite(),
MotionKind::TurnThreadCycle { cycle } => cycle.is_finite(),
MotionKind::CylindricalInterpolation { radius } => radius.map_or(true, |r| r.is_finite() && r > 0.0),
MotionKind::LinearRotary { to, axes } => to.is_finite() && axes.iter().all(|a| a.angle_deg.is_finite()),
other => other.end().map_or(true, |p| p.is_finite()),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
#[non_exhaustive]
pub enum Cycle {
Drill {
depth_z: f64,
retract_z: f64,
#[serde(default)]
dwell_seconds: f64,
},
Peck { depth_z: f64, retract_z: f64, peck: f64 },
ChipBreak {
depth_z: f64,
retract_z: f64,
peck: f64,
#[serde(default)]
back_off: f64,
},
Tap {
depth_z: f64,
retract_z: f64,
pitch: f64,
rigid: bool,
#[serde(default)]
left_hand: bool,
},
Ream { depth_z: f64, retract_z: f64 },
Bore {
depth_z: f64,
retract_z: f64,
#[serde(default)]
dwell_seconds: f64,
#[serde(default)]
shift: f64,
#[serde(default)]
feed_out: bool,
},
BackBore {
depth_z: f64,
retract_z: f64,
shift: f64,
#[serde(default)]
entry_z: f64,
},
}
impl Cycle {
pub fn depth_z(&self) -> f64 {
match self {
Cycle::Drill { depth_z, .. }
| Cycle::Peck { depth_z, .. }
| Cycle::ChipBreak { depth_z, .. }
| Cycle::Tap { depth_z, .. }
| Cycle::Ream { depth_z, .. }
| Cycle::Bore { depth_z, .. }
| Cycle::BackBore { depth_z, .. } => *depth_z,
}
}
pub fn retract_z(&self) -> f64 {
match self {
Cycle::Drill { retract_z, .. }
| Cycle::Peck { retract_z, .. }
| Cycle::ChipBreak { retract_z, .. }
| Cycle::Tap { retract_z, .. }
| Cycle::Ream { retract_z, .. }
| Cycle::Bore { retract_z, .. }
| Cycle::BackBore { retract_z, .. } => *retract_z,
}
}
pub fn is_finite(&self) -> bool {
let extra = match self {
Cycle::Drill { dwell_seconds, .. } => dwell_seconds.is_finite(),
Cycle::Peck { peck, .. } => peck.is_finite(),
Cycle::ChipBreak { peck, back_off, .. } => peck.is_finite() && back_off.is_finite(),
Cycle::Tap { pitch, .. } => pitch.is_finite(),
Cycle::Ream { .. } => true,
Cycle::Bore { dwell_seconds, shift, .. } => dwell_seconds.is_finite() && shift.is_finite(),
Cycle::BackBore { shift, entry_z, .. } => shift.is_finite() && entry_z.is_finite(),
};
extra && self.depth_z().is_finite() && self.retract_z().is_finite()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Motion {
#[serde(flatten)]
pub kind: MotionKind,
#[serde(default)]
pub feed: Feed,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_axis: Option<Point3>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path_id: Option<u32>,
}
impl Motion {
pub fn new(kind: MotionKind, feed: Feed) -> Self {
Self { kind, feed, tool_axis: None, path_id: None }
}
pub fn rapid(to: Point3) -> Self {
Self::new(MotionKind::Rapid { to }, Feed::RAPID)
}
pub fn plunge(to: Point3, feed: f64) -> Self {
Self::new(MotionKind::Plunge { to }, Feed::Rate(feed))
}
pub fn cut(to: Point3, feed: f64) -> Self {
Self::new(MotionKind::Cut { to }, Feed::Rate(feed))
}
pub fn retract(to: Point3, feed: Feed) -> Self {
Self::new(MotionKind::Retract { to }, feed)
}
pub fn link(to: Point3, feed: f64) -> Self {
Self::new(MotionKind::Link { to }, Feed::Rate(feed))
}
pub fn with_path(mut self, path_id: u32) -> Self {
self.path_id = Some(path_id);
self
}
pub fn end(&self) -> Option<Point3> {
self.kind.end()
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Toolpath {
pub operation: OperationId,
pub tool: ToolId,
#[serde(default)]
pub motions: Vec<Motion>,
#[serde(default)]
pub report: OperationReport,
}
impl Toolpath {
pub fn new(operation: impl Into<OperationId>, tool: impl Into<ToolId>) -> Self {
Self { operation: operation.into(), tool: tool.into(), motions: Vec::new(), report: OperationReport::default() }
}
pub fn cutting_length(&self) -> f64 {
let mut total = 0.0;
let mut at: Option<Point3> = None;
for m in &self.motions {
if let Some(end) = m.end() {
if let Some(start) = at {
if m.kind.is_cutting() {
total += start.distance(end);
}
}
at = Some(end);
}
}
total
}
pub fn first_non_finite(&self) -> Option<usize> {
self.motions.iter().position(|m| {
!m.kind.is_finite()
|| m.feed.value().map_or(false, |f| !f.is_finite())
|| m.tool_axis.map_or(false, |a| !a.is_finite())
})
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgramMotion {
pub operation: OperationId,
pub motion: Motion,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OperationStatus {
#[default]
Pending,
Ok,
Warning,
Error,
Cancelled,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Diagnostic {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<Point3>,
}
impl Diagnostic {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self { code: code.into(), message: message.into(), detail: None, location: None }
}
pub fn at(mut self, location: Point3) -> Self {
self.location = Some(location);
self
}
pub fn detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmptyRegion {
pub reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<super::TargetRef>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApproximationLimit {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bound: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct WorkCounts {
pub triangles: u64,
pub candidates: u64,
pub samples: u64,
pub contacts: u64,
pub passes: u64,
pub motions: u64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationReport {
#[serde(default)]
pub status: OperationStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<Diagnostic>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<Diagnostic>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub empty_regions: Vec<EmptyRegion>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub approximations: Vec<ApproximationLimit>,
#[serde(default)]
pub counts: WorkCounts,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub estimated_seconds: Option<f64>,
}
impl OperationReport {
pub fn warn(&mut self, d: Diagnostic) {
self.warnings.push(d);
if self.status == OperationStatus::Ok || self.status == OperationStatus::Pending {
self.status = OperationStatus::Warning;
}
}
pub fn fail(&mut self, d: Diagnostic) {
self.errors.push(d);
self.status = OperationStatus::Error;
}
pub fn finish(&mut self) {
self.status = if !self.errors.is_empty() {
OperationStatus::Error
} else if !self.warnings.is_empty() {
OperationStatus::Warning
} else {
OperationStatus::Ok
};
}
pub fn is_usable(&self) -> bool {
matches!(self.status, OperationStatus::Ok | OperationStatus::Warning)
}
}