use crate::automation::command::{parse_args, schema_of, Annotations, CommandSpec, Ctx, Empty, Handler, NoArgs, Outcome, Phase};
use crate::document::Document;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct LoadArgs {
pub json: String,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ImportFormat {
Step,
Stl,
Obj,
Iges,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ImportArgs {
pub format: ImportFormat,
pub name: String,
pub base64: String,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExportFormat {
Brep,
Step,
Stl,
Iges,
Dxf,
Svg,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ExportArgs {
pub format: ExportFormat,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct IndexArgs {
pub index: usize,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CloseArgs {
pub index: usize,
#[serde(default)]
pub discard: bool,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PartAttributeArgs {
pub field: String,
pub value: Value,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct Tab {
pub index: usize,
pub title: String,
pub name: Option<String>,
pub dirty: bool,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct Tabs {
pub active: usize,
pub tabs: Vec<Tab>,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct Exported {
pub format: String,
pub text: String,
pub bytes: usize,
}
fn tabs(ctx: &Ctx<'_>) -> Tabs {
let docs = &ctx.app.docs;
Tabs {
active: docs.active_index(),
tabs: docs
.iter()
.enumerate()
.map(|(i, d)| Tab { index: i, title: d.title(), name: d.name().map(str::to_string), dirty: d.is_dirty() })
.collect(),
}
}
fn doc_new(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let mut engine = ctx.app.docs.spawn_engine();
let _ = engine.set_history_json(crate::document::EMPTY_DOCUMENT);
let index = ctx.app.docs.open_document(Document::new(engine));
Ok(Outcome::Done(json!({ "index": index })))
}
fn doc_load(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: LoadArgs = parse_args(args)?;
let mut engine = ctx.app.docs.spawn_engine();
let report = engine.load_model_and_fit(&a.json)?;
let mut doc = Document::new(engine);
doc.set_name(a.name.clone());
doc.mark_clean();
let index = ctx.app.docs.open_document(doc);
Ok(Outcome::Done(json!({ "index": index, "name": a.name, "report": parse(&report) })))
}
fn doc_json(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let text = ctx.app.docs.engine().history_request_json();
Ok(Outcome::Done(json!({ "document": parse(&text), "bytes": text.len() })))
}
fn part_attributes_get(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let attributes = ctx.app.docs.engine().document_part_attributes();
Ok(Outcome::Done(json!({ "attributes": attributes })))
}
fn part_attribute_set(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: PartAttributeArgs = parse_args(args)?;
let engine = ctx.app.docs.engine_mut();
engine.set_document_part_attribute(&a.field, a.value)?;
Ok(Outcome::Done(json!({ "attributes": engine.document_part_attributes() })))
}
fn doc_import(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: ImportArgs = parse_args(args)?;
let bytes = base64_decode(&a.base64)?;
let engine = ctx.app.docs.engine_mut();
let report = match a.format {
ImportFormat::Step => engine.import_step_feature(&String::from_utf8_lossy(&bytes))?,
ImportFormat::Iges => engine.import_iges_feature(&String::from_utf8_lossy(&bytes))?,
ImportFormat::Stl => engine.import_stl_feature(&bytes)?,
ImportFormat::Obj => engine.import_obj_bytes_feature(&bytes)?,
};
Ok(Outcome::Done(json!({ "name": a.name, "report": parse(&report) })))
}
fn doc_export(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: ExportArgs = parse_args(args)?;
let document_name = ctx.app.docs.active().name().unwrap_or("Part").to_string();
let engine = ctx.app.docs.engine_mut();
let (format, text) = match a.format {
ExportFormat::Brep => ("brep", engine.history_request_json()),
ExportFormat::Step => ("step", engine.export_step_text_named(&document_name)?),
ExportFormat::Stl => ("stl", engine.export_stl_text()?),
ExportFormat::Iges => ("iges", engine.export_iges_text()?),
ExportFormat::Dxf => ("dxf", engine.export_flat_pattern_dxf()?),
ExportFormat::Svg => ("svg", engine.export_flat_pattern_svg()?),
};
let bytes = text.len();
serde_json::to_value(Exported { format: format.into(), text, bytes }).map(Outcome::Done).map_err(|e| e.to_string())
}
fn docs_list(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
serde_json::to_value(tabs(ctx)).map(Outcome::Done).map_err(|e| e.to_string())
}
fn doc_activate(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: IndexArgs = parse_args(args)?;
if a.index >= ctx.app.docs.len() {
return Err(format!("no document {} ({} open)", a.index, ctx.app.docs.len()));
}
ctx.app.docs.activate(a.index);
serde_json::to_value(tabs(ctx)).map(Outcome::Done).map_err(|e| e.to_string())
}
fn doc_close(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: CloseArgs = parse_args(args)?;
let Some(doc) = ctx.app.docs.get(a.index) else {
return Err(format!("no document {} ({} open)", a.index, ctx.app.docs.len()));
};
if doc.is_dirty() && !a.discard {
return Err(format!("document {} has unsaved changes; pass discard: true to close it anyway", a.index));
}
ctx.app.docs.close(a.index);
serde_json::to_value(tabs(ctx)).map(Outcome::Done).map_err(|e| e.to_string())
}
fn doc_mark_clean(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
ctx.app.docs.active_mut().mark_clean();
Ok(Outcome::Done(json!({})))
}
pub(crate) fn parse(text: &str) -> Value {
serde_json::from_str(text).unwrap_or_else(|_| Value::String(text.to_string()))
}
pub(crate) fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
fn val(c: u8) -> Result<u32, String> {
Ok(match c {
b'A'..=b'Z' => (c - b'A') as u32,
b'a'..=b'z' => (c - b'a') as u32 + 26,
b'0'..=b'9' => (c - b'0') as u32 + 52,
b'+' | b'-' => 62,
b'/' | b'_' => 63,
_ => return Err(format!("invalid base64 byte {c:#x}")),
})
}
let mut out = Vec::with_capacity(s.len() * 3 / 4);
let mut acc: u32 = 0;
let mut bits = 0;
for &c in s.as_bytes() {
if c == b'=' || c == b'\n' || c == b'\r' || c == b' ' {
continue;
}
acc = (acc << 6) | val(c)?;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push(((acc >> bits) & 0xff) as u8);
}
}
Ok(out)
}
pub static COMMANDS: &[CommandSpec] = &[
CommandSpec { name: "doc_new", group: "document", doc: "Open a new, empty document as a tab and make it active.", phase: Phase::Mutate, annotations: Annotations::MUTATE, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(doc_new) },
CommandSpec { name: "doc_load", group: "document", doc: "Open a `.BREP.json` document (passed as text) in a new tab, run its history and fit the camera. Returns the run report.", phase: Phase::Mutate, annotations: Annotations::MUTATE, args_schema: schema_of::<LoadArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(doc_load) },
CommandSpec { name: "doc_json", group: "document", doc: "The active document as `.BREP.json` (the history request with metadata).", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(doc_json) },
CommandSpec { name: "doc_import", group: "document", doc: "Append an Import 3D Model feature from STEP / IGES text or STL / OBJ bytes (base64). STL and OBJ go through mesh reconstruction on the runner.", phase: Phase::Mutate, annotations: Annotations::MUTATE, args_schema: schema_of::<ImportArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(doc_import) },
CommandSpec { name: "doc_export", group: "document", doc: "Export the active document as text: brep (native JSON), step, stl, iges, or the sheet-metal flat pattern as dxf / svg.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<ExportArgs>, result_schema: schema_of::<Exported>, handler: Handler::App(doc_export) },
CommandSpec { name: "part_attributes_get", group: "document", doc: "The active document's own BOM part attributes (`partAttributes`) — the record the toolbar's Part Properties dialog edits, and the one an assembly's BOM reads off this part.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(part_attributes_get) },
CommandSpec { name: "part_attribute_set", group: "document", doc: "Write ONE of the active document's own BOM part attributes (Part_Number, Material, Mass, or any custom field); a null or empty value removes it. One undo step per field.", phase: Phase::Mutate, annotations: Annotations::MUTATE, args_schema: schema_of::<PartAttributeArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(part_attribute_set) },
CommandSpec { name: "docs_list", group: "document", doc: "The open document tabs and which is active.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Tabs>, handler: Handler::App(docs_list) },
CommandSpec { name: "doc_activate", group: "document", doc: "Make the tab at `index` the active document.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<IndexArgs>, result_schema: schema_of::<Tabs>, handler: Handler::App(doc_activate) },
CommandSpec { name: "doc_close", group: "document", doc: "Close the tab at `index`; refuses a dirty document unless `discard` is true.", phase: Phase::Mutate, annotations: Annotations::DESTRUCTIVE, args_schema: schema_of::<CloseArgs>, result_schema: schema_of::<Tabs>, handler: Handler::App(doc_close) },
CommandSpec { name: "doc_mark_clean", group: "document", doc: "Mark the active document as saved (the host wrote `doc_json` to disk).", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(doc_mark_clean) },
];