feat: in-place save (File System Access), image paste, print wiring, web capture

The last high-severity web gap plus a small integration bundle.

In-place save: with the File System Access API (Chromium/Edge), Save
now works like desktop. Plain Save on a tab with a retained handle
writes straight through — no picker, no download, no "(1)" copies.
First save or format change opens showSaveFilePicker AT the confirm
gesture (the worker serialization that follows can outlast the
transient-activation window; retained handles need no activation to
write). Picker cancel cancels the save like desktop; picker or write
failure falls back to the legacy download with a note; Firefox/Safari
keep the download flow untouched (feature-detected). Handles are
keyed by tab id in a thread-local registry (Message must be Send —
JsValue cannot ride in messages or App state); closing a tab drops
its handle; Save As re-picks. Dirty clears only on successful
persistence, same race guard as before.

Image paste: Ctrl+V of a screenshot inserts it as an image reference
— the clipboard reader falls back to image/* items when neither the
entity clipboard nor text has anything (precedence preserved), the
bytes enter the image source registry as pasted-<timestamp>.<ext>,
and attach reuses the IMAGE command's model-creation path verbatim.

Browser print: beforeprint snapshots the WebGL canvas into a print-
only <img> (the capture_canvas technique), so the browser's own print
menu produces the drawing instead of a blank page.

Automation capture on web: the capture op no longer calls the
synchronous GPU-map screenshot (dead on wasm); main's frame-parked
capture waits for a presented frame and reads via capture_canvas — the
same workaround the save-thumbnail path uses — delivering the PNG as a
download.

(cherry picked from commit 6e89b36d)

Ported onto main (#1717 review): the web capture rides main's existing
frame-parked capture (one ControlCaptureFrame path, ignoring the timer
tick on wasm) instead of a second parked-capture state; closing a tab
drops its file handle inside main's on_tab_close.
This commit is contained in:
Afonsop authored and Hakan Seven committed 2026-10-12 12:02:05 +03:00
1 parent 0459f20129
commit 835e8d469f
33 files changed
+1084 -29

No files matched your search

+51
View File
@@ -915,6 +915,14 @@ pub(super) struct OpenCADStudio {
pending_native_thumbnail_save: Option<PendingNativeThumbnailSave>,
#[cfg(target_arch = "wasm32")]
pending_web_thumbnail_save: Option<PendingWebThumbnailSave>,
/// Web: whether the save in flight came from a plain Save
/// (Save/QSAVE or the save-before-close flow), so the tab's retained
/// File System Access handle (see io::fs_access) may be written
/// through directly; Save As opens the dialog modal and must re-pick.
/// Set true by `save_with_default_format`, false by
/// `open_save_dialog_window`.
#[cfg(target_arch = "wasm32")]
save_plain_reuse_fs_handle: bool,
/// One web autosave round is running; ticks that fire while it lasts are
/// dropped instead of queued (the next interval retries).
#[cfg(target_arch = "wasm32")]
@@ -2277,6 +2285,12 @@ pub enum Message {
PasteShortcutResolved(PasteFocus),
/// Completion of a system text clipboard read requested by PASTECLIP.
SystemClipboardPaste(SystemClipboardText),
/// Web: completion of the clipboard-image read the paste fallback starts
/// when no text was found — `Some((name, bytes))` attaches the image,
/// `None` reports the usual unsupported-content error. Native never
/// constructs it.
#[cfg(target_arch = "wasm32")]
SystemClipboardPasteImage(Option<(String, Vec<u8>)>),
/// Ctrl/Cmd+A — select all layer rows when the Layer Manager is open, or all
/// drawing objects otherwise (#236).
SelectAllShortcut,
@@ -2715,6 +2729,41 @@ pub enum Message {
camera_generation: u64,
result: Result<Vec<u8>, String>,
},
/// Web SAVE with the File System Access API: the showSaveFilePicker
/// promise settled. The picker ran in the save-confirm handler — the
/// user-gesture moment — because the serialization that follows may
/// outlast the transient-activation window; the retained handle (kept
/// in io::fs_access's tab-keyed registry, since a JsValue handle cannot
/// ride in a Send Message) is written to later without needing a
/// gesture. `Ok(Some(name))` = destination chosen (`name` is its real,
/// possibly renamed name); `Ok(None)` = the user dismissed the picker
/// (cancel the save, like desktop); `Err` = the picker itself failed
/// (fall back to the legacy download).
#[cfg(target_arch = "wasm32")]
WebSaveHandlePicked {
tab_id: u64,
filename: String,
ext: String,
version: codec::DxfVersion,
picked: Result<Option<String>, String>,
},
/// Web SAVE in-place completion: the retained FS Access handle's
/// createWritable write settled (on failure the task already fell back
/// to a download); run the shared save bookkeeping, including the OPFS
/// recent-copy store the download path performs. Same race-guard
/// markers as `WebSaveSerialized`.
#[cfg(target_arch = "wasm32")]
WebSaveWritten {
tab_id: u64,
filename: String,
autosave_key: String,
version: codec::DxfVersion,
geometry_epoch: u64,
edit_revision: u64,
camera_generation: u64,
result: Result<(), String>,
bytes: Vec<u8>,
},
/// Web WBLOCK: the serializer worker (or the inline fallback) produced
/// the block document bytes; download them under `filename`, then run
/// the same command-line bookkeeping as `WblockWriteFinished`.
@@ -4548,6 +4597,8 @@ impl OpenCADStudio {
#[cfg(target_arch = "wasm32")]
pending_web_thumbnail_save: None,
#[cfg(target_arch = "wasm32")]
save_plain_reuse_fs_handle: false,
#[cfg(target_arch = "wasm32")]
web_autosave_in_flight: false,
#[cfg(target_arch = "wasm32")]
web_autosave_warned: false,