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 <noreply@anthropic.com>
This commit is contained in:
parent
b2feeb716b
commit
d4a665a34c
3 changed files with 80 additions and 56 deletions
|
|
@ -27,7 +27,9 @@ const SNAP_ORDER: &[SnapType] = &[
|
||||||
SnapType::Nearest,
|
SnapType::Nearest,
|
||||||
SnapType::ApparentIntersection,
|
SnapType::ApparentIntersection,
|
||||||
SnapType::Parallel,
|
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 {
|
fn snap_id(s: SnapType) -> &'static str {
|
||||||
|
|
|
||||||
|
|
@ -340,17 +340,26 @@ impl OpenCADStudio {
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mirror the current grid/snap toggles onto tab `i`'s `*Active` VPort
|
/// Mirror the live grid display + grid-snap toggles onto tab `i`'s active
|
||||||
/// entries so a save writes the real display state rather than VPort
|
/// model tile so a save writes them to that viewport's VPort entry (#121).
|
||||||
/// defaults (#121). Cheap; safe to call every frame.
|
|
||||||
fn sync_vport_display(&mut self, i: usize) {
|
fn sync_vport_display(&mut self, i: usize) {
|
||||||
let grid_on = self.show_grid;
|
let grid_on = self.show_grid;
|
||||||
let snap_on =
|
let snap_on = self.snapper.is_on(crate::snap::SnapType::Grid);
|
||||||
self.snapper.snap_enabled && self.snapper.is_on(crate::snap::SnapType::Grid);
|
self.tabs[i].scene.set_active_tile_grid_snap(grid_on, snap_on);
|
||||||
let grid_spacing = self.snapper.grid_spacing as f64;
|
}
|
||||||
self.tabs[i]
|
|
||||||
.scene
|
/// Adopt the active viewport's grid display + grid-snap into the live
|
||||||
.set_active_vport_display(grid_on, snap_on, grid_spacing);
|
/// 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> {
|
fn update_inner(&mut self, msg: Message) -> Task<Message> {
|
||||||
|
|
@ -598,11 +607,9 @@ impl OpenCADStudio {
|
||||||
// selected — see #21.
|
// selected — see #21.
|
||||||
self.sync_ribbon_from_selection();
|
self.sync_ribbon_from_selection();
|
||||||
self.tabs[i].scene.restore_saved_camera();
|
self.tabs[i].scene.restore_saved_camera();
|
||||||
// Grid visibility is a per-drawing view setting — adopt the
|
// Grid/snap are per-drawing view settings — adopt the opened
|
||||||
// opened file's saved state rather than a global preference.
|
// file's active viewport state rather than a global preference.
|
||||||
if let Some(grid_on) = self.tabs[i].scene.active_vport_grid_on() {
|
self.adopt_view_display(i);
|
||||||
self.show_grid = grid_on;
|
|
||||||
}
|
|
||||||
self.sync_render_mode_to_active_tile(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].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||||
self.tabs[i].dirty = false;
|
self.tabs[i].dirty = false;
|
||||||
|
|
@ -1263,6 +1270,8 @@ impl OpenCADStudio {
|
||||||
// fresh tab's defaults (ByLayer) instead of inheriting the
|
// fresh tab's defaults (ByLayer) instead of inheriting the
|
||||||
// previous tab's last selection.
|
// previous tab's last selection.
|
||||||
self.sync_ribbon_from_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()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1276,6 +1285,8 @@ impl OpenCADStudio {
|
||||||
// CECOLOR / CELTYPE / CELWEIGHT (or its current selection
|
// CECOLOR / CELTYPE / CELWEIGHT (or its current selection
|
||||||
// if there is one), not the prior tab's choice.
|
// if there is one), not the prior tab's choice.
|
||||||
self.sync_ribbon_from_selection();
|
self.sync_ribbon_from_selection();
|
||||||
|
// Grid/snap follow the newly active drawing's viewport.
|
||||||
|
self.adopt_view_display(idx);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -2471,6 +2482,8 @@ impl OpenCADStudio {
|
||||||
{
|
{
|
||||||
self.tabs[i].scene.camera_generation += 1;
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
self.sync_render_mode_to_active_tile(i);
|
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.tabs[i].scene.camera_generation += 1;
|
||||||
self.sync_render_mode_to_active_tile(i);
|
self.sync_render_mode_to_active_tile(i);
|
||||||
|
// Grid/snap follow the newly active viewport.
|
||||||
|
self.adopt_view_display(i);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -475,6 +475,11 @@ pub(crate) struct ModelTile {
|
||||||
/// Visual style for this tile alone — each pane carries its own so
|
/// Visual style for this tile alone — each pane carries its own so
|
||||||
/// changing one tile's render mode never touches the others.
|
/// changing one tile's render mode never touches the others.
|
||||||
pub(crate) render_mode: acadrust::entities::ViewportRenderMode,
|
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"
|
/// Tolerance for matching two normalized tile coordinates as "the same"
|
||||||
|
|
@ -792,6 +797,8 @@ impl Scene {
|
||||||
},
|
},
|
||||||
camera: Camera::default(),
|
camera: Camera::default(),
|
||||||
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
|
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
|
||||||
|
grid_on: false,
|
||||||
|
snap_on: false,
|
||||||
}]),
|
}]),
|
||||||
active_model_tile: std::cell::Cell::new(0),
|
active_model_tile: std::cell::Cell::new(0),
|
||||||
selection: Rc::new(RefCell::new(SelectionState::default())),
|
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.
|
/// Set the model-space camera from the VPORT table's *Active entry.
|
||||||
/// Returns true if the entry was found and the camera was set.
|
/// Returns true if the entry was found and the camera was set.
|
||||||
fn apply_active_vport_camera(&mut self) -> bool {
|
fn apply_active_vport_camera(&mut self) -> bool {
|
||||||
// Restore the single tile's visual style from the *Active entry,
|
// Restore the single tile's visual style + grid/snap from the *Active
|
||||||
// independent of where the camera itself comes from below.
|
// entry, independent of where the camera itself comes from below.
|
||||||
if let Some(vp) = self.document.vports.iter().find(|v| v.name == "*Active") {
|
if let Some(vp) = self.document.vports.iter().find(|v| v.name == "*Active") {
|
||||||
let mode = vp.render_mode;
|
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 mut tiles = self.model_tiles.borrow_mut();
|
||||||
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
||||||
if let Some(t) = tiles.get_mut(active) {
|
if let Some(t) = tiles.get_mut(active) {
|
||||||
t.render_mode = mode;
|
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.
|
// 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),
|
rect: Self::vport_to_tile_rect(vp.lower_left, vp.upper_right),
|
||||||
camera: cam,
|
camera: cam,
|
||||||
render_mode: vp.render_mode,
|
render_mode: vp.render_mode,
|
||||||
|
grid_on: vp.grid_on,
|
||||||
|
snap_on: vp.snap_on,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -5573,15 +5585,6 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
let table_handle = self.document.vports.handle();
|
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
|
let preserved_vps: Vec<acadrust::tables::VPort> = self
|
||||||
.document
|
.document
|
||||||
.vports
|
.vports
|
||||||
|
|
@ -5613,41 +5616,32 @@ impl Scene {
|
||||||
let (ll, ur) = Self::tile_rect_to_vport(tile.rect);
|
let (ll, ur) = Self::tile_rect_to_vport(tile.rect);
|
||||||
let mut entry = self.vport_from_camera("*Active", &tile.camera, ll, ur);
|
let mut entry = self.vport_from_camera("*Active", &tile.camera, ll, ur);
|
||||||
entry.render_mode = tile.render_mode;
|
entry.render_mode = tile.render_mode;
|
||||||
if let Some((grid_on, snap_on, grid_spacing, snap_spacing, snap_base)) = display {
|
// Each viewport persists its own grid display + grid-snap (#121).
|
||||||
entry.grid_on = grid_on;
|
entry.grid_on = tile.grid_on;
|
||||||
entry.snap_on = snap_on;
|
entry.snap_on = tile.snap_on;
|
||||||
entry.grid_spacing = grid_spacing;
|
|
||||||
entry.snap_spacing = snap_spacing;
|
|
||||||
entry.snap_base = snap_base;
|
|
||||||
}
|
|
||||||
entry.handle = self.document.allocate_handle();
|
entry.handle = self.document.allocate_handle();
|
||||||
self.document.vports.add_allow_duplicate(entry);
|
self.document.vports.add_allow_duplicate(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stamp the grid/snap display state onto every `*Active` VPort entry so a
|
/// Mirror the live grid/snap toggles onto the *active* model tile so the
|
||||||
/// saved file reflects the in-app grid/snap toggles (otherwise other CAD
|
/// save writes them to that viewport's VPort entry. (#121)
|
||||||
/// apps read VPort defaults). `grid_spacing` is applied to both axes and
|
pub fn set_active_tile_grid_snap(&self, grid_on: bool, snap_on: bool) {
|
||||||
/// mirrored to the snap spacing. (#121)
|
let mut tiles = self.model_tiles.borrow_mut();
|
||||||
pub fn set_active_vport_display(&mut self, grid_on: bool, snap_on: bool, grid_spacing: f64) {
|
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
||||||
let spacing = acadrust::types::Vector2 { x: grid_spacing, y: grid_spacing };
|
if let Some(t) = tiles.get_mut(active) {
|
||||||
for vp in self.document.vports.iter_mut().filter(|v| v.name == "*Active") {
|
t.grid_on = grid_on;
|
||||||
vp.grid_on = grid_on;
|
t.snap_on = snap_on;
|
||||||
vp.snap_on = snap_on;
|
|
||||||
vp.grid_spacing = spacing;
|
|
||||||
vp.snap_spacing = spacing;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Grid visibility stored on the `*Active` VPort entry, applied to the app's
|
/// The active model tile's grid display + grid-snap, adopted into the live
|
||||||
/// grid toggle when a file loads (grid is a per-drawing view setting, not a
|
/// toggles on load and whenever the active viewport / tab changes. `None`
|
||||||
/// global preference). `None` when there is no active entry. (#121)
|
/// when there are no tiles. (#121)
|
||||||
pub fn active_vport_grid_on(&self) -> Option<bool> {
|
pub fn active_tile_grid_snap(&self) -> Option<(bool, bool)> {
|
||||||
self.document
|
let tiles = self.model_tiles.borrow();
|
||||||
.vports
|
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
||||||
.iter()
|
tiles.get(active).map(|t| (t.grid_on, t.snap_on))
|
||||||
.find(|v| v.name == "*Active")
|
|
||||||
.map(|v| v.grid_on)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the paper-space camera from the sheet viewport's stored view.
|
/// 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 mode = tiles[active].render_mode;
|
||||||
|
let (grid_on, snap_on) = (tiles[active].grid_on, tiles[active].snap_on);
|
||||||
tiles[active] = ModelTile {
|
tiles[active] = ModelTile {
|
||||||
rect: a,
|
rect: a,
|
||||||
camera: cam_now.clone(),
|
camera: cam_now.clone(),
|
||||||
render_mode: mode,
|
render_mode: mode,
|
||||||
|
grid_on,
|
||||||
|
snap_on,
|
||||||
};
|
};
|
||||||
tiles.insert(
|
tiles.insert(
|
||||||
active + 1,
|
active + 1,
|
||||||
|
|
@ -6355,6 +6352,8 @@ impl Scene {
|
||||||
rect: b,
|
rect: b,
|
||||||
camera: cam_now,
|
camera: cam_now,
|
||||||
render_mode: mode,
|
render_mode: mode,
|
||||||
|
grid_on,
|
||||||
|
snap_on,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -6402,13 +6401,17 @@ impl Scene {
|
||||||
pub fn set_model_tile_layout(&self, rects: Vec<iced::Rectangle>) {
|
pub fn set_model_tile_layout(&self, rects: Vec<iced::Rectangle>) {
|
||||||
let cam_now = self.camera.borrow().clone();
|
let cam_now = self.camera.borrow().clone();
|
||||||
// Every new pane inherits the active tile's current visual style.
|
// 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 tiles = self.model_tiles.borrow();
|
||||||
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
||||||
tiles
|
tiles
|
||||||
.get(active)
|
.get(active)
|
||||||
.map(|t| t.render_mode)
|
.map(|t| (t.render_mode, t.grid_on, t.snap_on))
|
||||||
.unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D)
|
.unwrap_or((
|
||||||
|
acadrust::entities::ViewportRenderMode::Wireframe2D,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
))
|
||||||
};
|
};
|
||||||
let tiles: Vec<ModelTile> = rects
|
let tiles: Vec<ModelTile> = rects
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -6416,6 +6419,8 @@ impl Scene {
|
||||||
rect,
|
rect,
|
||||||
camera: cam_now.clone(),
|
camera: cam_now.clone(),
|
||||||
render_mode: mode,
|
render_mode: mode,
|
||||||
|
grid_on,
|
||||||
|
snap_on,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
*self.model_tiles.borrow_mut() = if tiles.is_empty() {
|
*self.model_tiles.borrow_mut() = if tiles.is_empty() {
|
||||||
|
|
@ -6428,6 +6433,8 @@ impl Scene {
|
||||||
},
|
},
|
||||||
camera: cam_now,
|
camera: cam_now,
|
||||||
render_mode: mode,
|
render_mode: mode,
|
||||||
|
grid_on,
|
||||||
|
snap_on,
|
||||||
}]
|
}]
|
||||||
} else {
|
} else {
|
||||||
tiles
|
tiles
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue