fix(camera): precise zoom-to-cursor pivot at large coordinates

zoom_about_point picked the world point under the cursor twice via
view_proj.inverse() (the full f32 matrix) and shifted target by their
difference. At UTM coordinates that inverse carries ~0.5 m of f32 error
that didn't cancel between the two picks, so target drifted every zoom
step and the whole scene jumped as a block.

Compute the pivot as the cursor's offset *relative to target* in the
camera frame (small numbers, rotated to world) instead — it never touches
the large absolute target, so the before/after difference is exact and
the scene stays put while zooming.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-06-23 18:28:52 +03:00
1 parent 65413a2c2a
commit b3a1119b04
1 file changed
+24 -2
+24 -2
View File
@@ -287,15 +287,37 @@ impl Camera {
self.distance = (self.distance * (1.0 - delta * 0.1)).max(0.001);
}
/// World-space offset from `target` to the point under `screen` on the
/// target plane. Computed in the camera frame (small numbers) and rotated
/// to world — it never touches the large absolute target, so it stays
/// precise at UTM-scale coordinates. For perspective this is the offset
/// evaluated at the target plane (the correct pivot for zoom-to-cursor).
fn cursor_offset_on_target_plane(&self, screen: Point, bounds: Rectangle) -> Vec3 {
let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
let aspect = bounds.width / bounds.height;
let half_h = self.ortho_size();
let half_w = half_h * aspect;
let cam_right = self.rotation * Vec3::X;
let cam_up = self.rotation * Vec3::Y;
cam_right * (ndc_x * half_w) + cam_up * (ndc_y * half_h)
}
pub fn zoom_about_point(&mut self, screen: Point, bounds: Rectangle, delta: f32) {
if bounds.width <= 0.0 || bounds.height <= 0.0 {
self.zoom(delta);
return;
}
let before = self.pick_on_target_plane(screen, bounds);
// Keep the point under the cursor fixed by working with its offset
// *relative to target* before and after the zoom. Both offsets are
// small (camera-frame) numbers, so their difference is exact even at
// UTM coordinates — the old absolute view_proj.inverse() picks each
// carried ~0.5 m of f32 error that didn't cancel, making the whole
// scene jump on every zoom step.
let before = self.cursor_offset_on_target_plane(screen, bounds);
self.zoom(delta);
let after = self.pick_on_target_plane(screen, bounds);
let after = self.cursor_offset_on_target_plane(screen, bounds);
self.target += (before - after).as_dvec3();
}