use super::{EngineState, RefSelectState, RefSelectTarget};
use crate::engine_state::selection_filter::SelectionFilter;
use serde_json::Value;
pub fn set_cam_json_at(root: &mut Value, path: &[String], value: Value) -> Result<(), String> {
let Some((last, parents)) = path.split_last() else {
*root = value;
return Ok(());
};
let mut cur = root;
for seg in parents {
cur = step_into(cur, seg)?;
}
if let Some(id) = last.strip_prefix('#') {
let slot = select_by_id(cur, id)?;
*slot = value;
return Ok(());
}
if !cur.is_object() {
*cur = Value::Object(serde_json::Map::new());
}
cur.as_object_mut().expect("object by construction").insert(last.clone(), value);
Ok(())
}
fn step_into<'a>(cur: &'a mut Value, seg: &str) -> Result<&'a mut Value, String> {
if let Some(id) = seg.strip_prefix('#') {
return select_by_id(cur, id);
}
if !cur.is_object() {
*cur = Value::Object(serde_json::Map::new());
}
Ok(cur
.as_object_mut()
.expect("object by construction")
.entry(seg.to_string())
.or_insert(Value::Object(serde_json::Map::new())))
}
fn select_by_id<'a>(cur: &'a mut Value, id: &str) -> Result<&'a mut Value, String> {
let Some(array) = cur.as_array_mut() else {
return Err(format!("`#{id}` addresses a record, but the path is not at a list"));
};
array
.iter_mut()
.find(|record| record.get("id").and_then(Value::as_str) == Some(id))
.ok_or_else(|| format!("no CAM record `{id}`"))
}
impl EngineState {
#[allow(clippy::too_many_arguments)]
pub fn begin_ref_select_for_cam(
&mut self,
id: &str,
path: Vec<String>,
label: String,
filter: Vec<String>,
multiple: bool,
seed_names: Vec<String>,
) {
let restore_index = self.history.rollback();
self.selection_filter = SelectionFilter::from_ref_filter(&filter);
self.ref_select = Some(RefSelectState {
feature_id: id.to_string(),
path,
label,
filter,
multiple,
names: seed_names,
restore_index,
target: RefSelectTarget::Cam,
picks: 0,
claimed: false,
finish_requested: false,
});
self.sync_ref_select_emphasis();
}
pub(crate) fn cam_commit_refs(&mut self, path: &[String], names: &[String], multiple: bool) -> Result<(), String> {
let mut block = self.history.cam_block().cloned().ok_or_else(|| "the document has no CAM data".to_string())?;
let value = if multiple {
Value::Array(names.iter().cloned().map(Value::String).collect())
} else {
Value::String(names.first().cloned().unwrap_or_default())
};
set_cam_json_at(&mut block, path, value)?;
self.history.set_cam_block(Some(block), None);
self.history.break_coalescing();
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct CamOutcome {
pub turning_profile: Option<brep_cam::turning::profile::TurningProfile>,
pub setup_start: Option<brep_cam::SetupStart>,
pub toolpath: Option<brep_cam::model::Toolpath>,
pub error: Option<String>,
pub inputs_hash: u64,
pub work_units: u64,
}
#[derive(Debug, Default)]
pub struct CamGeneration {
queue: std::collections::VecDeque<(crate::runner::CamJob, u64)>,
in_flight: Option<(u64, String, u64)>,
next_id: u64,
cancelled: std::collections::BTreeSet<u64>,
pub results: std::collections::BTreeMap<String, CamOutcome>,
pub results_stamp: u64,
pub progress: Option<crate::runner::CamProgress>,
pub notice: Option<String>,
pub job_builder: Option<CamJobBuilder>,
}
pub type CamJobBuilder = fn(&EngineState, &str) -> Result<(crate::runner::CamJob, u64), String>;
impl CamGeneration {
pub fn busy(&self) -> bool {
self.in_flight.is_some() || !self.queue.is_empty()
}
pub fn queued(&self) -> Vec<String> {
self.queue.iter().map(|(job, _)| job.operation.id.as_str().to_string()).collect()
}
pub fn in_flight(&self) -> Option<&str> {
self.in_flight.as_ref().map(|(_, op, _)| op.as_str())
}
pub fn clear(&mut self) {
if let Some((id, _, _)) = self.in_flight.take() {
self.cancelled.insert(id);
}
self.queue.clear();
self.results.clear();
self.results_stamp += 1;
self.progress = None;
self.notice = None;
}
}
impl EngineState {
pub fn cam_named_axis(&self, name: &str) -> Option<([f64; 3], [f64; 3])> {
self.sketch_axes
.iter()
.find(|(n, _)| n == name)
.map(|(_, a)| ([a.point.x, a.point.y, a.point.z], [a.direction.x, a.direction.y, a.direction.z]))
}
#[allow(clippy::type_complexity)]
pub fn cam_chain_geometry(
&self,
chains: &[brep_cam::model::ChainRef],
) -> Result<(Vec<(String, String, brep_kernel::NurbsCurve)>, Vec<(String, brep_kernel::SketchProfile)>), String> {
use brep_cam::model::ChainSource;
let mut curves: Vec<(String, String, brep_kernel::NurbsCurve)> = Vec::new();
let mut profiles: Vec<(String, brep_kernel::SketchProfile)> = Vec::new();
for chain in chains {
match &chain.source {
ChainSource::SketchCurves { sketch, curves: ids } => {
for id in ids {
if curves.iter().any(|(s, g, _)| s == sketch && g == id) {
continue;
}
let name = format!("{sketch}:G{id}");
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)"))?;
match entry.1.as_slice() {
[curve] => curves.push((sketch.clone(), id.clone(), curve.clone())),
other => return Err(format!("sketch curve `{name}` is {} curves, not one", other.len())),
}
}
}
ChainSource::SketchProfile { sketch, .. } => {
if profiles.iter().any(|(s, _)| s == sketch) {
continue;
}
let profile = self.sketch_profiles.iter().find(|(n, _)| n == sketch).ok_or_else(|| format!("sketch `{sketch}` has no closed profile in the model"))?;
profiles.push((sketch.clone(), profile.1.clone()));
}
ChainSource::Edges { .. } => {}
}
}
Ok((curves, profiles))
}
}
impl EngineState {
pub fn cam_enqueue(&mut self, jobs: Vec<(crate::runner::CamJob, u64)>) -> Vec<String> {
let mut queued = Vec::new();
for (mut job, hash) in jobs {
let op = job.operation.id.as_str().to_string();
let already = self.cam.in_flight().is_some_and(|f| f == op) || self.cam.queue.iter().any(|(j, _)| j.operation.id.as_str() == op);
if already {
continue;
}
self.cam.next_id += 1;
job.id = self.cam.next_id;
queued.push(op);
self.cam.queue.push_back((job, hash));
}
self.cam.notice = None;
self.cam_pump();
self.dirty = true;
queued
}
pub fn cam_cancel(&mut self) -> usize {
let dropped = self.cam.queue.len() + usize::from(self.cam.in_flight.is_some());
self.cam.queue.clear();
if let Some((id, op, _)) = self.cam.in_flight.take() {
self.cam.cancelled.insert(id);
self.runner.cancel_cam();
self.cam.notice = Some(format!("cancelled while generating {op}"));
} else if dropped > 0 {
self.cam.notice = Some("cancelled".to_string());
}
self.cam.progress = None;
self.dirty = true;
dropped
}
pub fn cam_pump(&mut self) {
while let Some(progress) = self.runner.poll_cam_progress() {
if self.cam.in_flight.as_ref().is_some_and(|(id, _, _)| *id == progress.id) {
self.cam.progress = Some(progress);
self.dirty = true;
}
}
while let Some(reply) = self.runner.poll_cam() {
let debug = std::env::var_os("BREP_CAM_DEBUG").is_some();
if self.cam.cancelled.remove(&reply.id) {
if debug {
eprintln!("cam reply {} ({}): dropped, cancelled", reply.id, reply.operation);
}
continue;
}
let Some((id, op, hash)) = self.cam.in_flight.clone() else {
if debug {
eprintln!("cam reply {} ({}): dropped, nothing in flight", reply.id, reply.operation);
}
continue;
};
if id != reply.id {
if debug {
eprintln!("cam reply {} ({}): dropped, in flight is {id} ({op})", reply.id, reply.operation);
}
continue;
}
if debug {
eprintln!("cam reply {} ({}): landed, ok {}", reply.id, reply.operation, reply.toolpath.is_ok());
}
self.cam.in_flight = None;
self.cam.progress = None;
let (toolpath, error) = match reply.toolpath {
Ok(path) => (Some(path), None),
Err(message) => (None, Some(message)),
};
self.cam.results_stamp += 1;
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 });
self.dirty = true;
}
while self.cam.in_flight.is_none() {
let Some((job, hash)) = self.cam.queue.pop_front() else { break };
let id = job.id;
let op = job.operation.id.as_str().to_string();
let rebuilt = match self.cam.job_builder {
Some(build) => build(self, &op).map(|(mut job, hash)| {
job.id = id;
(job, hash)
}),
None => Ok((job, hash)),
};
let (job, hash) = match rebuilt {
Ok(built) => built,
Err(why) => {
self.cam.results_stamp += 1;
self.cam.results.insert(op, CamOutcome { toolpath: None, error: Some(why), inputs_hash: hash, work_units: 0, turning_profile: None, setup_start: None });
self.dirty = true;
continue;
}
};
{
if self.runner.submit_cam(job) {
self.cam.in_flight = Some((id, op, hash));
} else {
self.cam.results_stamp += 1;
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 });
}
self.dirty = true;
}
}
}
pub fn cam_hold_replies(&mut self, hold: bool) {
self.runner.hold_cam_replies(hold);
}
pub fn cam_generation_json(&self) -> serde_json::Value {
serde_json::json!({
"busy": self.cam.busy(),
"inFlight": self.cam.in_flight(),
"queue": self.cam.queued(),
"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 })),
"notice": self.cam.notice,
"results": self.cam.results.iter().map(|(op, r)| (op.clone(), serde_json::json!({
"ok": r.error.is_none(),
"error": r.error,
"inputsHash": r.inputs_hash.to_string(),
"workUnits": r.work_units,
"motions": r.toolpath.as_ref().map(|t| t.motions.len()).unwrap_or(0),
"cuttingLength": r.toolpath.as_ref().map(|t| t.cutting_length()),
"status": r.toolpath.as_ref().map(|t| format!("{:?}", t.report.status)),
"warnings": r.toolpath.as_ref().map(|t| t.report.warnings.len()).unwrap_or(0),
}))).collect::<serde_json::Map<String, serde_json::Value>>(),
})
}
}