brep_cam/cycle_time/
mod.rs1use crate::model::{ArcPlane, Cycle, Feed, Machine, Motion, MotionKind, Point3};
13use serde::{Deserialize, Serialize};
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct MotionTime {
19 pub seconds: f64,
20 pub length: f64,
22 pub cutting: bool,
23}
24
25#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct CycleTime {
29 pub total_seconds: f64,
30 pub cutting_seconds: f64,
31 pub rapid_seconds: f64,
32 pub dwell_seconds: f64,
33 pub tool_change_seconds: f64,
34 pub cutting_length: f64,
35 pub rapid_length: f64,
36 pub tool_changes: u32,
37 pub per_motion: Vec<MotionTime>,
39}
40
41pub fn move_seconds(length: f64, feed_per_minute: f64, acceleration: Option<f64>) -> f64 {
45 if !(length > 0.0) || !(feed_per_minute > 0.0) {
46 return 0.0;
47 }
48 let v = feed_per_minute / 60.0;
49 match acceleration {
50 Some(a) if a > 0.0 && a.is_finite() => {
51 let ramp = v * v / a; if length >= ramp {
53 length / v + v / a
54 } else {
55 2.0 * (length / a).sqrt()
56 }
57 }
58 _ => length / v,
59 }
60}
61
62pub fn arc_length(start: Point3, end: Point3, center: Point3, clockwise: bool, pitch: f64) -> f64 {
66 use std::f64::consts::TAU;
67 let r0 = ((start.x - center.x).powi(2) + (start.y - center.y).powi(2)).sqrt();
68 let r1 = ((end.x - center.x).powi(2) + (end.y - center.y).powi(2)).sqrt();
69 let radius = 0.5 * (r0 + r1);
70 let a0 = (start.y - center.y).atan2(start.x - center.x);
71 let a1 = (end.y - center.y).atan2(end.x - center.x);
72 let mut sweep = (if clockwise { a0 - a1 } else { a1 - a0 }).rem_euclid(TAU);
73 if sweep < 1e-12 {
74 sweep = TAU;
75 }
76 let dz = end.z - start.z;
77 if pitch.is_finite() && pitch.abs() > 0.0 && dz.abs() > 0.0 {
78 sweep = TAU * dz.abs() / pitch.abs();
79 }
80 let planar = radius * sweep;
81 (planar * planar + dz * dz).sqrt()
82}
83
84pub fn estimate_cycle_time(motions: &[Motion], machine: &Machine) -> CycleTime {
87 let mut out = CycleTime { per_motion: Vec::with_capacity(motions.len()), ..Default::default() };
88 let rapid_rate = machine.feed.rapid;
89 let max_cut = machine.feed.max_cutting;
90 let accel = machine.feed.acceleration;
91 let mut position: Option<Point3> = None;
92 for motion in motions {
93 let mut mt = MotionTime::default();
94 match &motion.kind {
95 MotionKind::Dwell { seconds } => {
96 mt.seconds = seconds.max(0.0);
97 out.dwell_seconds += mt.seconds;
98 }
99 MotionKind::ToolChange { .. } => {
100 mt.seconds = machine.tool_change_seconds.max(0.0);
101 out.tool_change_seconds += mt.seconds;
102 out.tool_changes += 1;
103 }
104 MotionKind::Cycle { at, cycle } => {
105 let feed = feed_rate(motion.feed, max_cut).unwrap_or(max_cut);
107 if let Some(p) = position {
108 let travel = p.distance(*at);
109 let t = move_seconds(travel, rapid_rate, accel);
110 mt.seconds += t;
111 mt.length += travel;
112 out.rapid_seconds += t;
113 out.rapid_length += travel;
114 }
115 let retract_z = cycle.retract_z().max(at.z);
116 let depth_z = match cycle {
117 Cycle::BackBore { entry_z, .. } => *entry_z,
118 other => other.depth_z(),
119 };
120 let depth = (retract_z - depth_z).abs();
121 let (mut feed_len, mut rapid_len) = (depth, depth);
122 match cycle {
123 Cycle::Peck { peck, .. } if *peck > 0.0 => {
124 let pecks = (depth / peck).ceil().max(1.0);
125 for k in 1..pecks as usize {
127 let reached = (k as f64 * peck).min(depth);
128 rapid_len += 2.0 * reached;
129 }
130 }
131 Cycle::ChipBreak { peck, back_off, .. } if *peck > 0.0 => {
132 let pecks = (depth / peck).ceil().max(1.0);
133 let back = if *back_off > 0.0 { *back_off } else { peck * 0.5 };
134 rapid_len += 2.0 * back * (pecks - 1.0);
135 feed_len += back * (pecks - 1.0);
136 }
137 Cycle::Ream { .. } | Cycle::Tap { .. } => {
138 feed_len += depth;
140 rapid_len -= depth;
141 }
142 Cycle::Bore { feed_out: true, .. } => {
143 feed_len += depth;
144 rapid_len -= depth;
145 }
146 _ => {}
147 }
148 let dwell = match cycle {
149 Cycle::Drill { dwell_seconds, .. } | Cycle::Bore { dwell_seconds, .. } => dwell_seconds.max(0.0),
150 _ => 0.0,
151 };
152 let tf = move_seconds(feed_len, feed, accel);
153 let tr = move_seconds(rapid_len.max(0.0), rapid_rate, accel);
154 mt.seconds += tf + tr + dwell;
155 mt.length += feed_len + rapid_len.max(0.0);
156 mt.cutting = true;
157 out.cutting_seconds += tf;
158 out.cutting_length += feed_len;
159 out.rapid_seconds += tr;
160 out.rapid_length += rapid_len.max(0.0);
161 out.dwell_seconds += dwell;
162 position = Some(Point3::new(at.x, at.y, retract_z));
163 }
164 kind => {
165 if let Some(end) = kind.end() {
166 if let Some(start) = position {
167 let length = match kind {
168 MotionKind::ArcCw { to, center, plane, pitch } | MotionKind::ArcCcw { to, center, plane, pitch } => {
169 if *plane == ArcPlane::Xy {
170 arc_length(start, *to, *center, matches!(kind, MotionKind::ArcCw { .. }), *pitch)
171 } else {
172 start.distance(*to)
173 }
174 }
175 _ => start.distance(end),
176 };
177 let is_rapid = matches!(motion.feed, Feed::Rapid(_));
178 let rate = if is_rapid { rapid_rate } else { feed_rate(motion.feed, max_cut).unwrap_or(max_cut) };
179 mt.seconds = move_seconds(length, rate, accel);
180 mt.length = length;
181 mt.cutting = kind.is_cutting();
182 if mt.cutting {
183 out.cutting_seconds += mt.seconds;
184 out.cutting_length += length;
185 } else {
186 out.rapid_seconds += mt.seconds;
187 out.rapid_length += length;
188 }
189 }
190 position = Some(end);
191 }
192 }
193 }
194 out.total_seconds += mt.seconds;
195 out.per_motion.push(mt);
196 }
197 out
198}
199
200fn feed_rate(feed: Feed, max_cutting: f64) -> Option<f64> {
202 match feed {
203 Feed::Rapid(_) => None,
204 Feed::Rate(r) if r.is_finite() && r > 0.0 => Some(r.min(max_cutting)),
205 Feed::Rate(_) => Some(max_cutting),
206 }
207}
208