feat: per-viewport grid/snap, adopted from the active viewport

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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-18 22:25:44 +03:00
1 parent b2feeb716b
commit d4a665a34c
3 files changed
+80 -56

No files matched your search

+3 -1
View File
@@ -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 {
+29 -14
View File
@@ -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<Message> {
@@ -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();
}
}
+48 -41
View File
@@ -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<acadrust::tables::VPort> = 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<bool> {
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<iced::Rectangle>) {
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<ModelTile> = 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