1use crate::features::techdb::{Fit, OperationRule, TechDb};
13use crate::features::{Feature, FeatureKind, Recognized, WallLine};
14use crate::feeds::WorkMaterial;
15use crate::holes::Hole;
16use crate::library::LibraryUnits;
17use crate::model::{
18 Chamfer2dParams, Contour2dParams, CutDirection, Depths, FaceParams, Lead, LeadKind, Machine, MachineKind,
19 Operation, OperationId, Pocket2dParams, Setup, SlotParams, Stepover, Strategy, TargetSelection, Tool, ToolId,
20 ToolKind, Toolpath,
21};
22use crate::planar::machining::{PlanarChain, PlanarInput, PlanarRegion};
23use crate::planar::{chain_capsule, Chain, Loop, Region, Vec2};
24use crate::{CamError, Cutter, CutterProfile, MeshSnapshot, Runner, Tolerance, TriangleIndex};
25use serde::{Deserialize, Serialize};
26
27mod hole_ops;
28pub mod multi;
29pub mod access;
30pub mod binding;
31pub mod report;
32pub use report::{report, Report};
33
34pub struct AutoInput<'a> {
36 pub recognized: &'a Recognized,
37 pub setup: &'a Setup,
38 pub tools: &'a [Tool],
40 pub machine: Option<&'a Machine>,
42 pub material: WorkMaterial,
43 pub techdb: &'a TechDb,
44 pub units: LibraryUnits,
45 pub stock_top: Option<f64>,
47}
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51pub enum OperationGeometry {
52 Planar(PlanarInput),
53 Holes(Vec<Hole>),
54}
55
56#[derive(Clone, Debug, PartialEq)]
57pub struct PlannedOperation {
58 pub operation: Operation,
59 pub tool: ToolId,
60 pub geometry: OperationGeometry,
61 pub feature: String,
62 pub role: String,
63 pub notes: Vec<String>,
64}
65
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct Skipped {
69 pub feature: String,
70 pub kind: String,
71 pub reason: String,
72}
73
74#[derive(Clone, Debug, Default, PartialEq)]
75pub struct Plan {
76 pub operations: Vec<PlannedOperation>,
77 pub skipped: Vec<Skipped>,
78 pub tool_changes: u32,
79}
80
81fn invalid(m: impl std::fmt::Display) -> CamError {
82 CamError::InvalidInput(format!("autoprogram: {m}"))
83}
84
85fn phase(role: &str) -> u8 {
86 match role {
87 "face" => 0,
88 "rough" => 1,
89 "finish" | "chamfer" => 2,
90 "spot" => 3,
91 "drill" => 4,
92 "tap" => 5,
93 _ => 6,
94 }
95}
96
97fn material_name(m: WorkMaterial) -> &'static str {
98 match m {
99 WorkMaterial::Aluminium => "aluminium",
100 WorkMaterial::MildSteel => "mildSteel",
101 WorkMaterial::AlloySteel => "alloySteel",
102 WorkMaterial::StainlessSteel => "stainlessSteel",
103 WorkMaterial::CastIron => "castIron",
104 WorkMaterial::Brass => "brass",
105 WorkMaterial::Titanium => "titanium",
106 WorkMaterial::Plastics => "plastics",
107 WorkMaterial::Wood => "wood",
108 WorkMaterial::Mdf => "mdf",
109 }
110}
111
112struct Fitting<'a> {
114 depth: f64,
115 region: Option<&'a Region>,
117 corner_radius: Option<f64>,
118 max_diameter: Option<f64>,
120 exact_diameter: Option<f64>,
121}
122
123fn choose_tool<'a>(rule: &OperationRule, tools: &'a [Tool], fit: &Fitting<'_>, clearance: Option<&(Region, Region)>, tol: &Tolerance) -> Result<&'a Tool, String> {
125 let mut candidates: Vec<&Tool> = tools.iter().filter(|t| rule.tool.kinds.contains(&t.kind)).collect();
126 if candidates.is_empty() {
127 return Err(format!("the library has no {:?} tool", rule.tool.kinds));
128 }
129 candidates.sort_by(|a, b| b.geometry.diameter.total_cmp(&a.geometry.diameter).then(a.number.cmp(&b.number)));
130 let mut why = Vec::new();
131 for t in candidates {
132 let d = t.geometry.diameter;
133 if t.geometry.cutting_length + tol.distance_eps < fit.depth {
134 why.push(format!("{}: cutting length {} below the depth {}", t.id, t.geometry.cutting_length, fit.depth));
135 continue;
136 }
137 if let Some((outline, allowed)) = clearance {
140 let margin = crate::pocket2d::wall_margin(tol, rule.tolerance);
141 let envelope = outline.offset(d + rule.stock_to_leave.radial + 2.0 * margin, tol)
142 .and_then(|expanded| expanded.difference(allowed, tol));
143 match envelope {
144 Ok(outside) if outside.area() <= tol.area_eps => {},
145 Ok(_) => { why.push(format!("{}: cutter sweep does not fit around the island", t.id)); continue; },
146 Err(e) => { why.push(format!("{}: island clearance could not be certified: {e}", t.id)); continue; },
147 }
148 }
149 match rule.tool.fit {
150 Fit::Largest => return Ok(t),
151 Fit::ExactDiameter => {
152 let want = fit.exact_diameter.ok_or("no exact diameter for the feature")?;
153 if (d - want).abs() <= 1e-6 {
154 return Ok(t);
155 }
156 why.push(format!("{}: diameter {} is not {}", t.id, d, want));
157 }
158 Fit::LargestFitting => {
159 if let Some(m) = fit.max_diameter {
160 if d > m + 1e-9 {
161 why.push(format!("{}: diameter {} above {}", t.id, d, m));
162 continue;
163 }
164 }
165 if let Some(r) = fit.corner_radius {
166 if 0.5 * d > r + 1e-9 {
167 why.push(format!("{}: radius {} above the corner radius {}", t.id, 0.5 * d, r));
168 continue;
169 }
170 }
171 if let Some(region) = fit.region {
172 let inner = region.offset(-(0.5 * d + crate::pocket2d::wall_margin(tol, rule.tolerance)), tol).map_err(|e| e.to_string())?;
173 if inner.is_empty() {
174 why.push(format!("{}: diameter {} does not fit the region", t.id, d));
175 continue;
176 }
177 }
178 return Ok(t);
179 }
180 }
181 }
182 Err(format!("no fitting tool: {}", why.join("; ")))
183}
184
185fn loop_chain(l: &Loop) -> Chain {
187 Chain { segments: l.segments.clone(), closed: true }
188}
189
190fn clearing_region(floor: &Region, open_edges: &[Chain], walls: &[WallLine], extend: f64, tol: &Tolerance) -> Result<Region, CamError> {
194 if open_edges.is_empty() {
195 return Ok(floor.clone());
196 }
197 let mut region = floor.clone();
198 for e in open_edges {
199 let cap = chain_capsule(e, extend, tol)?;
200 region = region.union(&cap, tol)?;
201 }
202 let big = 1.0e4;
203 for w in walls {
204 let t = w.inward.perp();
205 let corners = [
206 w.point.sub(t.scale(big)),
207 w.point.add(t.scale(big)),
208 w.point.add(t.scale(big)).add(w.inward.scale(big)),
209 w.point.sub(t.scale(big)).add(w.inward.scale(big)),
210 ];
211 let mut half = Loop::polygon(&corners);
212 if !half.is_ccw() {
213 half = half.reversed();
214 }
215 region = region.intersection(&Region::new(vec![half], tol)?, tol)?;
216 }
217 Ok(region)
218}
219
220fn protect_higher_material(mut region: Region, feature: &Feature, floor_z: f64, recognized: &Recognized, tol: &Tolerance) -> Result<Region, CamError> {
225 for other in &recognized.features {
226 if other.id == feature.id { continue; }
227 let (outline, z) = match &other.kind {
228 FeatureKind::Face { region, z } => (region, *z),
229 FeatureKind::Boss { outline, top_z, .. } => (outline, *top_z),
230 FeatureKind::Pocket { floor, floor_z, .. } | FeatureKind::Step { floor, floor_z, .. } => (floor, *floor_z),
231 _ => continue,
232 };
233 if z > floor_z + tol.distance_eps && !outline.loops.is_empty() { region = region.difference(outline, tol)?; }
234 }
235 Ok(region)
236}
237
238pub fn plan(input: &AutoInput<'_>) -> Result<Plan, CamError> {
240 let tol = Tolerance::default();
241 input.techdb.validate().map_err(invalid)?;
242 crate::library::validate_program_tools(input.tools).map_err(invalid)?;
243 let generic = Machine::new("generic-mill", "Generic 3-axis mill", MachineKind::Mill3);
244 let machine = input.machine.unwrap_or(&generic);
245 let factors = input.techdb.factors(material_name(input.material));
246 let mut planned: Vec<(u8, u32, usize, PlannedOperation)> = Vec::new();
247 let mut skipped = Vec::new();
248 for (fi, feature) in input.recognized.features.iter().enumerate() {
249 let kind = feature.kind.name();
250 if !feature.machinable {
251 skipped.push(Skipped { feature: feature.id.clone(), kind: kind.into(), reason: feature.reason.clone().unwrap_or_else(|| "not machinable in this setup".into()) });
252 continue;
253 }
254 if matches!(feature.kind, FeatureKind::Fillet { .. }) {
255 skipped.push(Skipped { feature: feature.id.clone(), kind: kind.into(), reason: "fillets need a 3D finishing strategy or a form cutter; not programmed".into() });
256 continue;
257 }
258 let Some(rule) = input.techdb.rule(kind) else {
259 skipped.push(Skipped { feature: feature.id.clone(), kind: kind.into(), reason: format!("the technology table has no rule for {kind}") });
260 continue;
261 };
262 let mut produced = 0;
263 let mut notes_for_skip = Vec::new();
264 for op_rule in &rule.operations {
265 let result=if op_rule.strategy=="drill" && matches!(feature.kind,FeatureKind::Hole(_)) {
266 hole_ops::plan_hole(input,feature,op_rule,machine)
267 } else {plan_operation(input, feature, op_rule, machine, &factors, &tol).map(|p|vec![p])};
268 match result {
269 Ok(ops) => { for p in ops {
270 p.operation.validate().map_err(|m| invalid(format!("{}: {m}", p.operation.id)))?;
271 let tool_number = input.tools.iter().find(|t| t.id == p.tool).map_or(0, |t| t.number);
272 planned.push((phase(&p.role), tool_number, fi, p));
273 produced += 1;
274 }}
275 Err(reason) => notes_for_skip.push(format!("{}: {reason}", op_rule.name)),
276 }
277 }
278 if produced == 0 {
279 skipped.push(Skipped { feature: feature.id.clone(), kind: kind.into(), reason: notes_for_skip.join("; ") });
280 } else if !notes_for_skip.is_empty() {
281 for (_, _, i, p) in planned.iter_mut() {
283 if *i == fi {
284 p.notes.extend(notes_for_skip.iter().cloned());
285 }
286 }
287 }
288 }
289 let diameter=|p:&PlannedOperation| input.tools.iter().find(|t|t.id==p.tool).unwrap().geometry.diameter;
292 planned.sort_by(|a, b| a.0.cmp(&b.0).then_with(||if a.0==1 {diameter(&b.3).total_cmp(&diameter(&a.3))}else{std::cmp::Ordering::Equal}).then(a.1.cmp(&b.1)).then(a.3.tool.as_str().cmp(b.3.tool.as_str())).then(a.2.cmp(&b.2)));
293 let mut ordered: Vec<PlannedOperation> = Vec::new();
294 let mut last_tool: Option<ToolId> = None;
295 let mut i = 0;
296 while i < planned.len() {
297 let ph = planned[i].0;
298 let mut j = i;
299 while j < planned.len() && planned[j].0 == ph {
300 j += 1;
301 }
302 let mut group: Vec<PlannedOperation> = planned.drain(i..j).map(|(_, _, _, p)| p).collect();
303 if let Some(lt) = last_tool.as_ref().filter(|_|ph!=1) {
304 let (same, rest): (Vec<_>, Vec<_>) = group.into_iter().partition(|p| &p.tool == lt);
306 group = same.into_iter().chain(rest).collect();
307 }
308 if let Some(last) = group.last() {
309 last_tool = Some(last.tool.clone());
310 }
311 ordered.extend(group);
312 let _ = &mut i;
314 }
315 let tool_changes = ordered.windows(2).filter(|w| w[0].tool != w[1].tool).count() as u32;
316 Ok(Plan { operations: ordered, skipped, tool_changes })
317}
318
319fn island_clearance(feature: &Feature, recognized: &Recognized, tol: &Tolerance) -> Result<Option<(Region, Region)>, CamError> {
323 let FeatureKind::Boss { outline, base_z, .. } = &feature.kind else { return Ok(None) };
324 let (lo,hi)=outline.bounds();
328 let mut allowed=Region::new(vec![Loop::rectangle(lo.sub(crate::Point2::new(1.0e4,1.0e4)),hi.add(crate::Point2::new(1.0e4,1.0e4)))],tol)?;
329 'parent: for parent in &recognized.features {
330 if parent.solid != feature.solid { continue; }
331 let (FeatureKind::Pocket { floor, floor_z, .. } | FeatureKind::Step { floor, floor_z, .. }) = &parent.kind else { continue };
332 if (base_z - floor_z).abs() > tol.distance_eps { continue; }
333 for (i, boundary) in floor.loops.iter().enumerate() {
334 if !boundary.is_ccw() && outline.loops == vec![boundary.reversed()] {
335 allowed = floor.clone();
336 allowed.loops.remove(i);
337 break 'parent;
338 }
339 }
340 }
341 allowed=protect_higher_material(allowed,feature,*base_z,recognized,tol)?;
342 Ok(Some((outline.clone(),allowed)))
343}
344
345fn plan_operation(input: &AutoInput<'_>, feature: &Feature, rule: &OperationRule, machine: &Machine, factors: &crate::features::techdb::MaterialFactors, tol: &Tolerance) -> Result<PlannedOperation, String> {
346 let setup = input.setup;
347 let stock_top = input.stock_top;
348 let id = OperationId::new(format!("{}-{}", feature.id, rule.name));
349 let mut notes = Vec::new();
350 let targets = TargetSelection::solids([feature.solid.clone()]);
351 let (fitting, strategy_kind) = match &feature.kind {
353 FeatureKind::OuterProfile { top_z,bottom_z,.. } => (Fitting {depth:stock_top.unwrap_or(*top_z)-bottom_z+rule.break_through,region:None,corner_radius:None,max_diameter:None,exact_diameter:None},"outer-profile"),
354 FeatureKind::Face { region, z } => {
355 let top = stock_top.unwrap_or(z + input.techdb.face_allowance);
356 (Fitting { depth: top - z, region: Some(region), corner_radius: None, max_diameter: None, exact_diameter: None }, "face")
357 }
358 FeatureKind::Pocket { floor, floor_z, top_z, corner_radius, .. } => (Fitting { depth: top_z - floor_z, region: Some(floor), corner_radius: *corner_radius, max_diameter: None, exact_diameter: None }, "pocket"),
359 FeatureKind::Step { floor, floor_z, top_z, .. } => (Fitting { depth: top_z - floor_z, region: Some(floor), corner_radius: None, max_diameter: None, exact_diameter: None }, "step"),
360 FeatureKind::Slot { width, floor_z, top_z, .. } => (Fitting { depth: top_z - floor_z, region: None, corner_radius: None, max_diameter: Some(*width), exact_diameter: None }, "slot"),
361 FeatureKind::Boss { top_z, base_z, .. } => (Fitting { depth: top_z - base_z, region: None, corner_radius: None, max_diameter: None, exact_diameter: None }, "boss"),
362 FeatureKind::Hole(h) => (Fitting { depth: h.depth, region: None, corner_radius: None, max_diameter: None, exact_diameter: Some(h.diameter) }, "hole"),
363 FeatureKind::Chamfer { width, .. } => (Fitting { depth: width + rule.tip_offset, region: None, corner_radius: None, max_diameter: None, exact_diameter: None }, "chamfer"),
364 FeatureKind::Fillet { .. } => return Err("fillets need a 3D finishing strategy or a form cutter; not programmed".into()),
365 };
366 let _ = strategy_kind;
367 if rule.strategy == "drill" {return Err("drill rule requires the compound-hole planner".into());}
368 let tool=choose_tool(rule, input.tools, &fitting, island_clearance(feature, input.recognized, tol).map_err(|e| e.to_string())?.as_ref(), tol)?;
369 if matches!(feature.kind,FeatureKind::Boss{..}) {
370 notes.push("boss contour finishes walls only; pocket-around requires the separately programmed enclosing floor (external stock boundary is not inferred)".into());
371 }
372 let d = tool.geometry.diameter;
373 let stepover = (rule.stepover * d * factors.stepover).max(tol.sample_eps);
374 let step_down = rule.step_down * d * factors.step_down;
375 let depths = |top: f64, bottom: f64| Depths { top: Some(top), bottom: Some(bottom), step_down, finishing_step_down: 0.0, finishing_passes: 0, depth_first: true };
376 let mut linking = crate::model::LinkingParams::default();
377 linking.ramp.kind = rule.ramp;
378 if rule.lead > 0.0 {
379 linking.lead_in = Lead { kind: LeadKind::Arc, radius: rule.lead * d, sweep_deg: 90.0, linear_length: 0.0, vertical_radius: 0.0 };
380 linking.lead_out = linking.lead_in;
381 }
382 let (strategy, geometry) = match (&feature.kind, rule.strategy.as_str()) {
383 (FeatureKind::OuterProfile {outline,top_z,bottom_z,walls},"contour2D")=>{
384 if *walls!=crate::features::ProfileWalls::Vertical {return Err("outer profile requires certified vertical walls".into());}
385 let aligned=setup.model_to_machine.rotation[2].iter().zip([0.,0.,1.]).all(|(a,b)|(a-b).abs()<tol.angle_eps);
388 let on_stock_bottom=aligned && match &setup.stock {
389 crate::Stock::BoundsOffset{bottom,..}=>*bottom==0.,
390 crate::Stock::Box{min,..}=>(setup.model_to_machine.apply_point(*min).z-bottom_z).abs()<=tol.distance_eps,
391 _=>false,
392 };
393 let breakthrough=if on_stock_bottom {rule.break_through}else{0.};
394 let params=Contour2dParams{depths:depths(stock_top.unwrap_or(*top_z),bottom_z-breakthrough),outside:true,roughing_passes:rule.roughing_passes,roughing_stepover:stepover,finishing_passes:rule.finishing_passes.max(1),finishing_overlap:0.,tabs:Vec::new(),direction:CutDirection::Climb};
395 (Strategy::Contour2D(params),PlanarInput{chains:outline.loops.iter().map(|l|PlanarChain{chain:loop_chain(l),z:*bottom_z}).collect(),stock_top,..Default::default()})
396 }
397 (FeatureKind::Face { region, z }, "face") => {
398 let top = stock_top.unwrap_or(z + input.techdb.face_allowance);
399 if top <= *z + tol.distance_eps { return Err("stock top is at or below the face; no facing allowance to remove".into()); }
400 let params = FaceParams { depths: depths(top, *z), stepover: Stepover { distance: stepover, direction: CutDirection::Climb }, pass_angle_deg: 0.0, pass_extension: rule.extension, zig_zag: rule.zig_zag };
401 (Strategy::Face(params), PlanarInput { regions: vec![PlanarRegion { region: region.clone(), z: *z, upward: true }], stock_top, ..Default::default() })
402 }
403 (FeatureKind::Pocket { floor, floor_z, top_z, open_edges, walls, .. }, "pocket2D") | (FeatureKind::Step { floor, floor_z, top_z, open_edges, walls, .. }, "pocket2D") => {
404 let region = clearing_region(floor, open_edges, walls, 0.5 * d + input.techdb.open_pocket_overhang, tol).map_err(|e| e.to_string())?;
405 let region = if open_edges.is_empty() { region } else {
406 protect_higher_material(region, feature, *floor_z, input.recognized, tol).map_err(|e| e.to_string())?
407 };
408 let mut pocket_depths=depths(stock_top.unwrap_or(*top_z).max(*top_z), *floor_z);
412 if pocket_depths.top.unwrap()-floor_z > tool.geometry.cutting_length + tol.distance_eps && pocket_depths.step_down <= 0. {
413 let rough_step=input.techdb.rule(feature.kind.name()).and_then(|r|r.operations.iter().find(|r|r.strategy=="pocket2D" && r.role=="rough" && r.step_down>0.)).map(|r|r.step_down)
414 .ok_or("single-depth pocket finishing exceeds flute reach; supply a roughing step-down rule")?;
415 pocket_depths.step_down=rough_step*d*factors.step_down;
416 notes.push("stock exceeds flute reach; clear finishing overhang in roughing-sized depth levels".into());
417 }
418 if pocket_depths.step_down>0. { pocket_depths.step_down=pocket_depths.step_down.min(tool.geometry.cutting_length); }
419 let params = Pocket2dParams { depths: pocket_depths, stepover: Stepover { distance: stepover, direction: CutDirection::Climb }, offset_pattern: true, wall_finish: false, keep_islands: true };
420 (Strategy::Pocket2D(params), PlanarInput { regions: vec![PlanarRegion { region, z: *floor_z, upward: true }], stock_top, ..Default::default() })
421 }
422 (FeatureKind::Pocket { floor, floor_z, top_z, .. }, "contour2D") => {
423 let mut chains = Vec::new();
425 for l in &floor.loops {
426 chains.push(PlanarChain { chain: loop_chain(l), z: *floor_z });
427 }
428 let mut wall_depths=depths(stock_top.unwrap_or(*top_z).max(*top_z),*floor_z);
429 if wall_depths.step_down<=0. && wall_depths.top.unwrap()-floor_z>tool.geometry.cutting_length+tol.distance_eps {
430 return Err("wall finishing exceeds flute reach; supply a positive step-down rule".into());
431 }
432 if wall_depths.step_down>0. {wall_depths.step_down=wall_depths.step_down.min(tool.geometry.cutting_length);}
433 let params = Contour2dParams { depths: wall_depths, outside: false, roughing_passes: rule.roughing_passes, roughing_stepover: stepover, finishing_passes: rule.finishing_passes.max(1), finishing_overlap: 0.0, tabs: Vec::new(), direction: CutDirection::Climb };
434 let outer: Vec<PlanarChain> = chains.iter().filter(|c| Loop::new(c.chain.segments.clone()).is_ccw()).cloned().collect();
437 let islands: Vec<PlanarChain> = chains.iter().filter(|c| !Loop::new(c.chain.segments.clone()).is_ccw()).cloned().collect();
438 if !islands.is_empty() {
439 notes.push(format!("{} island wall(s) are finished by the island's own boss contour", islands.len()));
440 }
441 (Strategy::Contour2D(params), PlanarInput { chains: outer, stock_top, ..Default::default() })
442 }
443 (FeatureKind::Slot { centre, width, floor_z, top_z }, "slot") => {
444 let params = SlotParams { depths: depths(*top_z, *floor_z), width: *width, trochoidal: false };
445 (Strategy::Slot(params), PlanarInput { chains: vec![PlanarChain { chain: centre.clone(), z: *floor_z }], stock_top, ..Default::default() })
446 }
447 (FeatureKind::Boss { outline, top_z, base_z, .. }, "contour2D") => {
448 let chains: Vec<PlanarChain> = outline.loops.iter().map(|l| PlanarChain { chain: loop_chain(l), z: *base_z }).collect();
449 let params = Contour2dParams { depths: depths(*top_z, *base_z), outside: true, roughing_passes: rule.roughing_passes, roughing_stepover: stepover, finishing_passes: rule.finishing_passes.max(1), finishing_overlap: 0.0, tabs: Vec::new(), direction: CutDirection::Climb };
450 (Strategy::Contour2D(params), PlanarInput { chains, stock_top, ..Default::default() })
451 }
452 (FeatureKind::Chamfer { chain, width, z, .. }, "chamfer2D") => {
453 let params = Chamfer2dParams { width: *width, tip_offset: rule.tip_offset };
454 (Strategy::Chamfer2D(params), PlanarInput { chains: vec![PlanarChain { chain: chain.clone(), z: *z }], stock_top, ..Default::default() })
455 }
456 (FeatureKind::Face { .. }, "contour2D") => return Err("outer profile: recognition supplies a top outline but no certified profile bottom or stock boundary; select an explicit contour depth (tabs default off)".into()),
457 (kind, strategy) => return Err(format!("the rule's strategy {strategy} does not apply to a {}", kind.name())),
458 };
459 let mut op = Operation::new(id, format!("{} {}", feature.kind.name(), rule.name), setup.id.clone(), tool.id.clone(), strategy);
460 op.targets = targets;
461 op.tolerance = rule.tolerance;
462 op.stock_to_leave = rule.stock_to_leave;
463 op.linking = linking;
464 op.feeds = feeds_for(tool, machine, input, Some(stepover), &mut notes);
465 Ok(PlannedOperation { operation: op, tool: tool.id.clone(), geometry: OperationGeometry::Planar(geometry), feature: feature.id.clone(), role: rule.role.clone(), notes })
466}
467
468fn feeds_for(tool: &Tool, machine: &Machine, input: &AutoInput<'_>, engagement: Option<f64>, notes: &mut Vec<String>) -> Option<crate::model::FeedsSpeeds> {
471 match crate::feeds::calculate(tool, machine, input.units, input.material, engagement) {
472 Ok(calc) => {
473 notes.extend(calc.adjustments.iter().map(|a| format!("feeds: {a}")));
474 Some(calc.feeds)
475 }
476 Err(e) => {
477 notes.push(format!("feeds: {e}; the tool's own feeds are used"));
478 None
479 }
480 }
481}
482
483#[derive(Clone, Debug, PartialEq)]
486pub struct GeneratedOperation {
487 pub operation: OperationId,
488 pub toolpath: Option<Toolpath>,
489 pub expanded: Option<Toolpath>,
490 pub error: Option<String>,
491}
492
493impl GeneratedOperation {
494 pub fn simulated(&self) -> Option<&Toolpath> {
496 self.expanded.as_ref().or(self.toolpath.as_ref())
497 }
498}
499
500pub fn generate(plan: &Plan, input: &AutoInput<'_>, mesh: &MeshSnapshot, index: Option<&dyn TriangleIndex>, tol: Tolerance, runner: &mut Runner<'_>) -> Result<Vec<GeneratedOperation>, CamError> {
503 let mut out = Vec::with_capacity(plan.operations.len());
504 for (i, p) in plan.operations.iter().enumerate() {
505 runner.stage("autoprogram-generate", &p.operation.name, i as u64, plan.operations.len() as u64)?;
506 let tool = input.tools.iter().find(|t| t.id == p.tool).ok_or_else(|| invalid(format!("tool {} missing", p.tool)))?;
507 let op = &p.operation;
508 let result: Result<(Toolpath, Option<Toolpath>), CamError> = match (&op.strategy, &p.geometry) {
509 (Strategy::Face(_)|Strategy::Pocket2D(_)|Strategy::Contour2D(_)|Strategy::Slot(_)|Strategy::Chamfer2D(_)|Strategy::Circular(_)|Strategy::ThreadMill(_), OperationGeometry::Planar(g)) => {
510 let dispatch_input=crate::dispatch::GenerateInput {operation:op,setup:input.setup,tools:input.tools,mesh,index,tolerance:tol,holes:&[]};
511 let context=crate::dispatch::GenerateContext {resolved_planar:Some((&op.id,g)),machine:if input.setup.machine.is_some(){input.machine}else{None},..Default::default()};
512 crate::dispatch::generate_with_context(&dispatch_input,&context,runner).map(|t|(t,None))
513 },
514 (Strategy::Drill(_), OperationGeometry::Holes(h)) => crate::drill::strategy::generate(op, input.setup, tool, h, mesh, tol, runner).map(|d| (d.canned, Some(d.expanded))),
515 (s, _) => Err(invalid(format!("no generator for {}", s.kind_name()))),
516 };
517 match result {
518 Ok((path, expanded)) => out.push(GeneratedOperation { operation: op.id.clone(), toolpath: Some(path), expanded, error: None }),
519 Err(CamError::Cancelled) => return Err(CamError::Cancelled),
520 Err(e) => out.push(GeneratedOperation { operation: op.id.clone(), toolpath: None, expanded: None, error: Some(e.to_string()) }),
521 }
522 }
523 Ok(out)
524}
525
526pub fn program_input(plan: &Plan, generated: &[GeneratedOperation], input: &AutoInput<'_>, name: &str) -> crate::program::ProgramInput {
529 let mut operations = Vec::new();
530 for p in &plan.operations {
531 let Some(g) = generated.iter().find(|g| g.operation == p.operation.id) else { continue };
532 let Some(path) = &g.toolpath else { continue };
533 let Some(tool) = input.tools.iter().find(|t| t.id == p.tool) else { continue };
534 operations.push(crate::program::OperationInput { operation: p.operation.clone(), tool: tool.clone(), toolpath: path.clone() });
535 }
536 crate::program::ProgramInput {
537 name: name.to_string(),
538 number: None,
539 units: match input.units {
540 LibraryUnits::Millimeters => crate::program::Units::Mm,
541 LibraryUnits::Inches => crate::program::Units::In,
542 },
543 machine: input.machine.cloned(),
544 setups: vec![crate::program::SetupInput { setup: input.setup.clone(), operations }],
545 ..Default::default()
546 }
547}
548
549pub fn cutter_of(plan: &Plan, input: &AutoInput<'_>, operation: &OperationId) -> Option<Cutter> {
551 let p = plan.operations.iter().find(|p| &p.operation.id == operation)?;
552 let tool = input.tools.iter().find(|t| t.id == p.tool)?;
553 let c = Cutter::from_tool(tool).ok()?;
554 let _ = c.radius();
555 Some(c)
556}
557