fix(grid): independent, clipped grid per viewport

Grid (and grid-snap) state was shared through the global show_grid flag, so
toggling it in model space also turned it on in paper space, and a floating
viewport's grid painted across the whole paper — and beyond the 3-D view when
zoomed in.

- Store grid/snap per view: a model tile in model space, the active floating
  viewport or the layout's sheet viewport in paper space, round-tripped via the
  viewport status flags.
- adopt_view_display on layout switch and viewport enter/exit so the live
  toggle follows the view you are in.
- Render one grid per view (paper_viewport_grid_views): the sheet over the
  paper, each floating viewport clipped to its screen rectangle.
- Clamp the grid clip to the widget so an overflowing viewport rectangle never
  paints grid outside the 3-D view.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-22 00:18:01 +03:00
1 parent ba834fb166
commit 26479b6faa
4 files changed
+122 -35

No files matched your search

+9 -1
View File
@@ -5589,6 +5589,10 @@ impl OpenCADStudio {
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.restore_saved_camera();
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
// Grid/snap are per-view: load the layout we just entered (its
// sheet viewport in paper space, the model tile in model space)
// so model and each layout keep independent grid state.
self.adopt_view_display(i);
// Paper-space tools live in the right-edge side toolbar now, so
// entering/leaving a layout no longer hijacks the ribbon tab.
let _ = going_to_paper;
@@ -6101,6 +6105,8 @@ impl OpenCADStudio {
// Clear paper-space selection before entering model space.
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.active_viewport = Some(handle);
// Grid/snap follow the entered viewport.
self.adopt_view_display(i);
self.refresh_properties();
self.command_line.push_output("MSPACE");
Task::none()
@@ -6110,8 +6116,10 @@ impl OpenCADStudio {
let i = self.active_tab;
// Clear model-space selection before returning to paper space.
self.tabs[i].scene.deselect_all();
self.refresh_properties();
self.tabs[i].scene.active_viewport = None;
// Grid/snap return to the paper sheet's own state.
self.adopt_view_display(i);
self.refresh_properties();
self.command_line.push_output("PSPACE");
Task::none()
}
+17 -25
View File
@@ -133,33 +133,25 @@ impl OpenCADStudio {
// the correct place and scale.
let vp_bounds = tab.scene.active_model_tile_bounds(vw, vh);
// Model layout: one grid per tile that has grid display on, drawn
// in its own bounds + camera so panes are independent and never
// flicker on hover. Paper layout: a single grid gated by show_grid.
let grid: Vec<overlay::GridParams> = if !is_paper {
tab.scene
.model_tile_grid_views(vw, vh)
.into_iter()
.map(|(bounds, cam)| {
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
overlay::GridParams {
view_proj: cam.view_proj(bounds),
bounds,
plane,
}
})
.collect()
} else if self.show_grid {
let cam = tab.scene.camera.borrow();
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
vec![overlay::GridParams {
view_proj: cam.view_proj(vp_bounds),
bounds: vp_bounds,
plane,
}]
// Each view draws its own grid, clipped to its own bounds, so they
// stay independent: model tiles in model space; the sheet plus each
// floating viewport (clipped to its rectangle) in paper space.
let grid_views = if !is_paper {
tab.scene.model_tile_grid_views(vw, vh)
} else {
Vec::new()
tab.scene.paper_viewport_grid_views(vw, vh)
};
let grid: Vec<overlay::GridParams> = grid_views
.into_iter()
.map(|(bounds, cam)| {
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
overlay::GridParams {
view_proj: cam.view_proj(bounds),
bounds,
plane,
}
})
.collect();
let ucs_icon = if self.show_ucs_icon && !is_paper {
let cam = tab.scene.camera.borrow();
+76 -6
View File
@@ -6276,9 +6276,26 @@ impl Scene {
}
}
/// 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) {
/// Mirror the live grid/snap toggles onto the active view's own store so the
/// state stays independent per viewport: a model tile in model space, the
/// layout's sheet viewport in paper space. (#121)
pub fn set_active_tile_grid_snap(&mut self, grid_on: bool, snap_on: bool) {
if self.current_layout != "Model" {
// Paper space: target the active floating viewport if the user is
// working inside one, otherwise the layout's sheet viewport. Each
// viewport keeps its own grid/snap (round-tripped via status flags).
let h = self
.active_viewport
.filter(|h| h.is_valid())
.unwrap_or_else(|| self.current_layout_sheet_viewport_handle());
if h.is_valid() {
if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(h) {
vp.status.grid_on = grid_on;
vp.status.snap_on = snap_on;
}
}
return;
}
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) {
@@ -6287,10 +6304,22 @@ impl Scene {
}
}
/// 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)
/// The active view's grid display + grid-snap, adopted into the live toggles
/// on load and whenever the active viewport / tab / layout changes. Reads the
/// model tile in model space, the sheet viewport in paper space. (#121)
pub fn active_tile_grid_snap(&self) -> Option<(bool, bool)> {
if self.current_layout != "Model" {
let h = self
.active_viewport
.filter(|h| h.is_valid())
.unwrap_or_else(|| self.current_layout_sheet_viewport_handle());
if h.is_valid() {
if let Some(EntityType::Viewport(vp)) = self.document.get_entity(h) {
return Some((vp.status.grid_on, vp.status.snap_on));
}
}
return Some((false, false));
}
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))
@@ -7133,6 +7162,47 @@ impl Scene {
.collect()
}
/// Grid views for a paper-space layout: the sheet (paper) grid covers the
/// whole canvas under the paper camera; each floating viewport whose grid is
/// on draws the model grid through its own camera, clipped to its screen
/// rectangle so it never spills onto the paper. Mirrors model tiles. (#121)
pub fn paper_viewport_grid_views(&self, vw: f32, vh: f32) -> Vec<(iced::Rectangle, Camera)> {
if self.current_layout == "Model" {
return Vec::new();
}
let layout_block = self.current_layout_block_handle();
let sheet = self.current_layout_sheet_viewport_handle();
// Snapshot (handle, grid_on, is_sheet) so the later camera_for_viewport
// calls don't overlap this immutable borrow of the document.
let vps: Vec<(Handle, bool, bool)> = self
.document
.entities()
.filter_map(|e| {
let EntityType::Viewport(vp) = e else { return None };
if vp.common.owner_handle != layout_block {
return None;
}
Some((vp.common.handle, vp.status.grid_on, vp.common.handle == sheet))
})
.collect();
let full = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
let mut out = Vec::new();
for (h, grid_on, is_sheet) in vps {
if !grid_on {
continue;
}
if is_sheet {
out.push((full, self.camera.borrow().clone()));
} else if let (Some(rect), Some(cam)) = (
self.viewport_screen_rect(h, (vw, vh)),
self.camera_for_viewport(h),
) {
out.push((rect, cam));
}
}
out
}
pub fn active_model_tile_bounds(&self, vw: f32, vh: f32) -> iced::Rectangle {
if self.current_layout != "Model" {
return iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
+20 -3
View File
@@ -284,11 +284,28 @@ impl canvas::Program<Message> for SelectionCanvas {
}
// ── Grid display ──────────────────────────────────────────────────
// One entry per pane with its grid on. Each is clipped to its own tile
// rectangle so lines never spill into neighbouring panes.
// One entry per pane with its grid on. Each is clipped to its own
// rectangle so lines never spill into neighbouring panes — and the clip
// is intersected with the widget so a zoomed-in viewport whose rectangle
// overflows the canvas never paints grid outside the 3-D view.
for g in &self.grid {
let gb = g.bounds;
frame.with_clip(gb, |f| draw_grid(f, g.view_proj, g.plane, gb));
let cx0 = gb.x.max(0.0);
let cy0 = gb.y.max(0.0);
let cx1 = (gb.x + gb.width).min(bounds.width);
let cy1 = (gb.y + gb.height).min(bounds.height);
if cx1 <= cx0 || cy1 <= cy0 {
continue;
}
let clip = iced::Rectangle {
x: cx0,
y: cy0,
width: cx1 - cx0,
height: cy1 - cy0,
};
// Project with the full viewport rect `gb` so the grid stays aligned;
// only the clip is clamped.
frame.with_clip(clip, |f| draw_grid(f, g.view_proj, g.plane, gb));
}
if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) {