fix(view): ZOOM Extents fills the window at the real aspect (#364)

fit_to_bounds fitted the bounding box's circumscribed DIAGONAL into the
viewport height regardless of shape, so a wide drawing filled only a
small central patch. Fit against the actual viewport aspect instead —
required half-height = max(|y|, |x|/aspect) over the view-space corners
— and trim the border margin from 20% to 10%. ZOOM EXTENTS/ALL and
ZOOM WINDOW both pass the live pane aspect.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-21 23:09:37 +03:00
1 parent ac14e0041d
commit ea860cab72
2 files changed
+21 -13

No files matched your search

+6 -2
View File
@@ -107,7 +107,9 @@ impl Scene {
if min == max {
return;
}
self.camera.borrow_mut().fit_to_bounds(min, max);
self.camera
.borrow_mut()
.fit_to_bounds(min, max, self.last_render_aspect.get().max(0.01));
self.camera_generation += 1;
}
@@ -850,7 +852,9 @@ impl Scene {
if min == max {
max += glam::Vec3::splat(1.0);
}
self.camera.borrow_mut().fit_to_bounds(min, max);
self.camera
.borrow_mut()
.fit_to_bounds(min, max, self.last_render_aspect.get().max(0.01));
self.camera_generation += 1;
}
+15 -11
View File
@@ -396,18 +396,22 @@ impl Camera {
/// horizontal zoom out with it: a 140-unit drawing carrying a single entity
/// 800 km below its plane zoomed out to 800 km and became a dot. The two
/// agree on a flat drawing, which is why it went unnoticed.
pub fn fit_to_bounds(&mut self, min: Vec3, max: Vec3) {
pub fn fit_to_bounds(&mut self, min: Vec3, max: Vec3, aspect: f32) {
self.target = ((min + max) * 0.5).as_dvec3();
let corners = self.bounds_in_view(min, max);
// Circumscribed radius across the screen plane — aspect-agnostic, so the
// content fits whatever the viewport's shape turns out to be.
let screen_r = corners
// Half-height the view needs so the box fits BOTH axes at the given
// viewport aspect. The old circumscribed-radius rule fitted the box's
// DIAGONAL into the height regardless of shape, which left a wide
// drawing filling only a small central patch of the window (#364).
let a = aspect.max(0.01);
let half_h = corners
.iter()
.fold(0.0_f32, |m, c| m.max(c.x.hypot(c.y)))
.fold(0.0_f32, |m, c| m.max(c.y.abs()).max(c.x.abs() / a))
.max(1e-6);
// `ortho_size` (the half-height) is `distance * tan(fov/2)`, so invert
// that to get the distance which just contains `screen_r`, plus margin.
self.distance = (screen_r / (self.fov_y * 0.5).tan() * 1.2).max(1e-3);
// that to get the distance which just contains `half_h`, plus a small
// border margin.
self.distance = (half_h / (self.fov_y * 0.5).tan() * 1.1).max(1e-3);
self.fit_depth_to_bounds(min, max);
}
@@ -588,11 +592,11 @@ mod tests {
let flat_max = Vec3::new(-1199960.0, -800000.0, 10.0);
let mut flat = Camera::default();
flat.fit_to_bounds(flat_min, flat_max);
flat.fit_to_bounds(flat_min, flat_max, 1.0);
// Same drawing, plus the benchmark's entity 800 km down.
let mut deep = Camera::default();
deep.fit_to_bounds(Vec3::new(flat_min.x, flat_min.y, -800017.5), flat_max);
deep.fit_to_bounds(Vec3::new(flat_min.x, flat_min.y, -800017.5), flat_max, 1.0);
// Top view: the outlier is along the eye direction, so the on-screen
// framing must barely move.
@@ -612,7 +616,7 @@ mod tests {
let min = Vec3::new(-1200100.0, -800081.5, -800017.5);
let max = Vec3::new(-1199960.0, -800000.0, 10.0);
let mut cam = Camera::default();
cam.fit_to_bounds(min, max);
cam.fit_to_bounds(min, max, 1.0);
let (near, far) = cam.ortho_depth_range();
// Depth of a point from the eye, along the view direction. Top view, so
@@ -633,7 +637,7 @@ mod tests {
let min = Vec3::new(-70.0, -40.0, 0.0);
let max = Vec3::new(70.0, 40.0, 0.0);
let mut cam = Camera::default();
cam.fit_to_bounds(min, max);
cam.fit_to_bounds(min, max, 1.0);
// Old rule: distance = 3-D diagonal * 1.5.
let old = (max - min).length() * 1.5;
let ratio = cam.distance / old;