1use super::*;
5use crate::features::Feature;
6use crate::model::{Point2, TargetSelection, ToolGeometry};
7use std::collections::{BTreeMap, BTreeSet};
8
9#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct FeatureIdentity {
14 pub id: String,
15 pub solid: String,
16 pub kind: String,
17 pub faces: Vec<String>,
18}
19
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct Binding {
23 pub geometry: OperationGeometry,
24 pub setup: Setup,
25 pub targets: TargetSelection,
26 pub tool_geometry: ToolGeometry,
27 pub tool_kind: ToolKind,
28 #[serde(with = "fingerprints")]
31 pub body_fingerprints: BTreeMap<String, u64>,
32 pub feature: FeatureIdentity,
33 pub key_dimensions: Vec<f64>,
36 pub planned_name: String,
37 pub role: String,
38 pub strategy: Strategy,
39 pub operation_tolerance: f64,
40}
41
42mod fingerprints {
43 use super::*;
44 pub fn serialize<S: serde::Serializer>(value: &BTreeMap<String, u64>, serializer: S) -> Result<S::Ok, S::Error> {
45 value.iter().map(|(k, v)| (k, format!("{v:016x}"))).collect::<BTreeMap<_, _>>().serialize(serializer)
46 }
47 pub fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<BTreeMap<String, u64>, D::Error> {
48 BTreeMap::<String, String>::deserialize(deserializer)?.into_iter().map(|(k, v)| {
49 u64::from_str_radix(&v, 16).map(|v| (k, v)).map_err(serde::de::Error::custom)
50 }).collect()
51 }
52}
53
54pub fn key_dimensions(feature: &Feature) -> Vec<f64> {
55 let bounds = |(a, b): (Point2, Point2)| vec![a.x, a.y, b.x, b.y];
56 match &feature.kind {
57 FeatureKind::Face { region, .. } => bounds(region.bounds()),
58 FeatureKind::Pocket { floor, islands, .. } => { let mut v = bounds(floor.bounds()); v.push(*islands as f64); v },
59 FeatureKind::Step { floor, .. } => bounds(floor.bounds()),
60 FeatureKind::OuterProfile { outline, .. } | FeatureKind::Boss { outline, .. } => bounds(outline.bounds()),
61 FeatureKind::Slot { centre, width, .. } => { let mut v = bounds(crate::planar::Loop::new(centre.segments.clone()).bounds()); v.push(*width); v },
62 FeatureKind::Hole(h) => vec![h.top.x, h.top.y, h.diameter],
63 FeatureKind::Chamfer { chain, width, .. } => { let mut v = bounds(crate::planar::Loop::new(chain.segments.clone()).bounds()); v.push(*width); v },
64 FeatureKind::Fillet { chain, radius, .. } => { let mut v = bounds(crate::planar::Loop::new(chain.segments.clone()).bounds()); v.push(*radius); v },
65 }
66}
67
68impl Binding {
69 pub fn new(planned: &PlannedOperation, feature: &Feature, setup: &Setup, tool: &Tool, body_fingerprints: BTreeMap<String, u64>) -> Self {
70 Self { geometry: planned.geometry.clone(), setup: setup.clone(), targets: planned.operation.targets.clone(),
71 tool_geometry: tool.geometry.clone(), tool_kind: tool.kind, body_fingerprints, feature: FeatureIdentity { id: feature.id.clone(), solid: feature.solid.clone(), kind: feature.kind.name().into(), faces: feature.faces.clone() }, key_dimensions: key_dimensions(feature),
72 planned_name: planned.operation.name.clone(), role: planned.role.clone(), strategy: planned.operation.strategy.clone(), operation_tolerance: planned.operation.tolerance }
73 }
74
75 pub fn validate_inputs(&self, operation: &Operation, setup: &Setup, tool: &Tool) -> Result<(), String> {
76 if self.setup.model_to_machine != setup.model_to_machine || self.setup.targets != setup.targets || self.setup.protected != setup.protected || self.setup.stock != setup.stock {
77 return Err("automatic programming setup geometry changed; re-run Program part automatically".into());
78 }
79 if self.targets != operation.targets || self.tool_geometry != tool.geometry || self.tool_kind != tool.kind || self.strategy != operation.strategy || self.operation_tolerance != operation.tolerance {
80 return Err("automatic programming targets, cutter, tolerance or passes changed; re-run Program part automatically".into());
81 }
82 Ok(())
83 }
84
85 pub fn matches(&self, feature: &Feature, tol: &Tolerance) -> bool {
86 let keys = key_dimensions(feature);
87 self.feature.solid == feature.solid && self.feature.kind == feature.kind.name()
88 && self.feature.faces.iter().collect::<BTreeSet<_>>() == feature.faces.iter().collect::<BTreeSet<_>>()
89 && keys.len() == self.key_dimensions.len()
90 && keys.iter().zip(&self.key_dimensions).all(|(a, b)| (a - b).abs() <= tol.distance_eps.max(1e-6))
91 }
92
93 pub fn rebind_with_mesh(&self, recognized: &Recognized, mesh: &MeshSnapshot, operation: &Operation, setup: &Setup, tools: &[Tool], machine: Option<&Machine>, stock_top: Option<f64>, tol: Tolerance) -> Result<(Operation, OperationGeometry), String> {
96 let (op, mut geometry) = self.rebind(recognized, operation, setup, tools, machine, stock_top, tol)?;
97 if matches!(op.strategy, Strategy::Pocket2D(_)) {
98 let feature = recognized.features.iter().find(|f| self.matches(f, &tol)).ok_or("rebound feature disappeared")?;
99 super::access::constrain_rebound(&mut geometry, feature, mesh, &tol).map_err(|e| e.to_string())?;
100 }
101 Ok((op, geometry))
102 }
103
104 pub fn rebind(&self, recognized: &Recognized, operation: &Operation, setup: &Setup, tools: &[Tool], machine: Option<&Machine>, stock_top: Option<f64>, tol: Tolerance) -> Result<(Operation, OperationGeometry), String> {
107 let matches: Vec<_> = recognized.features.iter().filter(|f| self.matches(f, &tol)).collect();
108 if matches.len() != 1 {
109 return Err(format!("feature {} no longer recognised; re-run Program part automatically{}", self.feature.id, if matches.len() > 1 { " (ambiguous identity)" } else { "" }));
110 }
111 let feature = matches[0];
112 let own: Vec<_> = tools.iter().filter(|t| t.id == operation.tool).cloned().collect();
113 let library = if matches!(feature.kind, FeatureKind::Hole(_)) { tools } else { own.as_slice() };
114 let db = TechDb::builtin();
115 let planned = plan(&AutoInput { recognized, setup, tools: library, machine, material: WorkMaterial::Aluminium,
116 techdb: &db, units: LibraryUnits::Millimeters, stock_top }).map_err(|e| e.to_string())?;
117 let updated = planned.operations.iter().find(|p| p.feature == feature.id && p.operation.name == self.planned_name && p.role == self.role && p.tool == operation.tool)
118 .ok_or_else(|| format!("feature {} cannot use its programmed tool/rule after model change; re-run Program part automatically", self.feature.id))?;
119 let mut op = operation.clone();
120 op.strategy = updated.operation.strategy.clone();
121 Ok((op, updated.geometry.clone()))
122 }
123}
124
125#[cfg(feature = "kernel")]
127pub fn recognize_bodies(bodies: &[(String, brep_kernel::BrepSolid)], setup: &Setup, mesh: &MeshSnapshot, chord: f64, tol: Tolerance, runner: &mut Runner<'_>) -> Result<Recognized, CamError> {
128 let mut carriers = Vec::new(); let mut regions = Vec::new(); let mut revolved = Vec::new();
129 for (name, solid) in bodies {
130 if !setup.targets.iter().any(|t| &t.solid == name) { continue; }
131 let body = crate::kernel_adapter::BodyInput { name, solid, protected_only: false };
132 carriers.extend(crate::kernel_adapter::features::face_carriers(&body, setup, &tol));
133 regions.extend(crate::kernel_adapter::planar::horizontal_faces(solid, &setup.model_to_machine, chord, &tol));
134 revolved.extend(crate::kernel_adapter::revolved_faces(&body, setup));
135 }
136 let holes = crate::holes::recognize(&revolved, mesh, tol, runner)?;
137 let recognized = crate::features::recognize(&crate::features::RecognitionInput { mesh, carriers: &carriers, regions: ®ions, holes: &holes, tolerance: tol })?;
138 Ok(recognized)
139}
140