CMMS embedding: Export to CAM - build STL, save, navigate to Kiri:Moto
Wires Message::StlExport's existing STLOUT command to the CMMS when embedded, instead of the native save-dialog flow (which was already non-functional on the web build regardless - handle_path() has no real filesystem to write to there, a pre-existing upstream gap, not something this touches). - Cargo.toml: add the Location web-sys feature (needed for window.location().set_href()). - src/sys.rs: post_stl_export_to_host() - POSTs STL bytes as a new attachment (cad_export_stl action, not cad_save/cad_save_new - STL isn't in CAD_EDITABLE_EXT, it needs its own allowlist), then navigates the browser straight to the CMMS's Kiri:Moto CAM page for the attachment it just created. Splits cfg.job_id's "type:id" owner token apart to build cam-edit.php's entity_type=/entity_id= params. - src/app/mod.rs + update/mod.rs: CmmsStlExportResult message/handler - only ever surfaces a failure (a success already navigated away). - src/app/update/mod.rs: Message::StlExport gains a wasm32+embedded branch that skips the file-picker entirely and calls the above directly.
This commit is contained in:
parent
64d0230952
commit
3f8b69c71b
4 changed files with 120 additions and 0 deletions
|
|
@ -82,6 +82,7 @@ js-sys = "0.3"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
web-sys = { version = "=0.3.85", features = [
|
web-sys = { version = "=0.3.85", features = [
|
||||||
"Window",
|
"Window",
|
||||||
|
"Location",
|
||||||
"Storage",
|
"Storage",
|
||||||
"StorageManager",
|
"StorageManager",
|
||||||
"Navigator",
|
"Navigator",
|
||||||
|
|
|
||||||
|
|
@ -1796,6 +1796,12 @@ pub enum Message {
|
||||||
/// to the host page instead of triggering a browser download.
|
/// to the host page instead of triggering a browser download.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
CmmsSaveResult(Result<(), String>),
|
CmmsSaveResult(Result<(), String>),
|
||||||
|
/// CMMS embedding (not upstream): result of "Export to CAM" - POSTing a
|
||||||
|
/// freshly-built STL as a new attachment and navigating to Kiri:Moto.
|
||||||
|
/// A successful result already navigated away, so this only ever
|
||||||
|
/// surfaces a failure to the command line.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
CmmsStlExportResult(Result<(), String>),
|
||||||
SaveFile,
|
SaveFile,
|
||||||
SaveAs,
|
SaveAs,
|
||||||
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -839,6 +839,20 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A success already navigated the browser to the CAM page
|
||||||
|
// (post_stl_export_to_host does this itself once the save
|
||||||
|
// response comes back) - only a failure is ever actually seen
|
||||||
|
// here, the whole point of surfacing it in the command line
|
||||||
|
// rather than just logging to the console.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
Message::CmmsStlExportResult(result) => {
|
||||||
|
if let Err(error) = result {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::tf!("Export to CAM failed: {error}").as_ref());
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::ImagePick => {
|
Message::ImagePick => {
|
||||||
Task::perform(crate::io::pick_image_file(), Message::ImagePickResult)
|
Task::perform(crate::io::pick_image_file(), Message::ImagePickResult)
|
||||||
}
|
}
|
||||||
|
|
@ -985,6 +999,31 @@ impl OpenCADStudio {
|
||||||
.push_error(crate::t!("STLOUT: no 3D mesh data in this drawing.").as_ref());
|
.push_error(crate::t!("STLOUT: no 3D mesh data in this drawing.").as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
// CMMS embedding (not upstream), "Export to CAM": when
|
||||||
|
// launched from the CMMS, skip the (broken on web anyway -
|
||||||
|
// handle_path() has no real filesystem to write to there)
|
||||||
|
// native save-dialog flow entirely and instead build STL
|
||||||
|
// bytes, POST them as a new attachment, and navigate
|
||||||
|
// straight to the Kiri:Moto CAM page for it.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
if crate::sys::cmms_config().is_some() {
|
||||||
|
let meshes: Vec<crate::scene::model::mesh_model::MeshModel> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.meshes
|
||||||
|
.values()
|
||||||
|
.filter_map(|s| s.lods.first().cloned())
|
||||||
|
.collect();
|
||||||
|
let filename = format!("{}.stl", self.tabs[i].tab_display_name());
|
||||||
|
return Task::perform(
|
||||||
|
async move {
|
||||||
|
let mesh_refs: Vec<_> = meshes.iter().collect();
|
||||||
|
let bytes = crate::io::stl::build_stl(&mesh_refs)
|
||||||
|
.ok_or_else(|| "no mesh data to export".to_string())?;
|
||||||
|
crate::sys::post_stl_export_to_host(&bytes, &filename).await
|
||||||
|
},
|
||||||
|
Message::CmmsStlExportResult,
|
||||||
|
);
|
||||||
|
}
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async {
|
async {
|
||||||
crate::sys::file_dialog()
|
crate::sys::file_dialog()
|
||||||
|
|
|
||||||
74
src/sys.rs
74
src/sys.rs
|
|
@ -442,6 +442,80 @@ pub async fn post_bytes_to_host(bytes: &[u8], filename: &str) -> Result<(), Stri
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CMMS embedding (not upstream): "Export to CAM" - POSTs STL bytes as a
|
||||||
|
/// brand-new attachment (mirrors post_bytes_to_host's create path but
|
||||||
|
/// against the cad_export_stl action, not cad_save/cad_save_new - STL
|
||||||
|
/// isn't in CAD_EDITABLE_EXT, it needs its own allowlist), then navigates
|
||||||
|
/// the browser straight to the Kiri:Moto CAM page for the file it just
|
||||||
|
/// created. `cfg.job_id` already carries the "type:id" owner token (see
|
||||||
|
/// public/vendor/cad-editor/edit.php's $ownerToken) - split it apart here
|
||||||
|
/// to build cam-edit.php's separate entity_type=/entity_id= params.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub async fn post_stl_export_to_host(bytes: &[u8], filename: &str) -> Result<(), String> {
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
|
||||||
|
let cfg = cmms_config().ok_or("not running inside the CMMS")?;
|
||||||
|
|
||||||
|
let form = web_sys::FormData::new().map_err(|_| "could not build form data")?;
|
||||||
|
form.append_with_str("action", "cad_export_stl")
|
||||||
|
.map_err(|_| "form append failed")?;
|
||||||
|
form.append_with_str("job_id", &cfg.job_id)
|
||||||
|
.map_err(|_| "form append failed")?;
|
||||||
|
form.append_with_str("original_name", filename)
|
||||||
|
.map_err(|_| "form append failed")?;
|
||||||
|
form.append_with_str("csrf_token", &cfg.csrf_token)
|
||||||
|
.map_err(|_| "form append failed")?;
|
||||||
|
|
||||||
|
let array = js_sys::Uint8Array::from(bytes);
|
||||||
|
let parts = js_sys::Array::new();
|
||||||
|
parts.push(&array.buffer());
|
||||||
|
let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)
|
||||||
|
.map_err(|_| "could not build blob")?;
|
||||||
|
form.append_with_blob("file", &blob)
|
||||||
|
.map_err(|_| "form append failed")?;
|
||||||
|
|
||||||
|
let window = web_sys::window().ok_or("no window")?;
|
||||||
|
let opts = web_sys::RequestInit::new();
|
||||||
|
opts.set_method("POST");
|
||||||
|
opts.set_body(&form);
|
||||||
|
let request = web_sys::Request::new_with_str_and_init(&cfg.save_url, &opts)
|
||||||
|
.map_err(|_| "could not build request")?;
|
||||||
|
let response: web_sys::Response = JsFuture::from(window.fetch_with_request(&request))
|
||||||
|
.await
|
||||||
|
.map_err(|_| "export request failed")?
|
||||||
|
.dyn_into()
|
||||||
|
.map_err(|_| "export response is invalid")?;
|
||||||
|
if !response.ok() {
|
||||||
|
return Err(format!("export failed: HTTP {}", response.status()));
|
||||||
|
}
|
||||||
|
let text = JsFuture::from(response.text().map_err(|_| "text() unavailable")?)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "export response body read failed")?;
|
||||||
|
let body = text.as_string().ok_or("export response body is not a string")?;
|
||||||
|
let parsed: serde_json::Value =
|
||||||
|
serde_json::from_str(&body).map_err(|_| "export response is not valid JSON")?;
|
||||||
|
let new_id = parsed
|
||||||
|
.get("id")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.ok_or("export response missing id")?;
|
||||||
|
|
||||||
|
let (entity_type, entity_id) = cfg
|
||||||
|
.job_id
|
||||||
|
.split_once(':')
|
||||||
|
.ok_or("malformed owner token")?;
|
||||||
|
let url = format!(
|
||||||
|
"/cam-edit.php?entity_type={}&entity_id={}&attachment_id={new_id}",
|
||||||
|
percent_encode(entity_type),
|
||||||
|
percent_encode(entity_id)
|
||||||
|
);
|
||||||
|
window
|
||||||
|
.location()
|
||||||
|
.set_href(&url)
|
||||||
|
.map_err(|_| "navigation to the CAM page failed")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Short platform string for bug reports: OS + architecture on the desktop,
|
/// Short platform string for bug reports: OS + architecture on the desktop,
|
||||||
/// the browser user-agent on the web.
|
/// the browser user-agent on the web.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue