forked from 1b-agent/opencadstudio-upstream
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:
1 parent
bf7f37aaf4
commit
d596b5a6be
8 files changed
+514
-341
No files matched your search
+105
-1
@@ -48,6 +48,11 @@ enum ScrollIntent {
|
||||
Pan { dx: f32, dy: f32 },
|
||||
}
|
||||
|
||||
/// How far a ZOOM Dynamic drag must travel vertically before the magnifier
|
||||
/// cursor flips between "+" and "-". Without the dead zone a hand holding
|
||||
/// still at the top of a drag strobes the glyph on every jittered pixel.
|
||||
const ZOOM_DIR_FLIP_PX: f32 = 2.0;
|
||||
|
||||
/// A wheel notch zooms; a trackpad's two fingers pan.
|
||||
///
|
||||
/// A wheel reports notches, a precise-scrolling device reports pixels, and that
|
||||
@@ -1238,7 +1243,14 @@ impl OpenCADStudio {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_nav_perf(crate::scene::NavPerfOp::Zoom, move_started);
|
||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
// Latch the direction for the magnifier cursor. A dead
|
||||
// zone keeps the +/- glyph from strobing when a slow drag
|
||||
// wobbles a pixel either side of where it started.
|
||||
if dy.abs() >= ZOOM_DIR_FLIP_PX {
|
||||
sel.zoom_dir_out = dy > 0.0;
|
||||
}
|
||||
sel.middle_last_pos = Some(p);
|
||||
return Task::none();
|
||||
}
|
||||
// Shift+MMB drag orbits the model view instead of panning
|
||||
@@ -3471,6 +3483,9 @@ impl OpenCADStudio {
|
||||
sel.box_current = Some(p);
|
||||
sel.box_crossing = false;
|
||||
sel.box_crossing_locked = true;
|
||||
// A fresh drag starts neutral: the magnifier shows "+" until
|
||||
// the pointer has actually moved far enough to mean zoom out.
|
||||
sel.zoom_dir_out = false;
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
@@ -3740,6 +3755,7 @@ impl OpenCADStudio {
|
||||
sel.box_anchor_world = None;
|
||||
sel.box_current = None;
|
||||
sel.box_crossing_locked = false;
|
||||
sel.zoom_dir_out = false;
|
||||
drop(sel);
|
||||
self.arm_hover_after_navigation(i);
|
||||
return Task::none();
|
||||
@@ -6727,6 +6743,94 @@ mod scroll_intent_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod zoom_dynamic_cursor_tests {
|
||||
use super::*;
|
||||
use crate::app::OpenCADStudio;
|
||||
use crate::ui::overlay::NavCursor;
|
||||
|
||||
fn zoom_armed_app() -> OpenCADStudio {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||
let _ = app.dispatch_command("ZOOM DYNAMIC");
|
||||
app
|
||||
}
|
||||
|
||||
fn drag_to(app: &mut OpenCADStudio, y: f32) {
|
||||
let _ = app.update(Message::ViewportMove(Point::new(400.0, y)));
|
||||
}
|
||||
|
||||
fn zoom_dir_out(app: &OpenCADStudio) -> bool {
|
||||
app.tabs[0].scene.selection.borrow().zoom_dir_out
|
||||
}
|
||||
|
||||
/// The magnifier has to carry the sign of the zoom actually under way:
|
||||
/// dragging up zooms in, dragging back down zooms out.
|
||||
#[test]
|
||||
fn the_magnifier_follows_the_drag_direction() {
|
||||
let mut app = zoom_armed_app();
|
||||
assert!(app.tabs[0].zoom_dynamic_mode);
|
||||
drag_to(&mut app, 300.0);
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
// A fresh drag starts neutral — "+" until told otherwise.
|
||||
assert!(!zoom_dir_out(&app));
|
||||
|
||||
drag_to(&mut app, 200.0);
|
||||
assert!(!zoom_dir_out(&app), "dragging up zooms in");
|
||||
|
||||
drag_to(&mut app, 400.0);
|
||||
assert!(zoom_dir_out(&app), "dragging down zooms out");
|
||||
|
||||
drag_to(&mut app, 250.0);
|
||||
assert!(!zoom_dir_out(&app), "dragging up again zooms in");
|
||||
|
||||
// Releasing disarms the direction, so the next hover shows "+".
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
assert!(!zoom_dir_out(&app));
|
||||
}
|
||||
|
||||
/// Sub-pixel wobble while a hand holds still must not strobe the glyph.
|
||||
#[test]
|
||||
fn jitter_below_the_dead_zone_does_not_flip_the_glyph() {
|
||||
let mut app = zoom_armed_app();
|
||||
drag_to(&mut app, 300.0);
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
drag_to(&mut app, 400.0);
|
||||
assert!(zoom_dir_out(&app));
|
||||
|
||||
// Back up by less than ZOOM_DIR_FLIP_PX: still zooming out.
|
||||
drag_to(&mut app, 399.0);
|
||||
assert!(zoom_dir_out(&app), "1 px of wobble must not flip the sign");
|
||||
}
|
||||
|
||||
/// Each navigation tool owns its own cursor. They used to share the pan
|
||||
/// hand, which read as "drag the sheet" during a zoom and an orbit.
|
||||
#[test]
|
||||
fn each_navigation_tool_reports_its_own_mode() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
for (command, want) in [
|
||||
("PAN", NavCursor::Pan),
|
||||
("ORBIT", NavCursor::Orbit),
|
||||
("ZOOM DYNAMIC", NavCursor::Zoom),
|
||||
] {
|
||||
let _ = app.dispatch_command(command);
|
||||
assert_eq!(
|
||||
crate::app::view::nav_cursor(&app.tabs[0]),
|
||||
want,
|
||||
"{command} must have its own cursor"
|
||||
);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(
|
||||
crate::app::view::nav_cursor(&app.tabs[0]),
|
||||
NavCursor::None,
|
||||
"Esc must disarm {command}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snap_elevation_tests {
|
||||
use super::snap_keeps_elevation;
|
||||
|
||||
Reference in new issue
Block a user