forked from 1b-agent/opencadstudio-upstream
perf: optimize large drawing workflows
This commit is contained in:
1 parent
1c7e4bb850
commit
71e0b88839
17 files changed
+1204
-430
No files matched your search
@@ -13,8 +13,11 @@ use iced;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
static NEXT_DOCUMENT_TAB_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
// ── Dynamic input ──────────────────────────────────────────────────────────
|
||||
|
||||
/// One quantity shown in the dynamic-input overlay near the cursor.
|
||||
@@ -154,9 +157,15 @@ impl DocumentTab {
|
||||
}
|
||||
|
||||
pub(super) struct DocumentTab {
|
||||
/// Stable identity across tab insert/remove operations. Background work
|
||||
/// must never target a tab by its transient vector index.
|
||||
pub(super) id: u64,
|
||||
pub(super) scene: Scene,
|
||||
pub(super) current_path: Option<PathBuf>,
|
||||
pub(super) dirty: bool,
|
||||
/// Monotonic committed-edit/undo/redo revision. Background save completion
|
||||
/// uses it with scene epochs so an older snapshot never clears newer work.
|
||||
pub(super) edit_revision: u64,
|
||||
pub(super) tab_title: String,
|
||||
pub(super) properties: PropertiesPanel,
|
||||
pub(super) layers: LayerPanel,
|
||||
@@ -413,9 +422,11 @@ impl DocumentTab {
|
||||
}
|
||||
}
|
||||
Self {
|
||||
id: NEXT_DOCUMENT_TAB_ID.fetch_add(1, Ordering::Relaxed),
|
||||
scene,
|
||||
current_path: None,
|
||||
dirty: false,
|
||||
edit_revision: 0,
|
||||
prev_selection: Vec::new(),
|
||||
tab_title: format!("Drawing{}", n),
|
||||
properties: PropertiesPanel::empty(),
|
||||
|
||||
@@ -117,6 +117,7 @@ impl OpenCADStudio {
|
||||
|
||||
fn push_undo_entry(&mut self, i: usize, snapshot: HistorySnapshot) {
|
||||
self.tabs[i].history.undo_stack.push(snapshot);
|
||||
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
||||
self.clear_redo_history(i);
|
||||
self.trim_history(i);
|
||||
}
|
||||
@@ -480,6 +481,7 @@ impl OpenCADStudio {
|
||||
had_full: bool,
|
||||
changes: &[(Handle, crate::scene::ChangeKind)],
|
||||
) {
|
||||
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
||||
{
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
if had_full {
|
||||
|
||||
+37
-2
@@ -739,6 +739,10 @@ pub(super) struct OpenCADStudio {
|
||||
// ── Unsaved-changes dialog ────────────────────────────────────────────
|
||||
/// Set when the user tries to close a tab or quit while there are unsaved changes.
|
||||
pending_close: Option<PendingClose>,
|
||||
/// Latest save job per stable tab id. Older completions may finish, but
|
||||
/// cannot mark a newer document state clean or redirect its path.
|
||||
active_save_jobs: std::collections::HashMap<u64, u64>,
|
||||
save_job_serial: u64,
|
||||
/// OS window for the unsaved-changes confirmation dialog.
|
||||
|
||||
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||
@@ -847,6 +851,35 @@ pub(super) enum PendingClose {
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum SavePurpose {
|
||||
Manual,
|
||||
SaveAs,
|
||||
Autosave,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum SaveContinuation {
|
||||
None,
|
||||
CloseTab,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SaveOutcome {
|
||||
job_id: u64,
|
||||
tab_id: u64,
|
||||
epoch: u64,
|
||||
revision: u64,
|
||||
camera_generation: u64,
|
||||
path: PathBuf,
|
||||
previous_autosave: Option<PathBuf>,
|
||||
set_current_path: bool,
|
||||
purpose: SavePurpose,
|
||||
continuation: SaveContinuation,
|
||||
result: Result<(), String>,
|
||||
}
|
||||
|
||||
/// Where a colour chosen in the standalone palette window should be applied.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ColorPickTarget {
|
||||
@@ -1404,8 +1437,8 @@ pub enum Message {
|
||||
AecDropBack,
|
||||
/// Periodic autosave tick — write `.sv$` recovery files for dirty tabs.
|
||||
AutoSave,
|
||||
/// Save-as path picked for the unsaved-changes → save → close flow.
|
||||
UnsavedPickedSavePath(Option<std::path::PathBuf>),
|
||||
/// Native background save/autosave completed.
|
||||
SaveFinished(SaveOutcome),
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
CommandInput(String),
|
||||
CommandSubmit,
|
||||
@@ -2418,6 +2451,8 @@ impl OpenCADStudio {
|
||||
opening: None,
|
||||
pending_opens: std::collections::VecDeque::new(),
|
||||
pending_close: None,
|
||||
active_save_jobs: std::collections::HashMap::new(),
|
||||
save_job_serial: 0,
|
||||
save_dialog_format: "DWG 2018".to_string(),
|
||||
save_dialog_filename: "drawing.dwg".to_string(),
|
||||
save_dialog_for_unsaved: false,
|
||||
|
||||
+76
-123
@@ -234,130 +234,83 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
}
|
||||
|
||||
pub(super) fn on_unsaved_dialog_save(&mut self) -> Task<Message> {
|
||||
match self.pending_close.take() {
|
||||
Some(crate::app::PendingClose::Tab(idx)) => {
|
||||
if let Some(path) = if cfg!(target_arch = "wasm32") { None } else { self.tabs[idx].current_path.clone() } {
|
||||
match crate::io::save(&self.tabs[idx].scene.document, &path) {
|
||||
Ok(()) => {
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", path.display()));
|
||||
self.tabs[idx].dirty = false;
|
||||
let close_win = self.close_unsaved_dialog_window();
|
||||
let close_tab = self.update(Message::TabClose(idx));
|
||||
return Task::batch(vec![close_win, close_tab]);
|
||||
}
|
||||
Err(e) => {
|
||||
// Keep dialog open for retry.
|
||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No path — close unsaved dialog and save a default
|
||||
// DWG 2018 via the native destination dialog (no
|
||||
// version picker; that is reserved for Save As).
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
||||
self.save_dialog_for_unsaved = true;
|
||||
let close_win = self.close_unsaved_dialog_window();
|
||||
let open_save = self.save_default_dwg2018(idx);
|
||||
return Task::batch([close_win, open_save]);
|
||||
}
|
||||
}
|
||||
Some(crate::app::PendingClose::Quit) => {
|
||||
if let Some(idx) = self.tabs.iter().position(|t| t.dirty) {
|
||||
if let Some(path) = if cfg!(target_arch = "wasm32") { None } else { self.tabs[idx].current_path.clone() } {
|
||||
match crate::io::save(&self.tabs[idx].scene.document, &path) {
|
||||
Ok(()) => {
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", path.display()));
|
||||
self.tabs[idx].dirty = false;
|
||||
}
|
||||
Err(e) => {
|
||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No path — close unsaved dialog and save a
|
||||
// default DWG 2018 via the native destination
|
||||
// dialog (version picker is Save-As only).
|
||||
self.active_tab = idx;
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
self.save_dialog_for_unsaved = true;
|
||||
let close_win = self.close_unsaved_dialog_window();
|
||||
let open_save = self.save_default_dwg2018(idx);
|
||||
return Task::batch([close_win, open_save]);
|
||||
}
|
||||
}
|
||||
if self.tabs.iter().any(|t| t.dirty) {
|
||||
// More dirty tabs — keep window open.
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
} else {
|
||||
let close_win = self.close_unsaved_dialog_window();
|
||||
return Task::batch(vec![close_win, self.exit_app()]);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let Some(pending) = self.pending_close.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
let (idx, continuation) = match pending {
|
||||
crate::app::PendingClose::Tab(idx) => {
|
||||
(idx, crate::app::SaveContinuation::CloseTab)
|
||||
}
|
||||
Task::none()
|
||||
crate::app::PendingClose::Quit => {
|
||||
let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) else {
|
||||
self.pending_close = None;
|
||||
return Task::batch([
|
||||
self.close_unsaved_dialog_window(),
|
||||
self.exit_app(),
|
||||
]);
|
||||
};
|
||||
(idx, crate::app::SaveContinuation::Quit)
|
||||
}
|
||||
};
|
||||
|
||||
if self.active_save_jobs.contains_key(&self.tabs[idx].id) {
|
||||
self.command_line
|
||||
.push_info("Save already running for this drawing.");
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if let Some(path) = self.tabs[idx].current_path.clone() {
|
||||
let version = self.tabs[idx].scene.document.version;
|
||||
self.prepare_native_save(idx);
|
||||
let close = self.close_unsaved_dialog_window();
|
||||
let save = self.queue_native_save(
|
||||
idx,
|
||||
path,
|
||||
version,
|
||||
crate::app::SavePurpose::Manual,
|
||||
continuation,
|
||||
false,
|
||||
);
|
||||
return Task::batch([close, save]);
|
||||
}
|
||||
|
||||
self.active_tab = idx;
|
||||
self.save_dialog_for_unsaved = true;
|
||||
let close = self.close_unsaved_dialog_window();
|
||||
let save = self.save_default_dwg2018(idx);
|
||||
return Task::batch([close, save]);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
match self.pending_close.take() {
|
||||
Some(crate::app::PendingClose::Tab(idx)) => {
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
||||
self.save_dialog_for_unsaved = true;
|
||||
let close = self.close_unsaved_dialog_window();
|
||||
let save = self.save_default_dwg2018(idx);
|
||||
Task::batch([close, save])
|
||||
}
|
||||
Some(crate::app::PendingClose::Quit) => {
|
||||
if let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) {
|
||||
self.active_tab = idx;
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
self.save_dialog_for_unsaved = true;
|
||||
let close = self.close_unsaved_dialog_window();
|
||||
let save = self.save_default_dwg2018(idx);
|
||||
Task::batch([close, save])
|
||||
} else {
|
||||
Task::batch([
|
||||
self.close_unsaved_dialog_window(),
|
||||
self.exit_app(),
|
||||
])
|
||||
}
|
||||
}
|
||||
None => Task::none(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_unsaved_picked_save_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
||||
let (_ext, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
||||
match self.pending_close.take() {
|
||||
Some(crate::app::PendingClose::Tab(idx)) => {
|
||||
match crate::io::save_as_version(
|
||||
&self.tabs[idx].scene.document,
|
||||
&path,
|
||||
version,
|
||||
) {
|
||||
Ok(()) => {
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", path.display()));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(self.autosave_target(idx));
|
||||
self.tabs[idx].current_path = Some(path);
|
||||
self.tabs[idx].dirty = false;
|
||||
return self.update(Message::TabClose(idx));
|
||||
}
|
||||
Err(e) => {
|
||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
||||
return self.open_unsaved_dialog_window();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(crate::app::PendingClose::Quit) => {
|
||||
let i = self.active_tab;
|
||||
match crate::io::save_as_version(
|
||||
&self.tabs[i].scene.document,
|
||||
&path,
|
||||
version,
|
||||
) {
|
||||
Ok(()) => {
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", path.display()));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(self.autosave_target(i));
|
||||
self.tabs[i].current_path = Some(path);
|
||||
self.tabs[i].dirty = false;
|
||||
if self.tabs.iter().any(|t| t.dirty) {
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
return self.open_unsaved_dialog_window();
|
||||
} else {
|
||||
return self.exit_app();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
return self.open_unsaved_dialog_window();
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
+254
-69
@@ -768,6 +768,213 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
||||
self.sync_vport_display(i);
|
||||
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
||||
self.stamp_header_sysvars(i);
|
||||
self.tabs[i].scene.document.header.user_real1 =
|
||||
self.tabs[i].scene.annotation_scale as f64;
|
||||
self.sync_truck_solids_to_acis(i);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(in crate::app) fn queue_native_save(
|
||||
&mut self,
|
||||
i: usize,
|
||||
path: std::path::PathBuf,
|
||||
version: acadrust::DxfVersion,
|
||||
purpose: crate::app::SavePurpose,
|
||||
continuation: crate::app::SaveContinuation,
|
||||
set_current_path: bool,
|
||||
) -> Task<Message> {
|
||||
let tab_id = self.tabs[i].id;
|
||||
if self.active_save_jobs.contains_key(&tab_id) {
|
||||
if purpose != crate::app::SavePurpose::Autosave {
|
||||
self.command_line
|
||||
.push_info("Save already running for this drawing.");
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let epoch = self.tabs[i].scene.geometry_epoch;
|
||||
let revision = self.tabs[i].edit_revision;
|
||||
let camera_generation = self.tabs[i].scene.camera_generation;
|
||||
let thumbnail = if purpose == crate::app::SavePurpose::Autosave {
|
||||
None
|
||||
} else {
|
||||
let scene = &self.tabs[i].scene;
|
||||
Some((
|
||||
scene.entity_wires(),
|
||||
scene.camera.borrow().clone(),
|
||||
scene.bg_color,
|
||||
version >= acadrust::DxfVersion::AC1027,
|
||||
self.vp_size,
|
||||
))
|
||||
};
|
||||
let clone_started = std::time::Instant::now();
|
||||
let mut snapshot = self.tabs[i].scene.document.clone();
|
||||
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if std::env::var_os("OCS_PERF").is_some() {
|
||||
eprintln!(
|
||||
"[perf] save-snapshot {:.1}ms entities={} objects={} purpose={purpose:?}",
|
||||
clone_ms,
|
||||
snapshot.entities().count(),
|
||||
snapshot.objects.len(),
|
||||
);
|
||||
}
|
||||
|
||||
self.save_job_serial = self.save_job_serial.wrapping_add(1);
|
||||
let job_id = self.save_job_serial;
|
||||
self.active_save_jobs.insert(tab_id, job_id);
|
||||
let previous_autosave = set_current_path.then(|| self.autosave_target(i));
|
||||
let backup = purpose != crate::app::SavePurpose::Autosave && self.backup_on_save;
|
||||
let worker_path = path.clone();
|
||||
|
||||
Task::perform(
|
||||
async move {
|
||||
let result = std::thread::spawn(move || {
|
||||
if let Some((wires, camera, bg_color, png, viewport)) = thumbnail {
|
||||
let started = std::time::Instant::now();
|
||||
snapshot.preview = crate::io::thumbnail::from_snapshot(
|
||||
&wires,
|
||||
&camera,
|
||||
bg_color,
|
||||
png,
|
||||
viewport,
|
||||
);
|
||||
if std::env::var_os("OCS_PERF").is_some() {
|
||||
eprintln!(
|
||||
"[perf] save-thumbnail {:.1}ms wires={}",
|
||||
started.elapsed().as_secs_f64() * 1000.0,
|
||||
wires.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
crate::io::save_owned_as_version_atomic(
|
||||
snapshot,
|
||||
&worker_path,
|
||||
version,
|
||||
backup,
|
||||
)
|
||||
})
|
||||
.join()
|
||||
.unwrap_or_else(|_| Err("save worker panicked".to_string()));
|
||||
crate::app::SaveOutcome {
|
||||
job_id,
|
||||
tab_id,
|
||||
epoch,
|
||||
revision,
|
||||
camera_generation,
|
||||
path,
|
||||
previous_autosave,
|
||||
set_current_path,
|
||||
purpose,
|
||||
continuation,
|
||||
result,
|
||||
}
|
||||
},
|
||||
Message::SaveFinished,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn on_save_finished(
|
||||
&mut self,
|
||||
outcome: crate::app::SaveOutcome,
|
||||
) -> Task<Message> {
|
||||
let latest = self.active_save_jobs.get(&outcome.tab_id).copied()
|
||||
== Some(outcome.job_id);
|
||||
if latest {
|
||||
self.active_save_jobs.remove(&outcome.tab_id);
|
||||
}
|
||||
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == outcome.tab_id) else {
|
||||
if outcome.purpose == crate::app::SavePurpose::Autosave {
|
||||
let _ = std::fs::remove_file(&outcome.path);
|
||||
}
|
||||
return Task::none();
|
||||
};
|
||||
if !latest {
|
||||
if outcome.purpose == crate::app::SavePurpose::Autosave {
|
||||
let _ = std::fs::remove_file(&outcome.path);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if let Err(error) = outcome.result {
|
||||
self.command_line
|
||||
.push_error(&format!("Save failed: {error}"));
|
||||
return match outcome.continuation {
|
||||
crate::app::SaveContinuation::CloseTab => {
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(i));
|
||||
self.open_unsaved_dialog_window()
|
||||
}
|
||||
crate::app::SaveContinuation::Quit => {
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
self.open_unsaved_dialog_window()
|
||||
}
|
||||
crate::app::SaveContinuation::None => Task::none(),
|
||||
};
|
||||
}
|
||||
|
||||
let snapshot_is_current = self.tabs[i].scene.geometry_epoch == outcome.epoch
|
||||
&& self.tabs[i].edit_revision == outcome.revision
|
||||
&& self.tabs[i].scene.camera_generation == outcome.camera_generation;
|
||||
let mut tasks = Vec::new();
|
||||
match outcome.purpose {
|
||||
crate::app::SavePurpose::Autosave => {
|
||||
self.command_line.push_output("Autosaved 1 drawing");
|
||||
}
|
||||
crate::app::SavePurpose::Manual | crate::app::SavePurpose::SaveAs => {
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", outcome.path.display()));
|
||||
self.recent_thumbs.remove(&outcome.path);
|
||||
if let Some(previous) = outcome.previous_autosave {
|
||||
if previous != outcome.path {
|
||||
let _ = std::fs::remove_file(previous);
|
||||
}
|
||||
}
|
||||
if outcome.set_current_path {
|
||||
self.tabs[i].current_path = Some(outcome.path.clone());
|
||||
tasks.push(self.push_recent(outcome.path.clone()));
|
||||
}
|
||||
if snapshot_is_current {
|
||||
self.tabs[i].dirty = false;
|
||||
let _ = std::fs::remove_file(outcome.path.with_extension("sv$"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match outcome.continuation {
|
||||
crate::app::SaveContinuation::None => {}
|
||||
crate::app::SaveContinuation::CloseTab if snapshot_is_current => {
|
||||
self.pending_close = None;
|
||||
tasks.push(self.close_unsaved_dialog_window());
|
||||
tasks.push(self.update(Message::TabClose(i)));
|
||||
}
|
||||
crate::app::SaveContinuation::Quit if snapshot_is_current => {
|
||||
self.pending_close = None;
|
||||
if self.tabs.iter().any(|tab| tab.dirty) {
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
tasks.push(self.open_unsaved_dialog_window());
|
||||
} else {
|
||||
tasks.push(self.close_unsaved_dialog_window());
|
||||
tasks.push(self.exit_app());
|
||||
}
|
||||
}
|
||||
crate::app::SaveContinuation::CloseTab => {
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(i));
|
||||
tasks.push(self.open_unsaved_dialog_window());
|
||||
}
|
||||
crate::app::SaveContinuation::Quit => {
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
tasks.push(self.open_unsaved_dialog_window());
|
||||
}
|
||||
}
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
pub(super) fn on_save_file(&mut self) -> Task<Message> {
|
||||
if self.read_only {
|
||||
self.command_line
|
||||
@@ -775,37 +982,28 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
// Stamp the live grid/snap toggles onto the VPort so the file
|
||||
// reflects them even if they came from settings with no
|
||||
// in-session toggle (#121).
|
||||
self.sync_vport_display(i);
|
||||
// Persist Ortho ($ORTHOMODE) + running OSNAP ($OSMODE) into the
|
||||
// drawing header so they survive save/reopen (per-drawing).
|
||||
self.stamp_header_sysvars(i);
|
||||
// Web serializes immediately below. Native preparation happens
|
||||
// once after the destination/version is known.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.sync_vport_display(i);
|
||||
self.stamp_header_sysvars(i);
|
||||
}
|
||||
// Native: save straight to the known path. Web has no path
|
||||
// (downloads instead), so always go through the Save dialog.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(path) = self.tabs[i].current_path.clone() {
|
||||
self.tabs[i].scene.document.header.user_real1 =
|
||||
self.tabs[i].scene.annotation_scale as f64;
|
||||
// A direct Save preserves the document's current version.
|
||||
if self.backup_on_save {
|
||||
crate::io::write_backup(&path);
|
||||
}
|
||||
self.sync_truck_solids_to_acis(i);
|
||||
let ver = self.tabs[i].scene.document.version;
|
||||
self.stamp_thumbnail(i, ver);
|
||||
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;
|
||||
// A clean save supersedes any autosave recovery copy.
|
||||
let _ = std::fs::remove_file(path.with_extension("sv$"));
|
||||
}
|
||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
||||
}
|
||||
return Task::none();
|
||||
self.prepare_native_save(i);
|
||||
return self.queue_native_save(
|
||||
i,
|
||||
path,
|
||||
ver,
|
||||
crate::app::SavePurpose::Manual,
|
||||
crate::app::SaveContinuation::None,
|
||||
false,
|
||||
);
|
||||
}
|
||||
self.save_dialog_for_unsaved = false;
|
||||
self.save_default_dwg2018(i)
|
||||
@@ -937,37 +1135,24 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
};
|
||||
let (_ext, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
||||
let i = self.active_tab;
|
||||
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
||||
// Persist Ortho / running OSNAP into the header (Save-As path).
|
||||
self.stamp_header_sysvars(i);
|
||||
self.sync_truck_solids_to_acis(i);
|
||||
self.stamp_thumbnail(i, version);
|
||||
if self.backup_on_save {
|
||||
crate::io::write_backup(&path);
|
||||
}
|
||||
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
||||
Ok(()) => {
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", path.display()));
|
||||
// Drop any prior autosave copy — including the temp one used
|
||||
// while the drawing was still unsaved — before the tab takes on
|
||||
// its new path.
|
||||
let _ = std::fs::remove_file(self.autosave_target(i));
|
||||
self.tabs[i].current_path = Some(path.clone());
|
||||
self.tabs[i].dirty = false;
|
||||
let _ = std::fs::remove_file(path.with_extension("sv$"));
|
||||
let recent = self.push_recent(path.clone());
|
||||
if self.save_dialog_for_unsaved {
|
||||
return Task::batch([
|
||||
recent,
|
||||
self.update(Message::UnsavedPickedSavePath(Some(path))),
|
||||
]);
|
||||
}
|
||||
return recent;
|
||||
self.prepare_native_save(i);
|
||||
let continuation = if self.save_dialog_for_unsaved {
|
||||
match self.pending_close {
|
||||
Some(crate::app::PendingClose::Tab(_)) => crate::app::SaveContinuation::CloseTab,
|
||||
Some(crate::app::PendingClose::Quit) => crate::app::SaveContinuation::Quit,
|
||||
None => crate::app::SaveContinuation::None,
|
||||
}
|
||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
||||
}
|
||||
Task::none()
|
||||
} else {
|
||||
crate::app::SaveContinuation::None
|
||||
};
|
||||
self.queue_native_save(
|
||||
i,
|
||||
path,
|
||||
version,
|
||||
crate::app::SavePurpose::SaveAs,
|
||||
continuation,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
/// AEC-drop warning → "Save anyway": accept the loss and proceed with the
|
||||
@@ -1019,25 +1204,25 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
/// touches the original file or the dirty flag.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
||||
let mut n = 0;
|
||||
let mut tasks = Vec::new();
|
||||
for i in 0..self.tabs.len() {
|
||||
if !self.tabs[i].dirty {
|
||||
if !self.tabs[i].dirty
|
||||
|| self.active_save_jobs.contains_key(&self.tabs[i].id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let version = self.tabs[i].scene.document.version;
|
||||
if let Ok(bytes) =
|
||||
crate::io::save_to_bytes(&self.tabs[i].scene.document, "dwg", version)
|
||||
{
|
||||
if std::fs::write(self.autosave_target(i), bytes).is_ok() {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
let target = self.autosave_target(i);
|
||||
tasks.push(self.queue_native_save(
|
||||
i,
|
||||
target,
|
||||
version,
|
||||
crate::app::SavePurpose::Autosave,
|
||||
crate::app::SaveContinuation::None,
|
||||
false,
|
||||
));
|
||||
}
|
||||
if n > 0 {
|
||||
self.command_line
|
||||
.push_output(&format!("Autosaved {n} drawing(s)"));
|
||||
}
|
||||
Task::none()
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
+4
-10
@@ -3770,17 +3770,11 @@ impl OpenCADStudio {
|
||||
|
||||
Message::AutoSave => self.on_autosave(),
|
||||
|
||||
Message::UnsavedPickedSavePath(Some(path)) => {
|
||||
self.on_unsaved_picked_save_path_some(path)
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::SaveFinished(outcome) => self.on_save_finished(outcome),
|
||||
|
||||
Message::UnsavedPickedSavePath(None) => {
|
||||
// User cancelled the save-as dialog — re-open the confirmation dialog.
|
||||
if self.pending_close.is_some() {
|
||||
return self.open_unsaved_dialog_window();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::SaveFinished(_) => Task::none(),
|
||||
|
||||
// ── Page Setup ────────────────────────────────────────────────
|
||||
Message::UpdateCheckResult(latest) => {
|
||||
|
||||
Reference in new issue
Block a user