use std::collections::{BTreeMap, HashMap};
use brep_cam::program::{assemble, OperationInput, ProgramInput, SetupInput};
use brep_render::engine_state::EngineState;
use egui;
use serde_json::{json, Value};
use crate::automation::hit_keys::HitKeyDoc;
use crate::document::cam::CamDocument;
use crate::panels::file::{SaveBody, SaveRequest};
#[derive(Clone, Debug, PartialEq)]
pub struct Posted {
pub program: String,
pub post: String,
pub operations: Vec<String>,
pub text: String,
pub file_name: String,
pub warnings: Vec<String>,
}
impl Posted {
pub fn to_json(&self) -> Value {
json!({
"program": self.program,
"post": self.post,
"operations": self.operations,
"text": self.text,
"fileName": self.file_name,
"warnings": self.warnings,
"lines": self.text.lines().count(),
})
}
}
pub fn program_input(state: &EngineState, doc: &CamDocument, id: &str) -> Result<ProgramInput, String> {
let program = doc.program(id).ok_or_else(|| format!("no NC program `{id}` (see cam_state ncPrograms)"))?;
if program.operations.is_empty() {
return Err(format!("NC program `{id}` has no operations"));
}
let mut setups: Vec<SetupInput> = Vec::new();
for op_id in &program.operations {
let op = doc.operation(op_id.as_str()).ok_or_else(|| format!("NC program `{id}` names a missing operation `{op_id}`"))?;
let tool = doc.tool(op.tool.as_str()).ok_or_else(|| format!("operation `{op_id}` names a missing tool `{}`", op.tool))?;
match crate::panels::cam_generate::status_of(state, doc, op_id.as_str()) {
crate::panels::cam_generate::OpStatus::Ok { .. } => {}
other => return Err(format!("operation `{op_id}` is {}: generate it first", other.word())),
}
let toolpath = state
.cam
.results
.get(op_id.as_str())
.and_then(|r| r.toolpath.clone())
.ok_or_else(|| format!("operation `{op_id}` has no generated toolpath: generate it first"))?;
let setup = doc.setup(op.setup.as_str()).ok_or_else(|| format!("operation `{op_id}` names a missing setup `{}`", op.setup))?;
let input = OperationInput { operation: op.clone(), tool: tool.clone(), toolpath };
match setups.last_mut() {
Some(last) if last.setup.id == setup.id => last.operations.push(input),
_ => setups.push(SetupInput { setup: setup.clone(), operations: vec![input] }),
}
}
let machine = doc.setup_machine(setups[0].setup.id.as_str());
let tool_change_seconds = machine.as_ref().map_or_else(|| brep_cam::model::Machine::default().tool_change_seconds, |m| m.tool_change_seconds);
Ok(ProgramInput { name: program.name.clone(), number: program.number, machine, setups, tool_change_seconds, ..Default::default() })
}
pub fn post_program(state: &EngineState, doc: &CamDocument, id: &str) -> Result<Posted, String> {
let input = program_input(state, doc, id)?;
let program = doc.program(id).expect("checked by program_input");
let operations: Vec<String> = program.operations.iter().map(|o| o.to_string()).collect();
let assembled = assemble(&input).map_err(|e| format!("NC program `{id}`: {e}"))?;
let output = run_post(&program.post, &program.properties, assembled)?;
Ok(Posted { program: id.to_string(), post: program.post.clone(), operations, text: output.text, file_name: output.file_name, warnings: output.warnings })
}
fn run_post(post: &str, properties: &std::collections::BTreeMap<String, serde_json::Value>, program: brep_cam::program::Program) -> Result<brep_cam::post::PostOutput, String> {
if brep_cam::post::builtin(post).is_none() {
return Err(format!("post `{post}` is not a built-in post (built-ins: {})", brep_cam::post::BUILTIN_POSTS.iter().map(|p| p.id).collect::<Vec<_>>().join(", ")));
}
let request = brep_cam::post::PostRequest { program, properties: properties.clone(), ..Default::default() };
brep_cam::post::run_builtin(post, &request).map_err(|e| format!("post `{post}`: {e}"))
}
#[derive(Default)]
pub struct CamPostPanel {
hits: HashMap<String, egui::Rect>,
pub posted: Option<Posted>,
pub error: Option<String>,
pub pending_save: Option<SaveRequest>,
pub program: Option<String>,
specs: HashMap<String, Result<BTreeMap<String, brep_cam::post::PropertySpec>, String>>,
}
fn post_field(key: &str, spec: &brep_cam::post::PropertySpec) -> Option<brep_render::style::FormField> {
use brep_render::style::FieldKind;
let kind = match spec.kind.as_str() {
"boolean" => FieldKind::Bool,
"enum" => FieldKind::Enum { variants: spec.values.iter().flatten().filter_map(Value::as_str).map(str::to_string).collect() },
"number" | "integer" => match spec.min {
Some(min) => FieldKind::BoundedScalar { min, max: spec.max.unwrap_or(f64::MAX), step: 1.0 },
None => FieldKind::Scalar { step: 1.0 },
},
_ => return None,
};
Some(brep_render::style::FormField { path: vec![key.to_string()], label: spec.title.clone(), group: String::new(), kind })
}
fn post_value(spec: &brep_cam::post::PropertySpec, written: Value) -> Option<Value> {
match spec.kind.as_str() {
"number" => written.as_f64().map(|n| json!(n)),
"integer" => written.as_f64().map(|n| json!(n.round() as i64)),
_ => Some(written),
}
}
fn shown_value<'a>(props: &'a BTreeMap<String, Value>, name: &str, spec: &'a brep_cam::post::PropertySpec) -> &'a Value {
props.get(name).unwrap_or(&spec.default)
}
impl CamPostPanel {
pub fn new() -> Self {
Self::default()
}
pub fn open(&mut self, state: &EngineState, doc: &CamDocument, id: &str) -> Result<&Posted, String> {
self.program = Some(id.to_string());
match post_program(state, doc, id) {
Ok(p) => {
self.error = None;
Ok(self.posted.insert(p))
}
Err(e) => {
self.posted = None;
self.error = Some(e.clone());
Err(e)
}
}
}
pub fn save(&mut self) -> Option<&SaveRequest> {
let p = self.posted.as_ref()?;
self.pending_save = Some(SaveRequest { title: format!("Save NC program {}", p.file_name), file_name: p.file_name.clone(), body: SaveBody::Text(p.text.clone()) });
self.pending_save.as_ref()
}
pub fn close(&mut self) {
self.posted = None;
self.error = None;
self.program = None;
}
fn specs_of(&mut self, post: &str) -> &Result<BTreeMap<String, brep_cam::post::PropertySpec>, String> {
self.specs.entry(post.to_string()).or_insert_with(|| match brep_cam::post::builtin(post) {
Some(b) => brep_cam::post::describe(b.source, b.id).map(|d| d.properties).map_err(|e| e.message),
None => Err(format!("post `{post}` is not a built-in post: its properties are set by name (cam_update_program)")),
})
}
pub fn hit_rects(&self) -> &HashMap<String, egui::Rect> {
&self.hits
}
pub fn state_json(&self) -> Value {
json!({
"open": self.posted.is_some() || self.error.is_some(),
"posted": self.posted.as_ref().map(|p| json!({ "program": p.program, "post": p.post, "operations": p.operations, "fileName": p.file_name, "lines": p.text.lines().count(), "warnings": p.warnings })),
"error": self.error,
})
}
pub fn show(&mut self, ui: &mut egui::Ui, doc: &CamDocument) -> Option<(String, BTreeMap<String, Value>)> {
self.hits.clear();
if self.posted.is_none() && self.error.is_none() {
return None;
}
let mut edit: Option<(String, BTreeMap<String, Value>)> = None;
let program = self.program.as_deref().and_then(|id| doc.program(id)).cloned();
ui.separator();
let mut save = false;
let mut close = false;
let title = self.posted.as_ref().map_or_else(|| "Post".to_string(), |p| format!("Post \u{00b7} {} ({})", p.file_name, p.post));
let header = egui::CollapsingHeader::new(title).id_salt("cam-post").default_open(true).show(ui, |ui| {
if let Some(e) = &self.error {
ui.colored_label(ui.visuals().error_fg_color, e);
}
if let Some(program) = &program {
let specs = self.specs_of(&program.post).clone();
let props = egui::CollapsingHeader::new(format!("Post properties ({})", program.post)).id_salt("cam-post-props").default_open(self.error.is_some()).show(ui, |ui| {
match &specs {
Err(e) => {
ui.label(egui::RichText::new(e).weak());
}
Ok(specs) => {
egui::Grid::new("cam-post-props-grid").num_columns(2).show(ui, |ui| {
for (name, spec) in specs {
ui.label(&spec.title).on_hover_text(if spec.description.is_empty() { name.as_str() } else { spec.description.as_str() });
let value = shown_value(&program.properties, name, spec).clone();
let mut changed: Option<Value> = None;
let key = format!("cam:post:prop:{name}");
let rect = match post_field(&key, spec) {
Some(field) => {
let mut doc = json!({ key.as_str(): value.clone() });
let mut actions = crate::form::FieldActions::default();
let (edited, rect) = ui.scope(|ui| {
ui.set_max_width(160.0);
crate::form::field_input(ui, &field, &mut doc, Some(&mut self.hits), &mut actions)
}).inner;
if edited {
changed = post_value(spec, doc[key.as_str()].take());
}
rect
}
None => {
let mut text = value.as_str().map(str::to_string).unwrap_or_else(|| if value.is_null() { String::new() } else { value.to_string() });
let r = crate::form::text_input(ui, &mut text, 160.0);
if r.lost_focus() && Value::String(text.clone()) != value { changed = Some(Value::String(text)); }
r.rect
}
};
self.hits.insert(key, rect);
if let Some(v) = changed {
edit = Some((program.id.clone(), BTreeMap::from([(name.clone(), v)])));
}
ui.end_row();
}
});
}
}
});
self.hits.insert("cam:post:props".into(), props.header_response.rect);
}
if let Some(p) = &self.posted {
for w in &p.warnings {
ui.label(egui::RichText::new(w).weak());
}
let mut text = p.text.as_str();
let preview = egui::ScrollArea::vertical().id_salt("cam-post-text").max_height(240.0).show(ui, |ui| {
ui.add(egui::TextEdit::multiline(&mut text).code_editor().desired_width(f32::INFINITY))
});
self.hits.insert("cam:post:text".into(), preview.inner_rect);
}
ui.horizontal(|ui| {
if self.posted.is_some() {
let b = ui.button("Save\u{2026}").on_hover_text("Save the NC file (a download in the browser)");
self.hits.insert("cam:post:save".into(), b.rect);
save = b.clicked();
}
let c = ui.button("Close");
self.hits.insert("cam:post:close".into(), c.rect);
close = c.clicked();
});
});
self.hits.insert("cam:post".into(), header.header_response.rect);
if save {
self.save();
}
if close {
self.close();
}
edit
}
}
pub static HIT_KEYS: &[HitKeyDoc] = &[
HitKeyDoc { panel: "cam", prefix: "cam:post:program:", meaning: "an NC program row's Post\u{2026} button: post it through its post and show the text", command: Some("cam_post_text") },
HitKeyDoc { panel: "cam", prefix: "cam:post:text", meaning: "the posted NC text's preview", command: None },
HitKeyDoc { panel: "cam", prefix: "cam:post:save", meaning: "save the posted NC file (the Save-file chooser on desktop, a download in the browser)", command: Some("cam_post_text") },
HitKeyDoc { panel: "cam", prefix: "cam:post:close", meaning: "close the post preview", command: None },
HitKeyDoc { panel: "cam", prefix: "cam:post:prop:", meaning: "one post property of the shown NC program (cam:post:prop:<name>): a checkbox, number, choice or text box; a change is written and the program posted again", command: Some("cam_update_program") },
HitKeyDoc { panel: "cam", prefix: "cam:post:props", meaning: "the shown NC program's post properties section", command: None },
HitKeyDoc { panel: "cam", prefix: "cam:post", meaning: "the post preview's header", command: None },
];