1use super::*;
4use crate::model::{Bounds3, Point3, RigidTransform, Stock};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum Axis { PositiveX, NegativeX, PositiveY, NegativeY, NegativeZ }
8impl Axis {
9 pub const ALL: [Self; 5] = [Self::PositiveX, Self::NegativeX, Self::PositiveY, Self::NegativeY, Self::NegativeZ];
10 pub fn name(self) -> &'static str { match self { Self::PositiveX => "+X", Self::NegativeX => "-X", Self::PositiveY => "+Y", Self::NegativeY => "-Y", Self::NegativeZ => "-Z" } }
11 pub fn vector(self) -> Point3 { match self { Self::PositiveX => Point3::X, Self::NegativeX => Point3::X.scale(-1.), Self::PositiveY => Point3::Y, Self::NegativeY => Point3::Y.scale(-1.), Self::NegativeZ => Point3::Z.scale(-1.) } }
12}
13
14#[derive(Clone, Debug)]
17pub struct Candidate { pub axis: Axis, pub setup: Setup, pub recognized: Recognized, pub stock_top: Option<f64> }
18#[derive(Clone, Debug)]
19pub struct SetupPlan {
20 pub axis: Option<Axis>,
21 pub setup: Setup,
22 pub recognized: Recognized,
23 pub plan: Plan,
24 pub covered: Vec<String>,
26}
27#[derive(Clone, Debug)]
28pub struct MultiPlan { pub setups: Vec<SetupPlan>, pub unreached: Vec<Skipped> }
29
30pub fn candidate_setup(first: &Setup, axis: Axis, model_stock: Bounds3) -> Result<Setup, CamError> {
31 let inverse = first.machine_to_model();
32 let z = axis.vector();
33 let x = if z.x.abs() > 0.5 { Point3::Y } else { Point3::X };
34 let mut setup = first.clone();
35 setup.id = format!("{}-auto-{}", first.id, axis.name()).into();
36 setup.name = format!("{} ยท {} approach", first.name, axis.name());
37 setup.model_to_machine = RigidTransform::from_frame(inverse.apply_point(Point3::ZERO), inverse.apply_vector(x), inverse.apply_vector(z)).map_err(invalid)?;
38 setup.stock = Stock::FromRest { setup: first.id.clone() };
39 let bounds = setup.model_to_machine.apply_bounds(&model_stock);
40 let gap = (first.heights.clearance - first.heights.retract).abs().max(5.);
41 setup.heights.clearance = bounds.max.z + 2. * gap;
42 setup.heights.retract = bounds.max.z + gap;
43 setup.heights.feed = bounds.max.z + 1.;
44 setup.heights.top = None;
45 setup.heights.bottom = None;
46 Ok(setup)
47}
48
49pub fn needed_axes(recognized: &Recognized) -> Vec<Axis> {
52 Axis::ALL.into_iter().filter(|axis| recognized.features.iter().any(|f| !f.machinable && f.direction.dot(axis.vector()) > 1. - 1e-6)).collect()
53}
54
55fn same_feature(original: &Feature, candidate: &Feature) -> bool {
56 let family = |kind: &FeatureKind| match kind { FeatureKind::Pocket { .. } | FeatureKind::Step { .. } | FeatureKind::Slot { .. } => "floor", _ => kind.name() };
57 original.solid == candidate.solid && !original.faces.is_empty()
58 && original.faces.first() == candidate.faces.first()
59 && family(&original.kind) == family(&candidate.kind)
60 && candidate.machinable && candidate.direction.dot(Point3::Z) > 1. - 1e-6
61}
62
63pub fn plan_candidates<F>(input: &AutoInput<'_>, first: Plan, candidates: &[Candidate], mut planner: F) -> Result<MultiPlan, CamError>
64where F: FnMut(&AutoInput<'_>) -> Result<Plan, CamError> {
65 let mut pending: Vec<_> = input.recognized.features.iter().filter(|f| !f.machinable).collect();
66 let mut planning_refusals=std::collections::BTreeMap::<String,Vec<String>>::new();
67 let mut setups = vec![SetupPlan { axis: None, setup: input.setup.clone(), recognized: input.recognized.clone(), covered: input.recognized.features.iter().filter(|f| first.operations.iter().any(|p| p.feature == f.id)).map(|f| f.id.clone()).collect(), plan: first }];
68 for axis in Axis::ALL {
69 let Some(candidate) = candidates.iter().find(|c| c.axis == axis) else { continue };
70 let mut pairs = Vec::new();
71 for original in &pending {
72 if original.direction.dot(axis.vector()) < 1. - 1e-6 { continue; }
73 let matches: Vec<_> = candidate.recognized.features.iter().filter(|f| same_feature(original, f)).collect();
74 if matches.len() == 1 { pairs.push((original.id.clone(), matches[0].id.clone())); }
75 }
76 if pairs.is_empty() { continue; }
77 let mut setup = candidate.setup.clone();
78 setup.stock = Stock::FromRest { setup: setups.last().unwrap().setup.id.clone() };
79 let mut plan = planner(&AutoInput { recognized: &candidate.recognized, setup: &setup, stock_top: candidate.stock_top, ..*input })?;
80 for (original,id) in &pairs {
81 if plan.operations.iter().any(|p| &p.feature==id) {continue;}
82 let reasons:Vec<_>=plan.skipped.iter().filter(|s| &s.feature==id).map(|s|s.reason.as_str()).collect();
83 let reason=if reasons.is_empty() {"the planner produced no operations".into()}else{reasons.join("; ")};
84 planning_refusals.entry(original.clone()).or_default().push(format!("{}: {reason}",axis.name()));
85 }
86 pairs.retain(|(_, id)| plan.operations.iter().any(|p| &p.feature == id));
87 if pairs.is_empty() { continue; }
88 plan.operations.retain(|p| pairs.iter().any(|(_, id)| id == &p.feature));
89 plan.skipped.retain(|s| pairs.iter().any(|(_, id)| id == &s.feature));
90 let mut recognized = candidate.recognized.clone();
91 recognized.features.retain(|f| pairs.iter().any(|(_, id)| id == &f.id));
92 let prefix = format!("auto{}:", axis.name());
95 for feature in &mut recognized.features { feature.id = format!("{prefix}{}", feature.id); }
96 if let Some(accounting) = &mut recognized.accounting {
100 crate::features::accounting::retain_claims(accounting, |id| pairs.iter().any(|(_, kept)| kept == id).then(|| format!("{prefix}{id}")));
101 for a in accounting.iter_mut().filter(|a| a.features.is_empty() && a.body.is_none()) {
102 if let Some(first) = input.recognized.features.iter().find(|f| f.solid == a.solid && f.faces.contains(&a.face)) {
103 a.body = Some(crate::features::BodyReason::OtherSetup { axis: "first".into(), feature: first.id.clone() });
104 a.settle();
105 }
106 }
107 }
108 if let Some(accounting) = &mut setups[0].recognized.accounting {
111 for a in accounting.iter_mut().filter(|a| a.features.is_empty() && a.body.is_none()) {
112 if let Some(cover) = recognized.features.iter().find(|f| f.solid == a.solid && f.faces.contains(&a.face)) {
113 a.body = Some(crate::features::BodyReason::OtherSetup { axis: axis.name().into(), feature: cover.id.clone() });
114 a.settle();
115 }
116 }
117 }
118 for op in &mut plan.operations {
119 op.feature = format!("{prefix}{}", op.feature);
120 op.operation.id = format!("{prefix}{}", op.operation.id).into();
121 op.operation.setup = setup.id.clone();
122 }
123 for skipped in &mut plan.skipped { skipped.feature = format!("{prefix}{}", skipped.feature); }
124 plan.tool_changes = plan.operations.windows(2).filter(|w| w[0].tool != w[1].tool).count() as u32;
125 let covered: Vec<_> = pairs.into_iter().map(|(original, _)| original).collect();
126 pending.retain(|f| !covered.contains(&f.id));
127 setups.push(SetupPlan { axis: Some(axis), setup, recognized, plan, covered });
128 }
129 let unreached = pending.into_iter().map(|f| {
130 if let Some(reasons)=planning_refusals.get(&f.id) {
131 return Skipped{feature:f.id.clone(),kind:f.kind.name().into(),reason:format!("axis-aligned access found for feature {}, but no machining plan: {}; choose fitting tools/rules or skip",f.id,reasons.join("; "))};
132 }
133 let mut reasons = vec![f.reason.clone().unwrap_or_else(|| "access is uncertified".into())];
134 for candidate in candidates {
135 for found in candidate.recognized.features.iter().filter(|c| c.solid == f.solid && c.faces.first() == f.faces.first()) {
136 if let Some(reason) = &found.reason { if !reasons.contains(reason) { reasons.push(reason.clone()); } }
137 }
138 }
139 Skipped { feature: f.id.clone(), kind: f.kind.name().into(), reason: format!(
140 "no additional axis-aligned setup certifies feature {}: {}; requires 3D/5-axis review or skip", f.id, reasons.join("; ")) }
141 }).collect();
142 Ok(MultiPlan { setups, unreached })
143}
144