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 <noreply@anthropic.com>
This commit is contained in:
parent
ba834fb166
commit
26479b6faa
4 changed files with 122 additions and 35 deletions
|
|
@ -5589,6 +5589,10 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.tabs[i].scene.restore_saved_camera();
|
self.tabs[i].scene.restore_saved_camera();
|
||||||
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;
|
||||||
|
// 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
|
// Paper-space tools live in the right-edge side toolbar now, so
|
||||||
// entering/leaving a layout no longer hijacks the ribbon tab.
|
// entering/leaving a layout no longer hijacks the ribbon tab.
|
||||||
let _ = going_to_paper;
|
let _ = going_to_paper;
|
||||||
|
|
@ -6101,6 +6105,8 @@ impl OpenCADStudio {
|
||||||
// Clear paper-space selection before entering model space.
|
// Clear paper-space selection before entering model space.
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.tabs[i].scene.active_viewport = Some(handle);
|
self.tabs[i].scene.active_viewport = Some(handle);
|
||||||
|
// Grid/snap follow the entered viewport.
|
||||||
|
self.adopt_view_display(i);
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
self.command_line.push_output("MSPACE");
|
self.command_line.push_output("MSPACE");
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -6110,8 +6116,10 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
// Clear model-space selection before returning to paper space.
|
// Clear model-space selection before returning to paper space.
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.refresh_properties();
|
|
||||||
self.tabs[i].scene.active_viewport = None;
|
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");
|
self.command_line.push_output("PSPACE");
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,33 +133,25 @@ impl OpenCADStudio {
|
||||||
// the correct place and scale.
|
// the correct place and scale.
|
||||||
let vp_bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
let vp_bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
||||||
|
|
||||||
// Model layout: one grid per tile that has grid display on, drawn
|
// Each view draws its own grid, clipped to its own bounds, so they
|
||||||
// in its own bounds + camera so panes are independent and never
|
// stay independent: model tiles in model space; the sheet plus each
|
||||||
// flicker on hover. Paper layout: a single grid gated by show_grid.
|
// floating viewport (clipped to its rectangle) in paper space.
|
||||||
let grid: Vec<overlay::GridParams> = if !is_paper {
|
let grid_views = if !is_paper {
|
||||||
tab.scene
|
tab.scene.model_tile_grid_views(vw, vh)
|
||||||
.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,
|
|
||||||
}]
|
|
||||||
} else {
|
} 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 ucs_icon = if self.show_ucs_icon && !is_paper {
|
||||||
let cam = tab.scene.camera.borrow();
|
let cam = tab.scene.camera.borrow();
|
||||||
|
|
|
||||||
|
|
@ -6276,9 +6276,26 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mirror the live grid/snap toggles onto the *active* model tile so the
|
/// Mirror the live grid/snap toggles onto the active view's own store so the
|
||||||
/// save writes them to that viewport's VPort entry. (#121)
|
/// state stays independent per viewport: a model tile in model space, the
|
||||||
pub fn set_active_tile_grid_snap(&self, grid_on: bool, snap_on: bool) {
|
/// 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 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) {
|
||||||
|
|
@ -6287,10 +6304,22 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The active model tile's grid display + grid-snap, adopted into the live
|
/// The active view's grid display + grid-snap, adopted into the live toggles
|
||||||
/// toggles on load and whenever the active viewport / tab changes. `None`
|
/// on load and whenever the active viewport / tab / layout changes. Reads the
|
||||||
/// when there are no tiles. (#121)
|
/// model tile in model space, the sheet viewport in paper space. (#121)
|
||||||
pub fn active_tile_grid_snap(&self) -> Option<(bool, bool)> {
|
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 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.get(active).map(|t| (t.grid_on, t.snap_on))
|
tiles.get(active).map(|t| (t.grid_on, t.snap_on))
|
||||||
|
|
@ -7133,6 +7162,47 @@ impl Scene {
|
||||||
.collect()
|
.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 {
|
pub fn active_model_tile_bounds(&self, vw: f32, vh: f32) -> iced::Rectangle {
|
||||||
if self.current_layout != "Model" {
|
if self.current_layout != "Model" {
|
||||||
return iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
return iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||||
|
|
|
||||||
|
|
@ -284,11 +284,28 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Grid display ──────────────────────────────────────────────────
|
// ── Grid display ──────────────────────────────────────────────────
|
||||||
// One entry per pane with its grid on. Each is clipped to its own tile
|
// One entry per pane with its grid on. Each is clipped to its own
|
||||||
// rectangle so lines never spill into neighbouring panes.
|
// 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 {
|
for g in &self.grid {
|
||||||
let gb = g.bounds;
|
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) {
|
if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue