From d0286fbacf5269f3a2519e23c9257c633b173d11 Mon Sep 17 00:00:00 2001 From: sLuCHa <142535358+sLuCHaa@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:57:10 +0200 Subject: [PATCH] perf(snap): keep point picking responsive on large drawings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Object snap ran several full scans of the drawing on every cursor move, and the Intersection / Apparent-Intersection passes compare pairs of in-range segments — O(k^2) in the number of wires whose bounding box overlaps the aperture. Zoomed out over a large drawing (or with long lines whose AABB spans the view) that pairwise scan grows to the whole document and freezes point picking, so MOVE/COPY/paste placement hung. Count the in-range wires once and gate only the pairwise passes on it: above the cap they are skipped (you cannot resolve a single intersection when the aperture spans thousands of segments anyway), while every single-wire snap (endpoint, midpoint, centre, perpendicular, nearest) keeps working — so point picking still snaps to corners at any zoom. --- src/snap.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/snap.rs b/src/snap.rs index f19835221..0fe6d1484 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -878,6 +878,19 @@ impl Snapper { && cursor_world.y - r <= wire.aabb[3] as f64 }; + // The per-wire snaps below skip out-of-aperture wires via `wire_in_range`, + // so they stay O(n) and snap at any zoom. Intersection / ApparentIntersection + // instead compare pairs of in-range segments — O(k²) — which hangs when the + // aperture spans the whole drawing. Count the in-range wires once and gate + // only those pairwise passes; the single-wire snaps always run. + const MAX_PAIRWISE_CANDIDATES: usize = 1_500; + let allow_pairwise = wires + .iter() + .filter(|w| wire_in_range(w)) + .take(MAX_PAIRWISE_CANDIDATES + 1) + .count() + <= MAX_PAIRWISE_CANDIDATES; + let mut try_pt = |world: glam::DVec3, snap_type: SnapType| { let screen = world_to_screen(world, view_rot, eye, bounds); if !in_bounds(screen) { @@ -1018,8 +1031,8 @@ impl Snapper { } } - // ── Intersection — segment-segment intersections ────────── - if self.is_on(SnapType::Intersection) { + // ── Intersection — segment-segment intersections (pairwise, gated) ── + if self.is_on(SnapType::Intersection) && allow_pairwise { for i in 0..wires.len() { if !wire_in_range(&wires[i]) { continue; @@ -1169,9 +1182,9 @@ impl Snapper { } } - // ── Apparent Intersection — screen-space intersections ───────────── + // ── Apparent Intersection — screen-space intersections (pairwise, gated) ── // L: pre-project each in-range wire's points to screen once, not once per segment pair. - if self.is_on(SnapType::ApparentIntersection) { + if self.is_on(SnapType::ApparentIntersection) && allow_pairwise { let screen_pts: Vec>> = wires .iter() .map(|w| {