From 390d42a63a2389410a0366cc431e359d310ef24c Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:49:49 +0300 Subject: [PATCH] Support planar circle tangent snapping --- src/entities/circle.rs | 4 +- src/scene/cache/block_cache.rs | 34 ++++++++++++++- src/scene/model/wire_model.rs | 9 +++- src/snap.rs | 77 ++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 4c905903c..af926009f 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -25,8 +25,10 @@ fn to_render(circle: &Circle) -> RenderEntity { // definition answers here, in the tessellation and in a trim. let curve = crate::entities::curve::circle_curve(circle); let snap_pts = crate::entities::curve::snap_from(&curve).snap_pts; - let tangent = TangentGeom::Circle { + let tangent = TangentGeom::PlanarCircle { center: [cwx as f32, cwy as f32, cwz as f32], + axis_x: [ax.0 as f32, ax.1 as f32, ax.2 as f32], + axis_y: [ay.0 as f32, ay.1 as f32, ay.2 as f32], radius: rf, }; diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index d8f79c6cd..3434de680 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -1227,7 +1227,8 @@ fn translated_prototype_wire( p2[axis] += delta_f32[axis]; } } - TangentGeom::Circle { center, .. } => { + TangentGeom::Circle { center, .. } + | TangentGeom::PlanarCircle { center, .. } => { for axis in 0..3 { center[axis] += delta_f32[axis]; } @@ -2282,6 +2283,37 @@ fn transform_tangent( radius: radius * s, } } + TangentGeom::PlanarCircle { + center, + axis_x, + axis_y, + radius, + } => { + let c = t.apply(Vector3::new( + center[0] as f64, + center[1] as f64, + center[2] as f64, + )); + let x = t.apply_rotation(Vector3::new( + axis_x[0] as f64, + axis_x[1] as f64, + axis_x[2] as f64, + )); + let y = t.apply_rotation(Vector3::new( + axis_y[0] as f64, + axis_y[1] as f64, + axis_y[2] as f64, + )); + let scale = (x.length() + y.length()) * 0.5; + let x = x.normalize(); + let y = y.normalize(); + TangentGeom::PlanarCircle { + center: [c.x as f32, c.y as f32, c.z as f32], + axis_x: [x.x as f32, x.y as f32, x.z as f32], + axis_y: [y.x as f32, y.y as f32, y.z as f32], + radius: radius * scale as f32, + } + } } } diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 0133640a4..fee9f8ceb 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -29,8 +29,15 @@ pub enum SnapHint { pub enum TangentGeom { /// Infinite line through these two world-space points. Line { p1: [f32; 3], p2: [f32; 3] }, - /// Circle/arc. + /// Round geometry in the world XY plane. Circle { center: [f32; 3], radius: f32 }, + /// Complete circle in an arbitrary world-space plane. + PlanarCircle { + center: [f32; 3], + axis_x: [f32; 3], + axis_y: [f32; 3], + radius: f32, + }, } /// A 1-D entity (line, arc, polyline) represented as an ordered set of diff --git a/src/snap.rs b/src/snap.rs index b34859f7b..c977bb217 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -1661,6 +1661,73 @@ impl Snapper { }); (w, edge_d * edge_d) } + TangentGeom::PlanarCircle { + center, + axis_x, + axis_y, + radius, + } => { + let cv = Vec3::from(*center); + let ux = Vec3::from(*axis_x).normalize_or_zero(); + let uy = Vec3::from(*axis_y).normalize_or_zero(); + let r = *radius; + let candidate = self.from_point.and_then(|from| { + let delta = from - cv; + let px = delta.dot(ux); + let py = delta.dot(uy); + let d2 = px * px + py * py; + if d2 <= r * r + 1.0e-6 { + return None; + } + let base = r * r / d2; + let side = r * (d2 - r * r).sqrt() / d2; + [ + cv + ux * (base * px - side * py) + + uy * (base * py + side * px), + cv + ux * (base * px + side * py) + + uy * (base * py - side * px), + ] + .into_iter() + .min_by(|a, b| { + let sa = world_to_screen(a.as_dvec3(), view_rot, eye, bounds); + let sb = world_to_screen(b.as_dvec3(), view_rot, eye, bounds); + dist2(sa, cursor_screen) + .total_cmp(&dist2(sb, cursor_screen)) + }) + }); + let fallback = || { + (0..wire.points.len()) + .map(|index| wp_f64(wire, index).as_vec3()) + .filter(|point| point.is_finite()) + .min_by(|a, b| { + let sa = + world_to_screen(a.as_dvec3(), view_rot, eye, bounds); + let sb = + world_to_screen(b.as_dvec3(), view_rot, eye, bounds); + dist2(sa, cursor_screen) + .total_cmp(&dist2(sb, cursor_screen)) + }) + .unwrap_or(cv) + }; + let world = candidate.unwrap_or_else(fallback); + let edge_d2 = wire + .points + .windows(2) + .enumerate() + .filter_map(|(index, _)| { + let a = wp_f64(wire, index); + let b = wp_f64(wire, index + 1); + (a.is_finite() && b.is_finite()).then(|| { + dist2_to_segment( + cursor_screen, + world_to_screen(a, view_rot, eye, bounds), + world_to_screen(b, view_rot, eye, bounds), + ) + }) + }) + .fold(f32::INFINITY, f32::min); + (world, edge_d2) + } }; let (tier, sub) = ( snap_tier(SnapType::Tangent), @@ -1687,6 +1754,16 @@ impl Snapper { ), radius: *radius as f64, }, + TangentGeom::PlanarCircle { center, radius, .. } => { + TangentObject::Circle { + center: glam::DVec3::new( + center[0] as f64, + center[1] as f64, + center[2] as f64, + ), + radius: *radius as f64, + } + } }; best = Some(SnapResult { world: world_pt.as_dvec3(),