From 572e9aca01c6d7daed52c633ef92dc4e8a39c2e8 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Jul 2026 01:23:27 +0300 Subject: [PATCH] fix(camera): keep ortho depth precision constant across zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The orthographic near/far span was ±1000x the camera distance, so zooming out ballooned the depth range and collapsed z-buffer precision: coincident solids, meshes and wires then flipped draw order (meshes drew in front of solids only when zoomed out, correct order when zoomed in). Size the depth half-range from the drawing's extent instead — the bbox diagonal on fit_to_bounds, the view height on a restored saved view — and hold it constant as the camera zooms, so precision no longer depends on distance. Falls back to the old distance-scaled range when unset. Co-Authored-By: Claude Opus 4.8 --- src/scene/camera_ops.rs | 4 ++++ src/scene/view/camera.rs | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 520b5c0d8..fc2ee61a2 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -230,6 +230,10 @@ impl Scene { projection: view::camera::Projection::Orthographic, yaw, pitch, + // Sized from the saved view height rather than a zoom-scaled range, + // so a restored view keeps stable depth precision. `view_height` is + // the visible extent; a few multiples cover the drawing's depth. + depth_half_range: (view_height as f32 * 4.0).max(1.0), }) } diff --git a/src/scene/view/camera.rs b/src/scene/view/camera.rs index 666b1f8b9..9e76ea1f4 100644 --- a/src/scene/view/camera.rs +++ b/src/scene/view/camera.rs @@ -39,6 +39,14 @@ pub struct Camera { // Kept in sync with `rotation` whenever orbit() or snap_angles() is called. pub yaw: f32, pub pitch: f32, + + /// Half-depth of the orthographic frustum in **world units**, sized from the + /// drawing's extent (set by `fit_to_bounds`). The ortho near/far are placed + /// at `distance ± depth_half_range`, so depth-buffer precision stays fixed + /// as the user zooms. `0.0` means "unset" — the projection falls back to a + /// distance-scaled range, whose precision collapses when zoomed out and + /// makes coincident solids / meshes / wires flip draw order. + pub depth_half_range: f32, } impl Default for Camera { @@ -55,6 +63,7 @@ impl Default for Camera { projection: Projection::Orthographic, yaw, pitch, + depth_half_range: 0.0, } } } @@ -94,7 +103,17 @@ impl Camera { /// A symmetric range gives the bias half the depth buffer of headroom on /// each side; ortho permits a negative near. fn ortho_depth_range(&self) -> (f32, f32) { - let r = (self.distance * 1000.0).max(1.0); + // Prefer a drawing-sized, zoom-independent half-range so depth-buffer + // precision stays constant as the user zooms. A distance-scaled range + // (the `else`) balloons the near/far span when zoomed out — at large + // `distance` the f32 depth buffer can no longer separate coincident + // solids / meshes / wires, so they flip draw order (issue: meshes drew + // in front of solids only when zoomed out). + let r = if self.depth_half_range > 0.0 { + self.depth_half_range + } else { + (self.distance * 1000.0).max(1.0) + }; (self.distance - r, self.distance + r) } @@ -342,6 +361,11 @@ impl Camera { self.target = ((min + max) * 0.5).as_dvec3(); let size = (max - min).length(); self.distance = size * 1.5; + // Size the depth range to the drawing's full diagonal (not the zoom + // distance): it covers the model's Z-extent even after panning to a + // corner, and keeps depth-buffer precision constant across zoom so + // coincident solids / meshes / wires never flip draw order. + self.depth_half_range = size.max(1.0); } // ── ViewCube snap ─────────────────────────────────────────────────────