forked from 1b-agent/opencadstudio-upstream
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:
1 parent
65413a2c2a
commit
b3a1119b04
1 file changed
+24
-2
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user