From 85627ebc8a4e17a13c491ce053793dd757e35b29 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 13 Oct 2026 10:12:07 +0300 Subject: [PATCH] feat(web): return saved drawings to an embedding page The web control channel gains a save operation that serializes the active drawing in the requested format and returns it as base64 in the reply, clearing the dirty flag, the counterpart of opening from bytes. A host page can also define window.openCadStudioSaveFile(name, bytes) to receive every saved or exported file in place of a browser download; while it is set no file picker opens. Fixes #1704 Fixes #1705 Co-Authored-By: Claude Opus 5.5 --- docs/automation/web.md | 27 ++++++++++++++++++++++++++ src/app/control/mod.rs | 43 ++++++++++++++++++++++++++++++++++++++++++ src/app/update/file.rs | 4 +++- src/sys.rs | 19 ++++++++++++++++++- 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/docs/automation/web.md b/docs/automation/web.md index a5944d36e..89b0b430a 100644 --- a/docs/automation/web.md +++ b/docs/automation/web.md @@ -58,6 +58,33 @@ earlier drawing is still opening the request is answered with `busy`; retry it o rather than a disk, so use `data_base64` for a file the page has not seen. `data_base64` is rejected on native builds; use `path` there. +## Getting the drawing back + +`save` serializes the active drawing and returns it in the reply, without a download: + +```json +{"op":"save","name":"plan.dwg","target_format":"dwg","target_version":"AC1032"} +``` + +The reply carries `name`, `target_format`, `target_version`, `dropped_on_save`, `bytes` +and `data_base64`, and the tab is marked clean. `target_format` and `target_version` +default to the drawing's own; a conversion that would drop unsupported records needs +`allow_lossy: true`, as on native. + +## Receiving saved files in the host page + +A page that embeds the app can take every saved or exported file itself instead of a +browser download by defining a function on the app's window before saving: + +```js +window.openCadStudioSaveFile = (name, bytes) => { + // name: file name, bytes: Uint8Array +}; +``` + +While it is defined, Save, Save As, the save-before-close prompt and the exports call it +with the file name and the bytes, and no file picker opens. Without it nothing changes. + ## Startup dialogs A dialog can be open when the page is ready. The donation prompt appears once per diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index a5524e652..a720733c4 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -1147,6 +1147,49 @@ impl OpenCADStudio { ) } } + // Web: no filesystem path; the drawing comes back in the reply. + #[cfg(target_arch = "wasm32")] + "save" => { + use base64::Engine as _; + let default_is_dxf = crate::io::source_is_dxf(None, &self.tabs[i].scene.document); + let (version, is_dxf) = super::automation::requested_save_target( + req, + self.tabs[i].scene.document.version, + default_is_dxf, + None, + ) + .map_err(|error| failure("invalid_target", error))?; + let dropped = + crate::io::dropped_on_save_count(&self.tabs[i].scene.document, version, is_dxf); + if dropped > 0 && req["allow_lossy"].as_bool() != Some(true) { + return Err(failure( + "lossy_conversion_not_acknowledged", + format!( + "Conversion would drop {dropped} unsupported record(s); set allow_lossy=true to acknowledge" + ), + )); + } + let ext = if is_dxf { "dxf" } else { "dwg" }; + self.sync_view_state_for_save(i); + super::update::util::sync_annotation_scale_header(&mut self.tabs[i].scene); + self.stamp_header_sysvars(i); + self.sync_solid_models_for_save(i); + self.tabs[i].scene.sync_native_parametric_graph(); + let bytes = crate::io::save_to_bytes(&self.tabs[i].scene.document, ext, version) + .map_err(|error| failure("save_failed", error))?; + self.tabs[i].dirty = false; + let name = req["name"].as_str().filter(|name| !name.is_empty()); + let name = name.map_or_else(|| format!("drawing.{ext}"), str::to_string); + self.set_control_result(json!({ + "name": name, + "target_format": ext, + "target_version": format!("{version:?}"), + "dropped_on_save": dropped, + "bytes": bytes.len(), + "data_base64": base64::engine::general_purpose::STANDARD.encode(&bytes), + })); + Task::none() + } #[cfg(not(target_arch = "wasm32"))] "save_verified" => { let result = self.save_verified_request(req)?; diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 6032c3b2d..5d5200ead 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -3882,8 +3882,10 @@ impl OpenCADStudio { // handle is secured at this gesture moment (or reused from a // previous pick — retained handles need no activation to write) // and only the write happens later. + // A registered host save hook takes the bytes through the + // download path, so no file picker opens. let route = crate::io::fs_access::save_route( - crate::io::fs_access::supported(), + crate::io::fs_access::supported() && crate::sys::save_hook().is_none(), self.save_plain_reuse_fs_handle, crate::io::fs_access::handle_ext(tab_id).as_deref(), ext, diff --git a/src/sys.rs b/src/sys.rs index 1ca7afae7..0bbcfb82d 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -597,10 +597,15 @@ pub fn download_bytes(name: &str, bytes: &[u8]) { let Some(window) = web_sys::window() else { return; }; + let array = js_sys::Uint8Array::from(bytes); + // A host page that embeds the app takes the file instead of a download. + if let Some(hook) = save_hook() { + let _ = hook.call2(&window, &wasm_bindgen::JsValue::from_str(name), &array); + return; + } let Some(document) = window.document() else { return; }; - let array = js_sys::Uint8Array::from(bytes); let parts = js_sys::Array::new(); parts.push(&array.buffer()); let Ok(blob) = web_sys::Blob::new_with_u8_array_sequence(&parts) else { @@ -618,6 +623,18 @@ pub fn download_bytes(name: &str, bytes: &[u8]) { let _ = web_sys::Url::revoke_object_url(&url); } +/// The `window.openCadStudioSaveFile(name, bytes)` function an embedding page +/// registers to receive saved and exported files, if any. +#[cfg(target_arch = "wasm32")] +pub fn save_hook() -> Option { + use wasm_bindgen::JsCast; + let window = web_sys::window()?; + js_sys::Reflect::get(window.as_ref(), &wasm_bindgen::JsValue::from_str("openCadStudioSaveFile")) + .ok()? + .dyn_into::() + .ok() +} + /// Show a plot preview as an in-page overlay. The plotted PDF arrives already /// rasterised to PNG pages: inline `` elements render in every browser, /// with no popup permission and no PDF viewer plugin — both of which embedded