diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 44da23656..4c0118256 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -9,26 +9,37 @@ fn rendered_wire_bounds( ) -> Option<(glam::DVec3, glam::DVec3)> { let mut min = glam::DVec3::splat(f64::INFINITY); let mut max = glam::DVec3::splat(f64::NEG_INFINITY); - let mut include = |point: glam::DVec3| { - if point.is_finite() { - min = min.min(point); - max = max.max(point); - } - }; + macro_rules! include_bound { + ($p:expr) => { + let point: glam::DVec3 = $p; + if point.is_finite() { + min = min.min(point); + max = max.max(point); + } + }; + } for wire in wires { let mut has_precise_position = false; + let hw = wire.world_width as f64 * 0.5; + for (index, &[x, y, z]) in wire.points.iter().enumerate() { let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]); - include(glam::DVec3::new( + let p = glam::DVec3::new( x as f64 + low[0] as f64, y as f64 + low[1] as f64, z as f64 + low[2] as f64, - )); + ); + if hw > 0.0 { + include_bound!(p - glam::DVec3::splat(hw)); + include_bound!(p + glam::DVec3::splat(hw)); + } else { + include_bound!(p); + } has_precise_position = true; } for vertex in &wire.text_verts { - include(glam::DVec3::new( + include_bound!(glam::DVec3::new( vertex.pos[0] as f64 + vertex.pos_low[0] as f64, vertex.pos[1] as f64 + vertex.pos_low[1] as f64, vertex.pos[2] as f64 + vertex.pos_low[2] as f64, @@ -36,11 +47,17 @@ fn rendered_wire_bounds( has_precise_position = true; } for &[x, y, z] in &wire.key_vertices { - include(glam::DVec3::new(x, y, z)); + let p = glam::DVec3::new(x, y, z); + if hw > 0.0 { + include_bound!(p - glam::DVec3::splat(hw)); + include_bound!(p + glam::DVec3::splat(hw)); + } else { + include_bound!(p); + } has_precise_position = true; } for &(point, _) in &wire.snap_pts { - include(point); + include_bound!(point); has_precise_position = true; } @@ -51,8 +68,8 @@ fn rendered_wire_bounds( && x0 <= x1 && y0 <= y1 { - include(glam::DVec3::new(x0 as f64, y0 as f64, fallback_z)); - include(glam::DVec3::new(x1 as f64, y1 as f64, fallback_z)); + include_bound!(glam::DVec3::new(x0 as f64 - hw, y0 as f64 - hw, fallback_z - hw)); + include_bound!(glam::DVec3::new(x1 as f64 + hw, y1 as f64 + hw, fallback_z + hw)); } } } @@ -1455,12 +1472,18 @@ impl Scene { continue; } let wire = &wires[c.idx]; + let hw = (wire.world_width * 0.5) as f32; for &[x, y, z] in &wire.points { if !x.is_finite() || !y.is_finite() || !z.is_finite() { continue; } - min = min.min(glam::Vec3::new(x, y, z)); - max = max.max(glam::Vec3::new(x, y, z)); + if hw > 0.0 { + min = min.min(glam::Vec3::new(x - hw, y - hw, z - hw)); + max = max.max(glam::Vec3::new(x + hw, y + hw, z + hw)); + } else { + min = min.min(glam::Vec3::new(x, y, z)); + max = max.max(glam::Vec3::new(x, y, z)); + } } } // Fold in 3D-solid mesh AABBs (not subject to the wire IQR reject). diff --git a/src/scene/view/camera.rs b/src/scene/view/camera.rs index a5ae72516..e34b550ba 100644 --- a/src/scene/view/camera.rs +++ b/src/scene/view/camera.rs @@ -246,23 +246,16 @@ 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) { - // 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). + // Generous headroom based on the current screen size so that rotating any + // geometry visible on screen in 3D never penetrates the near/far planes. + let view_extent = (self.ortho_size() * 3.0).max(10.0); + let r = if let Some((min, max)) = self.model_bounds { - // Depth extent along the CURRENT eye direction, recomputed each - // frame so orbiting a 3-D drawing never clips it (#473): as the - // view tilts off top, the box's width rotates onto the eye axis and - // the span grows to match. Same tight, zoom-independent precision as - // a fitted scalar, but always oriented to the live view. - self.depth_extent_in_view(min, max) + self.depth_extent_in_view(min, max).max(view_extent) } else if self.depth_half_range > 0.0 { - self.depth_half_range + self.depth_half_range.max(view_extent) } else { - (self.distance * 1000.0).max(1.0) + (self.distance * 1000.0).max(view_extent).max(10.0) }; (self.distance - r, self.distance + r) } @@ -590,7 +583,11 @@ impl Camera { .bounds_in_view(min, max) .iter() .fold(0.0_f32, |m, c| m.max(c.z.abs())); - (depth_r * 1.05).max(1.0) + let diag = (max - min).length() as f32; + // Generous headroom (1.5x depth extent + 25% of model diagonal + minimum clearance) + // so wide strokes, lineweights, camera orbit dynamics, and draw-order depth bias + // never penetrate the near clipping plane in orthographic 3D view. + (depth_r * 1.5 + diag * 0.25).max(10.0) } /// Fit the camera to `min..max` — pose and depth both.