From dd3ce6767c0967c18c294f89da79fc00f5ba2f92 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 12 Jul 2026 01:01:40 +0300 Subject: [PATCH] feat(layout): reopen a drawing in the space it was saved in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Files saved while a paper layout was active reopened in Model space because the loader hardcoded the current layout to "Model" and never consulted the space recorded in the file. The active space is a standard part of the format and acadrust already round-trips it: $TILEMODE (header.show_model_space) for model-vs-paper, and the CTAB current-tab variable for the exact layout name. - Read: on open, pick the current layout from CTAB (when it names a layout that exists), else $TILEMODE (Model, or the first paper layout). - Write: Scene::sync_active_space_to_document() mirrors the active layout back into the document (show_model_space + CTAB). It runs from set_current_layout, so every active-layout change — the status-bar tab, the Layout Manager's Set Current, create / rename / delete, and undo/redo — keeps the document in sync for the next save. $TILEMODE is the guaranteed mechanism (read and written for both DXF and DWG); CTAB refines the exact tab when the variable is present. Co-Authored-By: Claude Opus 4.8 --- src/app/update/file.rs | 23 ++++++++++++++++++++++- src/app/update/mod.rs | 8 +++----- src/app/update/viewport.rs | 4 ++-- src/io/mod.rs | 14 ++++++++++++++ src/scene/layout.rs | 1 + src/scene/mod.rs | 11 +++++++++++ 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index faea7921f..0bba88e4a 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -489,7 +489,28 @@ pub(super) fn on_open_file(&mut self) -> Task { } self.tabs[i].scene.selected = rustc_hash::FxHashSet::default(); self.tabs[i].scene.preview_wires = vec![]; - self.tabs[i].scene.current_layout = "Model".to_string(); + // Reopen in whichever space the file was saved in — the CTAB + // tab name when recorded, else the $TILEMODE model/paper flag — + // instead of always landing in Model. + { + let names = self.tabs[i].scene.layout_names(); + let saved = crate::io::saved_active_layout(&self.tabs[i].scene.document) + .and_then(|n| { + names.iter().find(|x| x.eq_ignore_ascii_case(&n)).cloned() + }); + self.tabs[i].scene.current_layout = match saved { + Some(n) => n, + None if self.tabs[i].scene.document.header.show_model_space => { + "Model".to_string() + } + // Paper was active but no CTAB — fall back to the first + // paper layout (names[0] is always "Model"). + None => names + .into_iter() + .nth(1) + .unwrap_or_else(|| "Model".to_string()), + }; + } // Rebuild the Isolate/Hide set from the entities the file itself // marks invisible (DXF code 60), so hidden objects stay hidden on // reopen and End Isolation can bring them back. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index f6b08ac4a..1fda1a44c 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2569,7 +2569,7 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "LAYOUT RENAME"); self.tabs[i].scene.rename_layout(&old_name, &new_name); if self.tabs[i].scene.current_layout == old_name { - self.tabs[i].scene.current_layout = new_name.clone(); + self.tabs[i].scene.set_current_layout(new_name.clone()); } self.layout_manager_selected = new_name.clone(); self.tabs[i].dirty = true; @@ -2610,8 +2610,7 @@ impl OpenCADStudio { self.tabs[i].dirty = true; // Switch to Model if active layout was deleted. if self.tabs[i].scene.current_layout == name { - self.tabs[i].scene.current_layout = "Model".to_string(); - self.tabs[i].scene.bump_geometry(); + self.tabs[i].scene.set_current_layout("Model".to_string()); } self.layout_manager_selected = "Model".to_string(); self.layout_manager_rename_buf = String::new(); @@ -2658,8 +2657,7 @@ impl OpenCADStudio { Message::LayoutManagerSetCurrent => { let i = self.active_tab; let name = self.layout_manager_selected.clone(); - self.tabs[i].scene.current_layout = name.clone(); - self.tabs[i].scene.bump_geometry(); + self.tabs[i].scene.set_current_layout(name.clone()); self.command_line .push_output(&format!("Switched to layout '{name}'.")); Task::none() diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 70cb82f32..975ac0a2b 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3050,7 +3050,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { } } } - self.tabs[i].scene.current_layout = new_name.clone(); + self.tabs[i].scene.set_current_layout(new_name.clone()); // Safety net — `add_layout` already creates the overall // sheet viewport; this covers any path that doesn't. self.tabs[i].scene.ensure_sheet_viewport(&new_name); @@ -3085,7 +3085,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { self.push_undo_snapshot(i, "LAYOUT RENAME"); self.tabs[i].scene.rename_layout(&orig, &new_name); if self.tabs[i].scene.current_layout == orig { - self.tabs[i].scene.current_layout = new_name.clone(); + self.tabs[i].scene.set_current_layout(new_name.clone()); } self.tabs[i].dirty = true; self.command_line diff --git a/src/io/mod.rs b/src/io/mod.rs index ab1ab5587..b735e1fac 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -530,6 +530,20 @@ fn set_vardict_value(doc: &mut CadDocument, name: &str, value: &str) { } } +/// The layout tab that was active when the drawing was saved — the `CTAB` +/// system variable (stored in the variable dictionary as a `DICTIONARYVAR`). +/// `None` when the file recorded no current tab. +pub fn saved_active_layout(doc: &CadDocument) -> Option { + vardict_value(doc, "CTAB").filter(|s| !s.is_empty()) +} + +/// Record `name` as the active layout tab (`CTAB`) so the next save round-trips +/// which space was open. No-op when the document has no `CTAB` entry yet; the +/// `$TILEMODE` header (model vs paper) remains the guaranteed fallback. +pub fn set_saved_active_layout(doc: &mut CadDocument, name: &str) { + set_vardict_value(doc, "CTAB", name); +} + /// Materialise the current-style choices into their format-specific storage /// before saving, treating the current-style *names* as the single source of /// truth: diff --git a/src/scene/layout.rs b/src/scene/layout.rs index 36068be47..d0ebe8d05 100644 --- a/src/scene/layout.rs +++ b/src/scene/layout.rs @@ -89,6 +89,7 @@ impl Scene { // If the deleted layout was active, fall back to Model space. if self.current_layout == name { self.current_layout = "Model".to_string(); + self.sync_active_space_to_document(); } self.bump_geometry(); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index b69c745ad..7816871dc 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1333,11 +1333,22 @@ impl Scene { pub fn set_current_layout(&mut self, name: String) { if self.current_layout != name { self.current_layout = name; + self.sync_active_space_to_document(); self.recolor_meshes(); self.bump_geometry(); } } + /// Mirror the active space (Model tile-mode vs a paper layout) into the + /// document's persisted settings so it round-trips on save: the `$TILEMODE` + /// header (`show_model_space`) and the `CTAB` current-tab variable. The + /// reader restores it via [`current_layout`] on open. Called whenever the + /// active layout changes; the file otherwise always reopened in Model. + pub fn sync_active_space_to_document(&mut self) { + self.document.header.show_model_space = self.current_layout == "Model"; + crate::io::set_saved_active_layout(&mut self.document, &self.current_layout); + } + /// Returns true if this viewport should display model-space content /// (i.e. it is a user viewport, not the sheet/overall viewport). ///