Skip to main content

brep_cam/autoprogram/
binding.rs

1//! Durable identity of an automatically programmed feature. Geometry is
2//! refreshed from exact bodies after edits; a lost or ambiguous feature is
3//! refused rather than silently machining a different region.
4use super::*;
5use crate::features::Feature;
6use crate::model::{Point2, TargetSelection, ToolGeometry};
7use std::collections::{BTreeMap, BTreeSet};
8
9/// Only persistent recognition identity belongs in the recipe. In particular,
10/// do not flatten a Hole's own `kind` into the feature enum's `kind` tag.
11#[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    /// Hex strings on the wire: JSON/JavaScript numbers cannot retain all
29    /// 64 fingerprint bits across the browser worker boundary.
30    #[serde(with = "fingerprints")]
31    pub body_fingerprints: BTreeMap<String, u64>,
32    pub feature: FeatureIdentity,
33    /// XY position and dimensions identify the feature while its depth may
34    /// change. Face identities must match as well.
35    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    /// Apply the same whole-part extension protection as the preview after
94    /// re-recognizing changed geometry in the worker's current setup frame.
95    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    /// Rebind only the operation's own rule, using its current cutter. Hole
105    /// sequences need the full library to retain pilot/recess/thread intent.
106    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/// Exact recognition shared by the preview/worker boundary.
126#[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: &regions, holes: &holes, tolerance: tol })?;
138    Ok(recognized)
139}
140