1use crate::automation::hit_keys::HitKeyDoc;
18use crate::document::cam::{self as cam_doc, resolve_frame, strategy_kinds, CamDocument, SetupFrame, SetupOrientation};
19use crate::form_view::{form_view, FormViewSpec};
20use crate::panels::file::{SaveBody, SaveRequest};
21use crate::panels::{cam_generate, cam_library, cam_machine, cam_operation, cam_setup, cam_tool};
22use crate::store::ModelStore;
23use brep_cam::model::{Point3, RigidTransform};
24use brep_render::engine_state::{EngineState, RefSelectTarget};
25use eframe::egui;
26use serde_json::{json, Value};
27use std::collections::{BTreeMap, HashMap};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CamRequest {
34 NewSetup,
35 NewTool,
36 NewOperation,
37 Generate,
38}
39
40pub fn cam_document(state: &EngineState) -> Result<CamDocument, String> {
42 cam_doc::load(state.history.cam_block())
43}
44
45pub fn write_cam_document(state: &mut EngineState, doc: &CamDocument, coalesce: Option<&str>) {
48 state.history.set_cam_block(cam_doc::store(doc), coalesce);
49 state.dirty = true;
50}
51
52fn write_cam_document_no_undo(state: &mut EngineState, doc: &CamDocument) {
55 state.history.set_cam_block_no_undo(cam_doc::store(doc));
56 state.dirty = true;
57}
58
59pub fn setup_frame(state: &EngineState, doc: &CamDocument, id: &str) -> Result<SetupFrame, String> {
62 let orientation = doc.orientation(id);
63 let face = |name: &str| cam_doc::classify_face(&face_triangles(state, name));
64 let edge = |name: &str| {
67 state.scene.edge_polyline_world(name).or_else(|| {
68 state.cam_named_axis(name).map(|(p, d)| vec![p, [p[0] + d[0], p[1] + d[1], p[2] + d[2]]])
69 })
70 };
71 resolve_frame(&orientation, &face, &edge)
72}
73
74fn face_triangles(state: &EngineState, name: &str) -> Vec<[[f64; 3]; 3]> {
77 for solid in state.scene.solids() {
78 let Some(face) = solid.faces.iter().find(|f| f.name == name) else { continue };
79 let (positions, indices) = (&solid.mesh.positions, &solid.mesh.indices);
80 let start = face.tri_start as usize;
81 let end = (start + face.tri_count as usize).min(indices.len() / 3);
82 let p = |i: u32| {
83 let v = positions[i as usize];
84 [v[0] as f64, v[1] as f64, v[2] as f64]
85 };
86 return (start..end).map(|t| [p(indices[t * 3]), p(indices[t * 3 + 1]), p(indices[t * 3 + 2])]).collect();
87 }
88 Vec::new()
89}
90
91pub fn update_setup(state: &mut EngineState, id: &str, patch: &Value, coalesce: Option<&str>) -> Result<(), String> {
94 edit_cam_document(state, coalesce, |doc| {
95 let mut orientation = doc.orientation(id);
96 let setup = doc.setup_mut(id).ok_or_else(|| format!("no setup `{id}`"))?;
97 cam_setup::apply_setup_form(patch, setup, &mut orientation)?;
98 if let brep_cam::model::Stock::FromRest { setup: source } = &setup.stock {
100 let source = source.as_str().to_string();
101 let at = |name: &str| doc.setups.iter().position(|s| s.id.as_str() == name);
102 match (at(&source), at(id)) {
103 (Some(s), Some(me)) if s < me => {}
104 (Some(_), _) => return Err(format!("stockSetup: setup `{source}` comes after `{id}`; a rest stock starts from an earlier setup")),
105 (None, _) => return Err(format!("stockSetup: no setup `{source}`")),
106 }
107 }
108 doc.set_orientation(id, orientation);
109 cam_setup::apply_machine_form(patch, doc, id)?;
110 Ok(())
111 })?;
112 sync_setup_frame(state, id);
113 Ok(())
114}
115
116pub fn sync_setup_frame(state: &mut EngineState, id: &str) -> Option<String> {
121 let doc = cam_document(state).ok()?;
122 let frame = match setup_frame(state, &doc, id) {
123 Ok(frame) => frame,
124 Err(why) => return Some(why),
125 };
126 let transform = match frame.transform() {
127 Ok(t) => t,
128 Err(why) => return Some(why),
129 };
130 let setup = doc.setup(id)?;
131 if setup.model_to_machine == transform {
132 return None;
133 }
134 let mut next = doc.clone();
135 next.setup_mut(id)?.model_to_machine = transform;
136 write_cam_document_no_undo(state, &next);
137 None
138}
139
140pub fn update_tool(state: &mut EngineState, id: &str, patch: &Value, coalesce: Option<&str>) -> Result<(), String> {
143 edit_cam_document(state, coalesce, |doc| {
144 let tool = doc.tool_mut(id).ok_or_else(|| format!("no tool `{id}`"))?;
145 cam_tool::apply_tool_form(patch, tool)
146 })
147}
148
149pub fn add_machine(state: &mut EngineState, base: &str, name: Option<&str>) -> Result<String, String> {
152 edit_cam_document(state, None, |doc| {
153 let source = doc.machine(base).ok_or_else(|| format!("base: no machine `{base}` (a built-in id or a document machine)"))?;
154 Ok(doc.add_machine(source, name))
155 })
156}
157
158pub fn update_machine(state: &mut EngineState, id: &str, patch: &Value, coalesce: Option<&str>) -> Result<(), String> {
161 edit_cam_document(state, coalesce, |doc| {
162 let machine = doc.machine_mut(id).ok_or_else(|| format!("no document machine `{id}` (a built-in is copied into the document first)"))?;
163 cam_machine::apply_machine_form(patch, machine)
164 })
165}
166
167pub fn duplicate_tool(state: &mut EngineState, id: &str) -> Result<String, String> {
170 edit_cam_document(state, None, |doc| {
171 let source = doc.tool(id).ok_or_else(|| format!("no tool `{id}`"))?.clone();
172 let number = doc.tools.iter().map(|t| t.number).max().unwrap_or(0) + 1;
173 let new_id = doc.next_id("T");
174 doc.tools.push(cam_tool::duplicate(&source, &new_id, number));
175 Ok(new_id)
176 })
177}
178
179pub fn calculate_tool_feeds(state: &mut EngineState, id: &str, material: &str) -> Result<Vec<String>, String> {
182 edit_cam_document(state, None, |doc| {
183 let tool = doc.tool_mut(id).ok_or_else(|| format!("no tool `{id}`"))?;
184 cam_tool::calculate_feeds(tool, material)
185 })
186}
187
188pub fn add_tool_from_library(state: &mut EngineState, store: &dyn ModelStore, library_id: &str) -> Result<String, String> {
191 let source = cam_library::tool(store, library_id)?;
192 edit_cam_document(state, None, |doc| {
193 let number = doc.tools.iter().map(|t| t.number).max().unwrap_or(0) + 1;
194 let id = doc.next_id("T");
195 let mut tool = source.clone();
196 tool.id = id.as_str().into();
197 tool.number = number;
198 if let Some(holder) = &mut tool.holder {
199 holder.id = format!("{id}-holder").into();
200 }
201 doc.tools.push(tool);
202 Ok(id)
203 })
204}
205
206pub fn import_tools(state: &mut EngineState, name: &str, text: &str) -> Result<(Vec<String>, Vec<String>), String> {
210 let report = cam_library::import_text(name, text)?;
211 let issues: Vec<String> = report.issues.iter().map(|i| format!("row {}: {}", i.row, i.message)).collect();
212 let ids = edit_cam_document(state, None, |doc| {
213 let mut ids = Vec::new();
214 let mut number = doc.tools.iter().map(|t| t.number).max().unwrap_or(0);
215 for mut tool in report.document.library.tools.clone() {
216 number += 1;
217 let id = doc.next_id("T");
218 tool.id = id.as_str().into();
219 tool.number = number;
220 if let Some(holder) = &mut tool.holder {
221 holder.id = format!("{id}-holder").into();
222 }
223 doc.tools.push(tool);
224 ids.push(id);
225 }
226 Ok(ids)
227 })?;
228 Ok((ids, issues))
229}
230
231pub fn export_document_tools(state: &EngineState) -> Result<String, String> {
233 let doc = cam_document(state)?;
234 let mut library = brep_cam::library::ToolLibrary::new("Document tools");
235 library.tools = doc.tools.clone();
236 brep_cam::library::interchange::LibraryDocument::new(library).to_json()
237}
238
239pub fn update_operation(state: &mut EngineState, id: &str, patch: &Value, coalesce: Option<&str>) -> Result<(), String> {
244 let owners = face_owners(state);
245 let edge_owners = edge_owners(state);
246 edit_cam_document(state, coalesce, |doc| {
247 let tools = doc.tools.clone();
248 crate::panels::cam_multiaxis::apply_source(patch, doc, id)?;
251 let operation = doc.operation_mut(id).ok_or_else(|| format!("no operation `{id}`"))?;
252 cam_operation::apply_operation_form(patch, operation, &tools)?;
253 cam_operation::resolve_face_owners(operation, &|face| owners.get(face).cloned());
254 cam_operation::resolve_edge_owners(operation, &|edge| edge_owners.get(edge).cloned());
255 Ok(())
256 })
257}
258
259fn edge_owners(state: &EngineState) -> HashMap<String, String> {
261 let mut owners = HashMap::new();
262 for solid in state.scene.solids() {
263 for edge in solid.edges.iter().filter(|e| !e.name.is_empty()) {
264 owners.insert(edge.name.clone(), solid.name.clone());
265 }
266 }
267 owners
268}
269
270fn face_owners(state: &EngineState) -> HashMap<String, String> {
272 let mut owners = HashMap::new();
273 for solid in state.scene.solids() {
274 for face in &solid.faces {
275 owners.insert(face.name.clone(), solid.name.clone());
276 }
277 }
278 owners
279}
280
281pub fn sync_operation_faces(state: &mut EngineState, id: &str) -> Result<(), String> {
287 let owners = face_owners(state);
288 let edge_owners = edge_owners(state);
289 let block = state.history.cam_block();
292 let pending = block.as_ref().is_some_and(|b| cam_doc::has_pending_chain_pick(b));
293 let boundary_pick = block.as_ref().and_then(|b| cam_doc::pending_boundary_pick(b, id));
294 let Ok(mut doc) = cam_document(state) else { return Ok(()) };
295 let setup = doc.operation(id).and_then(|op| doc.setup(op.setup.as_str())).cloned();
296 let tools = doc.tools.clone();
297 let Some(operation) = doc.operation_mut(id) else { return Ok(()) };
298 let faces = cam_operation::resolve_face_owners(operation, &|face| owners.get(face).cloned());
299 let edges = cam_operation::resolve_edge_owners(operation, &|edge| edge_owners.get(edge).cloned());
300 let mut refused = None;
301 if let Some(names) = &boundary_pick {
302 let chord = operation.tolerance / 4.0;
303 let sampled = setup
304 .as_ref()
305 .ok_or_else(|| format!("{id}: its setup is not defined"))
306 .and_then(|setup| boundary_from_profile(state, names, setup, chord))
307 .and_then(|loops| {
308 let loops: Vec<Value> = loops.iter().map(|l| json!(l.iter().map(|p| [p.x, p.y]).collect::<Vec<_>>())).collect();
309 cam_operation::apply_operation_form(&json!({ "strategy": { "containment": { "boundary": loops } } }), operation, &tools).map_err(|e| format!("{id}: {e}"))
310 });
311 refused = sampled.err();
312 }
313 if faces || edges || pending || boundary_pick.is_some() {
314 write_cam_document_no_undo(state, &doc);
315 }
316 refused.map_or(Ok(()), Err)
317}
318
319fn boundary_from_profile(state: &EngineState, names: &[String], setup: &brep_cam::model::Setup, chord: f64) -> Result<Vec<Vec<brep_cam::model::Point2>>, String> {
324 use brep_cam::model::{ChainSource, Point2};
325 let chain = cam_doc::chain_from_names(names).ok_or_else(|| "nothing was picked for the machining boundary".to_string())?;
326 if !matches!(chain.source, ChainSource::SketchProfile { .. }) {
327 return Err(format!("the machining boundary is picked as a sketch profile (a sketch's face), not `{}`", names.join(", ")));
328 }
329 let (_, profiles) = state.cam_chain_geometry(std::slice::from_ref(&chain))?;
330 let (sketch, profile) = profiles.first().ok_or_else(|| "the picked sketch has no profile".to_string())?;
331 let mut loops = Vec::new();
332 for region in &profile.regions {
333 for profile_loop in region {
334 let mut points: Vec<Point3> = Vec::new();
335 for curve in &profile_loop.curves {
336 for p in sample_curve(curve, chord)? {
337 let p = setup.model_to_machine.apply_point(Point3::new(p.x, p.y, p.z));
338 if points.last().is_none_or(|q: &Point3| q.distance(p) > 1e-9) {
339 points.push(p);
340 }
341 }
342 }
343 if points.len() > 1 && points[0].distance(*points.last().expect("non-empty")) <= 1e-9 {
344 points.pop();
345 }
346 let (lo, hi) = points.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p.z), hi.max(p.z)));
347 if hi - lo > chord.max(1e-6) {
348 return Err(format!("sketch `{sketch}` is not across the setup's Z (its profile spans {:.4} in machine Z); pick a profile parallel to the machine's XY", hi - lo));
349 }
350 loops.push(points.iter().map(|p| Point2::new(p.x, p.y)).collect());
351 }
352 }
353 if loops.is_empty() {
354 return Err(format!("sketch `{sketch}` has no closed profile"));
355 }
356 Ok(loops)
357}
358
359fn sample_curve(curve: &brep_render::brep_kernel::NurbsCurve, chord: f64) -> Result<Vec<brep_render::brep_kernel::Vec3>, String> {
363 use brep_render::brep_kernel::Vec3;
364 let [a, b] = curve.domain()?;
365 let at = |t: f64| curve.evaluate(t);
366 let dist = |p: Vec3, q: Vec3, m: Vec3| {
367 let (dx, dy, dz) = (q.x - p.x, q.y - p.y, q.z - p.z);
368 let (mx, my, mz) = (m.x - p.x, m.y - p.y, m.z - p.z);
369 let len2 = dx * dx + dy * dy + dz * dz;
370 let t = if len2 > 0.0 { ((mx * dx + my * dy + mz * dz) / len2).clamp(0.0, 1.0) } else { 0.0 };
371 ((mx - t * dx).powi(2) + (my - t * dy).powi(2) + (mz - t * dz).powi(2)).sqrt()
372 };
373 let mut out = vec![at(a)?];
374 let pieces = if curve.degree <= 1 { (curve.control_points.len() - 1).max(1) } else { 8 * (curve.control_points.len() - curve.degree).max(1) };
377 let mut stack: Vec<(f64, f64, u32)> = (0..pieces).rev().map(|i| (a + (b - a) * i as f64 / pieces as f64, a + (b - a) * (i + 1) as f64 / pieces as f64, 0)).collect();
378 while let Some((t0, t1, depth)) = stack.pop() {
379 let (p0, p1, pm) = (at(t0)?, at(t1)?, at(0.5 * (t0 + t1))?);
380 if curve.degree > 1 && depth < 16 && dist(p0, p1, pm) > chord {
381 stack.push((0.5 * (t0 + t1), t1, depth + 1));
382 stack.push((t0, 0.5 * (t0 + t1), depth + 1));
383 } else {
384 out.push(p1);
385 }
386 }
387 Ok(out)
388}
389
390pub fn begin_operation_pick(state: &mut EngineState, id: &str, key: &str) -> Result<(), String> {
393 let doc = cam_document(state)?;
394 let operation = doc.operation(id).ok_or_else(|| format!("no operation `{id}`"))?;
395 let (path, filter, multiple, label) = cam_operation::pick_path(id, key).ok_or_else(|| format!("`{key}` is not a reference field of an operation"))?;
396 let params = cam_operation::operation_form_params(operation);
397 let seed: Vec<String> = params["targets"].get(key).and_then(Value::as_array).map(|l| l.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default();
398 if state.ref_select_active() {
399 state.finish_ref_select();
400 }
401 state.begin_ref_select_for_cam(id, path, format!("{}: {label}", operation.name), filter, multiple, seed);
402 Ok(())
403}
404
405pub fn generate(state: &mut EngineState, ops: Option<&[String]>) -> Result<(Vec<String>, Vec<(String, String)>), String> {
409 let doc = cam_document(state)?;
410 let ids: Vec<String> = match ops {
411 Some(ids) => ids.to_vec(),
412 None => doc.operations.iter().filter(|op| op.enabled).map(|op| op.id.as_str().to_string()).collect(),
413 };
414 let mut jobs = Vec::new();
415 let mut refused = Vec::new();
416 for id in ids {
417 match cam_generate::job_for(state, &doc, &id) {
418 Ok(job) => jobs.push(job),
419 Err(why) => refused.push((id, why)),
420 }
421 }
422 state.cam.job_builder.get_or_insert(cam_generate::job_at_dispatch);
423 let queued = state.cam_enqueue(jobs);
424 Ok((queued, refused))
425}
426
427pub fn begin_setup_pick(state: &mut EngineState, id: &str, key: &str) -> Result<(), String> {
430 let doc = cam_document(state)?;
431 let setup = doc.setup(id).ok_or_else(|| format!("no setup `{id}`"))?;
432 let (path, filter, multiple, label) = cam_setup::pick_path(id, key).ok_or_else(|| format!("`{key}` is not a reference field of a setup"))?;
433 let params = cam_setup::setup_form_params(setup, &doc.orientation(id));
434 let seed: Vec<String> = match params.get(key) {
435 Some(Value::Array(list)) => list.iter().filter_map(Value::as_str).map(str::to_string).collect(),
436 Some(Value::String(s)) if !s.is_empty() => vec![s.clone()],
437 _ => Vec::new(),
438 };
439 if state.ref_select_active() {
440 state.finish_ref_select();
441 }
442 state.begin_ref_select_for_cam(id, path, format!("{}: {label}", setup.name), filter, multiple, seed);
443 Ok(())
444}
445
446pub fn edit_cam_document<T>(
450 state: &mut EngineState,
451 coalesce: Option<&str>,
452 change: impl FnOnce(&mut CamDocument) -> Result<T, String>,
453) -> Result<T, String> {
454 let mut doc = cam_document(state)?;
455 let out = change(&mut doc)?;
456 write_cam_document(state, &doc, coalesce);
457 Ok(out)
458}
459
460pub fn add_setup(state: &mut EngineState, name: Option<&str>) -> Result<String, String> {
462 edit_cam_document(state, None, |doc| Ok(doc.add_setup(name)))
463}
464
465pub fn add_tool(state: &mut EngineState, name: Option<&str>) -> Result<String, String> {
467 edit_cam_document(state, None, |doc| Ok(doc.add_tool(name)))
468}
469
470pub fn add_operation(state: &mut EngineState, setup: &str, tool: &str, kind: &str, name: Option<&str>) -> Result<String, String> {
473 edit_cam_document(state, None, |doc| doc.add_operation(setup, tool, kind, name))
474}
475
476pub fn add_program(state: &mut EngineState, name: Option<&str>, post: &str, operations: &[String]) -> Result<String, String> {
478 add_program_with(state, name, post, operations, &BTreeMap::new())
479}
480
481pub fn add_program_with(state: &mut EngineState, name: Option<&str>, post: &str, operations: &[String], properties: &BTreeMap<String, Value>) -> Result<String, String> {
484 edit_cam_document(state, None, |doc| {
485 let id = doc.add_program(name, post, operations)?;
486 if !properties.is_empty() {
487 doc.set_program_properties(&id, properties)?;
488 }
489 Ok(id)
490 })
491}
492
493pub fn set_program_properties(state: &mut EngineState, id: &str, patch: &BTreeMap<String, Value>) -> Result<(), String> {
495 edit_cam_document(state, None, |doc| doc.set_program_properties(id, patch))
496}
497
498pub fn remove_record(state: &mut EngineState, id: &str) -> Result<(), String> {
500 edit_cam_document(state, None, |doc| doc.remove(id))
501}
502
503#[derive(Clone, Debug, PartialEq)]
506pub struct SetupGeometry {
507 pub frame: Result<SetupFrame, String>,
508 pub stock: Option<(Point3, Point3)>,
509}
510
511fn geometry_stamp(state: &EngineState) -> (u64, u64) {
515 let mut scene: u64 = 0xcbf2_9ce4_8422_2325;
516 for solid in state.scene.solids() {
517 for byte in solid.name.bytes().chain([solid.visible as u8]).chain(solid.revision.to_le_bytes()) {
518 scene ^= byte as u64;
519 scene = scene.wrapping_mul(0x0000_0100_0000_01b3);
520 }
521 }
522 (state.history.revision(), scene)
523}
524
525pub fn cam_state_json(state: &EngineState, panel: &CamPanel) -> String {
532 let (doc, error) = match cam_document(state) {
533 Ok(doc) => (doc, None),
534 Err(why) => (CamDocument::default(), Some(why)),
535 };
536 json!({
537 "setups": doc.setups.iter().map(|s| {
538 let mut record = serde_json::to_value(s).unwrap_or(Value::Null);
539 if let Some(object) = record.as_object_mut() {
540 object.insert("operations".into(), json!(doc.operations_of(s.id.as_str()).map(|op| op.id.as_str()).collect::<Vec<_>>()));
541 }
542 record
543 }).collect::<Vec<_>>(),
544 "tools": doc.tools,
545 "operations": doc.operations,
546 "problems": doc.operations.iter().filter_map(|op| cam_operation::strategy_problem(op).map(|p| (op.id.as_str().to_string(), json!(p)))).collect::<serde_json::Map<String, Value>>(),
548 "ncPrograms": doc.nc_programs,
549 "multiAxisSources": doc.multi_axis_sources,
550 "idCounter": doc.id_counter,
551 "strategies": strategy_kinds(),
552 "orientations": doc.orientations,
553 "frames": doc.setups.iter().map(|s| {
556 let id = s.id.as_str();
557 let geometry = panel.geometry.get(id).cloned().unwrap_or_else(|| SetupGeometry { frame: setup_frame(state, &doc, id), stock: cam_setup::stock_bounds(s, &state.scene) });
558 let (frame, frame_error) = match geometry.frame {
559 Ok(frame) => (Some(frame), None),
560 Err(why) => (None, Some(why)),
561 };
562 let p = |p: Point3| json!([p.x, p.y, p.z]);
563 (id.to_string(), json!({
564 "origin": frame.map(|f| p(f.origin)),
565 "xAxis": frame.map(|f| p(f.x_axis)),
566 "zAxis": frame.map(|f| p(f.z_axis)),
567 "frameError": frame_error,
568 "stock": geometry.stock.map(|(min, max)| json!({ "min": p(min), "max": p(max) })),
569 "stockMachine": cam_setup::stock_bounds_machine(s, &state.scene).map(|b| json!({ "min": p(b.min), "max": p(b.max) })),
571 }))
572 }).collect::<serde_json::Map<String, Value>>(),
573 "selected": panel.selected,
574 "open": panel.open,
575 "notice": panel.notice,
576 "error": error,
577 "generation": state.cam_generation_json(),
580 "autoprogram": panel.autoprogram.state(state),
581 "statuses": doc.operations.iter().map(|op| {
582 let id = op.id.as_str();
583 let status = cam_generate::status_of(state, &doc, id);
584 let detail = match &status {
585 cam_generate::OpStatus::Ok { motions, cutting_length } => json!({ "motions": motions, "cuttingLength": cutting_length }),
586 cam_generate::OpStatus::Stale { motions } => json!({ "motions": motions }),
587 cam_generate::OpStatus::Error(e) => json!({ "error": e }),
588 _ => json!({}),
589 };
590 (id.to_string(), json!({ "status": status.word(), "detail": detail }))
591 }).collect::<serde_json::Map<String, Value>>(),
592 "scrub": panel.scrub,
593 "machines": doc.machines,
595 "builtinMachines": brep_cam::kinematics::library::builtin_ids(),
596 "hiddenToolpaths": panel.hidden_toolpaths.iter().collect::<Vec<_>>(),
597 })
598 .to_string()
599}
600
601#[derive(Debug, Default)]
605pub struct CamShellRequests {
606 pub pick_file: Option<(String, String)>,
608 pub save_files: Vec<SaveRequest>,
610}
611
612pub const IMPORT_TOOLS_TAG: &str = "cam-tools";
614
615pub struct CamPanel {
617 pub autoprogram: crate::panels::cam_autoprogram::AutoProgramPanel,
618 hits: HashMap<String, egui::Rect>,
619 selected: Option<String>,
621 request: Option<CamRequest>,
623 notice: Option<String>,
626 open: Option<String>,
628 picking_field: Option<String>,
632 overlay: String,
634 geometry: HashMap<String, SetupGeometry>,
639 geometry_stamp: Option<(u64, u64)>,
640 shell: CamShellRequests,
642 user_library: Option<Result<Vec<(String, String, u32)>, String>>,
645 import_report: Option<(Vec<String>, Vec<String>)>,
647 feeds_material: Option<String>,
650 scrub: Option<f64>,
653 pub sim: crate::panels::cam_sim::CamSimPanel,
655 pub post: crate::panels::cam_post::CamPostPanel,
657 overlay_ops: std::collections::BTreeSet<String>,
660 overlay_key: Option<(String, u64, Option<String>, Option<u64>, u64)>,
664 hidden_toolpaths: std::collections::BTreeSet<String>,
667 hidden_stamp: u64,
668}
669
670impl Default for CamPanel {
671 fn default() -> Self {
672 Self::new()
673 }
674}
675
676const SECTION_SETUPS: &str = "cam:section:setups";
678const SECTION_PROGRAMS: &str = "cam:section:programs";
679const SECTION_TOOLS: &str = "cam:section:tools";
680const SECTION_MACHINES: &str = "cam:section:machines";
681
682impl CamPanel {
683 pub fn new() -> Self {
684 Self {
685 autoprogram: Default::default(),
686 hits: HashMap::new(),
687 selected: None,
688 request: None,
689 notice: None,
690 open: None,
691 picking_field: None,
692 overlay: String::new(),
693 geometry: HashMap::new(),
694 geometry_stamp: None,
695 shell: CamShellRequests::default(),
696 user_library: None,
697 import_report: None,
698 feeds_material: None,
699 scrub: None,
700 sim: crate::panels::cam_sim::CamSimPanel::new(),
701 post: crate::panels::cam_post::CamPostPanel::new(),
702 overlay_ops: std::collections::BTreeSet::new(),
703 overlay_key: None,
704 hidden_toolpaths: std::collections::BTreeSet::new(),
705 hidden_stamp: 0,
706 }
707 }
708
709 pub fn set_toolpath_visible(&mut self, id: &str, visible: bool) {
711 let changed = if visible { self.hidden_toolpaths.remove(id) } else { self.hidden_toolpaths.insert(id.to_string()) };
712 if changed {
713 self.hidden_stamp += 1;
714 }
715 }
716
717 pub fn hidden_toolpaths(&self) -> &std::collections::BTreeSet<String> {
719 &self.hidden_toolpaths
720 }
721
722 pub fn set_scrub(&mut self, fraction: Option<f64>) {
724 self.scrub = fraction.map(|f| f.clamp(0.0, 1.0));
725 }
726
727 pub fn take_shell_requests(&mut self) -> CamShellRequests {
729 if let Some(save) = self.sim.pending_save.take() {
730 self.shell.save_files.push(save);
731 }
732 if let Some(save) = self.post.pending_save.take() {
733 self.shell.save_files.push(save);
734 }
735 std::mem::take(&mut self.shell)
736 }
737
738 pub fn deliver_picked(&mut self, state: &mut EngineState, name: &str, bytes: &[u8]) {
741 let text = String::from_utf8_lossy(bytes);
742 match import_tools(state, name, &text) {
743 Ok((ids, issues)) => {
744 self.notice = Some(format!(
745 "imported {} tool{} from {name}{}",
746 ids.len(),
747 if ids.len() == 1 { "" } else { "s" },
748 if issues.is_empty() { String::new() } else { format!(" ({} row{} skipped)", issues.len(), if issues.len() == 1 { "" } else { "s" }) }
749 ));
750 self.import_report = Some((ids, issues));
751 }
752 Err(why) => self.refuse(state, format!("import {name}: {why}")),
753 }
754 }
755
756 pub fn user_library(&self) -> Option<&Result<Vec<(String, String, u32)>, String>> {
758 self.user_library.as_ref()
759 }
760
761 pub fn refresh_user_library(&mut self, store: &dyn ModelStore) {
763 self.user_library = Some(cam_library::load(store).map(|lib| lib.library.tools.iter().map(|t| (t.id.as_str().to_string(), t.name.clone(), t.number)).collect()));
764 }
765
766 pub fn refresh_geometry(&mut self, state: &EngineState) {
771 let stamp = geometry_stamp(state);
772 if self.geometry_stamp == Some(stamp) {
773 return;
774 }
775 self.geometry.clear();
776 if let Ok(doc) = cam_document(state) {
777 for setup in &doc.setups {
778 let id = setup.id.as_str();
779 self.geometry.insert(
780 id.to_string(),
781 SetupGeometry { frame: setup_frame(state, &doc, id), stock: cam_setup::stock_bounds(setup, &state.scene) },
782 );
783 }
784 }
785 self.geometry_stamp = Some(stamp);
786 }
787
788 pub fn open_setup(&self) -> Option<&str> {
790 self.open.as_deref()
791 }
792
793 pub fn set_open(&mut self, id: Option<String>) {
795 if id != self.open {
796 self.picking_field = None;
797 }
798 self.open = id;
799 }
800
801 pub fn request(&mut self, request: CamRequest) {
803 self.request = Some(request);
804 }
805
806 pub fn selected(&self) -> Option<&str> {
808 self.selected.as_deref()
809 }
810
811 pub fn select(&mut self, id: Option<String>) {
814 self.selected = id;
815 }
816
817 fn target_setup(&self, doc: &CamDocument) -> Option<String> {
820 if let Some(selected) = &self.selected {
821 if doc.setup(selected).is_some() {
822 return Some(selected.clone());
823 }
824 if let Some(op) = doc.operation(selected) {
825 return Some(op.setup.as_str().to_string());
826 }
827 }
828 match doc.setups.as_slice() {
829 [only] => Some(only.id.as_str().to_string()),
830 _ => None,
831 }
832 }
833
834 fn target_tool(&self, doc: &CamDocument) -> Option<String> {
836 if let Some(selected) = &self.selected {
837 if doc.tool(selected).is_some() {
838 return Some(selected.clone());
839 }
840 }
841 match doc.tools.as_slice() {
842 [only] => Some(only.id.as_str().to_string()),
843 _ => None,
844 }
845 }
846
847 fn refuse(&mut self, state: &mut EngineState, why: String) {
850 state.push_notice(format!("CAM: {why}"));
851 self.notice = Some(why);
852 }
853
854 fn perform(&mut self, request: CamRequest, state: &mut EngineState) {
856 self.notice = None;
857 let doc = match cam_document(state) {
858 Ok(doc) => doc,
859 Err(why) => return self.refuse(state, why),
860 };
861 let outcome = match request {
862 CamRequest::NewSetup => add_setup(state, None),
863 CamRequest::NewTool => add_tool(state, None),
864 CamRequest::NewOperation => match (self.target_setup(&doc), self.target_tool(&doc)) {
865 (Some(setup), Some(tool)) => add_operation(state, &setup, &tool, "parallel", None),
866 (None, _) if doc.setups.is_empty() => Err("no setup yet: press New setup first".to_string()),
867 (None, _) => Err("select the setup the operation goes into".to_string()),
868 (_, None) if doc.tools.is_empty() => Err("no tool yet: press New tool first".to_string()),
869 (_, None) => Err("select the tool the operation uses".to_string()),
870 },
871 CamRequest::Generate => {
872 if doc.operations.is_empty() {
873 Err("nothing to generate: add an operation first".to_string())
874 } else {
875 match generate(state, None) {
876 Ok((queued, refused)) => {
877 if !refused.is_empty() {
878 self.notice = Some(refused.iter().map(|(_, why)| why.as_str()).collect::<Vec<_>>().join("; "));
879 }
880 if queued.is_empty() && refused.is_empty() {
881 self.notice = Some("every operation is already queued or generating".to_string());
882 }
883 return;
884 }
885 Err(why) => Err(why),
886 }
887 }
888 }
889 };
890 match outcome {
891 Ok(id) => self.selected = Some(id),
892 Err(why) => self.refuse(state, why),
893 }
894 }
895
896 pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState, store: &dyn ModelStore) {
899 self.hits.clear();
900 self.hits.insert("panel:clip".into(), ui.clip_rect());
902 if let Some(request) = self.request.take() {
903 self.perform(request, state);
904 }
905 self.refresh_geometry(state);
906 let (doc, error) = match cam_document(state) {
907 Ok(doc) => (doc, None),
908 Err(why) => (CamDocument::default(), Some(why)),
909 };
910
911 self.autoprogram.poll(state);
912 let mut request = None;
914 ui.horizontal_wrapped(|ui| {
915 for (key, label, hover, what) in [
916 ("cam:new:setup", "New setup", "Add a setup (work coordinate system, stock, heights)", CamRequest::NewSetup),
917 ("cam:new:tool", "New tool", "Add a tool to the document's library", CamRequest::NewTool),
918 ("cam:new:operation", "New operation", "Add an operation to the selected setup with the selected tool", CamRequest::NewOperation),
919 ("cam:generate", "Generate", "Compute the toolpaths of every operation that needs it", CamRequest::Generate),
920 ] {
921 let button = ui.button(label).on_hover_text(hover);
922 self.hits.insert(key.into(), button.rect);
923 if button.clicked() {
924 request = Some(what);
925 }
926 }
927 let auto = ui.button("Program part automatically");
928 self.hits.insert("cam:auto:open".into(), auto.rect);
929 if auto.clicked() {
930 self.open = None;
931 let setup = self.target_setup(&doc);
932 match setup.ok_or_else(|| "select a setup before programming".to_string()).and_then(|id| self.autoprogram.preview(state, &id)) { Ok(()) => {}, Err(why) => self.notice = Some(why) }
933 }
934 ui.add(
935 egui::Label::new(
936 egui::RichText::new(format!(
937 "{} setup{} | {} tool{} | {} operation{}",
938 doc.setups.len(),
939 if doc.setups.len() == 1 { "" } else { "s" },
940 doc.tools.len(),
941 if doc.tools.len() == 1 { "" } else { "s" },
942 doc.operations.len(),
943 if doc.operations.len() == 1 { "" } else { "s" },
944 ))
945 .weak(),
946 )
947 .wrap_mode(egui::TextWrapMode::Extend),
948 );
949 });
950 if let Some(error) = &error {
951 let label = ui.label(egui::RichText::new(error).color(egui::Color32::from_rgb(0xf8, 0x51, 0x49)));
952 self.hits.insert("cam:error".into(), label.rect);
953 }
954 if let Some(notice) = &self.notice {
955 let label = ui.label(egui::RichText::new(notice).weak().color(egui::Color32::from_rgb(0xd2, 0x99, 0x22)));
956 self.hits.insert("cam:notice".into(), label.rect);
957 }
958 if state.cam.busy() {
959 ui.horizontal_wrapped(|ui| {
960 let text = match (&state.cam.progress, state.cam.in_flight()) {
961 (Some(p), _) if p.total > 0 => format!("generating {}: {} {}/{}", p.operation, p.phase, p.current, p.total),
962 (Some(p), _) => format!("generating {}: {}", p.operation, p.phase),
963 (None, Some(op)) => format!("generating {op}\u{2026}"),
964 (None, None) => "generation queued\u{2026}".to_string(),
965 };
966 let queued = state.cam.queued().len();
967 let line = ui.label(egui::RichText::new(if queued > 0 { format!("{text} (+{queued} queued)") } else { text }).weak());
968 self.hits.insert("cam:progress".into(), line.rect);
969 let cancel = ui.button("Cancel").on_hover_text("Drop the queue; the operation generating stops at its next chunk where the runner can");
970 self.hits.insert("cam:cancel".into(), cancel.rect);
971 if cancel.clicked() {
972 state.cam_cancel();
973 }
974 });
975 } else if let Some(notice) = &state.cam.notice {
976 let line = ui.label(egui::RichText::new(notice).weak());
977 self.hits.insert("cam:progress".into(), line.rect);
978 }
979 ui.add_space(2.0);
980
981 if self.autoprogram.open {
982 self.autoprogram.show(ui, state, &mut self.hits);
983 if let Some(request) = request {
984 self.perform(request, state);
985 }
986 return;
987 }
988
989 if let Some(id) = self.open.clone() {
991 if doc.setup(&id).is_some() {
992 self.show_setup_form(ui, state, &doc, &id);
993 if let Some(request) = request {
994 self.perform(request, state);
995 }
996 return;
997 }
998 if doc.tool(&id).is_some() {
999 self.show_tool_form(ui, state, store, &doc, &id);
1000 if let Some(request) = request {
1001 self.perform(request, state);
1002 }
1003 return;
1004 }
1005 if doc.operation(&id).is_some() {
1006 self.show_operation_form(ui, state, &doc, &id);
1007 if let Some(request) = request {
1008 self.perform(request, state);
1009 }
1010 return;
1011 }
1012 if doc.machines.iter().any(|m| m.id.as_str() == id) {
1013 self.show_machine_form(ui, state, &doc, &id);
1014 if let Some(request) = request {
1015 self.perform(request, state);
1016 }
1017 return;
1018 }
1019 self.open = None;
1020 }
1021 self.push_overlay(state, &doc, self.selected.clone());
1022
1023 let mut clicked: Option<String> = None;
1025 let mut remove: Option<String> = None;
1026 let mut open_operation: Option<String> = None;
1027 let mut toggle_toolpath: Option<(String, bool)> = None;
1029 let row = |ui: &mut egui::Ui, hits: &mut HashMap<String, egui::Rect>, id: &str, text: String, selected: bool, clicked: &mut Option<String>, remove: &mut Option<String>| {
1030 ui.horizontal(|ui| {
1031 let label = ui.selectable_label(selected, text);
1032 hits.insert(format!("cam:row:{id}"), label.rect);
1033 if label.clicked() {
1034 *clicked = Some(id.to_string());
1035 }
1036 label.context_menu(|ui| {
1037 let delete = ui.button("Delete");
1038 hits.insert(format!("cam:menuitem:{id}:delete"), delete.rect);
1039 if delete.clicked() {
1040 *remove = Some(id.to_string());
1041 ui.close();
1042 }
1043 });
1044 });
1045 };
1046
1047 let setups = egui::CollapsingHeader::new(format!("Setups ({})", doc.setups.len()))
1048 .id_salt("cam-setups")
1049 .default_open(true)
1050 .show(ui, |ui| {
1051 if doc.setups.is_empty() {
1052 ui.label(egui::RichText::new("No setups \u{2014} press New setup").weak());
1053 }
1054 for setup in &doc.setups {
1055 let id = setup.id.as_str();
1056 let ops: Vec<_> = doc.operations_of(id).collect();
1057 let selected = self.selected.as_deref() == Some(id);
1058 let header = egui::CollapsingHeader::new(setup.name.as_str())
1059 .id_salt(("cam-setup", id))
1060 .default_open(true)
1061 .show(ui, |ui| {
1062 if ops.is_empty() {
1063 ui.label(egui::RichText::new("No operations \u{2014} press New operation").weak());
1064 }
1065 for op in ops {
1066 let op_id = op.id.as_str();
1067 let status = cam_generate::status_of(state, &doc, op_id);
1068 let text = format!("{} {} \u{00b7} {} \u{00b7} {}{}", status.mark(), op.name, op.strategy.kind_name(), op.tool, if op.enabled { "" } else { " \u{00b7} off" });
1069 ui.horizontal(|ui| {
1070 let hover = match &status {
1071 cam_generate::OpStatus::Ok { motions, cutting_length } => format!("generated: {motions} motions, {cutting_length:.2} cutting"),
1072 cam_generate::OpStatus::Stale { motions } => format!("stale: {motions} motions from inputs that have changed; generate again"),
1073 cam_generate::OpStatus::Error(e) => format!("failed: {e}"),
1074 other => other.word().to_string(),
1075 };
1076 let label = ui.selectable_label(self.selected.as_deref() == Some(op_id), text).on_hover_text(hover);
1077 self.hits.insert(format!("cam:row:{op_id}"), label.rect);
1078 if label.double_clicked() {
1079 open_operation = Some(op_id.to_string());
1080 } else if label.clicked() {
1081 clicked = Some(op_id.to_string());
1082 }
1083 label.context_menu(|ui| {
1084 let edit = ui.button("Edit operation\u{2026}");
1085 self.hits.insert(format!("cam:menuitem:{op_id}:edit"), edit.rect);
1086 if edit.clicked() {
1087 open_operation = Some(op_id.to_string());
1088 ui.close();
1089 }
1090 let hidden = self.hidden_toolpaths.contains(op_id);
1091 let toggle = ui.button(if hidden { "Show toolpath" } else { "Hide toolpath" });
1092 self.hits.insert(format!("cam:menuitem:{op_id}:toolpath"), toggle.rect);
1093 if toggle.clicked() {
1094 toggle_toolpath = Some((op_id.to_string(), hidden));
1095 ui.close();
1096 }
1097 let delete = ui.button("Delete");
1098 self.hits.insert(format!("cam:menuitem:{op_id}:delete"), delete.rect);
1099 if delete.clicked() {
1100 remove = Some(op_id.to_string());
1101 ui.close();
1102 }
1103 });
1104 });
1105 }
1106 });
1107 self.hits.insert(format!("cam:row:{id}"), header.header_response.rect);
1108 if header.header_response.double_clicked() {
1109 self.open = Some(id.to_string());
1110 self.selected = Some(id.to_string());
1111 } else if header.header_response.clicked() {
1112 clicked = Some(id.to_string());
1113 }
1114 header.header_response.context_menu(|ui| {
1115 let edit = ui.button("Edit setup\u{2026}");
1116 self.hits.insert(format!("cam:menuitem:{id}:edit"), edit.rect);
1117 if edit.clicked() {
1118 self.open = Some(id.to_string());
1119 ui.close();
1120 }
1121 });
1122 header.header_response.context_menu(|ui| {
1123 let delete = ui.button("Delete setup and its operations");
1124 self.hits.insert(format!("cam:menuitem:{id}:delete"), delete.rect);
1125 if delete.clicked() {
1126 remove = Some(id.to_string());
1127 ui.close();
1128 }
1129 });
1130 if selected {
1131 ui.painter().rect_stroke(
1132 header.header_response.rect,
1133 2.0,
1134 egui::Stroke::new(1.0, ui.visuals().selection.stroke.color),
1135 egui::StrokeKind::Inside,
1136 );
1137 }
1138 }
1139 });
1140 self.hits.insert(SECTION_SETUPS.into(), setups.header_response.rect);
1141
1142 let mut post_program: Option<String> = None;
1143 let programs = egui::CollapsingHeader::new(format!("NC Programs ({})", doc.nc_programs.len()))
1144 .id_salt("cam-programs")
1145 .default_open(true)
1146 .show(ui, |ui| {
1147 if doc.nc_programs.is_empty() {
1148 ui.label(egui::RichText::new("No NC programs \u{2014} add one, then Post\u{2026} it to an NC file").weak());
1149 }
1150 for program in &doc.nc_programs {
1151 let text = format!("{} \u{00b7} {} \u{00b7} {} op{}", program.name, program.post, program.operations.len(), if program.operations.len() == 1 { "" } else { "s" });
1152 ui.horizontal(|ui| {
1153 let b = ui.small_button("Post\u{2026}").on_hover_text("Post through the program's post: preview the NC text and save it");
1154 self.hits.insert(format!("cam:post:program:{}", program.id), b.rect);
1155 if b.clicked() {
1156 post_program = Some(program.id.clone());
1157 }
1158 row(ui, &mut self.hits, &program.id, text, self.selected.as_deref() == Some(program.id.as_str()), &mut clicked, &mut remove);
1159 });
1160 }
1161 });
1162 self.hits.insert(SECTION_PROGRAMS.into(), programs.header_response.rect);
1163 if let Some(id) = post_program {
1164 let _ = self.post.open(state, &doc, &id);
1165 }
1166
1167 let mut open_tool: Option<String> = None;
1168 let mut duplicate: Option<String> = None;
1169 let mut to_library: Option<String> = None;
1170 let mut from_library: Option<String> = None;
1171 let mut import = false;
1172 let mut export: Option<&'static str> = None;
1173 let tools = egui::CollapsingHeader::new(format!("Tool Library ({})", doc.tools.len()))
1174 .id_salt("cam-tools")
1175 .default_open(true)
1176 .show(ui, |ui| {
1177 ui.horizontal_wrapped(|ui| {
1178 let add = ui.menu_button("Add from user library", |ui| {
1179 if self.user_library.is_none() {
1180 self.refresh_user_library(store);
1181 }
1182 match self.user_library.as_ref() {
1183 Some(Ok(list)) if list.is_empty() => {
1184 ui.label(egui::RichText::new("The user library is empty \u{2014} save a tool to it from its row menu").weak());
1185 }
1186 Some(Ok(list)) => {
1187 for (id, name, number) in list.clone() {
1188 let entry = ui.button(format!("T{number} {name}"));
1189 self.hits.insert(format!("cam:library:{id}"), entry.rect);
1190 if entry.clicked() {
1191 from_library = Some(id);
1192 ui.close();
1193 }
1194 }
1195 }
1196 Some(Err(why)) => {
1197 ui.label(egui::RichText::new(why).color(egui::Color32::from_rgb(0xf8, 0x51, 0x49)));
1198 }
1199 None => {}
1200 }
1201 });
1202 self.hits.insert("cam:tools:library".into(), add.response.rect);
1203 if add.response.clicked() {
1204 self.refresh_user_library(store);
1205 }
1206 let import_button = ui.button("Import\u{2026}").on_hover_text("Add the tools of a Fusion 360 export (.json / .tools), a .csv sheet or a library file");
1207 self.hits.insert("cam:tools:import".into(), import_button.rect);
1208 import = import_button.clicked();
1209 let export_doc = ui.button("Export").on_hover_text("Save the document's tools as a library file");
1210 self.hits.insert("cam:tools:export".into(), export_doc.rect);
1211 if export_doc.clicked() {
1212 export = Some("document");
1213 }
1214 let export_user = ui.button("Export user library").on_hover_text("Save the user library as a library file");
1215 self.hits.insert("cam:tools:export-user".into(), export_user.rect);
1216 if export_user.clicked() {
1217 export = Some("user");
1218 }
1219 });
1220 if let Some((ids, issues)) = &self.import_report {
1221 for issue in issues {
1222 ui.label(egui::RichText::new(format!("skipped {issue}")).weak().color(egui::Color32::from_rgb(0xd2, 0x99, 0x22)));
1223 }
1224 let _ = ids;
1225 }
1226 if doc.tools.is_empty() {
1227 ui.label(egui::RichText::new("No tools \u{2014} press New tool").weak());
1228 }
1229 for tool in &doc.tools {
1230 let id = tool.id.as_str();
1231 let text = format!("T{} {} \u{00b7} {} \u{2300}{}", tool.number, tool.name, tool.kind.as_str(), tool.geometry.diameter);
1232 ui.horizontal(|ui| {
1233 let label = ui.selectable_label(self.selected.as_deref() == Some(id), text);
1234 self.hits.insert(format!("cam:row:{id}"), label.rect);
1235 if label.double_clicked() {
1236 open_tool = Some(id.to_string());
1237 } else if label.clicked() {
1238 clicked = Some(id.to_string());
1239 }
1240 label.context_menu(|ui| {
1241 let edit = ui.button("Edit tool\u{2026}");
1242 self.hits.insert(format!("cam:menuitem:{id}:edit"), edit.rect);
1243 if edit.clicked() {
1244 open_tool = Some(id.to_string());
1245 ui.close();
1246 }
1247 let dup = ui.button("Duplicate");
1248 self.hits.insert(format!("cam:menuitem:{id}:duplicate"), dup.rect);
1249 if dup.clicked() {
1250 duplicate = Some(id.to_string());
1251 ui.close();
1252 }
1253 let save = ui.button("Save to user library");
1254 self.hits.insert(format!("cam:menuitem:{id}:library"), save.rect);
1255 if save.clicked() {
1256 to_library = Some(id.to_string());
1257 ui.close();
1258 }
1259 let delete = ui.button("Delete");
1260 self.hits.insert(format!("cam:menuitem:{id}:delete"), delete.rect);
1261 if delete.clicked() {
1262 remove = Some(id.to_string());
1263 ui.close();
1264 }
1265 });
1266 });
1267 }
1268 });
1269 self.hits.insert(SECTION_TOOLS.into(), tools.header_response.rect);
1270
1271 let mut open_machine: Option<String> = None;
1273 let mut new_machine: Option<String> = None;
1274 let machines = egui::CollapsingHeader::new(format!("Machines ({})", doc.machines.len()))
1275 .id_salt("cam-machines")
1276 .default_open(true)
1277 .show(ui, |ui| {
1278 let add = ui.menu_button("New machine", |ui| {
1279 for base in brep_cam::kinematics::library::builtin_machines() {
1280 let id = base.id.as_str().to_string();
1281 let entry = ui.button(format!("{} \u{00b7} {}", base.name, cam_machine::kind_tag(&base.kind)));
1282 self.hits.insert(format!("cam:machines:new:{id}"), entry.rect);
1283 if entry.clicked() {
1284 new_machine = Some(id);
1285 ui.close();
1286 }
1287 }
1288 });
1289 self.hits.insert("cam:machines:new".into(), add.response.rect);
1290 if doc.machines.is_empty() {
1291 ui.label(egui::RichText::new("No machines of this document \u{2014} a setup can name a built-in directly").weak());
1292 }
1293 for machine in &doc.machines {
1294 let id = machine.id.as_str();
1295 let used = doc.setups.iter().filter(|s| s.machine.as_ref().is_some_and(|m| m.as_str() == id)).count();
1296 let text = format!(
1297 "{} \u{00b7} {}{}{}",
1298 machine.name,
1299 cam_machine::kind_tag(&machine.kind),
1300 machine.post.as_deref().map(|p| format!(" \u{00b7} {p}")).unwrap_or_default(),
1301 if used > 0 { format!(" \u{00b7} {used} setup{}", if used == 1 { "" } else { "s" }) } else { String::new() }
1302 );
1303 let label = ui.selectable_label(self.selected.as_deref() == Some(id), text);
1304 self.hits.insert(format!("cam:row:{id}"), label.rect);
1305 if label.double_clicked() {
1306 open_machine = Some(id.to_string());
1307 } else if label.clicked() {
1308 clicked = Some(id.to_string());
1309 }
1310 label.context_menu(|ui| {
1311 let edit = ui.button("Edit machine\u{2026}");
1312 self.hits.insert(format!("cam:menuitem:{id}:edit"), edit.rect);
1313 if edit.clicked() {
1314 open_machine = Some(id.to_string());
1315 ui.close();
1316 }
1317 let delete = ui.button("Delete");
1318 self.hits.insert(format!("cam:menuitem:{id}:delete"), delete.rect);
1319 if delete.clicked() {
1320 remove = Some(id.to_string());
1321 ui.close();
1322 }
1323 });
1324 }
1325 });
1326 self.hits.insert(SECTION_MACHINES.into(), machines.header_response.rect);
1327 if let Some(base) = new_machine {
1328 match add_machine(state, &base, None) {
1329 Ok(id) => {
1330 self.notice = None;
1331 self.selected = Some(id);
1332 }
1333 Err(why) => self.refuse(state, why),
1334 }
1335 }
1336 if let Some(id) = open_machine {
1337 self.selected = Some(id.clone());
1338 self.open = Some(id);
1339 }
1340 if let Some(id) = open_tool {
1341 self.selected = Some(id.clone());
1342 self.open = Some(id);
1343 }
1344 if let Some(id) = duplicate {
1345 match duplicate_tool(state, &id) {
1346 Ok(new_id) => self.selected = Some(new_id),
1347 Err(why) => self.refuse(state, why),
1348 }
1349 }
1350 if let Some(id) = to_library {
1351 let outcome = doc.tool(&id).ok_or_else(|| format!("no tool `{id}`")).and_then(|tool| cam_library::save_tool(store, tool));
1352 match outcome {
1353 Ok(()) => {
1354 self.notice = Some(format!("{id} saved to the user library"));
1355 self.user_library = None;
1356 }
1357 Err(why) => self.refuse(state, why),
1358 }
1359 }
1360 if let Some(library_id) = from_library {
1361 match add_tool_from_library(state, store, &library_id) {
1362 Ok(new_id) => self.selected = Some(new_id),
1363 Err(why) => self.refuse(state, why),
1364 }
1365 }
1366 if import {
1367 self.shell.pick_file = Some((IMPORT_TOOLS_TAG.to_string(), "Import tools".to_string()));
1368 }
1369 if let Some(which) = export {
1370 let outcome = if which == "user" { cam_library::export_text(store) } else { export_document_tools(state) };
1371 match outcome {
1372 Ok(text) => self.shell.save_files.push(SaveRequest {
1373 title: if which == "user" { "Export the user tool library".into() } else { "Export the document's tools".into() },
1374 file_name: if which == "user" { "user-tools.json".into() } else { "tools.json".into() },
1375 body: SaveBody::Text(text),
1376 }),
1377 Err(why) => self.refuse(state, why),
1378 }
1379 }
1380
1381 if let Some(id) = open_operation {
1383 self.selected = Some(id.clone());
1384 self.open = Some(id);
1385 }
1386 if let Some(id) = clicked {
1387 self.selected = if self.selected.as_deref() == Some(id.as_str()) { None } else { Some(id) };
1388 }
1389 if let Some((id, hidden)) = toggle_toolpath {
1390 self.set_toolpath_visible(&id, hidden);
1391 }
1392 if let Some(id) = remove {
1393 match remove_record(state, &id) {
1394 Ok(()) => {
1395 if self.selected.as_deref() == Some(id.as_str()) {
1396 self.selected = None;
1397 }
1398 }
1399 Err(why) => self.refuse(state, why),
1400 }
1401 }
1402 if let Some(request) = request {
1403 self.perform(request, state);
1404 }
1405 if error.is_none() {
1407 ui.separator();
1408 let setup_hint = self.selected.as_deref().and_then(|id| {
1409 doc.setup(id).map(|_| id.to_string()).or_else(|| doc.operation(id).map(|o| o.setup.to_string()))
1410 });
1411 self.sim.show(ui, state, &doc, setup_hint.as_deref());
1412 }
1413 if let Some((id, patch)) = self.post.show(ui, &doc) {
1415 match set_program_properties(state, &id, &patch) {
1416 Ok(()) => match cam_document(state) {
1417 Ok(doc) => {
1418 let _ = self.post.open(state, &doc, &id);
1419 }
1420 Err(e) => self.post.error = Some(e),
1421 },
1422 Err(e) => self.post.error = Some(e),
1423 }
1424 }
1425 }
1426
1427 fn show_setup_form(&mut self, ui: &mut egui::Ui, state: &mut EngineState, doc: &CamDocument, id: &str) {
1431 let Some(setup) = doc.setup(id) else { return };
1432 let orientation: SetupOrientation = doc.orientation(id);
1433 let fields = cam_setup::setup_form_fields();
1434 let mut params = cam_setup::setup_form_params(setup, &orientation);
1435 cam_setup::merge_machine_params(&mut params, doc, setup);
1436 let hidden = cam_setup::hidden_fields(¶ms);
1437 let title = format!("Setup {id}");
1438 let frame_error = self.geometry.get(id).and_then(|g| g.frame.clone().err());
1439 let banner = match (&frame_error, &self.notice) {
1440 (Some(why), _) => Some((why.as_str(), egui::Color32::from_rgb(0xf8, 0x51, 0x49))),
1441 (None, Some(notice)) => Some((notice.as_str(), egui::Color32::from_rgb(0xd2, 0x99, 0x22))),
1442 (None, None) => None,
1443 };
1444 let picking = state
1445 .ref_select_field()
1446 .filter(|f| f.target == RefSelectTarget::Cam && f.subject == id)
1447 .and_then(|f| self.picking_field.clone().map(|key| (vec![key], f.names)));
1448 let active = picking.as_ref().map(|(path, names)| crate::form::ActivePick { path, names });
1449 if active.is_some() {
1450 state.ref_select_claim();
1451 }
1452 let spec = FormViewSpec {
1453 title: &title,
1454 subtitle: Some(setup.name.as_str()),
1455 fields: &fields,
1456 hidden: Some(&hidden),
1457 banner,
1458 trailing: None,
1459 exit_label: "Return to tree",
1460 extra: None,
1461 rollback: false,
1462 hits_prefix: "cam:",
1463 read_only: None,
1464 picking: active,
1465 };
1466 let was_picking = self.picking_field.is_some();
1467 let out = form_view(ui, &spec, &mut params, Some(&mut self.hits));
1468 let anchor = self.hits.get("cam:form:feature").map(|rect| rect.min).unwrap_or(egui::Pos2::ZERO);
1469 self.hits.insert(format!("cam:form:setup:{id}"), egui::Rect::from_min_size(anchor, egui::Vec2::ZERO));
1470 self.push_overlay(state, doc, Some(id.to_string()));
1471 if let Some(activate) = out.ref_activate {
1472 let key = activate.path.first().cloned().unwrap_or_default();
1473 match begin_setup_pick(state, id, &key) {
1474 Ok(()) => {
1475 self.picking_field = Some(key);
1476 state.ref_select_claim();
1479 }
1480 Err(why) => self.refuse(state, why),
1481 }
1482 }
1483 if let Some(index) = out.pick_remove {
1484 state.ref_select_remove(index);
1485 }
1486 if out.pick_done {
1487 state.finish_ref_select();
1488 self.picking_field = None;
1489 if let Some(why) = sync_setup_frame(state, id) {
1490 self.notice = Some(why);
1491 }
1492 }
1493 if out.changed {
1494 self.notice = None;
1495 let coalesce = format!("setup:{id}:form");
1496 if let Err(why) = update_setup(state, id, ¶ms, Some(&coalesce)) {
1497 self.refuse(state, why);
1498 }
1499 }
1500 if out.exit_clicked {
1501 if state.ref_select_active() {
1502 state.finish_ref_select();
1503 }
1504 self.picking_field = None;
1505 self.open = None;
1506 }
1507 if was_picking && !state.ref_select_active() && self.picking_field.is_some() {
1508 self.picking_field = None;
1511 if let Some(why) = sync_setup_frame(state, id) {
1512 self.notice = Some(why);
1513 }
1514 }
1515 if !state.ref_select_active() {
1516 self.picking_field = None;
1517 }
1518 }
1519
1520 fn show_operation_form(&mut self, ui: &mut egui::Ui, state: &mut EngineState, doc: &CamDocument, id: &str) {
1525 let Some(operation) = doc.operation(id) else { return };
1526 let mut fields = cam_operation::operation_form_fields(operation, &doc.tools);
1527 fields.extend(crate::panels::cam_multiaxis::form_fields(doc, operation));
1528 let mut params = cam_operation::operation_form_params(operation);
1529 crate::panels::cam_multiaxis::merge_params(doc, id, &mut params);
1530 let hidden = cam_operation::hidden_fields(¶ms);
1531 let title = format!("Operation {id}");
1532 let subtitle = format!("{} \u{00b7} {} \u{00b7} {}", operation.strategy.kind_name(), operation.setup, operation.tool);
1533 let problem = cam_operation::strategy_problem(operation);
1534 let banner = match (&self.notice, &problem) {
1535 (Some(n), _) => Some((n.as_str(), egui::Color32::from_rgb(0xd2, 0x99, 0x22))),
1536 (None, Some(p)) => Some((p.as_str(), egui::Color32::from_rgb(0xd2, 0x99, 0x22))),
1537 (None, None) => None,
1538 };
1539 let picking = state
1540 .ref_select_field()
1541 .filter(|f| f.target == RefSelectTarget::Cam && f.subject == id)
1542 .and_then(|f| self.picking_field.clone().map(|key| (vec!["targets".to_string(), key], f.names)));
1543 let active = picking.as_ref().map(|(path, names)| crate::form::ActivePick { path, names });
1544 if active.is_some() {
1545 state.ref_select_claim();
1546 }
1547 let spec = FormViewSpec {
1548 title: &title,
1549 subtitle: Some(subtitle.as_str()),
1550 fields: &fields,
1551 hidden: Some(&hidden),
1552 banner,
1553 trailing: None,
1554 exit_label: "Return to tree",
1555 extra: None,
1556 rollback: false,
1557 hits_prefix: "cam:",
1558 read_only: None,
1559 picking: active,
1560 };
1561 let was_picking = self.picking_field.is_some();
1562 let out = form_view(ui, &spec, &mut params, Some(&mut self.hits));
1563 let anchor = self.hits.get("cam:form:feature").map(|rect| rect.min).unwrap_or(egui::Pos2::ZERO);
1564 self.hits.insert(format!("cam:form:operation:{id}"), egui::Rect::from_min_size(anchor, egui::Vec2::ZERO));
1565 self.push_overlay(state, doc, Some(operation.setup.as_str().to_string()));
1566 if let Some(activate) = out.ref_activate {
1567 let key = activate.path.last().cloned().unwrap_or_default();
1568 match begin_operation_pick(state, id, &key) {
1569 Ok(()) => {
1570 self.picking_field = Some(key);
1571 state.ref_select_claim();
1572 }
1573 Err(why) => self.refuse(state, why),
1574 }
1575 }
1576 if let Some(index) = out.pick_remove {
1577 state.ref_select_remove(index);
1578 }
1579 if out.pick_done {
1580 state.finish_ref_select();
1581 self.picking_field = None;
1582 if let Err(why) = sync_operation_faces(state, id) {
1583 self.refuse(state, why);
1584 }
1585 }
1586 if out.changed {
1587 self.notice = None;
1588 let coalesce = format!("operation:{id}:form");
1589 if let Err(why) = update_operation(state, id, ¶ms, Some(&coalesce)) {
1590 self.refuse(state, why);
1591 }
1592 }
1593 if let Some(index) = out.button_clicked.as_deref().and_then(|key| key.strip_prefix("removeChain")) {
1594 let patch = json!({ "targets": { "chains": { index: { "remove": true } } } });
1595 if let Err(why) = update_operation(state, id, &patch, None) {
1596 self.refuse(state, why);
1597 }
1598 }
1599 if out.exit_clicked {
1600 if state.ref_select_active() {
1601 state.finish_ref_select();
1602 }
1603 self.picking_field = None;
1604 self.open = None;
1605 }
1606 if was_picking && !state.ref_select_active() && self.picking_field.is_some() {
1607 self.picking_field = None;
1608 if let Err(why) = sync_operation_faces(state, id) {
1609 self.refuse(state, why);
1610 }
1611 }
1612 if !state.ref_select_active() {
1613 self.picking_field = None;
1614 }
1615 }
1616
1617 fn show_machine_form(&mut self, ui: &mut egui::Ui, state: &mut EngineState, doc: &CamDocument, id: &str) {
1623 let Some(machine) = doc.machines.iter().find(|m| m.id.as_str() == id) else { return };
1624 let fields = cam_machine::machine_form_fields(machine);
1625 let mut params = cam_machine::machine_form_params(machine);
1626 let hidden = cam_machine::hidden_fields(¶ms);
1627 let title = format!("Machine {id}");
1628 let banner = self.notice.as_ref().map(|n| (n.as_str(), egui::Color32::from_rgb(0xd2, 0x99, 0x22)));
1629 let spec = FormViewSpec {
1630 title: &title,
1631 subtitle: Some(machine.name.as_str()),
1632 fields: &fields,
1633 hidden: Some(&hidden),
1634 banner,
1635 trailing: None,
1636 exit_label: "Return to tree",
1637 extra: None,
1638 rollback: false,
1639 hits_prefix: "cam:",
1640 read_only: None,
1641 picking: None,
1642 };
1643 let out = form_view(ui, &spec, &mut params, Some(&mut self.hits));
1644 let anchor = self.hits.get("cam:form:feature").map(|rect| rect.min).unwrap_or(egui::Pos2::ZERO);
1645 self.hits.insert(format!("cam:form:machine:{id}"), egui::Rect::from_min_size(anchor, egui::Vec2::ZERO));
1646 self.push_overlay(state, doc, None);
1647 if out.changed {
1648 self.notice = None;
1649 let coalesce = format!("machine:{id}:form");
1650 if let Err(why) = update_machine(state, id, ¶ms, Some(&coalesce)) {
1651 self.refuse(state, why);
1652 }
1653 }
1654 if out.exit_clicked {
1655 self.open = None;
1656 }
1657 }
1658
1659 fn show_tool_form(&mut self, ui: &mut egui::Ui, state: &mut EngineState, store: &dyn ModelStore, doc: &CamDocument, id: &str) {
1660 let Some(tool) = doc.tool(id) else { return };
1661 let fields = cam_tool::tool_form_fields();
1662 let mut params = cam_tool::tool_form_params(tool);
1663 if let Some(material) = &self.feeds_material {
1664 params["feedsMaterial"] = json!(material);
1665 }
1666 let hidden = cam_tool::hidden_fields(¶ms);
1667 let title = format!("Tool {id}");
1668 let banner = self.notice.as_ref().map(|n| (n.as_str(), egui::Color32::from_rgb(0xd2, 0x99, 0x22)));
1669 let spec = FormViewSpec {
1670 title: &title,
1671 subtitle: Some(tool.name.as_str()),
1672 fields: &fields,
1673 hidden: Some(&hidden),
1674 banner,
1675 trailing: None,
1676 exit_label: "Return to tree",
1677 extra: None,
1678 rollback: false,
1679 hits_prefix: "cam:",
1680 read_only: None,
1681 picking: None,
1682 };
1683 let out = form_view(ui, &spec, &mut params, Some(&mut self.hits));
1684 let anchor = self.hits.get("cam:form:feature").map(|rect| rect.min).unwrap_or(egui::Pos2::ZERO);
1685 self.hits.insert(format!("cam:form:tool:{id}"), egui::Rect::from_min_size(anchor, egui::Vec2::ZERO));
1686 self.push_overlay(state, doc, None);
1687 if out.changed {
1688 self.notice = None;
1689 self.feeds_material = params.get("feedsMaterial").and_then(Value::as_str).map(str::to_string);
1691 let coalesce = format!("tool:{id}:form");
1692 if let Err(why) = update_tool(state, id, ¶ms, Some(&coalesce)) {
1693 self.refuse(state, why);
1694 }
1695 }
1696 if out.button_clicked.as_deref() == Some("calculateFeeds") {
1697 let material = params.get("feedsMaterial").and_then(Value::as_str).unwrap_or("aluminium").to_string();
1698 match calculate_tool_feeds(state, id, &material) {
1699 Ok(adjustments) => {
1700 self.notice = Some(if adjustments.is_empty() { format!("feeds calculated for {material}") } else { format!("feeds calculated for {material}: {}", adjustments.join("; ")) });
1701 }
1702 Err(why) => self.refuse(state, why),
1703 }
1704 }
1705 if out.exit_clicked {
1706 self.open = None;
1707 self.user_library = None;
1708 }
1709 let _ = store;
1710 }
1711
1712 fn push_overlay(&mut self, state: &mut EngineState, doc: &CamDocument, id: Option<String>) {
1715 self.refresh_geometry(state);
1716 let group = id
1717 .as_deref()
1718 .filter(|id| doc.setup(id).is_some())
1719 .and_then(|id| self.geometry.get(id))
1720 .map(|geometry| {
1721 let frame = geometry.frame.clone().unwrap_or_default();
1722 cam_setup::overlay_group(&frame, geometry.stock)
1723 })
1724 .unwrap_or_else(|| json!({ "name": "cam-setup" }));
1725 let key = (group.to_string(), state.cam.results_stamp, self.selected.clone(), self.scrub.map(f64::to_bits), self.hidden_stamp);
1726 if self.overlay_key.as_ref() == Some(&key) {
1727 return;
1728 }
1729 self.overlay_key = Some(key);
1730 let mut groups = vec![group];
1733 let mut live: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
1734 for (op, result) in &state.cam.results {
1735 if self.hidden_toolpaths.contains(op) {
1736 continue;
1737 }
1738 if let Some(toolpath) = &result.toolpath {
1739 let selected = self.selected.as_deref() == Some(op.as_str());
1740 let operation = doc.operation(op);
1741 let to_model = operation.and_then(|o| doc.setup(o.setup.as_str())).map(|s| s.machine_to_model()).unwrap_or_default();
1742 let profile = if selected && self.scrub.is_some() {
1743 operation.and_then(|o| doc.tool(o.tool.as_str())).and_then(cam_generate::tool_profile)
1744 } else {
1745 None
1746 };
1747 groups.push(cam_generate::toolpath_group(op, Some(toolpath), selected, if selected { self.scrub } else { None }, &to_model, profile.as_ref()));
1748 live.insert(op.clone());
1749 }
1750 }
1751 for gone in self.overlay_ops.difference(&live) {
1752 groups.push(cam_generate::toolpath_group(gone, None, false, None, &RigidTransform::default(), None));
1753 }
1754 self.overlay_ops = live;
1755 let json = json!({ "groups": groups }).to_string();
1756 if json != self.overlay {
1757 let _ = state.widgets.set_overlay_json(&json);
1758 state.dirty = true;
1759 self.overlay = json;
1760 }
1761 }
1762
1763 pub fn hits_json(&self) -> String {
1765 let mut all = self.hits.clone();
1766 all.extend(self.sim.hit_rects().iter().map(|(k, r)| (k.clone(), *r)));
1767 all.extend(self.post.hit_rects().iter().map(|(k, r)| (k.clone(), *r)));
1768 crate::automation::hit_rects::hits_json(&all)
1769 }
1770
1771 pub fn clear_hits(&mut self) {
1774 self.hits.clear();
1775 self.sim.clear_hits();
1776 }
1777}
1778
1779pub static HIT_KEYS: &[HitKeyDoc] = &[
1780 HitKeyDoc { panel: "cam", prefix: "cam:auto:", meaning: "automatic programming preview, feature checkboxes, create and close", command: Some("cam_autoprogram") },
1781 HitKeyDoc { panel: "cam", prefix: "panel:clip", meaning: "the visible region of the pane (scroll_into_view scrolls a cam:* widget inside it)", command: None },
1782 HitKeyDoc { panel: "cam", prefix: "cam:new:setup", meaning: "add a setup (the toolbar's New setup)", command: Some("cam_add_setup") },
1783 HitKeyDoc { panel: "cam", prefix: "cam:new:tool", meaning: "add a tool to the document's library (the toolbar's New tool)", command: Some("cam_add_tool") },
1784 HitKeyDoc { panel: "cam", prefix: "cam:new:operation", meaning: "add an operation to the selected setup with the selected tool (the toolbar's New operation)", command: Some("cam_add_operation") },
1785 HitKeyDoc { panel: "cam", prefix: "cam:generate", meaning: "generate the toolpaths of every operation that needs it (the toolbar's Generate)", command: Some("cam_generate") },
1786 HitKeyDoc { panel: "cam", prefix: "cam:progress", meaning: "the generation line under the header: the operation generating, its phase and count, the queue; after a cancel, what was cancelled", command: None },
1787 HitKeyDoc { panel: "cam", prefix: "cam:cancel", meaning: "cancel the generation: the queue is dropped at once, the operation in flight stops at its next chunk where the runner can", command: Some("cam_cancel") },
1788 HitKeyDoc { panel: "cam", prefix: "cam:error", meaning: "why the document's `cam` block cannot be read (every edit is refused until it is repaired)", command: None },
1789 HitKeyDoc { panel: "cam", prefix: "cam:notice", meaning: "what the last action said that was not a record (a refusal, a count)", command: None },
1790 HitKeyDoc { panel: "cam", prefix: "cam:section:", meaning: "a section header (cam:section:setups, cam:section:programs, cam:section:tools, cam:section:machines) \u{2014} click folds it", command: None },
1791 HitKeyDoc { panel: "cam", prefix: "cam:machines:", meaning: "the Machines section's New machine menu (cam:machines:new) and, while it is open, one entry per built-in machine to copy (cam:machines:new:<built-in id>)", command: Some("cam_add_machine") },
1792 HitKeyDoc { panel: "cam", prefix: "cam:row:", meaning: "a setup, operation, NC program, tool or document machine row (cam:row:<id>) \u{2014} a click selects it (again to clear); right-click for its menu", command: Some("cam_select") },
1793 HitKeyDoc { panel: "cam", prefix: "cam:menuitem:", meaning: "one entry of the OPEN row menu (cam:menuitem:<id>:delete; a setup's or an operation's :edit; an operation's :toolpath = Hide / Show toolpath; a machine's :edit; a tool's :edit, :duplicate and :library) \u{2014} published only while the menu is up", command: Some("cam_remove") },
1794 HitKeyDoc { panel: "cam", prefix: "cam:form:", meaning: "the open setup, tool, operation or machine dialog (cam:form:setup:<id>, cam:form:tool:<id>, cam:form:operation:<id>, cam:form:machine:<id>, cam:form:feature, cam:form:return)", command: Some("cam_open") },
1795 HitKeyDoc { panel: "cam", prefix: "cam:field:", meaning: "one control of the open dialog (cam:field:<path>, nested paths joined by `.`, plus #activate on a reference field): a setup's name, wcsNumber, zRef, xRef, originRef, stockKind and its parameters, clearance, retract, feed, targets, protected; a tool's name, number, kind, description, material, the geometry, holderFamily/holderBore/holderHeight, spindleRpm, spindleDirection, cuttingFeed, plungeFeed, feedsMaterial, calculateFeeds, coolant; an operation's name, enabled, strategyKind, tool, feedsOverride, feeds.*, targets.targetSolids / driveFaces / avoidFaces / protectedSolids, targets.chainEdges / chainProfile (pickers that add a chain), the chain list targets.chains.<i>.source.kind / summary / closed / reversed / side / offset / regions / startSet / start and its targets.removeChain<i> button, heightsOverride, heights.*, tolerance, stockToLeave.*, strategy.* (its strategy's parameters) and linking.*; a document machine's name, kind, post, toolChangeSeconds, spindle.*, feed.*, accelerationSet, travelSet, travel.min / max, kinematics, yAxis, axes.<i>.name / direction / origin / limitsSet / limitsMin / limitsMax and collision (read only)", command: Some("cam_update_setup") },
1796 HitKeyDoc { panel: "cam", prefix: "cam:tools:", meaning: "the Tool Library section's actions: cam:tools:library (the Add-from-user-library menu), cam:tools:import (the file chooser), cam:tools:export (the document's tools as a library file), cam:tools:export-user", command: Some("cam_library") },
1797 HitKeyDoc { panel: "cam", prefix: "cam:library:", meaning: "one entry of the OPEN Add-from-user-library menu (cam:library:<library tool id>) \u{2014} published only while the menu is up", command: Some("cam_library") },
1798];
1799