fix(snap): shrink OSNAP pickup radius to ¼ of crosshair arm

OSNAP registered snapping over the full crosshair-arm radius (60 px),
making it too easy to snap and leaving little room to click just outside
a snap point. Split out a dedicated osnap_radius_px (¼ of the arm) used
only for object-snap matching; tracking, polar and extension pickup keep
the larger snap_radius_px.

Closes #108

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-17 04:56:42 +03:00
commit d041bea32a

View file

@ -87,8 +87,11 @@ pub struct Snapper {
pub enabled: HashSet<SnapType>, pub enabled: HashSet<SnapType>,
/// World-space grid spacing. /// World-space grid spacing.
pub grid_spacing: f32, pub grid_spacing: f32,
/// Pixel-radius threshold. /// Pixel-radius threshold for tracking / polar / extension pickup.
pub snap_radius_px: f32, pub snap_radius_px: f32,
/// Pixel-radius threshold for object snapping (OSNAP). Kept smaller than
/// `snap_radius_px` so it is easy to click just outside a snap point.
pub osnap_radius_px: f32,
/// Object Snap Tracking on/off (F11). /// Object Snap Tracking on/off (F11).
pub otrack_enabled: bool, pub otrack_enabled: bool,
/// Acquired OST points (world XZ, Y=0 plane). /// Acquired OST points (world XZ, Y=0 plane).
@ -114,6 +117,7 @@ impl Default for Snapper {
enabled, enabled,
grid_spacing: 1.0, grid_spacing: 1.0,
snap_radius_px: CROSSHAIR_ARM, snap_radius_px: CROSSHAIR_ARM,
osnap_radius_px: CROSSHAIR_ARM * 0.25,
otrack_enabled: false, otrack_enabled: false,
tracking_points: Vec::new(), tracking_points: Vec::new(),
last_snap_world: None, last_snap_world: None,
@ -302,6 +306,7 @@ impl Snapper {
}, },
grid_spacing: self.grid_spacing, grid_spacing: self.grid_spacing,
snap_radius_px: self.snap_radius_px, snap_radius_px: self.snap_radius_px,
osnap_radius_px: self.osnap_radius_px,
otrack_enabled: false, otrack_enabled: false,
tracking_points: Vec::new(), tracking_points: Vec::new(),
last_snap_world: None, last_snap_world: None,
@ -323,7 +328,7 @@ impl Snapper {
return None; return None;
} }
let mut best: Option<SnapResult> = None; let mut best: Option<SnapResult> = None;
let mut best_d2 = self.snap_radius_px * self.snap_radius_px; let mut best_d2 = self.osnap_radius_px * self.osnap_radius_px;
// World-space snap radius — derived from the view scale so wires whose // World-space snap radius — derived from the view scale so wires whose
// entire extent is clearly outside the snap circle can be skipped cheaply // entire extent is clearly outside the snap circle can be skipped cheaply
@ -333,7 +338,7 @@ impl Snapper {
let world_snap_r = { let world_snap_r = {
let s = view_proj.col(0).x.abs() * bounds.width * 0.5; let s = view_proj.col(0).x.abs() * bounds.width * 0.5;
if s > 1e-6 { if s > 1e-6 {
self.snap_radius_px / s self.osnap_radius_px / s
} else { } else {
f32::MAX f32::MAX
} }
@ -514,7 +519,7 @@ impl Snapper {
p0 - p1, p0 - p1,
view_proj, view_proj,
bounds, bounds,
self.snap_radius_px, self.osnap_radius_px,
) { ) {
try_pt(ext, SnapType::Extension); try_pt(ext, SnapType::Extension);
} }
@ -529,7 +534,7 @@ impl Snapper {
p_last - p_prev, p_last - p_prev,
view_proj, view_proj,
bounds, bounds,
self.snap_radius_px, self.osnap_radius_px,
) { ) {
try_pt(ext, SnapType::Extension); try_pt(ext, SnapType::Extension);
} }