1use 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 #[serde(default = "default_face_allowance")]
25 pub face_allowance: f64,
26 #[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 pub feature: String,
60 pub operations: Vec<OperationRule>,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub enum Fit {
67 Largest,
69 LargestFitting,
72 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 pub strategy: String,
90 pub role: String,
92 pub tool: ToolRule,
93 pub stepover: f64,
95 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 #[serde(default)]
104 pub lead: f64,
105 #[serde(default = "default_ramp")]
106 pub ramp: RampKind,
107 #[serde(default)]
109 pub extension: f64,
110 #[serde(default = "default_true")]
111 pub zig_zag: bool,
112 #[serde(default)]
114 pub tip_offset: f64,
115 #[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 pub fn rule(&self, feature: &str) -> Option<&Rule> {
211 self.rules.iter().find(|r| r.feature == feature)
212 }
213
214 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}