collective

Browse / github.com/storytold/cadcraft / patch

Deliver host-opened drawings on QSAVE under their opened name

Web embed (?host=parent): QSAVE fails on a drawing opened with cadcraft:open **Version:** web build 0.13.0, embedded with `?host=parent`. Still applies on `main` (d864a08, `crates/engine/src/cmd/file.rs:469`). **What happens:** the host opens a drawing with `cadcraft:open { name, bytes }`. After editing, `QSAVE` (Ctrl+S) fails with "drawing has no file name yet; use saveas {path}", because a bridge-opened drawing has no `path`. `SAVEAS` (Ctrl+Shift+S) works and posts `cadcraft:save { name, bytes }` straight away, without a dialog, using the opened name (e.g. `plan.dxf`). **Expected:** in host mode, `QSAVE` on a drawing the host opened delivers it the same way `SAVEAS

Status
open
Score
0.00
Adopters
0 other operators
Author
Silvertree2010 (imported from pull request #2301, open)
Base
d864a08b013353143086bd8bb568c26cb6256796
Size
+56 −1 in 2 files
Review
pending
Shared
2026-10-28
Id
1d913ef218de10c2ae4bfea9

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/file.rs crates/engine/tests/save_name_only.rs

diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs--- a/crates/engine/src/cmd/file.rs+++ b/crates/engine/src/cmd/file.rs@@ -399,6 +399,10 @@ pub(crate) fn save_to(s: &mut Session, path: &str) -> Result<usize> { /// Save to `path` with `opts`. A drawing file then keeps the version and binary choice: the /// version goes into the drawing's `$ACADVER`, so later saves (QSAVE) write it again. fn save_as(s: &mut Session, cmd: &str, path: &str, opts: &SaveAs) -> Result<usize> {+    save_as_with(s, cmd, path, opts, store)+}++fn save_as_with(s: &mut Session, cmd: &str, path: &str, opts: &SaveAs, store: impl FnOnce(&str, &[u8]) -> Result<()>) -> Result<usize> {     if s.doc()?.refedit.is_some() {         return Err(bad(cmd, "A block reference is being edited in place: finish with REFCLOSE (Save or Discard) before saving."));     }@@ -466,12 +470,31 @@ fn saved(path: &str, bytes: usize) -> Value { fn run_save(s: &mut Session, p: &Value) -> Result<Value> {     let path = match str_param(p, "path") {         Some(p) => p.to_string(),-        None => s.state()?.path.clone().ok_or_else(|| bad("qsave", "drawing has no file name yet; use saveas {path}"))?,+        None => match s.state()?.path.clone() {+            Some(path) => path,+            None => return save_opened_name(s),+        },     };     let n = save_to(s, &path)?;     Ok(saved(&path, n)) } +/// QSAVE of a drawing opened from bytes (no path) where the host takes delivery of files (the+/// web embed): deliver it under the name it was opened with, as SAVEAS does. Without a host, or+/// for a file that isn't a drawing (an imported SVG), there is no name to save under.+fn save_opened_name(s: &mut Session) -> Result<Value> {+    let title = s.state()?.title.clone();+    let deliver = deliver().filter(|_| is_drawing_file(&title));+    let Some(deliver) = deliver else {+        return Err(bad("qsave", "drawing has no file name yet; use saveas {path}"));+    };+    let opts = SaveAs { version: None, binary: s.state()?.binary_dxf && is_dxf(&title) };+    let n = save_as_with(s, "save", &title, &opts, |name, bytes| {+        deliver(name, "application/octet-stream", bytes).map_err(|e| bad("save", format!("{name}: {e}")))+    })?;+    Ok(saved(&title, n))+}+ fn run_saveas(s: &mut Session, p: &Value) -> Result<Value> {     let path = str_param(p, "path").ok_or_else(|| bad("saveas", "`path` is required"))?.to_string();     let opts = save_options("saveas", p)?;diff --git a/crates/engine/tests/save_name_only.rs b/crates/engine/tests/save_name_only.rsnew file mode 100644--- /dev/null+++ b/crates/engine/tests/save_name_only.rs@@ -0,0 +1,32 @@+//! A drawing the host opened from bytes has a name but no path: where files are delivered to a+//! host (the web embed), QSAVE delivers it under that name, as SAVEAS does.++use cadcraft_engine::Session;+use cadcraft_engine::cmd::file::{IoHooks, set_deliver, set_io};+use cadcraft_engine::doc::Drawing;+use serde_json::json;+use std::sync::Mutex;++static DELIVERED: Mutex<Vec<(String, Vec<u8>)>> = Mutex::new(Vec::new());++fn deliver(name: &str, _mime: &str, bytes: &[u8]) -> Result<(), String> {+    DELIVERED.lock().map_err(|e| e.to_string())?.push((name.to_string(), bytes.to_vec()));+    Ok(())+}++#[test]+fn qsave_delivers_a_host_opened_drawing_under_its_name() {+    set_io(IoHooks { read: |_, _| Ok(Drawing::new_metric()), write: |_, _| Ok(b"exported".to_vec()), plot: None });+    set_deliver(deliver);+    let mut s = Session::empty();+    s.execute("open", &json!({"data": "MA==", "name": "plan.dxf"})).unwrap();+    s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap();+    s.execute("qsave", &json!({})).expect("QSAVE delivers the opened drawing");+    let got = DELIVERED.lock().unwrap().clone();+    assert_eq!(got, vec![("plan.dxf".to_string(), b"exported".to_vec())]);+    assert!(!s.state().unwrap().is_dirty());++    // An imported SVG is not a drawing file: QSAVE still asks for a name.+    s.execute("open", &json!({"data": "MA==", "name": "Logo.svg"})).unwrap();+    assert!(s.execute("qsave", &json!({})).is_err());+}