Skip to main content

brep_render/engine_state/
cam_ops.rs

1//! The CAM workbench's engine-side seams: the reference picker for a CAM
2//! record's field, committing into the document's `cam` block.
3//!
4//! The engine never interprets the block — the application's CAM document
5//! module owns its shape — so a pick is committed by JSON PATH from the
6//! block's root, with one convention that keeps a path stable while the
7//! picker runs: a `#<id>` segment selects, in the array the path is at, the
8//! object whose `id` is `<id>` (`["setups", "#SETUP1", "targets"]`), so a
9//! record is addressed by identity rather than by an index another edit
10//! could move. Nothing rolls and nothing re-runs: a CAM reference names the
11//! finished model, which is what is on screen.
12
13use super::{EngineState, RefSelectState, RefSelectTarget};
14use crate::engine_state::selection_filter::SelectionFilter;
15use serde_json::Value;
16
17/// Write `value` at `path` under `root`, creating objects on the way; a
18/// `#<id>` segment resolves to the element of the array at that point whose
19/// `id` is `<id>`. An `#<id>` segment on a non-array, or one that matches no
20/// element, is an error naming the segment — never a silent new record.
21pub fn set_cam_json_at(root: &mut Value, path: &[String], value: Value) -> Result<(), String> {
22    let Some((last, parents)) = path.split_last() else {
23        *root = value;
24        return Ok(());
25    };
26    let mut cur = root;
27    for seg in parents {
28        cur = step_into(cur, seg)?;
29    }
30    if let Some(id) = last.strip_prefix('#') {
31        let slot = select_by_id(cur, id)?;
32        *slot = value;
33        return Ok(());
34    }
35    if !cur.is_object() {
36        *cur = Value::Object(serde_json::Map::new());
37    }
38    cur.as_object_mut().expect("object by construction").insert(last.clone(), value);
39    Ok(())
40}
41
42fn step_into<'a>(cur: &'a mut Value, seg: &str) -> Result<&'a mut Value, String> {
43    if let Some(id) = seg.strip_prefix('#') {
44        return select_by_id(cur, id);
45    }
46    if !cur.is_object() {
47        *cur = Value::Object(serde_json::Map::new());
48    }
49    Ok(cur
50        .as_object_mut()
51        .expect("object by construction")
52        .entry(seg.to_string())
53        .or_insert(Value::Object(serde_json::Map::new())))
54}
55
56fn select_by_id<'a>(cur: &'a mut Value, id: &str) -> Result<&'a mut Value, String> {
57    let Some(array) = cur.as_array_mut() else {
58        return Err(format!("`#{id}` addresses a record, but the path is not at a list"));
59    };
60    array
61        .iter_mut()
62        .find(|record| record.get("id").and_then(Value::as_str) == Some(id))
63        .ok_or_else(|| format!("no CAM record `{id}`"))
64}
65
66impl EngineState {
67    /// Enter reference-selection mode for CAM record `id`'s field at `path`
68    /// (from the block's root, see the module doc). Nothing rolls.
69    #[allow(clippy::too_many_arguments)]
70    pub fn begin_ref_select_for_cam(
71        &mut self,
72        id: &str,
73        path: Vec<String>,
74        label: String,
75        filter: Vec<String>,
76        multiple: bool,
77        seed_names: Vec<String>,
78    ) {
79        let restore_index = self.history.rollback();
80        self.selection_filter = SelectionFilter::from_ref_filter(&filter);
81        self.ref_select = Some(RefSelectState {
82            feature_id: id.to_string(),
83            path,
84            label,
85            filter,
86            multiple,
87            names: seed_names,
88            restore_index,
89            target: RefSelectTarget::Cam,
90            picks: 0,
91            claimed: false,
92            finish_requested: false,
93        });
94        self.sync_ref_select_emphasis();
95    }
96
97    /// Commit a finished CAM pick: the names (a list, or the first one)
98    /// written at `path` under the `cam` block as ONE undo step. A block that
99    /// is missing is an error (there is no record to write into); a path
100    /// that resolves to no record is the setter's error.
101    pub(crate) fn cam_commit_refs(&mut self, path: &[String], names: &[String], multiple: bool) -> Result<(), String> {
102        let mut block = self.history.cam_block().cloned().ok_or_else(|| "the document has no CAM data".to_string())?;
103        let value = if multiple {
104            Value::Array(names.iter().cloned().map(Value::String).collect())
105        } else {
106            Value::String(names.first().cloned().unwrap_or_default())
107        };
108        set_cam_json_at(&mut block, path, value)?;
109        self.history.set_cam_block(Some(block), None);
110        // A pick is a discrete change (it ends with Finish): the next edit of
111        // the same record's field is its own undo step.
112        self.history.break_coalescing();
113        Ok(())
114    }
115}
116
117
118// ============================================================================
119// Generation: the CAM jobs on the runner, their results and their progress.
120// ============================================================================
121
122/// One operation's generation outcome, kept by operation id. Derived data:
123/// never written to the document; dropped on a document switch; STALE when
124/// the inputs it was computed from (`inputs_hash`) have moved.
125#[derive(Debug, Clone)]
126pub struct CamOutcome {
127    pub turning_profile: Option<brep_cam::turning::profile::TurningProfile>,
128    /// The stock the operation's setup starts from, as generation resolved it.
129    pub setup_start: Option<brep_cam::SetupStart>,
130    pub toolpath: Option<brep_cam::model::Toolpath>,
131    pub error: Option<String>,
132    /// The hash of the inputs the job carried (the application computes it
133    /// over the operation, its setup, its tool and the bodies' revisions).
134    pub inputs_hash: u64,
135    pub work_units: u64,
136}
137
138/// The generation queue and its results. ONE job is on the runner at a
139/// time; the next is submitted when its reply lands, so a cancel between
140/// operations is immediate on every platform (see `runner::CamJob`).
141#[derive(Debug, Default)]
142pub struct CamGeneration {
143    queue: std::collections::VecDeque<(crate::runner::CamJob, u64)>,
144    in_flight: Option<(u64, String, u64)>,
145    next_id: u64,
146    /// Job ids cancelled while in flight: their reply is dropped.
147    cancelled: std::collections::BTreeSet<u64>,
148    pub results: std::collections::BTreeMap<String, CamOutcome>,
149    /// Bumped whenever `results` changes, so a viewer can key a cache on it
150    /// instead of comparing toolpaths.
151    pub results_stamp: u64,
152    pub progress: Option<crate::runner::CamProgress>,
153    /// What the last cancel or refusal said, for the pane.
154    pub notice: Option<String>,
155    /// Rebuilds an operation's job from the document as it stands, at the
156    /// moment it is submitted (the application's `job_for`): the job then
157    /// carries the results of the operations generated before it in the same
158    /// queue (lane C7 pre-operation stock) and any edit made while it waited.
159    /// `None`: the job is submitted as queued.
160    pub job_builder: Option<CamJobBuilder>,
161}
162
163/// See [`CamGeneration::job_builder`].
164pub type CamJobBuilder = fn(&EngineState, &str) -> Result<(crate::runner::CamJob, u64), String>;
165
166impl CamGeneration {
167    /// Whether a job is on the runner or waiting.
168    pub fn busy(&self) -> bool {
169        self.in_flight.is_some() || !self.queue.is_empty()
170    }
171
172    /// The operation ids waiting, in order, after the one in flight.
173    pub fn queued(&self) -> Vec<String> {
174        self.queue.iter().map(|(job, _)| job.operation.id.as_str().to_string()).collect()
175    }
176
177    /// The operation in flight.
178    pub fn in_flight(&self) -> Option<&str> {
179        self.in_flight.as_ref().map(|(_, op, _)| op.as_str())
180    }
181
182    /// Forget every result and job (a document switch).
183    pub fn clear(&mut self) {
184        if let Some((id, _, _)) = self.in_flight.take() {
185            self.cancelled.insert(id);
186        }
187        self.queue.clear();
188        self.results.clear();
189        self.results_stamp += 1;
190        self.progress = None;
191        self.notice = None;
192    }
193}
194
195impl EngineState {
196    /// A feature's published axis by name (`AxisFixture:Axis`, from the last
197    /// history run): a point on it and its direction, for a CAM setup's Z.
198    pub fn cam_named_axis(&self, name: &str) -> Option<([f64; 3], [f64; 3])> {
199        self.sketch_axes
200            .iter()
201            .find(|(n, _)| n == name)
202            .map(|(_, a)| ([a.point.x, a.point.y, a.point.z], [a.direction.x, a.direction.y, a.direction.z]))
203    }
204
205    /// The sketch curves and profiles `chains` name, from the last history
206    /// run: `{sketch}:G{id}` segments and each sketch's world profile. A name
207    /// the run did not produce is refused by name (the sketch was deleted,
208    /// rolled back past, or the id is stale), so the job never reaches the
209    /// generator with a hole in it.
210    #[allow(clippy::type_complexity)]
211    pub fn cam_chain_geometry(
212        &self,
213        chains: &[brep_cam::model::ChainRef],
214    ) -> Result<(Vec<(String, String, brep_kernel::NurbsCurve)>, Vec<(String, brep_kernel::SketchProfile)>), String> {
215        use brep_cam::model::ChainSource;
216        let mut curves: Vec<(String, String, brep_kernel::NurbsCurve)> = Vec::new();
217        let mut profiles: Vec<(String, brep_kernel::SketchProfile)> = Vec::new();
218        for chain in chains {
219            match &chain.source {
220                ChainSource::SketchCurves { sketch, curves: ids } => {
221                    for id in ids {
222                        if curves.iter().any(|(s, g, _)| s == sketch && g == id) {
223                            continue;
224                        }
225                        let name = format!("{sketch}:G{id}");
226                        let entry = self.sketch_paths.iter().find(|(n, _)| *n == name).ok_or_else(|| format!("sketch curve `{name}` is not in the model (run the history, or pick the chain again)"))?;
227                        match entry.1.as_slice() {
228                            [curve] => curves.push((sketch.clone(), id.clone(), curve.clone())),
229                            other => return Err(format!("sketch curve `{name}` is {} curves, not one", other.len())),
230                        }
231                    }
232                }
233                ChainSource::SketchProfile { sketch, .. } => {
234                    if profiles.iter().any(|(s, _)| s == sketch) {
235                        continue;
236                    }
237                    let profile = self.sketch_profiles.iter().find(|(n, _)| n == sketch).ok_or_else(|| format!("sketch `{sketch}` has no closed profile in the model"))?;
238                    profiles.push((sketch.clone(), profile.1.clone()));
239                }
240                ChainSource::Edges { .. } => {}
241            }
242        }
243        Ok((curves, profiles))
244    }
245}
246
247impl EngineState {
248    /// Queue operations for generation: each `(job, inputs_hash)` is one
249    /// runner job; an operation already queued or in flight is not queued
250    /// twice. The jobs' ids are minted here.
251    pub fn cam_enqueue(&mut self, jobs: Vec<(crate::runner::CamJob, u64)>) -> Vec<String> {
252        let mut queued = Vec::new();
253        for (mut job, hash) in jobs {
254            let op = job.operation.id.as_str().to_string();
255            let already = self.cam.in_flight().is_some_and(|f| f == op) || self.cam.queue.iter().any(|(j, _)| j.operation.id.as_str() == op);
256            if already {
257                continue;
258            }
259            self.cam.next_id += 1;
260            job.id = self.cam.next_id;
261            // A result for this operation is superseded the moment it is
262            // queued again: the pane shows it as regenerating.
263            queued.push(op);
264            self.cam.queue.push_back((job, hash));
265        }
266        self.cam.notice = None;
267        self.cam_pump();
268        self.dirty = true;
269        queued
270    }
271
272    /// Cancel: the queue is dropped at once; the job in flight is told to stop
273    /// at its next chunk where the runner can (the native thread) and its
274    /// reply is dropped either way; every result already landed stays.
275    pub fn cam_cancel(&mut self) -> usize {
276        let dropped = self.cam.queue.len() + usize::from(self.cam.in_flight.is_some());
277        self.cam.queue.clear();
278        if let Some((id, op, _)) = self.cam.in_flight.take() {
279            self.cam.cancelled.insert(id);
280            self.runner.cancel_cam();
281            self.cam.notice = Some(format!("cancelled while generating {op}"));
282        } else if dropped > 0 {
283            self.cam.notice = Some("cancelled".to_string());
284        }
285        self.cam.progress = None;
286        self.dirty = true;
287        dropped
288    }
289
290    /// Submit the next job when none is in flight and fold every landed reply
291    /// into the results. Called every frame with the other pumps.
292    pub fn cam_pump(&mut self) {
293        while let Some(progress) = self.runner.poll_cam_progress() {
294            if self.cam.in_flight.as_ref().is_some_and(|(id, _, _)| *id == progress.id) {
295                self.cam.progress = Some(progress);
296                self.dirty = true;
297            }
298        }
299        while let Some(reply) = self.runner.poll_cam() {
300            let debug = std::env::var_os("BREP_CAM_DEBUG").is_some();
301            if self.cam.cancelled.remove(&reply.id) {
302                if debug {
303                    eprintln!("cam reply {} ({}): dropped, cancelled", reply.id, reply.operation);
304                }
305                continue;
306            }
307            let Some((id, op, hash)) = self.cam.in_flight.clone() else {
308                if debug {
309                    eprintln!("cam reply {} ({}): dropped, nothing in flight", reply.id, reply.operation);
310                }
311                continue;
312            };
313            if id != reply.id {
314                if debug {
315                    eprintln!("cam reply {} ({}): dropped, in flight is {id} ({op})", reply.id, reply.operation);
316                }
317                continue;
318            }
319            if debug {
320                eprintln!("cam reply {} ({}): landed, ok {}", reply.id, reply.operation, reply.toolpath.is_ok());
321            }
322            self.cam.in_flight = None;
323            self.cam.progress = None;
324            let (toolpath, error) = match reply.toolpath {
325                Ok(path) => (Some(path), None),
326                Err(message) => (None, Some(message)),
327            };
328            self.cam.results_stamp += 1;
329            self.cam.results.insert(op, CamOutcome { turning_profile: reply.turning_profile, setup_start: reply.setup_start, toolpath, error, inputs_hash: hash, work_units: reply.work_units });
330            self.dirty = true;
331        }
332        while self.cam.in_flight.is_none() {
333            let Some((job, hash)) = self.cam.queue.pop_front() else { break };
334            let id = job.id;
335            let op = job.operation.id.as_str().to_string();
336            // Built again at dispatch: an operation that cannot be built now
337            // (its inputs went bad while it waited) records why, by name.
338            let rebuilt = match self.cam.job_builder {
339                Some(build) => build(self, &op).map(|(mut job, hash)| {
340                    job.id = id;
341                    (job, hash)
342                }),
343                None => Ok((job, hash)),
344            };
345            let (job, hash) = match rebuilt {
346                Ok(built) => built,
347                Err(why) => {
348                    self.cam.results_stamp += 1;
349                    self.cam.results.insert(op, CamOutcome { toolpath: None, error: Some(why), inputs_hash: hash, work_units: 0, turning_profile: None, setup_start: None });
350                    self.dirty = true;
351                    continue;
352                }
353            };
354            {
355                if self.runner.submit_cam(job) {
356                    self.cam.in_flight = Some((id, op, hash));
357                } else {
358                    self.cam.results_stamp += 1;
359            self.cam.results.insert(op, CamOutcome { turning_profile: None, setup_start: None, toolpath: None, error: Some("this runner cannot generate toolpaths".to_string()), inputs_hash: hash, work_units: 0 });
360                }
361                self.dirty = true;
362            }
363        }
364    }
365
366    /// The generation state for the pane and the automation.
367    /// TEST HOLD for automation scripts (see `HistoryRunner::hold_cam_replies`):
368    /// while held, a generation job stays in flight on this side however fast
369    /// the runner finished it, so a scripted cancel always meets it; releasing
370    /// lets the kept replies land (a cancelled one is dropped as usual).
371    pub fn cam_hold_replies(&mut self, hold: bool) {
372        self.runner.hold_cam_replies(hold);
373    }
374
375    pub fn cam_generation_json(&self) -> serde_json::Value {
376        serde_json::json!({
377            "busy": self.cam.busy(),
378            "inFlight": self.cam.in_flight(),
379            "queue": self.cam.queued(),
380            "progress": self.cam.progress.as_ref().map(|p| serde_json::json!({ "operation": p.operation, "phase": p.phase, "message": p.message, "current": p.current, "total": p.total })),
381            "notice": self.cam.notice,
382            "results": self.cam.results.iter().map(|(op, r)| (op.clone(), serde_json::json!({
383                "ok": r.error.is_none(),
384                "error": r.error,
385                "inputsHash": r.inputs_hash.to_string(),
386                "workUnits": r.work_units,
387                "motions": r.toolpath.as_ref().map(|t| t.motions.len()).unwrap_or(0),
388                "cuttingLength": r.toolpath.as_ref().map(|t| t.cutting_length()),
389                "status": r.toolpath.as_ref().map(|t| format!("{:?}", t.report.status)),
390                "warnings": r.toolpath.as_ref().map(|t| t.report.warnings.len()).unwrap_or(0),
391            }))).collect::<serde_json::Map<String, serde_json::Value>>(),
392        })
393    }
394}
395