From d4a665a34c79e04afa27f6ce53f321263d85b13c Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 18 Jun 2026 22:25:44 +0300 Subject: [PATCH] feat: per-viewport grid/snap, adopted from the active viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make grid-snap a per-drawing view setting like grid visibility: drop it from the persisted OSNAP set (settings.txt) and round-trip it through each viewport's VPort SNAPMODE instead. Grid display and grid-snap are now stored per model tile (like render mode), written to and read from each tile's *Active VPort entry. The live toggles mirror the active tile, and grid/snap are adopted from the active viewport whenever it changes — on file load, tab switch, model-tile activation, and new drawing — so they follow the active viewport rather than a global preference. Co-Authored-By: Claude Opus 4.8 --- src/app/settings.rs | 4 +- src/app/update.rs | 43 +++++++++++++++------- src/scene/mod.rs | 89 ++++++++++++++++++++++++--------------------- 3 files changed, 80 insertions(+), 56 deletions(-) diff --git a/src/app/settings.rs b/src/app/settings.rs index e7f8c445b..b4bdcafe4 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -27,7 +27,9 @@ const SNAP_ORDER: &[SnapType] = &[ SnapType::Nearest, SnapType::ApparentIntersection, SnapType::Parallel, - SnapType::Grid, + // Grid snap (SNAPMODE) is a per-drawing view setting stored on the VPort, + // not a global OSNAP preference, so it is deliberately excluded from the + // persisted set. (#121) ]; fn snap_id(s: SnapType) -> &'static str { diff --git a/src/app/update.rs b/src/app/update.rs index e249765c2..60dfa7617 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -340,17 +340,26 @@ impl OpenCADStudio { self.persist_settings_if_changed(); } - /// Mirror the current grid/snap toggles onto tab `i`'s `*Active` VPort - /// entries so a save writes the real display state rather than VPort - /// defaults (#121). Cheap; safe to call every frame. + /// Mirror the live grid display + grid-snap toggles onto tab `i`'s active + /// model tile so a save writes them to that viewport's VPort entry (#121). fn sync_vport_display(&mut self, i: usize) { let grid_on = self.show_grid; - let snap_on = - self.snapper.snap_enabled && self.snapper.is_on(crate::snap::SnapType::Grid); - let grid_spacing = self.snapper.grid_spacing as f64; - self.tabs[i] - .scene - .set_active_vport_display(grid_on, snap_on, grid_spacing); + let snap_on = self.snapper.is_on(crate::snap::SnapType::Grid); + self.tabs[i].scene.set_active_tile_grid_snap(grid_on, snap_on); + } + + /// Adopt the active viewport's grid display + grid-snap into the live + /// toggles. Called on load and whenever the active tab or viewport changes, + /// so grid/snap follow the active viewport rather than a global setting. + fn adopt_view_display(&mut self, i: usize) { + if let Some((grid_on, snap_on)) = self.tabs[i].scene.active_tile_grid_snap() { + self.show_grid = grid_on; + if snap_on { + self.snapper.enabled.insert(crate::snap::SnapType::Grid); + } else { + self.snapper.enabled.remove(&crate::snap::SnapType::Grid); + } + } } fn update_inner(&mut self, msg: Message) -> Task { @@ -598,11 +607,9 @@ impl OpenCADStudio { // selected — see #21. self.sync_ribbon_from_selection(); self.tabs[i].scene.restore_saved_camera(); - // Grid visibility is a per-drawing view setting — adopt the - // opened file's saved state rather than a global preference. - if let Some(grid_on) = self.tabs[i].scene.active_vport_grid_on() { - self.show_grid = grid_on; - } + // Grid/snap are per-drawing view settings — adopt the opened + // file's active viewport state rather than a global preference. + self.adopt_view_display(i); self.sync_render_mode_to_active_tile(i); self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; self.tabs[i].dirty = false; @@ -1263,6 +1270,8 @@ impl OpenCADStudio { // fresh tab's defaults (ByLayer) instead of inheriting the // previous tab's last selection. self.sync_ribbon_from_selection(); + // A fresh drawing starts with grid/snap off (its tile defaults). + self.adopt_view_display(self.active_tab); Task::none() } @@ -1276,6 +1285,8 @@ impl OpenCADStudio { // CECOLOR / CELTYPE / CELWEIGHT (or its current selection // if there is one), not the prior tab's choice. self.sync_ribbon_from_selection(); + // Grid/snap follow the newly active drawing's viewport. + self.adopt_view_display(idx); } Task::none() } @@ -2471,6 +2482,8 @@ impl OpenCADStudio { { self.tabs[i].scene.camera_generation += 1; self.sync_render_mode_to_active_tile(i); + // Grid/snap follow the newly active viewport. + self.adopt_view_display(i); } } @@ -3092,6 +3105,8 @@ impl OpenCADStudio { { self.tabs[i].scene.camera_generation += 1; self.sync_render_mode_to_active_tile(i); + // Grid/snap follow the newly active viewport. + self.adopt_view_display(i); return Task::none(); } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 3e4aa9e40..69e65ac02 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -475,6 +475,11 @@ pub(crate) struct ModelTile { /// Visual style for this tile alone — each pane carries its own so /// changing one tile's render mode never touches the others. pub(crate) render_mode: acadrust::entities::ViewportRenderMode, + /// Grid display + grid-snap for this viewport alone, round-tripped through + /// its VPort entry. The app mirrors the *active* tile's pair into the live + /// grid/snap toggles. (#121) + pub(crate) grid_on: bool, + pub(crate) snap_on: bool, } /// Tolerance for matching two normalized tile coordinates as "the same" @@ -792,6 +797,8 @@ impl Scene { }, camera: Camera::default(), render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D, + grid_on: false, + snap_on: false, }]), active_model_tile: std::cell::Cell::new(0), selection: Rc::new(RefCell::new(SelectionState::default())), @@ -5383,14 +5390,17 @@ impl Scene { /// Set the model-space camera from the VPORT table's *Active entry. /// Returns true if the entry was found and the camera was set. fn apply_active_vport_camera(&mut self) -> bool { - // Restore the single tile's visual style from the *Active entry, - // independent of where the camera itself comes from below. + // Restore the single tile's visual style + grid/snap from the *Active + // entry, independent of where the camera itself comes from below. if let Some(vp) = self.document.vports.iter().find(|v| v.name == "*Active") { let mode = vp.render_mode; + let (grid_on, snap_on) = (vp.grid_on, vp.snap_on); let mut tiles = self.model_tiles.borrow_mut(); let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); if let Some(t) = tiles.get_mut(active) { t.render_mode = mode; + t.grid_on = grid_on; + t.snap_on = snap_on; } } // Prefer our named View entry — survives DWG save without being overridden. @@ -5542,6 +5552,8 @@ impl Scene { rect: Self::vport_to_tile_rect(vp.lower_left, vp.upper_right), camera: cam, render_mode: vp.render_mode, + grid_on: vp.grid_on, + snap_on: vp.snap_on, }) }) .collect(); @@ -5573,15 +5585,6 @@ impl Scene { } let table_handle = self.document.vports.handle(); - // Carry the grid/snap display state from the existing `*Active` entry - // across the rebuild — these flags track the in-app toggles and must - // not reset to VPort defaults each camera sync. (#121) - let display = self - .document - .vports - .iter() - .find(|v| v.name == "*Active") - .map(|v| (v.grid_on, v.snap_on, v.grid_spacing, v.snap_spacing, v.snap_base)); let preserved_vps: Vec = self .document .vports @@ -5613,41 +5616,32 @@ impl Scene { let (ll, ur) = Self::tile_rect_to_vport(tile.rect); let mut entry = self.vport_from_camera("*Active", &tile.camera, ll, ur); entry.render_mode = tile.render_mode; - if let Some((grid_on, snap_on, grid_spacing, snap_spacing, snap_base)) = display { - entry.grid_on = grid_on; - entry.snap_on = snap_on; - entry.grid_spacing = grid_spacing; - entry.snap_spacing = snap_spacing; - entry.snap_base = snap_base; - } + // Each viewport persists its own grid display + grid-snap (#121). + entry.grid_on = tile.grid_on; + entry.snap_on = tile.snap_on; entry.handle = self.document.allocate_handle(); self.document.vports.add_allow_duplicate(entry); } } - /// Stamp the grid/snap display state onto every `*Active` VPort entry so a - /// saved file reflects the in-app grid/snap toggles (otherwise other CAD - /// apps read VPort defaults). `grid_spacing` is applied to both axes and - /// mirrored to the snap spacing. (#121) - pub fn set_active_vport_display(&mut self, grid_on: bool, snap_on: bool, grid_spacing: f64) { - let spacing = acadrust::types::Vector2 { x: grid_spacing, y: grid_spacing }; - for vp in self.document.vports.iter_mut().filter(|v| v.name == "*Active") { - vp.grid_on = grid_on; - vp.snap_on = snap_on; - vp.grid_spacing = spacing; - vp.snap_spacing = spacing; + /// Mirror the live grid/snap toggles onto the *active* model tile so the + /// save writes them to that viewport's VPort entry. (#121) + pub fn set_active_tile_grid_snap(&self, grid_on: bool, snap_on: bool) { + let mut tiles = self.model_tiles.borrow_mut(); + let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); + if let Some(t) = tiles.get_mut(active) { + t.grid_on = grid_on; + t.snap_on = snap_on; } } - /// Grid visibility stored on the `*Active` VPort entry, applied to the app's - /// grid toggle when a file loads (grid is a per-drawing view setting, not a - /// global preference). `None` when there is no active entry. (#121) - pub fn active_vport_grid_on(&self) -> Option { - self.document - .vports - .iter() - .find(|v| v.name == "*Active") - .map(|v| v.grid_on) + /// The active model tile's grid display + grid-snap, adopted into the live + /// toggles on load and whenever the active viewport / tab changes. `None` + /// when there are no tiles. (#121) + pub fn active_tile_grid_snap(&self) -> Option<(bool, bool)> { + let tiles = self.model_tiles.borrow(); + let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); + tiles.get(active).map(|t| (t.grid_on, t.snap_on)) } /// Set the paper-space camera from the sheet viewport's stored view. @@ -6344,10 +6338,13 @@ impl Scene { ) }; let mode = tiles[active].render_mode; + let (grid_on, snap_on) = (tiles[active].grid_on, tiles[active].snap_on); tiles[active] = ModelTile { rect: a, camera: cam_now.clone(), render_mode: mode, + grid_on, + snap_on, }; tiles.insert( active + 1, @@ -6355,6 +6352,8 @@ impl Scene { rect: b, camera: cam_now, render_mode: mode, + grid_on, + snap_on, }, ); } @@ -6402,13 +6401,17 @@ impl Scene { pub fn set_model_tile_layout(&self, rects: Vec) { let cam_now = self.camera.borrow().clone(); // Every new pane inherits the active tile's current visual style. - let mode = { + let (mode, grid_on, snap_on) = { let tiles = self.model_tiles.borrow(); let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1)); tiles .get(active) - .map(|t| t.render_mode) - .unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D) + .map(|t| (t.render_mode, t.grid_on, t.snap_on)) + .unwrap_or(( + acadrust::entities::ViewportRenderMode::Wireframe2D, + false, + false, + )) }; let tiles: Vec = rects .into_iter() @@ -6416,6 +6419,8 @@ impl Scene { rect, camera: cam_now.clone(), render_mode: mode, + grid_on, + snap_on, }) .collect(); *self.model_tiles.borrow_mut() = if tiles.is_empty() { @@ -6428,6 +6433,8 @@ impl Scene { }, camera: cam_now, render_mode: mode, + grid_on, + snap_on, }] } else { tiles