1use crate::panels::{cam, cam_setup};
6use crate::document::cam::CamDocument;
7use brep_cam::autoprogram::{self, AutoInput, Plan, PlannedOperation};
8use brep_cam::features::{Recognized, RecognitionInput, TechDb};
9use brep_cam::kernel_adapter::{BodyInput, machine_snapshot, revolved_faces};
10use brep_cam::kernel_adapter::features::face_carriers;
11use brep_cam::kernel_adapter::planar::horizontal_faces;
12use brep_cam::library::LibraryUnits;
13use brep_cam::feeds::WorkMaterial;
14use brep_cam::model::{Setup, Tool, ToolGeometry, ToolKind};
15use brep_cam::{NullSink, Runner, Tolerance};
16use eframe::egui;
17use serde_json::{json, Value};
18use std::collections::{BTreeMap, BTreeSet, HashMap};
19use brep_render::engine_state::EngineState;
20
21#[derive(Default)]
23pub struct AutoProgramPanel {
24 pending: Option<String>,
25 preview: Option<Preview>,
26 error: Option<String>,
27 pub open: bool,
28}
29
30
31struct Preview {
32 setup: String,
33 enabled: bool,
34 additional: Vec<SetupPreview>,
35 unreached: Vec<autoprogram::Skipped>,
36 document: CamDocument,
37 revisions: Vec<(String, u64)>,
38 recognized: Recognized,
39 plan: Plan,
40 tools: Vec<Tool>,
41 fallback: BTreeSet<String>,
42 selected: BTreeSet<String>,
43}
44
45struct SetupPreview {
46 orientation: crate::document::cam::SetupOrientation,
47 proposal: autoprogram::multi::SetupPlan,
48 enabled: bool,
49 selected: BTreeSet<String>,
50}
51
52fn body_names(setup: &Setup) -> BTreeSet<String> {
53 let mut names: BTreeSet<_> = setup.targets.iter().chain(&setup.protected).map(|r| r.solid.clone()).collect();
54 if let brep_cam::model::Stock::FromSolid { solid } = &setup.stock { names.insert(solid.clone()); }
55 names
56}
57
58fn revisions(state: &EngineState, setup: &Setup) -> Result<Vec<(String, u64)>, String> {
59 body_names(setup).into_iter().map(|name| {
60 let revision = state.scene.solid(&name).ok_or_else(|| format!("body `{name}` is not on screen"))?.revision;
61 Ok((name, revision))
62 }).collect()
63}
64
65fn fallback_tools(document: &CamDocument) -> Result<Vec<Tool>, String> {
69 let mut tools = Vec::new();
70 let mut number = document.tools.iter().map(|t| t.number).max().unwrap_or(0);
71 for (kind, diameter, length) in [
72 (ToolKind::FaceMill, 40., 5.),
73 (ToolKind::Flat, 10., 30.), (ToolKind::Flat, 8., 30.),
74 (ToolKind::Flat, 6., 30.), (ToolKind::Flat, 4., 25.), (ToolKind::Flat, 2., 20.),
75 (ToolKind::Drill, 3., 40.), (ToolKind::Drill, 4., 40.),
76 (ToolKind::Drill, 6., 40.), (ToolKind::Drill, 8., 40.), (ToolKind::Drill, 10., 40.),
77 (ToolKind::SpotDrill, 10., 15.), (ToolKind::Chamfer, 10., 15.),
78 ] {
79 number = number.checked_add(1).ok_or("tool numbers are exhausted")?;
80 let mut id = format!("auto-builtin-{number}");
81 while document.tool(&id).is_some() { id.push('_'); }
82 let mut tool = Tool::new(id, number, kind, ToolGeometry::flat(diameter, length));
83 tool.name = format!("Builtin {diameter} mm {kind:?}");
84 tool.description = "Automatic programming fallback; verify the available physical tool before machining".into();
85 tool.material = "carbide".into();
86 tool.geometry.flute_count = Some(if matches!(kind, ToolKind::Drill | ToolKind::SpotDrill) { 2 } else { 3 });
87 if matches!(kind, ToolKind::Drill | ToolKind::SpotDrill | ToolKind::Chamfer) {
88 tool.geometry.included_angle_deg = Some(if kind == ToolKind::Drill { 118. } else { 90. });
89 }
90 brep_cam::library::validate_tool(&tool)?;
91 tools.push(tool);
92 }
93 Ok(tools)
94}
95
96fn phase(role: &str) -> u8 {
97 match role { "face" => 0, "rough" => 1, "finish" | "chamfer" => 2, "spot" => 3, "drill" => 4, "tap" => 5, _ => 6 }
98}
99
100fn plan_with_fallback(input: &AutoInput<'_>, fallback: &[Tool]) -> Result<Plan, String> {
103 let mut combined = input.tools.to_vec();
104 combined.extend_from_slice(fallback);
105 let mut operations = Vec::<PlannedOperation>::new();
106 let mut reasons = BTreeMap::<String, Vec<String>>::new();
107 for rule in &input.techdb.rules {
108 for operation in &rule.operations {
109 let mut db = input.techdb.clone();
110 db.rules = vec![brep_cam::features::techdb::Rule { feature: rule.feature.clone(), operations: vec![operation.clone()] }];
111 let first = autoprogram::plan(&AutoInput { techdb: &db, ..*input }).map_err(|e| e.to_string())?;
112 let second = autoprogram::plan(&AutoInput { techdb: &db, tools: &combined, ..*input }).map_err(|e| e.to_string())?;
113 for feature in input.recognized.features.iter().filter(|f| f.kind.name() == rule.feature) {
114 let own: Vec<_> = first.operations.iter().filter(|p| p.feature == feature.id).cloned().collect();
115 let chosen: Vec<_> = if own.is_empty() { second.operations.iter().filter(|p| p.feature == feature.id).cloned().collect() } else { own };
116 if chosen.is_empty() {
117 if let Some(reason) = second.skipped.iter().find(|s| s.feature == feature.id) {
118 reasons.entry(feature.id.clone()).or_default().push(reason.reason.clone());
119 }
120 }
121 operations.extend(chosen);
122 }
123 }
124 }
125 let mut skipped = Vec::new();
126 for f in &input.recognized.features {
127 let why = reasons.remove(&f.id).unwrap_or_default();
128 if !operations.iter().any(|p| p.feature == f.id) {
129 skipped.push(autoprogram::Skipped { feature: f.id.clone(), kind: f.kind.name().into(), reason: if why.is_empty() {
130 f.reason.clone().unwrap_or_else(|| format!("no builtin technology rule for {}", f.kind.name()))
131 } else { why.join("; ") } });
132 } else {
133 for p in operations.iter_mut().filter(|p| p.feature == f.id) { p.notes.extend(why.clone()); }
134 }
135 }
136 let tool = |p: &PlannedOperation| combined.iter().find(|t| t.id == p.tool).expect("planner selected an input tool");
137 let index = |p: &PlannedOperation| input.recognized.features.iter().position(|f| f.id == p.feature).unwrap_or(usize::MAX);
138 operations.sort_by(|a, b| phase(&a.role).cmp(&phase(&b.role))
139 .then_with(|| if phase(&a.role) == 1 { tool(b).geometry.diameter.total_cmp(&tool(a).geometry.diameter) } else { std::cmp::Ordering::Equal })
140 .then(tool(a).number.cmp(&tool(b).number)).then(a.tool.as_str().cmp(b.tool.as_str())).then(index(a).cmp(&index(b))));
141 let mut ordered = Vec::<PlannedOperation>::new();
142 for ph in 0..=6 {
143 let group: Vec<_> = operations.iter().filter(|p| phase(&p.role) == ph).cloned().collect();
144 if ph != 1 {
145 if let Some(last) = ordered.last() {
146 let (same, rest): (Vec<_>, Vec<_>) = group.into_iter().partition(|p| p.tool == last.tool);
147 ordered.extend(same); ordered.extend(rest);
148 continue;
149 }
150 }
151 ordered.extend(group);
152 }
153 let tool_changes = ordered.windows(2).filter(|w| w[0].tool != w[1].tool).count() as u32;
154 Ok(Plan { operations: ordered, skipped, tool_changes })
155}
156
157fn recognize_setup(state: &EngineState, setup: &Setup) -> Result<(Recognized, brep_cam::MeshSnapshot), String> {
158 let tolerance = Tolerance::default();
159 let chord = 0.01;
160 let (mesh, unresolved) = machine_snapshot(setup, chord, tolerance, |name| state.scene.exact_solid(name)).map_err(|e| e.to_string())?;
161 if !unresolved.is_empty() { return Err(format!("exact topology unavailable for {}", unresolved.join(", "))); }
162 let mut carriers = Vec::new();
163 let mut regions = Vec::new();
164 let mut revolved = Vec::new();
165 for name in setup.targets.iter().map(|r| r.solid.as_str()).collect::<BTreeSet<_>>() {
166 let solid = state.scene.exact_solid(name).ok_or_else(|| format!("exact topology unavailable for `{name}`"))?;
167 let body = BodyInput { name, solid, protected_only: false };
168 carriers.extend(face_carriers(&body, setup, &tolerance));
169 regions.extend(horizontal_faces(solid, &setup.model_to_machine, chord, &tolerance));
170 revolved.extend(revolved_faces(&body, setup));
171 }
172 let mut sink = NullSink;
173 let mut runner = Runner::detached(&mut sink);
174 let holes = brep_cam::holes::recognize(&revolved, &mesh, tolerance, &mut runner).map_err(|e| e.to_string())?;
175 let recognized = brep_cam::features::recognize(&RecognitionInput { mesh: &mesh, carriers: &carriers, regions: ®ions, holes: &holes, tolerance }).map_err(|e| e.to_string())?;
176 Ok((recognized, mesh))
177}
178
179fn build_preview(state: &EngineState, doc: CamDocument, setup_id: &str) -> Result<Preview, String> {
180 let setup = doc.setup(setup_id).ok_or_else(|| format!("no setup `{setup_id}`"))?;
181 if setup.targets.is_empty() { return Err("select the setup's target bodies before programming".into()); }
182 let revisions = revisions(state, setup)?;
183 let (recognized, first_mesh) = recognize_setup(state, setup)?;
184 let stock_top = cam_setup::stock_bounds(setup, &state.scene).map(|(min, max)| {
185 (0..8).map(|i| setup.model_to_machine.apply_point(brep_cam::model::Point3::new(
186 if i & 1 == 0 { min.x } else { max.x }, if i & 2 == 0 { min.y } else { max.y }, if i & 4 == 0 { min.z } else { max.z })).z)
187 .fold(f64::NEG_INFINITY, f64::max)
188 });
189 let db = TechDb::builtin();
190 let machine = doc.setup_machine(setup_id);
191 if setup.machine.is_some() && machine.is_none() { return Err("the setup's selected machine is missing".into()); }
192 let extra = fallback_tools(&doc)?;
193 let input = AutoInput { recognized: &recognized, setup, tools: &doc.tools,
194 machine: machine.as_ref(), material: WorkMaterial::Aluminium, techdb: &db, units: LibraryUnits::Millimeters, stock_top };
195 let mut plan = plan_with_fallback(&input, &extra)?;
196 autoprogram::access::constrain_open_extensions(&mut plan, &recognized, &first_mesh, &Tolerance::default()).map_err(|e| e.to_string())?;
197 let axes = autoprogram::multi::needed_axes(&recognized);
198 let mut candidates = Vec::new();
199 let mut orientations = BTreeMap::new();
200 let mut candidate_meshes = BTreeMap::new();
201 if !axes.is_empty() {
202 let (min, max) = cam_setup::stock_bounds(setup, &state.scene)
203 .ok_or("additional setups need a resolved initial stock boundary; regenerate/review the carried setup first")?;
204 let bounds = brep_cam::model::Bounds3 { min, max };
205 for axis in axes {
206 let mut proposed = autoprogram::multi::candidate_setup(setup, axis, bounds).map_err(|e| e.to_string())?;
207 let source = recognized.features.iter().find(|f| !f.machinable && f.direction.dot(axis.vector()) > 1. - 1e-6).ok_or("approach feature disappeared")?;
208 let face = source.faces.first().ok_or("approach feature has no persistent face")?;
209 let origin = setup.machine_to_model().apply_point(brep_cam::model::Point3::ZERO);
210 let mut orientation = crate::document::cam::SetupOrientation { z_ref: Some(face.clone()),
211 origin_ref: Some(format!("auto@{},{},{}", origin.x, origin.y, origin.z)), ..Default::default() };
212 let mut temporary = doc.clone();
213 temporary.setups.push(proposed.clone());
214 temporary.set_orientation(proposed.id.as_str(), orientation.clone());
215 let frame = cam::setup_frame(state, &temporary, proposed.id.as_str())?;
216 let expected = setup.machine_to_model().apply_vector(axis.vector());
217 orientation.flip_z = frame.z_axis.dot(expected) < 0.;
218 temporary.set_orientation(proposed.id.as_str(), orientation.clone());
219 proposed.model_to_machine = cam::setup_frame(state, &temporary, proposed.id.as_str())?.transform()?;
220 let top = proposed.model_to_machine.apply_bounds(&bounds).max.z;
221 proposed.heights.clearance = top + 20.; proposed.heights.retract = top + 10.; proposed.heights.feed = top + 5.;
222 orientations.insert(proposed.id.to_string(), orientation);
223 let (recognized, mesh) = recognize_setup(state, &proposed)?;
224 candidate_meshes.insert(proposed.id.to_string(), mesh);
225 candidates.push(autoprogram::multi::Candidate { axis,
226 stock_top: Some(proposed.model_to_machine.apply_bounds(&bounds).max.z), setup: proposed, recognized });
227 }
228 }
229 let mut multi = autoprogram::multi::plan_candidates(&input, plan, &candidates, |input| {
230 let mut plan = plan_with_fallback(input, &extra).map_err(brep_cam::CamError::InvalidInput)?;
231 let mesh = candidate_meshes.get(input.setup.id.as_str()).ok_or_else(|| brep_cam::CamError::InvalidInput("candidate mesh disappeared".into()))?;
232 autoprogram::access::constrain_open_extensions(&mut plan, input.recognized, mesh, &Tolerance::default())?;
233 Ok(plan)
234 }).map_err(|e| e.to_string())?;
235 let first = multi.setups.remove(0);
236 let plan = first.plan;
237 let selected = plan.operations.iter().map(|p| p.feature.clone()).collect();
238 let additional = multi.setups.into_iter().map(|proposal| SetupPreview {
239 orientation: orientations.remove(proposal.setup.id.as_str()).expect("candidate orientation"),
240 selected: proposal.plan.operations.iter().map(|p| p.feature.clone()).collect(), proposal, enabled: true,
241 }).collect();
242 let fallback = extra.iter().map(|t| t.id.to_string()).collect();
243 let mut tools = doc.tools.clone(); tools.extend(extra);
244 Ok(Preview { setup: setup_id.into(), enabled: true, additional, unreached: multi.unreached,
245 document: doc, revisions, recognized, plan, tools, fallback, selected })
246}
247
248impl AutoProgramPanel {
249 pub fn preview(&mut self, state: &mut EngineState, setup: &str) -> Result<(), String> {
250 if let Some(why) = cam::sync_setup_frame(state, setup) { return Err(why); }
251 let doc = cam::cam_document(state)?;
252 let setup_record = doc.setup(setup).ok_or_else(|| format!("no setup `{setup}`"))?;
253 let names = body_names(setup_record);
254 revisions(state, setup_record)?;
255 state.request_exact_solids(names.iter().map(String::as_str));
256 self.preview = None;
257 self.error = None;
258 self.pending = Some(setup.into());
259 self.open = true;
260 self.poll(state);
261 Ok(())
262 }
263
264 pub fn poll(&mut self, state: &mut EngineState) {
265 let Some(id) = self.pending.clone() else { return };
266 state.pump_topology();
267 if state.topology_pending() { return; }
268 self.pending = None;
269 match cam::cam_document(state).and_then(|doc| build_preview(state, doc, &id)) {
270 Ok(preview) => self.preview = Some(preview),
271 Err(why) => self.error = Some(why),
272 }
273 }
274
275 pub fn select(&mut self, feature: &str, selected: bool) -> Result<(), String> {
276 let p = self.preview.as_mut().ok_or("preview the part first")?;
277 let (recognized, plan, selection) = if p.recognized.features.iter().any(|f| f.id == feature) {
278 (&p.recognized, &p.plan, &mut p.selected)
279 } else if let Some(s) = p.additional.iter_mut().find(|s| s.proposal.recognized.features.iter().any(|f| f.id == feature)) {
280 (&s.proposal.recognized, &s.proposal.plan, &mut s.selected)
281 } else { return Err(format!("no recognized feature `{feature}`")); };
282 let _ = recognized;
283 if selected && !plan.operations.iter().any(|p| p.feature == feature) { return Err(format!("feature `{feature}` has no planned operations")); }
284 if selected { selection.insert(feature.into()); } else { selection.remove(feature); }
285 Ok(())
286 }
287
288 pub fn select_setup(&mut self, setup: &str, selected: bool) -> Result<(), String> {
289 let p = self.preview.as_mut().ok_or("preview the part first")?;
290 if p.setup == setup { p.enabled = selected; }
291 else if let Some(s) = p.additional.iter_mut().find(|s| s.proposal.setup.id.as_str() == setup) { s.enabled = selected; }
292 else { return Err(format!("no proposed setup `{setup}`")); }
293 Ok(())
294 }
295
296 fn current<'a>(&'a self, state: &EngineState) -> Result<&'a Preview, String> {
297 let preview = self.preview.as_ref().ok_or("preview the part first")?;
298 let doc = cam::cam_document(state)?;
299 if doc != preview.document || revisions(state, doc.setup(&preview.setup).ok_or("setup was removed")?)? != preview.revisions {
300 return Err("the model or CAM inputs changed; preview the part again".into());
301 }
302 Ok(preview)
303 }
304
305 pub fn create(&mut self, state: &mut EngineState) -> Result<Vec<String>, String> {
306 let p = self.current(state)?;
307 let mut doc = p.document.clone();
308 let first = doc.setup(&p.setup).ok_or("setup was removed")?.clone();
309 let mut groups = Vec::new();
310 if p.enabled { groups.push((None, first.clone(), &p.recognized, &p.plan, &p.selected)); }
311 for s in p.additional.iter().filter(|s| s.enabled) {
312 groups.push((Some(s.orientation.clone()), s.proposal.setup.clone(), &s.proposal.recognized, &s.proposal.plan, &s.selected));
313 }
314 if !groups.iter().any(|(_,_,_,plan,selection)| plan.operations.iter().any(|op| selection.contains(&op.feature))) {
315 return Err("select at least one programmable feature".into());
316 }
317 let fingerprints: BTreeMap<_, _> = body_names(&first).into_iter().map(|name| {
318 let (_, hash) = state.scene.exact_solid_fingerprinted(&name).ok_or_else(|| format!("exact body `{name}` changed; preview again"))?;
319 Ok((name, hash))
320 }).collect::<Result<_, String>>()?;
321 let mut tools = BTreeMap::<String, String>::new();
322 for (_, _, _, plan, selection) in &groups {
323 for planned in plan.operations.iter().filter(|op| selection.contains(&op.feature)) {
324 let id = planned.tool.to_string();
325 if tools.contains_key(&id) { continue; }
326 if !p.fallback.contains(&id) { tools.insert(id.clone(), id); continue; }
327 let mut tool = p.tools.iter().find(|t| t.id == planned.tool).ok_or("planned tool disappeared")?.clone();
328 let new_id = doc.next_id("T");
329 tool.id = new_id.as_str().into();
330 tool.number = doc.tools.iter().map(|t| t.number).max().unwrap_or(0).checked_add(1).ok_or("tool numbers are exhausted")?;
331 if let Some(holder) = &mut tool.holder { holder.id = format!("{new_id}-holder").into(); }
332 doc.tools.push(tool); tools.insert(id, new_id);
333 }
334 }
335 let mut created = Vec::new();
336 let mut previous = first.id.clone();
337 for (orientation, mut setup, recognized, plan, selection) in groups {
338 let selected: Vec<_> = plan.operations.iter().filter(|op| selection.contains(&op.feature)).collect();
339 if selected.is_empty() { continue; }
340 if let Some(orientation) = orientation {
341 setup.id = doc.next_id("SETUP").into();
342 setup.stock = brep_cam::model::Stock::FromRest { setup: previous.clone() };
344 doc.set_orientation(setup.id.as_str(), orientation);
345 doc.setups.push(setup.clone());
346 }
347 for planned in selected {
348 let mut planned = planned.clone();
349 let id = doc.next_id("OP");
350 planned.operation.id = id.as_str().into();
351 planned.operation.setup = setup.id.clone();
352 planned.operation.tool = tools[planned.tool.as_str()].as_str().into();
353 planned.tool = planned.operation.tool.clone();
354 let tool = doc.tool(planned.tool.as_str()).ok_or("planned tool disappeared")?;
355 let feature = recognized.features.iter().find(|f| f.id == planned.feature).ok_or("planned feature disappeared")?;
356 let binding = brep_cam::autoprogram::binding::Binding::new(&planned, feature, &setup, tool, fingerprints.clone());
357 doc.autoprogram_inputs.insert(id.clone(), binding);
358 doc.operations.push(planned.operation); created.push(id);
359 }
360 previous = setup.id;
361 }
362 cam::write_cam_document(state, &doc, None);
363 self.close();
364 Ok(created)
365 }
366
367 pub fn state(&self, state: &EngineState) -> Value {
368 let programmed: Vec<_> = cam::cam_document(state).ok().map(|doc| doc.operations.iter().filter_map(|op| {
369 let binding = doc.autoprogram_inputs.get(op.id.as_str())?;
370 let path = state.cam.results.get(op.id.as_str()).and_then(|r| r.toolpath.as_ref());
371 let minimum_cut_z = path.and_then(|path| path.motions.iter().filter_map(|m| match &m.kind {
372 brep_cam::model::MotionKind::Cut { to } | brep_cam::model::MotionKind::ArcCw { to, .. } | brep_cam::model::MotionKind::ArcCcw { to, .. } => Some(to.z), _ => None,
373 }).reduce(f64::min));
374 Some(json!({ "id": op.id, "setup": op.setup, "feature": binding.feature.id, "kind": binding.feature.kind,
375 "status": crate::panels::cam_generate::status_of(state, &doc, op.id.as_str()).word(), "minimumCutZ": minimum_cut_z,
376 "warnings": path.map(|p| &p.report.warnings), "reRecognised": path.is_some_and(|p| p.report.warnings.iter().any(|w| w.code == "autoprogram-re-recognised")) }))
377 }).collect()).unwrap_or_default();
378 let all_programmed_ok = !programmed.is_empty() && programmed.iter().all(|op| op["status"] == "ok");
379 let Some(p) = &self.preview else { return json!({ "open": self.open, "busy": self.pending.is_some(), "error": self.error, "features": [], "operations": [], "allProgrammedOk": all_programmed_ok, "programmed": programmed }); };
380 let feature_rows = |recognized: &Recognized, plan: &Plan, selected: &BTreeSet<String>| recognized.features.iter().map(|f| json!({
381 "id": f.id, "kind": f.kind.name(), "solid": f.solid, "selected": selected.contains(&f.id),
382 "operations": plan.operations.iter().filter(|op| op.feature == f.id).map(|op| json!({
383 "id": op.operation.id, "name": op.operation.name, "strategy": op.operation.strategy.kind_name(),
384 "role": op.role, "tool": op.tool, "toolName": p.tools.iter().find(|t| t.id == op.tool).map(|t| &t.name),
385 "toolSource": if p.fallback.contains(op.tool.as_str()) { "builtin" } else { "document" }, "notes": op.notes
386 })).collect::<Vec<_>>(),
387 "refusals": plan.skipped.iter().filter(|s| s.feature == f.id).map(|s| &s.reason).collect::<Vec<_>>()
388 })).collect::<Vec<_>>();
389 let features = feature_rows(&p.recognized, &p.plan, &p.selected);
390 let first = p.document.setup(&p.setup).expect("preview setup");
391 let mut setups = vec![json!({ "id": p.setup, "name": first.name, "axis": "+Z", "selected": p.enabled,
392 "additional": false, "orientation": first.model_to_machine, "stock": first.stock,
393 "features": features, "operationCount": if p.enabled { p.plan.operations.iter().filter(|op| p.selected.contains(&op.feature)).count() } else { 0 } })];
394 let mut previous = p.setup.clone();
395 for proposed in &p.additional {
396 let proposal = &proposed.proposal;
397 let count = if proposed.enabled { proposal.plan.operations.iter().filter(|op| proposed.selected.contains(&op.feature)).count() } else { 0 };
398 setups.push(json!({ "id": proposal.setup.id, "name": proposal.setup.name, "axis": proposal.axis.map(|a| a.name()),
399 "selected": proposed.enabled, "additional": true, "orientation": proposal.setup.model_to_machine,
400 "stock": brep_cam::model::Stock::FromRest { setup: previous.as_str().into() }, "covered": proposal.covered,
401 "features": feature_rows(&proposal.recognized, &proposal.plan, &proposed.selected), "operationCount": count }));
402 if count > 0 { previous = proposal.setup.id.to_string(); }
403 }
404 let operation_count: u64 = setups.iter().filter_map(|s| s["operationCount"].as_u64()).sum();
405
406 json!({ "open": self.open, "busy": false, "error": self.error, "stale": self.current(state).is_err(), "allProgrammedOk": all_programmed_ok, "programmed": programmed,
407 "setup": p.setup, "setups": setups, "setupCount": setups.len(), "unreached": p.unreached, "features": features, "featureCount": features.len(),
408 "selectedCount": p.selected.len(), "operationCount": operation_count,
409 "unassignedFaces": p.recognized.unassigned, "material": "aluminium",
410 "notice": "Document tools are preferred. Tools labelled Builtin are proposed fallback tools. Cutting data assumes aluminium." })
411 }
412
413 pub fn close(&mut self) { self.open = false; self.pending = None; self.preview = None; self.error = None; }
414
415 pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState, hits: &mut HashMap<String, egui::Rect>) {
418 self.poll(state);
419 if !self.open { return; }
420 ui.separator();
421 ui.heading("Program part automatically");
422 ui.label("Preview every feature before creating operations. Builtin tools are proposed only where document tools do not fit. Cutting data: aluminium.");
423 if self.pending.is_some() { ui.spinner(); ui.label("Reading exact part geometry…"); }
424 if let Some(why) = &self.error { ui.colored_label(egui::Color32::RED, why); }
425 if let Some(p) = &mut self.preview {
426 ui.label("Select setups before creating operations. Later setups continue from the preceding selected setup's stock.");
427 let mut groups = vec![(p.setup.as_str(), "Current setup", &mut p.enabled, &p.recognized, &p.plan, &mut p.selected)];
428 for extra in &mut p.additional {
429 groups.push((extra.proposal.setup.id.as_str(), extra.proposal.setup.name.as_str(), &mut extra.enabled,
430 &extra.proposal.recognized, &extra.proposal.plan, &mut extra.selected));
431 }
432 for (id, name, enabled, recognized, plan, selected) in groups {
433 let response = ui.checkbox(enabled, name);
434 hits.insert(format!("cam:auto:setup:{id}"), response.rect);
435 if id != p.setup { ui.small("Stock: result of the preceding selected setup. Refixture and verify the work offset before running this setup."); }
436 ui.add_enabled_ui(*enabled, |ui| {
437 for feature in &recognized.features {
438 let ops: Vec<_> = plan.operations.iter().filter(|op| op.feature == feature.id).collect();
439 let mut checked = selected.contains(&feature.id);
440 let response = ui.add_enabled(!ops.is_empty(), egui::Checkbox::new(&mut checked, format!("{} · {} · {}", feature.id, feature.kind.name(), feature.solid)));
441 hits.insert(format!("cam:auto:feature:{}", feature.id), response.rect);
442 if response.changed() { if checked { selected.insert(feature.id.clone()); } else { selected.remove(&feature.id); } }
443 for op in ops {
444 let tool = p.tools.iter().find(|t| t.id == op.tool).expect("planned tool");
445 ui.label(format!("{} — {} ({})", op.operation.name, tool.name, if p.fallback.contains(op.tool.as_str()) { "Builtin fallback" } else { "Document tool" }));
446 for note in &op.notes { ui.small(note); }
447 }
448 for refusal in plan.skipped.iter().filter(|s| s.feature == feature.id) { ui.label(format!("Not programmed here: {}", refusal.reason)); }
449 }
450 });
451 }
452 for refusal in &p.unreached { ui.label(format!("Not reached: {}", refusal.reason)); }
453 }
454 if let Err(why) = self.current(state) {
455 if self.preview.is_some() { ui.colored_label(egui::Color32::YELLOW, why); }
456 }
457 ui.horizontal(|ui| {
458 let create = ui.add_enabled(self.current(state).is_ok(), egui::Button::new("Create selected operations"));
459 hits.insert("cam:auto:create".into(), create.rect);
460 if create.clicked() { if let Err(why) = self.create(state) { self.error = Some(why); } }
461 let close = ui.button("Close");
462 hits.insert("cam:auto:close".into(), close.rect);
463 if close.clicked() { self.close(); }
464 });
465 }
466}
467
468#[cfg(feature = "automation")]
469#[derive(serde::Deserialize, schemars::JsonSchema)]
470#[serde(deny_unknown_fields)]
471pub struct CommandArgs {
472 pub action: String,
474 #[serde(default)]
475 pub setup: Option<String>,
476 #[serde(default)]
477 pub feature: Option<String>,
478 #[serde(default)]
479 pub selected: Option<bool>,
480}
481
482#[cfg(feature = "automation")]
483pub fn command(ctx: &mut crate::automation::command::Ctx<'_>, args: Value) -> Result<crate::automation::command::Outcome, String> {
484 use crate::automation::command::{parse_args, Outcome};
485 let args: CommandArgs = parse_args(args)?;
486 ctx.app.cam.autoprogram.poll(ctx.app.docs.engine_mut());
487 match args.action.as_str() {
488 "preview" => {
489 let setup = args.setup.ok_or("preview requires setup")?;
490 ctx.app.cam.set_open(None);
491 ctx.app.cam.autoprogram.preview(ctx.app.docs.engine_mut(), &setup)?;
492 }
493 "select" => ctx.app.cam.autoprogram.select(&args.feature.ok_or("select requires feature")?, args.selected.ok_or("select requires selected")?)?,
494 "selectSetup" => ctx.app.cam.autoprogram.select_setup(&args.setup.ok_or("selectSetup requires setup")?, args.selected.ok_or("selectSetup requires selected")?)?,
495 "create" => return Ok(Outcome::Done(json!({ "created": ctx.app.cam.autoprogram.create(ctx.app.docs.engine_mut())? }))),
496 "close" => ctx.app.cam.autoprogram.close(),
497 "state" => {},
498 _ => return Err("action must be preview, state, select, selectSetup, create or close".into()),
499 }
500 Ok(Outcome::Done(ctx.app.cam.autoprogram.state(ctx.app.docs.engine())))
501}