From 251ad436d78de6de2b16653ab50f43b9c50f388b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 20 Jul 2026 21:11:30 +0300 Subject: [PATCH] fix(viewport): frame model-documentation drawing views correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A floating viewport with a real saved view — a non-zero view target, as a model documentation drawing view or a detail/section viewport carries — now uses that view directly instead of running the median-cluster overlap test. That test measured against the densest sub-cluster (a big symbol library beside the small view geometry) and wrongly auto-fit these viewports onto the library. A fully uninitialised view (target AND centre both zero) frames its saved origin and stays empty rather than auto-fitting the whole model in; the auto-fit rescue is kept only for a stale target=(0,0,0) paired with a non-zero pre-georeference centre. Also widen the restored-view ortho depth half-range (view_height ×4 → ×64): a thin projection such as a section/side view has content far deeper than its visible height (its cut plane sits ~half an object-depth from the view target), so the tighter range clipped the view outline behind the far plane. Orthographic depth precision is linear, so the wider range costs no z-fighting. Co-Authored-By: Claude Opus 4.8 --- src/scene/camera_ops.rs | 13 ++++++++---- src/scene/paper.rs | 47 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 116dfc8a5..99a208992 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -242,10 +242,15 @@ 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), + // Sized from the saved view height (not a zoom-scaled range) so a + // restored view keeps stable depth precision. A THIN projection — + // e.g. a model-documentation section/side view whose cut plane sits + // half an object-depth from the view target — has content much + // deeper than its visible height, so the depth half-range must be a + // generous multiple: `×4` clipped a section view's outline that was + // ~6× the view height deep. Orthographic depth precision is linear, + // so the wide range costs no z-fighting. + depth_half_range: (view_height as f32 * 64.0).max(1.0), }) } diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 6c6e2a39b..db7097537 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -324,6 +324,16 @@ impl Scene { let saved_h = vp.view_height.abs(); let aspect_d = (vp.width / vp.height.max(1.0)).max(1e-9); let cluster_half = self.local_extent_max.max(1.0) as f64; + // Full model-space bounds. The overlap test below accepts the saved view + // when its frame touches ANY drawn geometry — not just the dense median + // cluster (`local_center ± cluster_half`). A drawing with a second, + // sparser cluster (e.g. model-documentation view geometry sitting apart + // from a big symbol library) has viewports legitimately aimed at that + // second cluster; testing only the dense one wrongly auto-fits them onto + // the library. Fall back to the cluster box when no extents are known. + let full_bounds = self.model_space_extents().map(|(mn, mx)| { + (mn.x as f64, mn.y as f64, mx.x as f64, mx.y as f64) + }); // Absolute drawing centre. Geometry now reaches the scene at absolute // (UTM) coordinates — the old code centred the overlap test and the // auto-fit on the origin, which was right only while world_offset @@ -346,6 +356,25 @@ impl Scene { .unwrap_or((0.0, 0.0)) }; + // A non-zero `view_target` is a deliberately-aimed saved view (a model + // documentation drawing view, a detail/section viewport, any viewport + // panned onto a specific WCS point). It is authoritative — use it as-is. + // The overlap test / auto-fit below only rescues the STALE default, + // where `view_target == (0,0,0)` points at empty WCS while the model + // sits at UTM. Guarding on the target avoids the tight median cluster + // (which collapses onto the densest sub-cluster) wrongly rejecting a + // valid view that frames a smaller, off-centre sub-cluster. + let target_set = + vp.view_target.x.abs() > 1e-6 || vp.view_target.y.abs() > 1e-6; + // A fully-uninitialised view (target AND centre both zero) is a stale + // placeholder viewport — frame its saved view (the origin) and leave it + // empty rather than auto-fitting the whole model into it. The auto-fit + // rescue is meant only for a stale target=(0,0,0) paired with a NON-zero + // (pre-georeference) view_centre that points at empty WCS while the model + // sits far away — that case still falls through to the overlap test. + let center_set = + vp.view_center.x.abs() > 1e-6 || vp.view_center.y.abs() > 1e-6; + if let Some(cam) = self.camera_from_view( vp.view_direction, vp.view_target, @@ -356,13 +385,23 @@ impl Scene { saved_h, vp.twist_angle, ) { + if target_set || !center_set { + return Some(cam); + } let half_h = saved_h * 0.5; let half_w = half_h * aspect_d; let (tx, ty) = (cam.target.x as f64, cam.target.y as f64); - let overlaps = tx + half_w >= cx - cluster_half - && tx - half_w <= cx + cluster_half - && ty + half_h >= cy - cluster_half - && ty - half_h <= cy + cluster_half; + // Prefer the true model bounds; fall back to the median cluster box. + let (bx0, by0, bx1, by1) = full_bounds.unwrap_or(( + cx - cluster_half, + cy - cluster_half, + cx + cluster_half, + cy + cluster_half, + )); + let overlaps = tx + half_w >= bx0 + && tx - half_w <= bx1 + && ty + half_h >= by0 + && ty - half_h <= by1; if overlaps { return Some(cam); }