diff --git a/src/app/update.rs b/src/app/update.rs index fe51167e4..9bb502015 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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() } diff --git a/src/app/view.rs b/src/app/view.rs index cc8bf2952..5b7e6d04b 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -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 = 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 = 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(); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f661d0e36..d5ce4fb80 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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 }; diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 079363eed..59ad5b61a 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -284,11 +284,28 @@ impl canvas::Program 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) {