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
@@ -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)?;new file mode 100644@@ -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());+}