fix(viewports): fit extents per pane

Closes #554
This commit is contained in:
Hakan Seven committed 2026-07-29 23:03:06 +03:00
1 parent f99337cb9a
commit 3897fb2661
5 files changed
+93 -8

No files matched your search

+7 -1
View File
@@ -695,6 +695,11 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"ZOOM EXTENTS ALL" | "ZOOM EXTENTS ALL VIEWPORTS" | "ZEA" => {
self.tabs[i].scene.fit_all_model_viewports();
self.command_line.push_output("Zoom Extents — All Viewports");
}
"ZOOM EXTENTS" | "ZOOMEXTENTS" | "ZE" => {
self.tabs[i].scene.fit_all();
self.command_line.push_output("Zoom Extents");
@@ -751,10 +756,11 @@ impl OpenCADStudio {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"ZOOM",
"ZOOM [Window / Extents / All / In / Out / Scale]:",
"ZOOM [Window / Extents / Extents All / All / In / Out / Scale]:",
vec![
("Window", "WINDOW", None),
("Extents", "EXTENTS", None),
("Extents All", "EXTENTS ALL", None),
("All", "ALL", None),
("In", "IN", None),
("Out", "OUT", None),
+76 -4
View File
@@ -98,6 +98,25 @@ impl Scene {
self.camera_generation += 1;
}
/// Aspect ratio of the active camera's actual render rectangle. Model
/// panes are separate shader widgets, so the full-canvas render aspect is
/// wrong whenever a tiled viewport is active.
pub(super) fn active_camera_aspect(&self) -> f32 {
if self.current_layout == "Model" {
let (canvas_w, canvas_h) = self.selection.borrow().vp_size;
let tiles = self.model_tiles.borrow();
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
if let Some(tile) = tiles.get(active) {
let width = tile.rect.width * canvas_w;
let height = tile.rect.height * canvas_h;
if width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0 {
return width / height;
}
}
}
self.last_render_aspect.get().max(0.01)
}
/// Fit the camera to a world-space bounding box (corners p1, p2).
pub fn zoom_to_window(&mut self, p1: glam::Vec3, p2: glam::Vec3) {
let min = p1.min(p2);
@@ -105,9 +124,10 @@ impl Scene {
if min == max {
return;
}
let aspect = self.active_camera_aspect();
self.camera
.borrow_mut()
.fit_to_bounds(min, max, self.last_render_aspect.get().max(0.01));
.fit_to_bounds(min, max, aspect);
self.camera_generation += 1;
}
@@ -925,9 +945,61 @@ impl Scene {
if min == max {
max += glam::Vec3::splat(1.0);
}
self.camera
.borrow_mut()
.fit_to_bounds(min, max, self.last_render_aspect.get().max(0.01));
let aspect = self.active_camera_aspect();
self.camera.borrow_mut().fit_to_bounds(min, max, aspect);
self.camera_generation += 1;
}
/// Fit every tiled Model viewport independently. The drawing bounds are
/// shared, but each tile keeps its own orientation and uses its own screen
/// aspect when calculating the required zoom.
pub fn fit_all_model_viewports(&mut self) {
if self.current_layout != "Model" || self.model_tiles.borrow().len() <= 1 {
self.fit_all();
return;
}
let generation_before = self.camera_generation;
self.fit_all();
if self.camera_generation == generation_before {
return;
}
let Some((min, max)) = self.camera.borrow().fitted_model_bounds() else {
return;
};
let (canvas_w, canvas_h) = self.selection.borrow().vp_size;
let fallback = self.last_render_aspect.get().max(0.01);
let aspects: Vec<f32> = self
.model_tiles
.borrow()
.iter()
.map(|tile| {
let width = tile.rect.width * canvas_w;
let height = tile.rect.height * canvas_h;
if width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0 {
width / height
} else {
fallback
}
})
.collect();
let active = self
.active_model_tile
.get()
.min(aspects.len().saturating_sub(1));
let live_camera = self.camera.borrow_mut();
let mut tiles = self.model_tiles.borrow_mut();
for (index, tile) in tiles.iter_mut().enumerate() {
let aspect = aspects.get(index).copied().unwrap_or(fallback);
if index == active {
tile.camera = live_camera.clone();
} else {
tile.camera.fit_to_bounds(min, max, aspect);
}
}
if let Some(aspect) = aspects.get(active) {
self.last_render_aspect.set(*aspect);
}
self.camera_generation += 1;
}
+2 -3
View File
@@ -137,9 +137,8 @@ impl Scene {
return;
}
self.camera
.borrow_mut()
.fit_to_bounds(min, max, self.last_render_aspect.get().max(0.01));
let aspect = self.active_camera_aspect();
self.camera.borrow_mut().fit_to_bounds(min, max, aspect);
self.camera_generation += 1;
}
}
+4
View File
@@ -464,6 +464,10 @@ impl Camera {
self.depth_half_range = self.depth_extent_in_view(min, max);
}
pub(crate) fn fitted_model_bounds(&self) -> Option<(Vec3, Vec3)> {
self.model_bounds
}
// ── ViewCube snap ─────────────────────────────────────────────────────
/// Snap to a canonical view direction (called by ViewCubeSnap).
+4
View File
@@ -1923,6 +1923,10 @@ impl Scene {
};
let active = self.active_model_tile.get();
let is_active = tile_idx == active;
if is_active && canvas.1 > 0.0 {
self.set_render_aspect(canvas.0 / canvas.1);
self.set_render_pixel_scale(canvas.0, canvas.1);
}
let nav_build_started = iced::time::Instant::now();
let perf_nav = if is_active {
self.take_nav_perf()