forked from 1b-agent/opencadstudio-upstream
fix(viewport): clip pick/snap to the viewport rect (no off-screen hits)
The GPU scissors floating-viewport content to the viewport rectangle, but the CPU hit-test/snap wire set runs past it (the frustum cull keeps a margin and lines extend beyond the rect), so box selection, lasso, click and object snap could reach geometry clipped out of the viewport. Reject candidates whose projected screen point falls outside `bounds` (the pane rectangle), mirroring the scissor. `bounds` is the full canvas in model space, so every change is a no-op there. - snap: reject out-of-bounds points in try_pt, the grid block and tangent. - click_hit / click_hits_all: no hit when the cursor is off-pane. - box_hit: clamp the selection box to the pane rectangle. - poly_hit: drop wire points projected off-pane. - hatch click: no hit when the cursor is off-pane. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
ed711671c6
commit
02d40bd2ec
2 files changed
+47
-8
No files matched your search
@@ -30,6 +30,11 @@ pub fn click_hit<'a>(
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Option<&'a str> {
|
||||
// A click outside the pane rectangle (e.g. on the paper around a floating
|
||||
// viewport) must not reach geometry scissored out of the viewport.
|
||||
if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height {
|
||||
return None;
|
||||
}
|
||||
let mut best_dist = CLICK_THRESHOLD_PX;
|
||||
let mut best: Option<&str> = None;
|
||||
|
||||
@@ -98,6 +103,9 @@ pub fn click_hits_all<'a>(
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<&'a str> {
|
||||
if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut hits: Vec<(f32, &str)> = Vec::new();
|
||||
for wire in wires {
|
||||
let mut prev: Option<Point> = None;
|
||||
@@ -305,12 +313,15 @@ pub fn box_hit<'a>(
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<&'a str> {
|
||||
let min_x = corner_a.x.min(corner_b.x);
|
||||
let max_x = corner_a.x.max(corner_b.x);
|
||||
let min_y = corner_a.y.min(corner_b.y);
|
||||
let max_y = corner_a.y.max(corner_b.y);
|
||||
// Clamp the selection box to the pane rectangle so it can't reach geometry
|
||||
// the GPU scissored out of a floating viewport (the hit-test wire set runs
|
||||
// past the visible rect). No-op in model space, where bounds is the canvas.
|
||||
let min_x = corner_a.x.min(corner_b.x).max(0.0);
|
||||
let max_x = corner_a.x.max(corner_b.x).min(bounds.width);
|
||||
let min_y = corner_a.y.min(corner_b.y).max(0.0);
|
||||
let max_y = corner_a.y.max(corner_b.y).min(bounds.height);
|
||||
|
||||
// Ignore zero-area boxes.
|
||||
// Ignore zero-area boxes (including a box clamped entirely off-pane).
|
||||
if (max_x - min_x) < 1.0 || (max_y - min_y) < 1.0 {
|
||||
return vec![];
|
||||
}
|
||||
@@ -457,6 +468,13 @@ pub fn poly_hit<'a>(
|
||||
continue;
|
||||
}
|
||||
let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds);
|
||||
// Reject points the GPU scissored out of a floating viewport so
|
||||
// the lasso can't reach clipped geometry. No-op in model space.
|
||||
if sp.x < 0.0 || sp.x > bounds.width || sp.y < 0.0 || sp.y > bounds.height {
|
||||
all_inside = false;
|
||||
prev = None;
|
||||
continue;
|
||||
}
|
||||
if crossing {
|
||||
if point_in_polygon(sp, poly) {
|
||||
hit = true;
|
||||
@@ -643,6 +661,11 @@ fn hatch_contains_screen_point(
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> bool {
|
||||
// A cursor outside the pane rectangle can't pick a hatch scissored out of a
|
||||
// floating viewport. No-op in model space (bounds is the canvas).
|
||||
if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height {
|
||||
return false;
|
||||
}
|
||||
// boundary verts are stored as small f32 offsets from
|
||||
// `world_origin` (f64). Reconstruct offset-rel WCS before
|
||||
// projecting to screen.
|
||||
|
||||
+19
-3
@@ -490,6 +490,16 @@ impl Snapper {
|
||||
let mut best_rank = u8::MAX;
|
||||
let mut best_d2 = f32::MAX;
|
||||
|
||||
// Reject candidates projecting outside the pane rectangle. The GPU
|
||||
// scissors viewport content to exactly `bounds`, but the hit-test wire
|
||||
// set reaches past it (the cull keeps a margin and lines run beyond the
|
||||
// rect), so without this a snap could land on geometry clipped out of
|
||||
// the viewport. `bounds` is the full canvas in model space, so this is a
|
||||
// no-op there.
|
||||
let in_bounds = |s: Point| -> bool {
|
||||
s.x >= 0.0 && s.x <= bounds.width && s.y >= 0.0 && s.y <= bounds.height
|
||||
};
|
||||
|
||||
// ── Grid snap — a SEPARATE system from object snap ───────────────────
|
||||
// Grid snap has its own toggle (`grid_snap_on`) and is independent of
|
||||
// the object-snap master (`snap_enabled`) and the object-snap mode set.
|
||||
@@ -511,7 +521,7 @@ impl Snapper {
|
||||
let gp = origin + ax * ux + ay * uy + az * uz;
|
||||
let screen = world_to_screen(gp, view_rot, eye, bounds);
|
||||
let d2 = dist2(screen, cursor_screen);
|
||||
if d2 < radius2 {
|
||||
if d2 < radius2 && in_bounds(screen) {
|
||||
best = Some(SnapResult {
|
||||
world: gp,
|
||||
screen,
|
||||
@@ -570,6 +580,9 @@ impl Snapper {
|
||||
|
||||
let mut try_pt = |world: glam::DVec3, snap_type: SnapType| {
|
||||
let screen = world_to_screen(world, view_rot, eye, bounds);
|
||||
if !in_bounds(screen) {
|
||||
return;
|
||||
}
|
||||
let d2 = dist2(screen, cursor_screen);
|
||||
// `!(d2 < radius2)` (not `d2 >= radius2`) so a NaN distance from
|
||||
// degenerate geometry is rejected: with priority selection a NaN
|
||||
@@ -855,10 +868,13 @@ impl Snapper {
|
||||
}
|
||||
};
|
||||
let rank = snap_priority(SnapType::Tangent);
|
||||
if d2 < radius2 && (rank < best_rank || (rank == best_rank && d2 < best_d2)) {
|
||||
let screen_pt = world_to_screen(world_pt.as_dvec3(), view_rot, eye, bounds);
|
||||
if d2 < radius2
|
||||
&& in_bounds(screen_pt)
|
||||
&& (rank < best_rank || (rank == best_rank && d2 < best_d2))
|
||||
{
|
||||
best_rank = rank;
|
||||
best_d2 = d2;
|
||||
let screen_pt = world_to_screen(world_pt.as_dvec3(), view_rot, eye, bounds);
|
||||
let tangent_obj = match tg {
|
||||
TangentGeom::Line { p1, p2 } => TangentObject::Line {
|
||||
p1: glam::DVec3::new(p1[0] as f64, p1[1] as f64, p1[2] as f64),
|
||||
|
||||
Reference in new issue
Block a user