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