diff --git a/Cargo.lock b/Cargo.lock index 58971a6a3..39c5cdc33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/HakanSeven12/acadrust?branch=main#ad507470661689a85a999166daf7f22ee507e63c" +source = "git+https://github.com/HakanSeven12/acadrust?branch=main#3dbae0556208221822257ead37a8d445dcc7d768" dependencies = [ "ahash 0.8.12", "anyhow", @@ -3410,7 +3410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", - "memchr 1.0.2", + "memchr 2.8.2", "nom 8.0.0", ] diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 84c967220..89df077c8 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -23,6 +23,20 @@ use iced::{mouse, Point, Task}; impl OpenCADStudio { pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task { + // Default the format dropdown to the loaded file's version so a + // round-trip preserves it (e.g. an R2004 drawing offers "DWG 2004", + // not "DWG 2018"). The save still falls back to the source version if + // the user picks an incompatible one (dwg_save_version). + let is_dxf = self.tabs[tab_idx] + .current_path + .as_ref() + .and_then(|p| p.extension()) + .and_then(|e| e.to_str()) + .map(|e| e.eq_ignore_ascii_case("dxf")) + .unwrap_or(false); + let doc_version = self.tabs[tab_idx].scene.document.version; + self.save_dialog_format = crate::io::format_for_version(doc_version, is_dxf); + // Pre-fill filename and folder from current path or defaults. if let Some(p) = &self.tabs[tab_idx].current_path.clone() { if let Some(name) = p.file_name() { @@ -244,9 +258,11 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven } pub(super) fn on_unsaved_picked_save_path_some(&mut self, path: std::path::PathBuf) -> Task { - let (_, version) = crate::io::parse_save_format(&self.save_dialog_format); + let (ext, version) = crate::io::parse_save_format(&self.save_dialog_format); match self.pending_close.take() { Some(crate::app::PendingClose::Tab(idx)) => { + // Fall back to source version for version-locked data. + let version = self.dwg_save_version(idx, ext, version); match crate::io::save_as_version( &self.tabs[idx].scene.document, &path, @@ -268,6 +284,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven } Some(crate::app::PendingClose::Quit) => { let i = self.active_tab; + // Fall back to source version for version-locked data. + let version = self.dwg_save_version(i, ext, version); match crate::io::save_as_version( &self.tabs[i].scene.document, &path, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index a6d42d556..84ff81689 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -518,6 +518,43 @@ pub(super) fn on_open_file(&mut self) -> Task { Task::none() } + /// Resolve the effective DWG save version for `tab`. + /// + /// A drawing that carries data locked to its source DWG version (e.g. + /// AEC/Civil3D objects with no published spec, raw MLEADER/Surface records, + /// EED) can only be written losslessly in that version. When the requested + /// version is in a different encoding family, fall back to the source + /// version so nothing is dropped and the file opens cleanly elsewhere, and + /// note it on the command line. Non-DWG / convertible saves are unchanged. + /// Shared by every save path (Save, Save As, save-before-close). + pub(crate) fn dwg_save_version( + &mut self, + tab: usize, + ext: &str, + requested: acadrust::DxfVersion, + ) -> acadrust::DxfVersion { + if ext != "dwg" { + return requested; + } + let fallback = { + let doc = &self.tabs[tab].scene.document; + match doc.dwg_source_version { + Some(src) if src != requested && doc.has_version_locked_data(requested) => Some(src), + _ => None, + } + }; + match fallback { + Some(src) => { + self.command_line.push_output(&format!( + "Saved in source DWG version ({src:?}) to preserve objects that cannot be \ + converted to the selected version." + )); + src + } + None => requested, + } + } + pub(super) fn on_save_file(&mut self) -> Task { if self.read_only { self.command_line @@ -535,7 +572,17 @@ pub(super) fn on_open_file(&mut self) -> Task { 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; - match crate::io::save(&self.tabs[i].scene.document, &path) { + // Preserve the document's version, but fall back to the + // source version if it carries version-locked data (so a + // direct Save can't silently drop AEC/Civil3D objects). + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + let requested = self.tabs[i].scene.document.version; + let version = self.dwg_save_version(i, &ext, requested); + match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) { Ok(()) => { self.command_line .push_output(&format!("Saved: {}", path.display())); @@ -567,6 +614,10 @@ pub(super) fn on_open_file(&mut self) -> Task { let i = self.active_tab; sync_annotation_scale_header(&mut self.tabs[i].scene); + // Fall back to the source version for files with version-locked + // data (e.g. AEC/Civil3D objects) so nothing is dropped. + let version = self.dwg_save_version(i, ext, version); + // Native: write to the chosen path. Web: download the bytes // under the chosen name (no filesystem). #[cfg(not(target_arch = "wasm32"))] diff --git a/src/io/mod.rs b/src/io/mod.rs index f4c525535..3aa646c8a 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -423,6 +423,25 @@ pub fn parse_save_format(format: &str) -> (&'static str, acadrust::DxfVersion) { (ext, version) } +/// Reverse of [`parse_save_format`]: the Save-dialog format string for a +/// version + DXF/DWG choice (e.g. `AC1018, is_dxf=false` -> `"DWG 2004"`). +/// Used to default the Save-As dropdown to the loaded file's version so a +/// round-trip preserves it instead of silently offering "DWG 2018". +pub fn format_for_version(version: acadrust::DxfVersion, is_dxf: bool) -> String { + use acadrust::DxfVersion::*; + let year = match version { + AC1032 => "2018", + AC1027 => "2013", + AC1024 => "2010", + AC1021 => "2007", + AC1018 => "2004", + AC1015 => "2000", + AC1014 => "R14", + _ => "2018", + }; + format!("{} {}", if is_dxf { "DXF" } else { "DWG" }, year) +} + // ── Plot Style Table ────────────────────────────────────────────────────── /// Show a file-open dialog and load the selected CTB or STB file.