Skip to main content

brep_cam/features/
techdb.rs

1//! The technology table (lane C13 row 2): data, not code. A feature type
2//! maps to an ordered list of operation rules; each rule names the strategy,
3//! how to choose the tool, and the cutting parameters as fractions of the
4//! chosen tool's diameter; per-material factors scale stepover and step
5//! down. The built-in table is `BREP_cam/data/techdb.json`; a shop's own
6//! table loads through [`TechDb::from_json`].
7
8use crate::model::{RampKind, StockToLeave, ToolKind};
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11
12pub const BUILTIN_TECHDB: &str = include_str!("../../data/techdb.json");
13
14#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16pub struct TechDb {
17    pub format: String,
18    pub version: u32,
19    #[serde(default)]
20    pub name: String,
21    #[serde(default)]
22    pub units: String,
23    /// Stock assumed above a face to face when the stock top is unknown.
24    #[serde(default = "default_face_allowance")]
25    pub face_allowance: f64,
26    /// How far the tool runs out past an open pocket's open edges.
27    #[serde(default = "default_overhang")]
28    pub open_pocket_overhang: f64,
29    #[serde(default)]
30    pub material_factors: BTreeMap<String, MaterialFactors>,
31    pub rules: Vec<Rule>,
32}
33
34fn default_face_allowance() -> f64 {
35    2.0
36}
37
38fn default_overhang() -> f64 {
39    1.0
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase", deny_unknown_fields)]
44pub struct MaterialFactors {
45    pub stepover: f64,
46    pub step_down: f64,
47}
48
49impl Default for MaterialFactors {
50    fn default() -> Self {
51        Self { stepover: 1.0, step_down: 1.0 }
52    }
53}
54
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase", deny_unknown_fields)]
57pub struct Rule {
58    /// Feature kind name (`features::FeatureKind::name`).
59    pub feature: String,
60    pub operations: Vec<OperationRule>,
61}
62
63/// How a tool is picked from the library for one operation.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub enum Fit {
67    /// The largest tool of the kinds.
68    Largest,
69    /// The largest tool that fits the feature (corner radius, slot width,
70    /// pocket width) with a cutting length at least the depth.
71    LargestFitting,
72    /// The tool whose diameter equals the feature's (holes).
73    ExactDiameter,
74}
75
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase", deny_unknown_fields)]
78pub struct ToolRule {
79    pub kinds: Vec<ToolKind>,
80    pub fit: Fit,
81}
82
83#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase", deny_unknown_fields)]
85pub struct OperationRule {
86    pub name: String,
87    /// Strategy kind name: `face`, `pocket2D`, `contour2D`, `slot`,
88    /// `chamfer2D`, `drill`.
89    pub strategy: String,
90    /// `face`, `rough`, `finish`, `drill`, `chamfer` (orders the program).
91    pub role: String,
92    pub tool: ToolRule,
93    /// Stepover as a fraction of the tool diameter.
94    pub stepover: f64,
95    /// Step down as a fraction of the tool diameter (`0`: one level).
96    pub step_down: f64,
97    pub stock_to_leave: StockToLeave,
98    #[serde(default)]
99    pub finishing_passes: u32,
100    #[serde(default)]
101    pub roughing_passes: u32,
102    /// Lead arc radius as a fraction of the tool diameter (`0`: none).
103    #[serde(default)]
104    pub lead: f64,
105    #[serde(default = "default_ramp")]
106    pub ramp: RampKind,
107    /// Facing: extension past the stock, zig-zag.
108    #[serde(default)]
109    pub extension: f64,
110    #[serde(default = "default_true")]
111    pub zig_zag: bool,
112    /// Chamfer tip offset below the chamfer's lower edge.
113    #[serde(default)]
114    pub tip_offset: f64,
115    /// Extra outside-profile depth when the part reaches the stock bottom.
116    #[serde(default, skip_serializing_if="is_zero")]
117    pub break_through: f64,
118    #[serde(default = "default_tolerance")]
119    pub tolerance: f64,
120}
121
122fn is_zero(v:&f64)->bool {*v==0.}
123
124fn default_ramp() -> RampKind {
125    RampKind::Plunge
126}
127
128fn default_true() -> bool {
129    true
130}
131
132fn default_tolerance() -> f64 {
133    0.01
134}
135
136impl TechDb {
137    pub fn builtin() -> TechDb {
138        Self::from_json(BUILTIN_TECHDB).expect("the built-in technology table validates")
139    }
140
141    pub fn from_json(text: &str) -> Result<TechDb, String> {
142        let db: TechDb = serde_json::from_str(text).map_err(|e| e.to_string())?;
143        db.validate()?;
144        Ok(db)
145    }
146
147    pub fn to_json(&self) -> Result<String, String> {
148        self.validate()?;
149        serde_json::to_string_pretty(self).map(|s| s + "\n").map_err(|e| e.to_string())
150    }
151
152    pub fn validate(&self) -> Result<(), String> {
153        if self.format != "brep-techdb" || self.version != 1 {
154            return Err("unsupported technology table format/version".into());
155        }
156        if !self.face_allowance.is_finite() || self.face_allowance < 0.0 || !self.open_pocket_overhang.is_finite() || self.open_pocket_overhang < 0.0 {
157            return Err("face allowance and open pocket overhang must be finite and non-negative".into());
158        }
159        for (m, f) in &self.material_factors {
160            if !(f.stepover > 0.0 && f.stepover.is_finite() && f.step_down > 0.0 && f.step_down.is_finite()) {
161                return Err(format!("material {m}: factors must be finite and positive"));
162            }
163        }
164        if self.rules.is_empty() {
165            return Err("the technology table has no rules".into());
166        }
167        let mut seen = std::collections::BTreeSet::new();
168        for r in &self.rules {
169            if !seen.insert(r.feature.as_str()) {
170                return Err(format!("duplicate rule for feature {}", r.feature));
171            }
172            if r.operations.is_empty() {
173                return Err(format!("rule for {} has no operations", r.feature));
174            }
175            let mut names = std::collections::BTreeSet::new();
176            for o in &r.operations {
177                if o.name.trim().is_empty() || !names.insert(o.name.as_str()) {
178                    return Err(format!("rule for {}: empty or duplicate operation name", r.feature));
179                }
180                if !matches!(o.strategy.as_str(), "face" | "pocket2D" | "contour2D" | "slot" | "chamfer2D" | "drill") {
181                    return Err(format!("rule for {}: unknown strategy {}", r.feature, o.strategy));
182                }
183                if !matches!(o.role.as_str(), "face" | "rough" | "finish" | "drill" | "chamfer") {
184                    return Err(format!("rule for {}: unknown role {}", r.feature, o.role));
185                }
186                if o.tool.kinds.is_empty() {
187                    return Err(format!("rule for {}: operation {} names no tool kinds", r.feature, o.name));
188                }
189                for (n, v) in [("stepover", o.stepover), ("step down", o.step_down), ("lead", o.lead), ("extension", o.extension), ("tip offset", o.tip_offset), ("breakthrough", o.break_through)] {
190                    if !v.is_finite() || v < 0.0 {
191                        return Err(format!("rule for {}: {n} must be finite and non-negative", r.feature));
192                    }
193                }
194                if o.stepover > 1.0 {
195                    return Err(format!("rule for {}: stepover fraction above 1", r.feature));
196                }
197                if !o.tolerance.is_finite() || o.tolerance <= 0.0 {
198                    return Err(format!("rule for {}: tolerance must be positive", r.feature));
199                }
200                o.stock_to_leave.validate()?;
201                if o.stock_to_leave.radial < 0.0 || o.stock_to_leave.axial < 0.0 {
202                    return Err(format!("rule for {}: stock to leave must not be negative", r.feature));
203                }
204            }
205        }
206        Ok(())
207    }
208
209    /// The rule for a feature kind name.
210    pub fn rule(&self, feature: &str) -> Option<&Rule> {
211        self.rules.iter().find(|r| r.feature == feature)
212    }
213
214    /// Factors for a material (kebab-case or camelCase name), identity when absent.
215    pub fn factors(&self, material: &str) -> MaterialFactors {
216        let camel: String = {
217            let mut out = String::new();
218            let mut up = false;
219            for ch in material.chars() {
220                if ch == '-' || ch == '_' {
221                    up = true;
222                } else if up {
223                    out.extend(ch.to_uppercase());
224                    up = false;
225                } else {
226                    out.push(ch);
227                }
228            }
229            out
230        };
231        self.material_factors.get(material).or_else(|| self.material_factors.get(&camel)).copied().unwrap_or_default()
232    }
233}