From d1efa4ea7c56824f2d2d34880f8e669dc8dd26e0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 28 Jun 2026 02:46:13 +0300 Subject: [PATCH] feat(io): preserve source DWG version for files with version-locked data Civil3D/AEC objects (and raw MLEADER/Surface records, EED) can only be written losslessly in their source DWG version. On save, fall back to the source version when the chosen version is an incompatible encoding family (acadrust CadDocument::has_version_locked_data), with a command-line notice, across every save path (Save, Save As, save-before-close/quit) via a shared dwg_save_version helper. Default the Save-As format dropdown to the loaded file's version so a round-trip preserves it. Picks up acadrust DWG-writer conformance fixes (3dbae05) so a Civil3D R2004 drawing round-trips through OCS and opens in AutoCAD with no errors. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 4 +-- src/app/update/dialog.rs | 20 ++++++++++++++- src/app/update/file.rs | 53 +++++++++++++++++++++++++++++++++++++++- src/io/mod.rs | 19 ++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) 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.