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