forked from 1b-agent/opencadstudio-upstream
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:
1 parent
0459f20129
commit
835e8d469f
33 files changed
+1084
-29
No files matched your search
@@ -1541,9 +1541,27 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
image
|
||||
.save_with_format(&path, image::ImageFormat::Png)
|
||||
.map_err(|e| e.to_string())?;
|
||||
// No filesystem behind a web "path": encode the PNG in memory and
|
||||
// hand it to the browser as a download named after the request's
|
||||
// file name — the delivery every other web output uses.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let mut png = Vec::new();
|
||||
image
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let download = path
|
||||
.rsplit(['/', '\\'])
|
||||
.next()
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or("capture.png")
|
||||
.to_string();
|
||||
crate::sys::download_bytes(&download, &png);
|
||||
}
|
||||
|
||||
let doc_unit = crate::app::properties::insunits_name(
|
||||
self.tabs[self.active_tab]
|
||||
@@ -1624,6 +1642,19 @@ impl OpenCADStudio {
|
||||
self.control.capture_frame_pending = false;
|
||||
return Task::none();
|
||||
};
|
||||
// Web: iced's window screenshot needs a synchronous GPU map wasm
|
||||
// cannot provide; the canvas readback (the save-thumbnail technique)
|
||||
// only sees the drawing right after a presented frame, before the
|
||||
// browser clears the WebGL buffer — so wait for a real frame.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
if !redrew {
|
||||
return Task::none();
|
||||
}
|
||||
self.control.capture_frame_pending = false;
|
||||
self.control.capture_after_redraw = true;
|
||||
return Task::done(Message::ControlScreenshot(path, crate::sys::capture_canvas()));
|
||||
}
|
||||
self.control.capture_frame_pending = false;
|
||||
self.control.capture_after_redraw = redrew;
|
||||
let Some(window) = self.main_window else {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -195,6 +195,13 @@ impl OpenCADStudio {
|
||||
// This tab is closing for good — drop its autosave recovery copy.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(self.autosave_target(idx));
|
||||
// …and, on the web, its retained in-place save handle: the next tab
|
||||
// to reuse that id must start with a fresh pick instead of silently
|
||||
// writing over the closed drawing's file.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(closing_id) = closing_tab_id {
|
||||
crate::io::fs_access::drop_handle(closing_id);
|
||||
}
|
||||
// Only-tab case: when the lone non-start tab closes, fall
|
||||
// back to the Start tab if it exists; otherwise spawn a
|
||||
// fresh blank drawing (legacy behaviour).
|
||||
@@ -2095,6 +2102,68 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a clipboard image read on the web (a pasted screenshot) the way
|
||||
/// the IMAGE command's pick does: register the bytes name-keyed, decode
|
||||
/// for the file's true pixel size, then commit the entity — but placed
|
||||
/// directly at the cursor (0,0 before any pointer move) with the command's
|
||||
/// default width, skipping the pick prompts a paste should not run.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) fn paste_clipboard_image(&mut self, name: String, bytes: Vec<u8>) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].is_start {
|
||||
self.command_line.push_error(
|
||||
crate::tr!("common", "no-drawing-open-use-new-or-open-first").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
crate::scene::model::image_model::register_source(&name, std::sync::Arc::new(bytes));
|
||||
let Some(decoded) = crate::scene::model::image_model::resolve_image(&name) else {
|
||||
self.command_line.push_error(
|
||||
crate::tr!("clipboard", "image-unsupported", name = name.clone()).as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let (pixels_w, pixels_h) = (decoded.source_width, decoded.source_height);
|
||||
// Same default footprint as the IMAGE command's Enter placement: one
|
||||
// world unit per 100 px (min 1), height from the true aspect ratio.
|
||||
let width = (pixels_w as f64 / 100.0).max(1.0);
|
||||
let aspect = if pixels_h == 0 {
|
||||
1.0
|
||||
} else {
|
||||
pixels_w as f64 / pixels_h as f64
|
||||
};
|
||||
let height = width / aspect;
|
||||
let origin = self.tabs[i].last_cursor_world;
|
||||
let mut image = codec::entities::RasterImage::with_size(
|
||||
&name,
|
||||
codec::types::Vector3::new(origin.x, origin.y, origin.z),
|
||||
pixels_w as f64,
|
||||
pixels_h as f64,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
image.flags = codec::entities::ImageDisplayFlags::SHOW_IMAGE
|
||||
| codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
|
||||
let short = std::path::Path::new(&name)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(&name)
|
||||
.to_string();
|
||||
self.command_line.push_output(
|
||||
crate::tr!(
|
||||
"clipboard",
|
||||
"pasted-image",
|
||||
name = short,
|
||||
width = pixels_w,
|
||||
height = pixels_h
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
self.apply_cmd_result(crate::command::CmdResult::CommitAndExit(
|
||||
codec::EntityType::RasterImage(image),
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn on_qselect_open(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.selection.borrow_mut().menu.open_at = None;
|
||||
|
||||
@@ -22,6 +22,15 @@ use iced::{mouse, Point, Task};
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task<Message> {
|
||||
// Every entry here is Save-As semantics (the SAVEAS command, the
|
||||
// recovery save-as, the native failure redirects): the user is
|
||||
// choosing a destination, so on the web the FS Access picker must
|
||||
// open even when the tab already holds a handle (plain Save sets
|
||||
// the flag back in save_with_default_format).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.save_plain_reuse_fs_handle = false;
|
||||
}
|
||||
// Default the format dropdown to the loaded file's own format — its
|
||||
// DWG-vs-DXF kind (from the extension) and its version (from the parsed
|
||||
// document) — so Save-As round-trips the format instead of silently
|
||||
|
||||
+257
-26
@@ -2026,7 +2026,7 @@ impl OpenCADStudio {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if self.web_session_focus_pending && self.web_autosave_restore_all_remaining == 0 {
|
||||
self.web_session_focus_pending = false;
|
||||
self.select_remembered_session_tab();
|
||||
let _ = self.select_remembered_session_tab();
|
||||
}
|
||||
|
||||
// Missing-on-open: neutral command-line notice plus a flag
|
||||
@@ -2765,10 +2765,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
/// Web SAVE completion: the serializer worker (or inline fallback) has the
|
||||
/// drawing bytes. The download and tab bookkeeping that used to run
|
||||
/// synchronously inside `on_web_save_screenshot`, unchanged in semantics —
|
||||
/// except the dirty flag is only cleared when the serialized snapshot is
|
||||
/// still the tab's current state.
|
||||
/// drawing bytes. When the tab holds a File System Access handle the
|
||||
/// bytes go to it in place (createWritable needs no user gesture, so the
|
||||
/// write may happen long after the pick); completion continues in
|
||||
/// `on_web_save_written`. Otherwise the legacy download fires — the only
|
||||
/// artifact of the save, launched even if the tab was closed while
|
||||
/// serialization was in flight.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) fn on_web_save_serialized(
|
||||
&mut self,
|
||||
@@ -2789,9 +2791,111 @@ impl OpenCADStudio {
|
||||
return self.finish_web_save(false);
|
||||
}
|
||||
};
|
||||
// In-place write through the handle picked at the confirm gesture
|
||||
// (or an earlier one). The bookkeeping runs at write completion
|
||||
// (WebSaveWritten) so a failed write cannot clear the dirty flag;
|
||||
// the bytes ride back with the completion so the OPFS recent store
|
||||
// gets the same copy the download path uses.
|
||||
if let Some(picked_name) = crate::io::fs_access::handle_name(tab_id) {
|
||||
let download_name = picked_name.clone();
|
||||
return Task::perform(
|
||||
async move {
|
||||
let result = if let Err(error) =
|
||||
crate::io::fs_access::write_handle(tab_id, &bytes).await
|
||||
{
|
||||
// Keep the save alive: the drawing still lands
|
||||
// somewhere via the legacy download (which needs no
|
||||
// activation), and the error travels with the
|
||||
// completion message for the command-line note.
|
||||
crate::sys::download_bytes(&download_name, &bytes);
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
(result, bytes)
|
||||
},
|
||||
move |(result, bytes)| Message::WebSaveWritten {
|
||||
tab_id,
|
||||
filename: picked_name,
|
||||
autosave_key,
|
||||
version,
|
||||
geometry_epoch,
|
||||
edit_revision,
|
||||
camera_generation,
|
||||
result,
|
||||
bytes,
|
||||
},
|
||||
);
|
||||
}
|
||||
// The download is the only artifact of the save — fire it even if the
|
||||
// tab was closed while serialization was in flight.
|
||||
crate::sys::download_bytes(&filename, &bytes);
|
||||
self.finalize_web_save(
|
||||
tab_id,
|
||||
filename,
|
||||
autosave_key,
|
||||
version,
|
||||
geometry_epoch,
|
||||
edit_revision,
|
||||
camera_generation,
|
||||
bytes,
|
||||
)
|
||||
}
|
||||
|
||||
/// Web SAVE in-place completion: the retained handle's write settled.
|
||||
/// Success keeps the handle for the next plain Save; failure drops it
|
||||
/// (the task already downloaded a copy — note that in the command line,
|
||||
/// and re-pick on the next save instead of failing the same way again).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) fn on_web_save_written(
|
||||
&mut self,
|
||||
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>,
|
||||
) -> Task<Message> {
|
||||
if let Err(error) = result {
|
||||
crate::io::fs_access::drop_handle(tab_id);
|
||||
self.command_line.push_error(
|
||||
crate::tf!(
|
||||
"In-place save failed ({error}); a copy was downloaded instead"
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
self.finalize_web_save(
|
||||
tab_id,
|
||||
filename,
|
||||
autosave_key,
|
||||
version,
|
||||
geometry_epoch,
|
||||
edit_revision,
|
||||
camera_generation,
|
||||
bytes,
|
||||
)
|
||||
}
|
||||
|
||||
/// Shared completion half of the web save — download and in-place write
|
||||
/// alike: recent-copy store task, tab bookkeeping under the race guard
|
||||
/// (the dirty flag only clears when the persisted snapshot is still the
|
||||
/// tab's current state), autosave-key retirement and the "Saved" line.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn finalize_web_save(
|
||||
&mut self,
|
||||
tab_id: u64,
|
||||
filename: String,
|
||||
autosave_key: String,
|
||||
version: codec::DxfVersion,
|
||||
geometry_epoch: u64,
|
||||
edit_revision: u64,
|
||||
camera_generation: u64,
|
||||
bytes: Vec<u8>,
|
||||
) -> Task<Message> {
|
||||
let cache_name = std::path::Path::new(&filename)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
@@ -3610,6 +3714,14 @@ impl OpenCADStudio {
|
||||
/// save-before-close flow — the version picker is reserved for Save As.
|
||||
pub(in crate::app) fn save_with_default_format(&mut self, tab_idx: usize) -> Task<Message> {
|
||||
self.active_tab = tab_idx;
|
||||
// Every caller of this entry (Save/QSAVE, the save-before-close
|
||||
// flow) is plain-save semantics: on the web, the tab's retained
|
||||
// FS Access handle may be written through directly. Save As opens
|
||||
// the dialog modal instead (open_save_dialog_window resets this).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.save_plain_reuse_fs_handle = true;
|
||||
}
|
||||
self.save_dialog_format = if self.tabs[tab_idx].recovery_save_as_required {
|
||||
let document = &self.tabs[tab_idx].scene.document;
|
||||
let is_dxf =
|
||||
@@ -3708,7 +3820,9 @@ impl OpenCADStudio {
|
||||
);
|
||||
Task::batch([close, pick])
|
||||
}
|
||||
// Web: no filesystem — serialize and hand the browser a download.
|
||||
// Web: no filesystem path — the destination is either a retained
|
||||
// File System Access handle (in-place write), a fresh
|
||||
// showSaveFilePicker pick, or the legacy download.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let close = self.close_save_dialog_window();
|
||||
@@ -3718,30 +3832,147 @@ impl OpenCADStudio {
|
||||
self.sync_solid_models_for_save(i);
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
let tab_id = self.tabs[i].id;
|
||||
let bounds =
|
||||
crate::ui::wrap_bar::dropdown_bounds(crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID);
|
||||
let Some(bounds) = bounds else {
|
||||
return Task::batch([
|
||||
close,
|
||||
Task::done(Message::WebSaveScreenshot {
|
||||
tab_id,
|
||||
filename,
|
||||
ext: ext.to_string(),
|
||||
version,
|
||||
bounds: None,
|
||||
screenshot: None,
|
||||
}),
|
||||
]);
|
||||
};
|
||||
self.pending_web_thumbnail_save = Some(crate::app::PendingWebThumbnailSave {
|
||||
// File System Access routing, decided HERE: showSaveFilePicker
|
||||
// may only run while the page holds the Save click's transient
|
||||
// user activation, and the thumbnail capture + worker
|
||||
// serialization that follow can outlast that window. So the
|
||||
// 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.
|
||||
let route = crate::io::fs_access::save_route(
|
||||
crate::io::fs_access::supported(),
|
||||
self.save_plain_reuse_fs_handle,
|
||||
crate::io::fs_access::handle_ext(tab_id).as_deref(),
|
||||
ext,
|
||||
);
|
||||
match route {
|
||||
crate::io::fs_access::SaveRoute::WriteInPlace => {
|
||||
// Plain Save on a matching handle — THE point: Ctrl+S
|
||||
// updates the picked file, no dialog at all. The real
|
||||
// name (the user may have renamed it in the picker)
|
||||
// drives the save bookkeeping.
|
||||
let picked_name =
|
||||
crate::io::fs_access::handle_name(tab_id).unwrap_or(filename);
|
||||
Task::batch([
|
||||
close,
|
||||
self.web_save_capture(tab_id, picked_name, ext.to_string(), version),
|
||||
])
|
||||
}
|
||||
crate::io::fs_access::SaveRoute::PickHandle => {
|
||||
// The picker IS the save dialog here: it opens inside
|
||||
// the click's activation window, before any serialize
|
||||
// task starts. Cancelling it cancels the save (same as
|
||||
// a native save-dialog cancel); a picker failure falls
|
||||
// back to the legacy download.
|
||||
let suggested = filename;
|
||||
let pick_suggested = suggested.clone();
|
||||
Task::batch([
|
||||
close,
|
||||
Task::perform(
|
||||
async move {
|
||||
crate::io::fs_access::pick_save_handle(
|
||||
tab_id,
|
||||
&pick_suggested,
|
||||
ext,
|
||||
)
|
||||
.await
|
||||
},
|
||||
move |picked| Message::WebSaveHandlePicked {
|
||||
tab_id,
|
||||
filename: suggested,
|
||||
ext: ext.to_string(),
|
||||
version,
|
||||
picked,
|
||||
},
|
||||
),
|
||||
])
|
||||
}
|
||||
// Firefox/Safari (no showSaveFilePicker): untouched legacy
|
||||
// download flow.
|
||||
crate::io::fs_access::SaveRoute::Download => {
|
||||
Task::batch([
|
||||
close,
|
||||
self.web_save_capture(tab_id, filename, ext.to_string(), version),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Web save, destination-known half shared by the legacy download path
|
||||
/// and both File System Access paths: queue the viewport thumbnail
|
||||
/// capture (or skip straight to the serializer when the bounds are
|
||||
/// unavailable), which continues in `on_web_save_screenshot`.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn web_save_capture(
|
||||
&mut self,
|
||||
tab_id: u64,
|
||||
filename: String,
|
||||
ext: String,
|
||||
version: codec::DxfVersion,
|
||||
) -> Task<Message> {
|
||||
let bounds = crate::ui::wrap_bar::dropdown_bounds(crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID);
|
||||
let Some(bounds) = bounds else {
|
||||
return Task::done(Message::WebSaveScreenshot {
|
||||
tab_id,
|
||||
filename,
|
||||
ext: ext.to_string(),
|
||||
ext,
|
||||
version,
|
||||
bounds,
|
||||
bounds: None,
|
||||
screenshot: None,
|
||||
});
|
||||
self.thumbnail_capture_clean = true;
|
||||
close
|
||||
};
|
||||
self.pending_web_thumbnail_save = Some(crate::app::PendingWebThumbnailSave {
|
||||
tab_id,
|
||||
filename,
|
||||
ext,
|
||||
version,
|
||||
bounds,
|
||||
});
|
||||
self.thumbnail_capture_clean = true;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Web SAVE: the showSaveFilePicker promise from the confirm handler
|
||||
/// settled. `Ok(Some(name))` continues the save against the picked
|
||||
/// file's real name (the registry already retains the handle, replacing
|
||||
/// any previous one — Save As re-picks); `Ok(None)` (dismissed picker)
|
||||
/// cancels the save like a native save-dialog cancel; `Err` keeps the
|
||||
/// save alive through the legacy download, with a note that the copy
|
||||
/// was downloaded.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) fn on_web_save_handle_picked(
|
||||
&mut self,
|
||||
tab_id: u64,
|
||||
filename: String,
|
||||
ext: String,
|
||||
version: codec::DxfVersion,
|
||||
picked: Result<Option<String>, String>,
|
||||
) -> Task<Message> {
|
||||
match picked {
|
||||
Ok(Some(picked_name)) => {
|
||||
self.web_save_capture(tab_id, picked_name, ext, version)
|
||||
}
|
||||
Ok(None) => {
|
||||
// The picker was the save dialog: dismissing it means no
|
||||
// save — the download fallback must NOT fire (that would
|
||||
// surprise the user with a stray "drawing (1).dwg"). If
|
||||
// this came from the unsaved-changes → save → close flow,
|
||||
// re-show the confirmation so the tab isn't lost.
|
||||
if self.save_dialog_for_unsaved && self.pending_close.is_some() {
|
||||
return self.open_unsaved_dialog_window();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Err(error) => {
|
||||
self.command_line.push_error(
|
||||
crate::tf!(
|
||||
"Save destination picker failed: {error}; downloading a copy instead"
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
self.web_save_capture(tab_id, filename, ext, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+63
-3
@@ -6810,9 +6810,24 @@ impl OpenCADStudio {
|
||||
self.focus_cmd_input()
|
||||
}
|
||||
Text::EmptyOrUnsupported => {
|
||||
self.command_line
|
||||
.push_error(crate::tr!("clipboard", "no-supported-content").as_ref());
|
||||
Task::none()
|
||||
// Web: no text — the clipboard may still hold an IMAGE
|
||||
// (a pasted screenshot). Read it through the async
|
||||
// Clipboard API and attach it the way the IMAGE
|
||||
// command's pick does; only when that also finds
|
||||
// nothing does the unsupported-content error surface.
|
||||
// Native has no image route off the sync clipboard.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Task::perform(
|
||||
crate::sys::read_clipboard_image(),
|
||||
Message::SystemClipboardPasteImage,
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::tr!("clipboard", "no-supported-content").as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
Text::Unavailable => {
|
||||
self.command_line
|
||||
@@ -6832,6 +6847,19 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// Web: the clipboard-image fallback of a text-less paste either
|
||||
// attaches the image or falls back to the same error the text
|
||||
// path reports when nothing readable was on the clipboard.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::SystemClipboardPasteImage(image) => match image {
|
||||
Some((name, bytes)) => self.paste_clipboard_image(name, bytes),
|
||||
None => {
|
||||
self.command_line
|
||||
.push_error(crate::tr!("clipboard", "no-supported-content").as_ref());
|
||||
Task::none()
|
||||
}
|
||||
},
|
||||
|
||||
Message::SelectAllShortcut => {
|
||||
let i = self.active_tab;
|
||||
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
||||
@@ -10275,6 +10303,38 @@ impl OpenCADStudio {
|
||||
result,
|
||||
),
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::WebSaveHandlePicked {
|
||||
tab_id,
|
||||
filename,
|
||||
ext,
|
||||
version,
|
||||
picked,
|
||||
} => self.on_web_save_handle_picked(tab_id, filename, ext, version, picked),
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::WebSaveWritten {
|
||||
tab_id,
|
||||
filename,
|
||||
autosave_key,
|
||||
version,
|
||||
geometry_epoch,
|
||||
edit_revision,
|
||||
camera_generation,
|
||||
result,
|
||||
bytes,
|
||||
} => self.on_web_save_written(
|
||||
tab_id,
|
||||
filename,
|
||||
autosave_key,
|
||||
version,
|
||||
geometry_epoch,
|
||||
edit_revision,
|
||||
camera_generation,
|
||||
result,
|
||||
bytes,
|
||||
),
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::WebSaveallSerialized { skipped, results } => {
|
||||
self.on_web_saveall_serialized(skipped, results)
|
||||
|
||||
Reference in new issue
Block a user