1use brep_cam::model::{ChainRef, ChainSource, EdgeRef, Machine, Operation, OperationId, Point3, RigidTransform, Setup, Strategy, TargetRef, Tool, ToolGeometry, ToolKind};
22use serde::{Deserialize, Serialize};
23use serde_json::{json, Value};
24use std::collections::BTreeMap;
25
26#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase", default)]
31pub struct NcProgram {
32 pub id: String,
33 pub name: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub number: Option<u32>,
37 pub post: String,
39 pub operations: Vec<OperationId>,
41 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
44 pub properties: BTreeMap<String, Value>,
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase", default)]
56pub struct SetupOrientation {
57 #[serde(skip_serializing_if = "Option::is_none")]
60 pub z_ref: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
67 pub z_dir: Option<String>,
68 #[serde(skip_serializing_if = "std::ops::Not::not")]
71 pub flip_z: bool,
72 #[serde(skip_serializing_if = "Option::is_none")]
75 pub x_ref: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
79 pub origin_ref: Option<String>,
80}
81
82impl SetupOrientation {
83 pub fn is_empty(&self) -> bool {
84 self.z_ref.is_none() && self.z_dir.is_none() && self.x_ref.is_none() && self.origin_ref.is_none() && !self.flip_z
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq)]
91pub enum FaceShape {
92 Planar { point: Point3, normal: Point3 },
93 Cylindrical { point: Point3, axis: Point3 },
94}
95
96fn eigen3(mut m: [[f64; 3]; 3]) -> [(f64, [f64; 3]); 3] {
99 let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
100 for _ in 0..50 {
101 let (mut p, mut q, mut largest) = (0, 1, 0.0f64);
102 for (i, j) in [(0, 1), (0, 2), (1, 2)] {
103 if m[i][j].abs() > largest {
104 largest = m[i][j].abs();
105 p = i;
106 q = j;
107 }
108 }
109 if largest < 1e-15 {
110 break;
111 }
112 let theta = 0.5 * (2.0 * m[p][q]).atan2(m[q][q] - m[p][p]);
113 let (c, s) = (theta.cos(), theta.sin());
114 let mut r = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
115 r[p][p] = c;
116 r[q][q] = c;
117 r[p][q] = s;
118 r[q][p] = -s;
119 let mul = |a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]| {
121 let mut o = [[0.0; 3]; 3];
122 for i in 0..3 {
123 for j in 0..3 {
124 o[i][j] = (0..3).map(|k| a[i][k] * b[k][j]).sum();
125 }
126 }
127 o
128 };
129 let rt = [[r[0][0], r[1][0], r[2][0]], [r[0][1], r[1][1], r[2][1]], [r[0][2], r[1][2], r[2][2]]];
130 m = mul(&mul(&rt, &m), &r);
131 v = mul(&v, &r);
132 }
133 let mut out = [0, 1, 2].map(|k| (m[k][k], [v[0][k], v[1][k], v[2][k]]));
134 out.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
135 out
136}
137
138pub fn classify_face(triangles: &[[[f64; 3]; 3]]) -> Option<FaceShape> {
146 let (mut area, mut centroid, mut mean, mut cov) = (0.0f64, [0.0f64; 3], [0.0f64; 3], [[0.0f64; 3]; 3]);
147 for [a, b, c] in triangles {
148 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
149 let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
150 let n = [ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0]];
151 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
152 if len <= 1e-18 {
153 continue;
154 }
155 let w = 0.5 * len;
156 let u = [n[0] / len, n[1] / len, n[2] / len];
157 for i in 0..3 {
158 centroid[i] += w * (a[i] + b[i] + c[i]) / 3.0;
159 mean[i] += w * u[i];
160 for j in 0..3 {
161 cov[i][j] += w * u[i] * u[j];
162 }
163 }
164 area += w;
165 }
166 if area <= 1e-18 {
167 return None;
168 }
169 for row in cov.iter_mut() {
170 for x in row.iter_mut() {
171 *x /= area;
172 }
173 }
174 let point = Point3::new(centroid[0] / area, centroid[1] / area, centroid[2] / area);
175 let [(l1, e1), (l2, _), (l3, e3)] = eigen3(cov);
176 const FLAT: f64 = 1e-6;
177 if l2 + l3 < FLAT && l1 > 1.0 - 1e-3 {
178 let s = if e1[0] * mean[0] + e1[1] * mean[1] + e1[2] * mean[2] < 0.0 { -1.0 } else { 1.0 };
180 return Some(FaceShape::Planar { point, normal: Point3::new(s * e1[0], s * e1[1], s * e1[2]) });
181 }
182 if l3 < FLAT && l2 > FLAT {
183 let k = (0..3).max_by(|&i, &j| e3[i].abs().partial_cmp(&e3[j].abs()).unwrap_or(std::cmp::Ordering::Equal)).unwrap_or(2);
186 let s = if e3[k] < 0.0 { -1.0 } else { 1.0 };
187 return Some(FaceShape::Cylindrical { point, axis: Point3::new(s * e3[0], s * e3[1], s * e3[2]) });
188 }
189 None
190}
191
192#[derive(Clone, Copy, Debug, PartialEq)]
196pub struct SetupFrame {
197 pub origin: Point3,
198 pub x_axis: Point3,
199 pub z_axis: Point3,
200}
201
202impl Default for SetupFrame {
203 fn default() -> Self {
204 Self { origin: Point3::ZERO, x_axis: Point3::new(1.0, 0.0, 0.0), z_axis: Point3::new(0.0, 0.0, 1.0) }
205 }
206}
207
208impl SetupFrame {
209 pub fn transform(&self) -> Result<RigidTransform, String> {
212 RigidTransform::from_frame(self.origin, self.x_axis, self.z_axis)
213 .map_err(|e| format!("the setup's frame is not usable: {e:?}"))
214 }
215}
216
217pub fn resolve_frame(
224 orientation: &SetupOrientation,
225 face_shape: &dyn Fn(&str) -> Option<FaceShape>,
226 edge_polyline: &dyn Fn(&str) -> Option<Vec<[f64; 3]>>,
227) -> Result<SetupFrame, String> {
228 let mut frame = SetupFrame::default();
229 let mut z_plane_point = None;
230 if let Some(name) = &orientation.z_ref {
231 let (z, point) = match face_shape(name) {
232 Some(FaceShape::Planar { point, normal }) => (normal, point),
233 Some(FaceShape::Cylindrical { point, axis }) => (axis, point),
234 None => match edge_polyline(name) {
235 Some(line) => {
236 let (Some(&a), Some(&b)) = (line.first(), line.last()) else {
237 return Err(format!("Z edge `{name}` has no length"));
238 };
239 let chord = Point3::new(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
240 let length = chord.length();
241 let straight = length > 1e-12
242 && line.iter().all(|p| {
243 let d = Point3::new(p[0] - a[0], p[1] - a[1], p[2] - a[2]);
244 let t = d.dot(chord) / (length * length);
245 let off = Point3::new(d.x - chord.x * t, d.y - chord.y * t, d.z - chord.z * t);
246 off.length() <= 1e-6 * length.max(1.0)
247 });
248 if !straight {
249 return Err(format!("Z edge `{name}` is not straight: pick a straight edge, a planar face or a cylindrical face"));
250 }
251 (chord, Point3::new(a[0], a[1], a[2]))
252 }
253 None => return Err(format!("Z reference `{name}` is not a planar face, a cylindrical face, an edge or a feature's axis of the model")),
254 },
255 };
256 frame.z_axis = if orientation.flip_z { Point3::new(-z.x, -z.y, -z.z) } else { z };
257 z_plane_point = Some(point);
258 } else if let Some(dir) = &orientation.z_dir {
259 let z = parse_direction(dir)?;
260 frame.z_axis = if orientation.flip_z { Point3::new(-z.x, -z.y, -z.z) } else { z };
261 } else if orientation.flip_z {
262 frame.z_axis = Point3::new(0.0, 0.0, -1.0);
263 }
264 if let Some(edge) = &orientation.x_ref {
265 let line = edge_polyline(edge).ok_or_else(|| format!("X edge `{edge}` is not an edge of the model"))?;
266 let (Some(first), Some(last)) = (line.first(), line.last()) else {
267 return Err(format!("X edge `{edge}` has no length"));
268 };
269 frame.x_axis = Point3::new(last[0] - first[0], last[1] - first[1], last[2] - first[2]);
270 } else {
271 let z = frame.z_axis.normalized().unwrap_or(Point3::new(0.0, 0.0, 1.0));
273 frame.x_axis = if z.x.abs() > 0.9 { Point3::new(0.0, 1.0, 0.0) } else { Point3::new(1.0, 0.0, 0.0) };
274 }
275 frame.origin = match &orientation.origin_ref {
276 Some(name) => match vertex_position(name) {
277 Some(position) => position,
278 None => match face_shape(name) {
279 Some(FaceShape::Planar { point, .. } | FaceShape::Cylindrical { point, .. }) => point,
280 None => return Err(format!("origin `{name}` is neither a vertex nor a planar or cylindrical face of the model")),
281 },
282 },
283 None => z_plane_point.unwrap_or(Point3::ZERO),
284 };
285 frame.transform()?;
286 Ok(frame)
287}
288
289pub fn parse_direction(text: &str) -> Result<Point3, String> {
293 let t = text.trim();
294 let axis = |sign: f64, c: &str| -> Option<Point3> {
295 match c {
296 "x" => Some(Point3::new(sign, 0.0, 0.0)),
297 "y" => Some(Point3::new(0.0, sign, 0.0)),
298 "z" => Some(Point3::new(0.0, 0.0, sign)),
299 _ => None,
300 }
301 };
302 let lower = t.to_ascii_lowercase();
303 let named = match lower.as_str() {
304 s if s.len() == 2 && s.starts_with('-') => axis(-1.0, &s[1..]),
305 s if s.len() == 2 && s.starts_with('+') => axis(1.0, &s[1..]),
306 s if s.len() == 1 => axis(1.0, s),
307 _ => None,
308 };
309 if let Some(d) = named {
310 return Ok(d);
311 }
312 let parts: Vec<Option<f64>> = t.split(',').map(|p| p.trim().parse::<f64>().ok().filter(|v| v.is_finite())).collect();
313 if let [Some(x), Some(y), Some(z)] = parts[..] {
314 if let Some(d) = Point3::new(x, y, z).normalized() {
315 return Ok(d);
316 }
317 }
318 Err(format!("Z direction `{t}` is neither an axis (+X, -Y, ...) nor three numbers x,y,z of a non-zero vector"))
319}
320
321pub fn vertex_position(name: &str) -> Option<Point3> {
323 let (_, coords) = name.rsplit_once('@')?;
324 let mut parts = coords.split(',').map(|p| p.trim().parse::<f64>().ok());
325 let position = Point3::new(parts.next()??, parts.next()??, parts.next()??);
326 if parts.next().is_some() {
327 return None;
328 }
329 Some(position)
330}
331
332#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase", default)]
335pub struct CamDocument {
336 pub setups: Vec<Setup>,
337 pub tools: Vec<Tool>,
338 pub operations: Vec<Operation>,
339 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
341 pub autoprogram_inputs: BTreeMap<String, brep_cam::autoprogram::binding::Binding>,
342 pub nc_programs: Vec<NcProgram>,
343 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
345 pub orientations: BTreeMap<String, SetupOrientation>,
346 #[serde(skip_serializing_if = "Vec::is_empty")]
349 pub machines: Vec<Machine>,
350 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
354 pub multi_axis_sources: BTreeMap<String, String>,
355 pub id_counter: u64,
359}
360
361pub fn strategy_kinds() -> &'static [&'static str] {
368 Strategy::KINDS
369}
370
371pub fn strategy_of_kind(kind: &str) -> Result<Strategy, String> {
373 let kind = kind.trim();
374 if kind.is_empty() {
375 return Err("an operation needs a strategy".to_string());
376 }
377 Strategy::default_of(kind).ok_or_else(|| format!("no strategy `{kind}` (one of: {})", strategy_kinds().join(", ")))
378}
379
380impl CamDocument {
381 pub fn is_empty(&self) -> bool {
383 self.setups.is_empty() && self.tools.is_empty() && self.operations.is_empty() && self.nc_programs.is_empty() && self.machines.is_empty()
384 }
385
386 pub fn orientation(&self, setup_id: &str) -> SetupOrientation {
388 self.orientations.get(setup_id).cloned().unwrap_or_default()
389 }
390
391 pub fn set_orientation(&mut self, setup_id: &str, orientation: SetupOrientation) {
393 if orientation.is_empty() {
394 self.orientations.remove(setup_id);
395 } else {
396 self.orientations.insert(setup_id.to_string(), orientation);
397 }
398 }
399
400 pub fn next_id(&mut self, prefix: &str) -> String {
402 self.id_counter += 1;
403 format!("{prefix}{}", self.id_counter)
404 }
405
406 pub fn setup(&self, id: &str) -> Option<&Setup> {
407 self.setups.iter().find(|s| s.id.as_str() == id)
408 }
409
410 pub fn setup_mut(&mut self, id: &str) -> Option<&mut Setup> {
411 self.setups.iter_mut().find(|s| s.id.as_str() == id)
412 }
413
414 pub fn tool(&self, id: &str) -> Option<&Tool> {
415 self.tools.iter().find(|t| t.id.as_str() == id)
416 }
417
418 pub fn tool_mut(&mut self, id: &str) -> Option<&mut Tool> {
419 self.tools.iter_mut().find(|t| t.id.as_str() == id)
420 }
421
422 pub fn operation(&self, id: &str) -> Option<&Operation> {
423 self.operations.iter().find(|op| op.id.as_str() == id)
424 }
425
426 pub fn operation_mut(&mut self, id: &str) -> Option<&mut Operation> {
427 self.operations.iter_mut().find(|op| op.id.as_str() == id)
428 }
429
430 pub fn program(&self, id: &str) -> Option<&NcProgram> {
431 self.nc_programs.iter().find(|p| p.id == id)
432 }
433
434 pub fn operations_of<'a>(&'a self, setup_id: &'a str) -> impl Iterator<Item = &'a Operation> + 'a {
436 self.operations.iter().filter(move |op| op.setup.as_str() == setup_id)
437 }
438
439 pub fn has(&self, id: &str) -> bool {
441 self.setup(id).is_some()
442 || self.tool(id).is_some()
443 || self.operation(id).is_some()
444 || self.program(id).is_some()
445 || self.machines.iter().any(|m| m.id.as_str() == id)
446 }
447
448 pub fn add_setup(&mut self, name: Option<&str>) -> String {
450 let id = self.next_id("SETUP");
451 let name = name.map(str::to_string).unwrap_or_else(|| format!("Setup {}", self.setups.len() + 1));
452 self.setups.push(Setup::new(id.as_str(), name));
453 id
454 }
455
456 pub fn machine(&self, id: &str) -> Option<Machine> {
460 self.machines.iter().find(|m| m.id.as_str() == id).cloned().or_else(|| brep_cam::kinematics::library::builtin(id))
461 }
462
463 pub fn setup_machine(&self, id: &str) -> Option<Machine> {
465 self.setup(id)?.machine.as_ref().and_then(|m| self.machine(m.as_str()))
466 }
467
468 pub fn add_machine(&mut self, mut base: Machine, name: Option<&str>) -> String {
470 let id = self.next_id("M");
471 base.id = id.as_str().into();
472 base.name = name.map(str::to_string).unwrap_or_else(|| format!("{} (custom)", base.name));
473 self.machines.push(base);
474 id
475 }
476
477 pub fn machine_mut(&mut self, id: &str) -> Option<&mut Machine> {
478 self.machines.iter_mut().find(|m| m.id.as_str() == id)
479 }
480
481 pub fn add_tool(&mut self, name: Option<&str>) -> String {
482 let id = self.next_id("T");
483 let number = self.tools.iter().map(|t| t.number).max().unwrap_or(0) + 1;
484 let mut tool = Tool::new(id.as_str(), number, ToolKind::Flat, ToolGeometry::default());
485 tool.name = name.map(str::to_string).unwrap_or_else(|| format!("Tool {number}"));
486 self.tools.push(tool);
487 id
488 }
489
490 pub fn add_operation(&mut self, setup: &str, tool: &str, kind: &str, name: Option<&str>) -> Result<String, String> {
494 if self.setup(setup).is_none() {
495 return Err(if self.setups.is_empty() {
496 "no setup yet: press New setup first".to_string()
497 } else {
498 format!("no setup `{setup}`")
499 });
500 }
501 if self.tool(tool).is_none() {
502 return Err(if self.tools.is_empty() {
503 "no tool yet: press New tool first".to_string()
504 } else {
505 format!("no tool `{tool}`")
506 });
507 }
508 let strategy = strategy_of_kind(kind)?;
509 let id = self.next_id("OP");
510 let name = name.map(str::to_string).unwrap_or_else(|| format!("{} {}", strategy.kind_name(), self.operations.len() + 1));
511 self.operations.push(Operation::new(id.as_str(), name, setup, tool, strategy));
512 Ok(id)
513 }
514
515 pub fn add_program(&mut self, name: Option<&str>, post: &str, operations: &[String]) -> Result<String, String> {
518 for op in operations {
519 if self.operation(op).is_none() {
520 return Err(format!("no operation `{op}`"));
521 }
522 }
523 let post = if post.trim().is_empty() {
525 operations
526 .first()
527 .and_then(|op| self.operation(op))
528 .and_then(|op| self.setup_machine(op.setup.as_str()))
529 .and_then(|m| m.post)
530 .unwrap_or_default()
531 } else {
532 post.to_string()
533 };
534 let post = post.as_str();
535 let id = self.next_id("NC");
536 let name = name.map(str::to_string).unwrap_or_else(|| format!("Program {}", self.nc_programs.len() + 1));
537 self.nc_programs.push(NcProgram {
538 id: id.clone(),
539 name,
540 number: None,
541 post: post.to_string(),
542 operations: operations.iter().map(|op| OperationId::new(op.as_str())).collect(),
543 properties: BTreeMap::new(),
544 });
545 Ok(id)
546 }
547
548 pub fn set_program_properties(&mut self, id: &str, patch: &BTreeMap<String, Value>) -> Result<(), String> {
553 let program = self.nc_programs.iter_mut().find(|p| p.id == id).ok_or_else(|| format!("no NC program `{id}`"))?;
554 if let Some(post) = brep_cam::post::builtin(&program.post) {
555 let definition = brep_cam::post::describe(post.source, post.id).map_err(|e| format!("post `{}`: {}", post.id, e.message))?;
556 for (name, value) in patch {
557 let Some(spec) = definition.properties.get(name) else {
558 return Err(format!("post `{}` has no property `{name}` (it has: {})", post.id, definition.properties.keys().cloned().collect::<Vec<_>>().join(", ")));
559 };
560 if let (Some(values), Some(v)) = (&spec.values, value.as_str()) {
561 if !values.iter().any(|x| x.as_str() == Some(v)) {
562 return Err(format!("post `{}` property `{name}` must be one of {}, not `{v}`", post.id, values.iter().map(|x| x.as_str().map(str::to_string).unwrap_or_else(|| x.to_string())).collect::<Vec<_>>().join(", ")));
563 }
564 }
565 }
566 }
567 for (name, value) in patch {
568 if value.is_null() {
569 program.properties.remove(name);
570 } else {
571 program.properties.insert(name.clone(), value.clone());
572 }
573 }
574 Ok(())
575 }
576
577 pub fn multi_axis_source(&self, op_id: &str) -> Option<&str> {
579 self.multi_axis_sources.get(op_id).map(String::as_str)
580 }
581
582 pub fn set_multi_axis_source(&mut self, op_id: &str, source: Option<String>) {
585 match source {
586 Some(s) => {
587 self.multi_axis_sources.insert(op_id.to_string(), s);
588 }
589 None => {
590 self.multi_axis_sources.remove(op_id);
591 }
592 }
593 }
594
595 pub fn remove(&mut self, id: &str) -> Result<(), String> {
599 if self.tool(id).is_some() && self.operations.iter().any(|op| op.tool.as_str() == id) {
600 return Err(format!("tool `{id}` is used by an operation; remove the operation first"));
601 }
602 if self.machines.iter().any(|m| m.id.as_str() == id) {
603 if let Some(setup) = self.setups.iter().find(|s| s.machine.as_ref().is_some_and(|m| m.as_str() == id)) {
604 return Err(format!("machine `{id}` is used by setup `{}`; choose another machine there first", setup.id));
605 }
606 }
607 if !self.has(id) {
608 return Err(format!("no CAM record `{id}`"));
609 }
610 let gone: Vec<String> = if self.setup(id).is_some() {
611 self.operations_of(id).map(|op| op.id.as_str().to_string()).chain(std::iter::once(id.to_string())).collect()
612 } else {
613 vec![id.to_string()]
614 };
615 self.setups.retain(|s| s.id.as_str() != id);
616 self.orientations.remove(id);
617 self.multi_axis_sources.retain(|finish, source| !gone.contains(finish) && !gone.contains(source));
618 self.tools.retain(|t| t.id.as_str() != id);
619 self.machines.retain(|m| m.id.as_str() != id);
620 self.operations.retain(|op| !gone.contains(&op.id.as_str().to_string()));
621 self.autoprogram_inputs.retain(|id, _| !gone.contains(id));
622 self.nc_programs.retain(|p| p.id != id);
623 for program in &mut self.nc_programs {
624 program.operations.retain(|op| !gone.contains(&op.as_str().to_string()));
625 }
626 Ok(())
627 }
628}
629
630fn upgrade_row1(value: &mut Value) {
635 if let Some(ops) = value.get_mut("operations").and_then(Value::as_array_mut) {
636 for op in ops {
637 if let Some(kind) = op.get("strategy").and_then(Value::as_str).map(str::to_string) {
638 op["strategy"] = json!({ "kind": kind });
639 }
640 }
641 }
642}
643
644pub const CHAIN_PICK_KEY: &str = "chainPick";
653
654pub const BOUNDARY_PICK_KEY: &str = "boundaryPick";
659
660pub fn pick_names(value: &Value) -> Vec<String> {
662 match value {
663 Value::String(s) => vec![s.clone()],
664 Value::Array(list) => list.iter().filter_map(Value::as_str).map(str::to_string).collect(),
665 _ => Vec::new(),
666 }
667}
668
669pub fn has_pending_chain_pick(block: &Value) -> bool {
672 block
673 .get("operations")
674 .and_then(Value::as_array)
675 .is_some_and(|ops| ops.iter().any(|op| op.get("targets").and_then(|t| t.get(CHAIN_PICK_KEY)).is_some()))
676}
677
678pub fn pending_boundary_pick(block: &Value, id: &str) -> Option<Vec<String>> {
681 block
682 .get("operations")?
683 .as_array()?
684 .iter()
685 .find(|op| op.get("id").and_then(Value::as_str) == Some(id))?
686 .get("targets")?
687 .get(BOUNDARY_PICK_KEY)
688 .map(pick_names)
689}
690
691pub fn chain_from_names(names: &[String]) -> Option<ChainRef> {
698 let names: Vec<&str> = names.iter().map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
699 if names.is_empty() {
700 return None;
701 }
702 let profile = |n: &str| n.strip_suffix(":PROFILE").or_else(|| n.strip_suffix(":FACE")).filter(|s| !s.is_empty()).map(str::to_string);
703 if names.len() == 1 {
704 if let Some(sketch) = profile(names[0]) {
705 return Some(ChainRef { source: ChainSource::SketchProfile { sketch, regions: Vec::new() }, closed: true, ..Default::default() });
706 }
707 }
708 let curve = |n: &str| -> Option<(String, String)> {
709 let (sketch, id) = n.rsplit_once(":G")?;
710 (!sketch.is_empty() && !id.is_empty() && id.chars().all(|c| c.is_ascii_digit())).then(|| (sketch.to_string(), id.to_string()))
711 };
712 if let Some(parsed) = names.iter().map(|n| curve(n)).collect::<Option<Vec<_>>>() {
713 let sketch = parsed[0].0.clone();
714 if parsed.iter().all(|(s, _)| *s == sketch) {
715 let mut curves: Vec<String> = Vec::new();
716 for (_, id) in parsed {
717 if !curves.contains(&id) {
718 curves.push(id);
719 }
720 }
721 return Some(ChainRef { source: ChainSource::SketchCurves { sketch, curves }, ..Default::default() });
722 }
723 }
724 let mut edges: Vec<EdgeRef> = Vec::new();
725 for name in names {
726 if !edges.iter().any(|e| e.edge == name) {
727 edges.push(EdgeRef::new("", name));
728 }
729 }
730 Some(ChainRef { source: ChainSource::Edges { edges }, ..Default::default() })
731}
732
733fn normalise_target_refs(value: &mut Value) {
734 if let Some(setups) = value.get_mut("setups").and_then(Value::as_array_mut) {
735 for setup in setups {
736 for key in ["targets", "protected"] {
737 if let Some(list) = setup.get_mut(key).and_then(Value::as_array_mut) {
738 for entry in list.iter_mut() {
739 if let Some(name) = entry.as_str().map(str::to_string) {
740 *entry = serde_json::to_value(TargetRef::solid(name)).unwrap_or(Value::Null);
741 }
742 }
743 }
744 }
745 }
746 }
747 if let Some(operations) = value.get_mut("operations").and_then(Value::as_array_mut) {
748 for operation in operations {
749 if let Some(targets) = operation.get_mut("targets").and_then(Value::as_object_mut) {
752 if let Some(pick) = targets.remove(CHAIN_PICK_KEY) {
753 if let Some(chain) = chain_from_names(&pick_names(&pick)) {
754 let entry = serde_json::to_value(chain).unwrap_or(Value::Null);
755 match targets.get_mut("chains").and_then(Value::as_array_mut) {
756 Some(list) => list.push(entry),
757 None => {
758 targets.insert("chains".to_string(), json!([entry]));
759 }
760 }
761 }
762 }
763 }
764 for key in ["driveFaces", "avoidFaces"] {
765 if let Some(list) = operation.get_mut("targets").and_then(|t| t.get_mut(key)).and_then(Value::as_array_mut) {
766 for entry in list.iter_mut() {
767 if let Some(name) = entry.as_str().map(str::to_string) {
768 *entry = json!({ "solid": "", "face": name });
769 }
770 }
771 }
772 }
773 }
774 }
775}
776
777fn normalise_orientations(value: &mut Value) {
781 let Some(orientations) = value.get_mut("orientations").and_then(Value::as_object_mut) else {
782 return;
783 };
784 for orientation in orientations.values_mut() {
785 if let Some(fields) = orientation.as_object_mut() {
786 fields.retain(|_, v| !matches!(v, Value::String(s) if s.trim().is_empty()));
787 }
788 }
789}
790
791pub fn load(block: Option<&Value>) -> Result<CamDocument, String> {
794 let Some(block) = block else {
795 return Ok(CamDocument::default());
796 };
797 let mut value = block.clone();
798 upgrade_row1(&mut value);
799 normalise_target_refs(&mut value);
800 normalise_orientations(&mut value);
801 serde_json::from_value(value).map_err(|e| format!("the document's `cam` block cannot be read: {e}"))
802}
803
804pub fn store(doc: &CamDocument) -> Option<Value> {
806 if doc.is_empty() {
807 return None;
808 }
809 serde_json::to_value(doc).ok()
810}
811