forked from 1b-agent/opencadstudio-upstream
feat(wasm): save files from the browser
Wire up save for the web build: - `io::save_to_bytes`: serialize a document to a byte buffer (DXF via write_to_vec, DWG via write_to_writer over a Cursor). - `io::download_web`: browser save dialog → write bytes (triggers a download). - Save and Save As on wasm both go through a new `web_save` helper: serialize the active tab and offer a download (no persistent path or in-app save dialog on the web). Format follows the tab name's extension, defaulting to DWG. Result reported via `WebSaveResult`. Native build + 75 tests unchanged. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
1ab892c635
commit
9d378a0533
3 files changed
+108
-17
No files matched your search
@@ -678,6 +678,9 @@ pub enum Message {
|
||||
FileOpened(Result<(String, PathBuf, CadDocument, crate::scene::DerivedCaches), String>),
|
||||
SaveFile,
|
||||
SaveAs,
|
||||
/// Result of the web (browser download) save: file name or error.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
WebSaveResult(Result<String, String>),
|
||||
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||
SaveDialogFormatChanged(String),
|
||||
SaveDialogFilenameChanged(String),
|
||||
|
||||
+76
-17
@@ -49,6 +49,39 @@ fn format_size(bytes: u64) -> String {
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Web save: serialize the active document to bytes and offer a browser
|
||||
/// download. Used for both Save and Save As on wasm (there is no persistent
|
||||
/// path or in-app save dialog). Format follows the tab name's extension,
|
||||
/// defaulting to DWG.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn web_save(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.document.header.user_real1 =
|
||||
self.tabs[i].scene.annotation_scale as f64;
|
||||
let base = self.tabs[i].tab_display_name();
|
||||
let lower = base.to_lowercase();
|
||||
let name = if lower.ends_with(".dwg") || lower.ends_with(".dxf") {
|
||||
base
|
||||
} else {
|
||||
format!("{base}.dwg")
|
||||
};
|
||||
let ext = if name.to_lowercase().ends_with(".dxf") {
|
||||
"dxf"
|
||||
} else {
|
||||
"dwg"
|
||||
};
|
||||
match crate::io::save_to_bytes(&self.tabs[i].scene.document, ext) {
|
||||
Ok(bytes) => {
|
||||
self.tabs[i].dirty = false;
|
||||
Task::perform(crate::io::download_web(name, bytes), Message::WebSaveResult)
|
||||
}
|
||||
Err(e) => {
|
||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
||||
let task = self.update_inner(msg);
|
||||
// After every message, mirror the active command step's prompt so
|
||||
@@ -709,30 +742,56 @@ impl OpenCADStudio {
|
||||
Message::ObjImportPath(None) => Task::none(),
|
||||
|
||||
Message::SaveFile => {
|
||||
let i = self.active_tab;
|
||||
if let Some(path) = &self.tabs[i].current_path {
|
||||
let path = path.clone();
|
||||
self.tabs[i].scene.document.header.user_real1 =
|
||||
self.tabs[i].scene.annotation_scale as f64;
|
||||
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
||||
Ok(()) => {
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", path.display()));
|
||||
self.tabs[i].dirty = false;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.web_save()
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let i = self.active_tab;
|
||||
if let Some(path) = &self.tabs[i].current_path {
|
||||
let path = path.clone();
|
||||
self.tabs[i].scene.document.header.user_real1 =
|
||||
self.tabs[i].scene.annotation_scale as f64;
|
||||
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
||||
Ok(()) => {
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", path.display()));
|
||||
self.tabs[i].dirty = false;
|
||||
}
|
||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
||||
}
|
||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
||||
Task::none()
|
||||
} else {
|
||||
self.save_dialog_for_unsaved = false;
|
||||
self.open_save_dialog_window(i)
|
||||
}
|
||||
Task::none()
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
Message::SaveAs => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.web_save()
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let i = self.active_tab;
|
||||
self.save_dialog_for_unsaved = false;
|
||||
self.open_save_dialog_window(i)
|
||||
}
|
||||
}
|
||||
|
||||
Message::SaveAs => {
|
||||
let i = self.active_tab;
|
||||
self.save_dialog_for_unsaved = false;
|
||||
self.open_save_dialog_window(i)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::WebSaveResult(result) => {
|
||||
match result {
|
||||
Ok(name) => self.command_line.push_output(&format!("Saved: {name}")),
|
||||
Err(e) if e != "Cancelled" => {
|
||||
self.command_line.push_error(&format!("Save failed: {e}"))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SaveDialogFormatChanged(fmt) => {
|
||||
|
||||
@@ -356,6 +356,35 @@ pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
||||
save_as_version(doc, path, doc.version)
|
||||
}
|
||||
|
||||
/// Serialize a document to an in-memory byte buffer, format chosen by `ext`
|
||||
/// (`dxf` → DXF, anything else → DWG). Used by the web build, which hands the
|
||||
/// bytes to a browser download instead of writing a path.
|
||||
pub fn save_to_bytes(doc: &CadDocument, ext: &str) -> Result<Vec<u8>, String> {
|
||||
let mut doc = doc.clone();
|
||||
sync_current_styles_on_save(&mut doc);
|
||||
match ext.to_lowercase().as_str() {
|
||||
"dxf" => DxfWriter::new(&doc).write_to_vec().map_err(|e| e.to_string()),
|
||||
_ => {
|
||||
let mut buf = std::io::Cursor::new(Vec::new());
|
||||
DwgWriter::write_to_writer(&mut buf, &doc).map_err(|e| e.to_string())?;
|
||||
Ok(buf.into_inner())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Web "save as": offer a download of `bytes` under `name` via the browser's
|
||||
/// save dialog. Returns the file name on success.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn download_web(name: String, bytes: Vec<u8>) -> Result<String, String> {
|
||||
let handle = rfd::AsyncFileDialog::new()
|
||||
.set_file_name(&name)
|
||||
.save_file()
|
||||
.await
|
||||
.ok_or_else(|| "Cancelled".to_string())?;
|
||||
handle.write(&bytes).await.map_err(|e| e.to_string())?;
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
// ── Post-load fixups ──────────────────────────────────────────────────────
|
||||
|
||||
// Resolve the current text / dimension / multiline style from the handle the
|
||||
|
||||
Reference in new issue
Block a user