fix(view): zoom cursor per navigation tool, and ZOOM EXTENTS frames everything

Two viewport-navigation defects, both measured against commercial solutions.

The cursor stayed a hand during a zoom. `selection_overlay` took one boolean
for all three interactive navigation tools, so ZOOM Dynamic and ORBIT both
showed PAN's grab hand — which reads as "drag the sheet", the wrong promise
for either. Split it into a `NavCursor` enum: PAN keeps the open/closed hand,
ORBIT gets four-way arrows, and ZOOM Dynamic gets the magnifier carrying the
sign of the zoom actually under way — "+" dragging up, "-" dragging down,
latched with a 2 px dead zone so a hand held still cannot strobe the glyph.
ZOOM Dynamic also reused `box_anchor` to remember where a drag began, so a
selection marquee rubber-banded across the screen while the view zoomed; the
drawing is now suppressed, the state left alone for `reproject_box_anchor`.

ZOOM EXTENTS did not reach the whole drawing, for three separate reasons, now
answered by one shared `Scene::visible_block_bounds`:

  - A quartile filter dropped any wire whose centroid sat outside the
    inter-quartile consensus of the others. It was meant to stop one junk wire
    from a bad parse poisoning the box, but it could not tell parser junk from
    an outlying detail, a survey point, or a block placed away from the rest,
    and silently refused to frame them. Gone; the absolute 1e16 sentinel gate
    the paper path already used is the only filter, and the XLine / Ray display
    segments that motivated the heuristic are handled explicitly.

  - HATCH tessellates to no wires at all, and the model path never read the
    fill models, so a drawing whose outermost geometry is a fill — or which is
    nothing but fills — had nothing to frame. It now folds them in, reusing the
    renderer's own warm cache on the hot path rather than re-exploding every
    INSERT per ZOOM EXTENTS.

  - Inside a floating viewport the fit came from `compute_model_space_extents`,
    which walked only `key_vertices` — empty for every tessellated curve.
    Circles, arcs, ellipses and splines contributed nothing. Fixing it also
    corrects plot Extents, sheet-set framing, viewport auto-fit and SpaceMouse
    model bounds, which all inherited that blind spot.

Bounds stay f64 into the camera: a UTM drawing lost whole metres to the old
f32 round-trip. Drawing extents also no longer read the camera — the result is
cached per geometry epoch, and a floating viewport's camera is derived FROM
the extents, so `camera_for_viewport` recursed into a stack overflow.

`model_space_extents_mixed_entities_accuracy` asserted the curve-blind result;
it now asserts the true bounds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-10-01 16:27:26 +03:00
1 parent bf7f37aaf4
commit d596b5a6be
8 files changed
+514 -341

No files matched your search

+18 -1
View File
@@ -45,6 +45,23 @@ fn crosshair_background(tab: &DocumentTab, is_paper: bool) -> [f32; 4] {
tab.scene.paper_bg_color
}
/// Which navigation tool the viewport cursor should advertise. The three flags
/// are mutually exclusive — every command entry point clears all of them before
/// arming one (`crate::app::commands`) — so the order here only settles a state
/// that cannot occur.
pub(in crate::app) fn nav_cursor(tab: &DocumentTab) -> crate::ui::overlay::NavCursor {
use crate::ui::overlay::NavCursor;
if tab.pan_mode {
NavCursor::Pan
} else if tab.orbit_mode {
NavCursor::Orbit
} else if tab.zoom_dynamic_mode {
NavCursor::Zoom
} else {
NavCursor::None
}
}
/// Clear gap (px) kept between the render-mode bar (top-left) and the ViewCube
/// (top-right) before the cube is judged to collide and hides.
const VIEWCUBE_GAP: f32 = 12.0;
@@ -894,7 +911,7 @@ bg={bg_ms:.1}ms n={view_count}"
dividers,
pane_move_rect,
pane_drop_rect,
tab.pan_mode || tab.orbit_mode || tab.zoom_dynamic_mode,
nav_cursor(tab),
self.ribbon.open_dropdown.is_some(),
hover_locked,
crosshair_background(tab, is_paper),