From 3c97111d8f8567c4daad140d9d6c4191b54ac3ac Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 9 Jun 2026 23:44:31 +0300 Subject: [PATCH] fix(polar): only snap near a polar ray (#70) POLAR locked the cursor to the nearest angle everywhere, so it could never point between angles. Snap only when the cursor is within the snap aperture (in screen pixels) of a polar ray; otherwise leave it free, so POLAR behaves as if off when pointing away from every angle. The polar guide line now shows only while actually snapped. Co-Authored-By: Claude Opus 4.8 --- src/app/helpers.rs | 28 ++++++++++++++++++++++++++++ src/app/update.rs | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/app/helpers.rs b/src/app/helpers.rs index 0488410cf..5f8efd309 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -150,6 +150,34 @@ pub(super) fn polar_constrain(pt: glam::Vec3, base: glam::Vec3, step_deg: f32) - ) } +/// Polar constraint that only engages when the cursor is within `tol_px` +/// screen pixels of the nearest polar ray; otherwise the cursor is left free +/// so POLAR behaves as if off when pointing away from every angle (issue #70). +pub(super) fn polar_constrain_near( + pt: glam::Vec3, + base: glam::Vec3, + step_deg: f32, + view_proj: glam::Mat4, + bounds: iced::Rectangle, + tol_px: f32, +) -> glam::Vec3 { + let snapped = polar_constrain(pt, base, step_deg); + let to_screen = |w: glam::Vec3| { + let ndc = view_proj.project_point3(w); + ( + (ndc.x + 1.0) * 0.5 * bounds.width, + (1.0 - ndc.y) * 0.5 * bounds.height, + ) + }; + let (cx, cy) = to_screen(pt); + let (sx, sy) = to_screen(snapped); + if ((cx - sx).powi(2) + (cy - sy).powi(2)).sqrt() <= tol_px { + snapped + } else { + pt + } +} + // ── Clipboard / selection helpers ────────────────────────────────────────── /// Compute the centroid of a set of wire models (average of all points). diff --git a/src/app/update.rs b/src/app/update.rs index 90710a511..1848e593c 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1,5 +1,5 @@ use super::helpers::{ - ortho_constrain, parse_coord, polar_constrain, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, + ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind, }; use super::{Message, OpenCADStudio, POLY_START_DELAY_MS}; @@ -2253,7 +2253,14 @@ impl OpenCADStudio { if self.ortho_mode { snapped = ortho_constrain(snapped, base); } else if self.polar_mode { - snapped = polar_constrain(snapped, base, self.polar_increment_deg); + snapped = polar_constrain_near( + snapped, + base, + self.polar_increment_deg, + vp_mat, + bounds, + self.snapper.snap_radius_px, + ); } } @@ -2440,7 +2447,14 @@ impl OpenCADStudio { if self.ortho_mode { pt = ortho_constrain(pt, base); } else if self.polar_mode { - pt = polar_constrain(pt, base, self.polar_increment_deg); + pt = polar_constrain_near( + pt, + base, + self.polar_increment_deg, + view_proj, + bounds, + self.snapper.snap_radius_px, + ); } } pt @@ -2484,7 +2498,14 @@ impl OpenCADStudio { if let Some(base) = self.last_point { let dx = effective.x - base.x; let dy = effective.y - base.y; - if (dx * dx + dy * dy).sqrt() > 1e-4 { + // Only show the guide while POLAR is actually engaged + // — i.e. the point is snapped onto a polar ray, not + // floating free near no angle (issue #70). + let step = self.polar_increment_deg.to_radians(); + let angle = dy.atan2(dx); + let snapped = step > 1e-6 + && ((angle / step).round() * step - angle).abs() < 1e-3; + if snapped && (dx * dx + dy * dy).sqrt() > 1e-4 { let far = 1e5_f32; let dir = glam::Vec3::new(dx, dy, 0.0).normalize(); let far_pos = base + dir * far; @@ -2911,7 +2932,14 @@ impl OpenCADStudio { if self.ortho_mode { pt = ortho_constrain(pt, base); } else if self.polar_mode { - pt = polar_constrain(pt, base, self.polar_increment_deg); + pt = polar_constrain_near( + pt, + base, + self.polar_increment_deg, + vp_mat, + bounds, + self.snapper.snap_radius_px, + ); } } pt