From e512e57a7c6da695f84dc46173976c667083e690 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 5 Jul 2026 11:58:44 +0300 Subject: [PATCH] fix(snap): correct circle tangent point + no endpoint on closed curves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two object-snap fixes: - Tangent on a circle built the snap point as `(cx + r·nx, cy, cy + r·ny)` — the y-offset landed in Z and Y was stuck at the centre, so the tangent point sat at the wrong place. Build it as `(cx + r·nx, cy + r·ny, cz)`. - Endpoint snapped to a full circle / ellipse: for tessellated curves it snaps the first/last point, but a closed curve's first/last is a seam, not an endpoint. Skip Endpoint for closed curves — identified by their Quadrant snap hints, which arcs never carry — so arcs still snap their real endpoints. Closes #274, closes #275 Co-Authored-By: Claude Opus 4.8 --- src/snap.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/snap.rs b/src/snap.rs index a11e119ee..f94fa3e05 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -802,14 +802,25 @@ impl Snapper { ); } } else { - // Tessellated curves (Circle, Arc, Ellipse): only arc endpoints. - if let Some(&p) = wire.points.first() { - try_pt(glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), SnapType::Endpoint); - } - if wire.points.len() > 1 { - if let Some(&p) = wire.points.last() { + // Tessellated curves (Circle, Arc, Ellipse): only an OPEN + // one (an arc) has real endpoints. A full circle / ellipse is + // closed — its tessellation's first/last is a seam point, not + // an endpoint — and is the only tessellated curve that carries + // Quadrant snap hints (arcs never do), so emit no Endpoint for + // those (#275). + let closed = wire + .snap_pts + .iter() + .any(|(_, h)| matches!(h, SnapHint::Quadrant)); + if !closed { + if let Some(&p) = wire.points.first() { try_pt(glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), SnapType::Endpoint); } + if wire.points.len() > 1 { + if let Some(&p) = wire.points.last() { + try_pt(glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), SnapType::Endpoint); + } + } } } } @@ -1101,7 +1112,10 @@ impl Snapper { } else { (1.0, 0.0) }; - let w = Vec3::new(cv.x + radius * nx, cv.y, cv.y + radius * ny); + // Circle lies in its own plane at cv.z; the point + // facing the cursor is center + radius·(nx, ny) in XY + // (the y-offset must land in Y, not Z — #274). + let w = Vec3::new(cv.x + radius * nx, cv.y + radius * ny, cv.z); (w, edge_d * edge_d) } };