diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 74afeb614..828e5016f 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -40,6 +40,10 @@ fn to_truck(arc: &Arc) -> TruckEntity { }; let cv = Vec3::new(cwx as f32, cwy as f32, cwz as f32); + // Arc-length centre — one well-defined midpoint snap. Circles and + // ellipses (closed curves) deliberately don't emit this; see #34. + let mid_pt_3 = arc_pt(mid_a); + let mv = Vec3::new(mid_pt_3.x as f32, mid_pt_3.y as f32, mid_pt_3.z as f32); let tangent = TangentGeom::Circle { center: [cwx as f32, cwy as f32, cwz as f32], radius: r as f32, @@ -73,7 +77,7 @@ fn to_truck(arc: &Arc) -> TruckEntity { pts.push([pe.x + t * nx, pe.y + t * ny, pe.z + t * nz]); return TruckEntity { object: TruckObject::Lines(pts), - snap_pts: vec![(cv, SnapHint::Center)], + snap_pts: vec![(cv, SnapHint::Center), (mv, SnapHint::Midpoint)], tangent_geoms: vec![tangent], key_vertices: vec![], fill_tris: vec![], @@ -88,7 +92,7 @@ fn to_truck(arc: &Arc) -> TruckEntity { let edge = builder::circle_arc(&v_start, &v_end, p_mid); TruckEntity { object: TruckObject::Curve(edge), - snap_pts: vec![(cv, SnapHint::Center)], + snap_pts: vec![(cv, SnapHint::Center), (mv, SnapHint::Midpoint)], tangent_geoms: vec![tangent], key_vertices: vec![], fill_tris: vec![], diff --git a/src/scene/wire_model.rs b/src/scene/wire_model.rs index 2dc916c55..dbb357819 100644 --- a/src/scene/wire_model.rs +++ b/src/scene/wire_model.rs @@ -10,6 +10,10 @@ pub enum SnapHint { Quadrant, /// Insertion point of text or block. Insertion, + /// Midpoint of a curve that has one well-defined midpoint (an arc's + /// arc-length centre, a spline's `t = 0.5`). Lines / polylines do + /// not use this — their midpoints are derived from `key_vertices`. + Midpoint, } /// Geometric primitive used by the tangent-snap engine. diff --git a/src/snap/mod.rs b/src/snap/mod.rs index 724ac6ac4..215940598 100644 --- a/src/snap/mod.rs +++ b/src/snap/mod.rs @@ -334,6 +334,7 @@ impl Snapper { SnapHint::Node => SnapType::Node, SnapHint::Quadrant => SnapType::Quadrant, SnapHint::Insertion => SnapType::Insertion, + SnapHint::Midpoint => SnapType::Midpoint, }; if self.is_on(snap_type) { try_pt(world, snap_type); @@ -367,13 +368,17 @@ impl Snapper { } // ── Midpoint ─────────────────────────────────────────────────────── + // Only explicit vertex sets (Line, LwPolyline) contribute per-segment + // midpoints. Tessellated curves (Circle, Arc, Ellipse, Spline) emit a + // single `SnapHint::Midpoint` snap_pt where one exists — iterating + // every chord here would otherwise turn a circle's tessellation into + // a haze of false midpoint hits. See #34. if self.is_on(SnapType::Midpoint) { for wire in wires { if !wire_in_range(wire) { continue; } if !wire.key_vertices.is_empty() { - // Use explicit vertices for accurate per-segment midpoints. for seg in wire.key_vertices.windows(2) { let a = Vec3::from(seg[0]); let b = Vec3::from(seg[1]); @@ -381,16 +386,6 @@ impl Snapper { try_pt((a + b) * 0.5, SnapType::Midpoint); } } - } else { - // Tessellated curves: midpoint of whole arc (first↔last midpoint skipped - // for closed curves; arcs use their own quadrant snaps instead). - for seg in wire.points.windows(2) { - let a = Vec3::from(seg[0]); - let b = Vec3::from(seg[1]); - if a.distance_squared(b) > 1e-12 { - try_pt((a + b) * 0.5, SnapType::Midpoint); - } - } } } } diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 6d98e3484..f20835cb8 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -489,10 +489,10 @@ impl canvas::Program for SelectionCanvas { }; match snap_type { SnapType::Endpoint => { - let half = 5.0_f32; + let h = 5.0_f32; let rect = canvas::Path::rectangle( - Point::new(sp.x - half, sp.y - half), - Size::new(half * 2.0, half * 2.0), + Point::new(sp.x - h, sp.y - h), + Size::new(h * 2.0, h * 2.0), ); frame.stroke(&rect, stroke); } @@ -506,6 +506,149 @@ impl canvas::Program for SelectionCanvas { }); frame.stroke(&path, stroke); } + SnapType::Center => { + let r = 5.5_f32; + let path = canvas::Path::circle(sp, r); + frame.stroke(&path, stroke); + } + SnapType::Node => { + // Circle with an inscribed X. + let r = 5.5_f32; + let cpath = canvas::Path::circle(sp, r); + frame.stroke(&cpath, stroke.clone()); + let d = r * std::f32::consts::FRAC_1_SQRT_2; + let x1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - d, sp.y - d)); + b.line_to(Point::new(sp.x + d, sp.y + d)); + }); + let x2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - d, sp.y + d)); + b.line_to(Point::new(sp.x + d, sp.y - d)); + }); + frame.stroke(&x1, stroke.clone()); + frame.stroke(&x2, stroke); + } + SnapType::Quadrant => { + let r = 6.0_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y)); + b.line_to(Point::new(sp.x, sp.y + r)); + b.line_to(Point::new(sp.x - r, sp.y)); + b.close(); + }); + frame.stroke(&path, stroke); + } + SnapType::Intersection => { + let r = 5.0_f32; + let p1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + }); + let p2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + }); + frame.stroke(&p1, stroke.clone()); + frame.stroke(&p2, stroke); + } + SnapType::ApparentIntersection => { + // X like Intersection, framed by a small square so the + // two are visually distinguishable. + let r = 5.0_f32; + let rect = canvas::Path::rectangle( + Point::new(sp.x - r, sp.y - r), + Size::new(r * 2.0, r * 2.0), + ); + frame.stroke(&rect, stroke.clone()); + let xr = r - 1.5; + let p1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - xr, sp.y - xr)); + b.line_to(Point::new(sp.x + xr, sp.y + xr)); + }); + let p2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - xr, sp.y + xr)); + b.line_to(Point::new(sp.x + xr, sp.y - xr)); + }); + frame.stroke(&p1, stroke.clone()); + frame.stroke(&p2, stroke); + } + SnapType::Insertion => { + // Two overlapping rectangles (a small "tag" glyph). + let r = 5.0_f32; + let inner = canvas::Path::rectangle( + Point::new(sp.x - r * 0.5, sp.y - r), + Size::new(r, r * 2.0), + ); + let outer = canvas::Path::rectangle( + Point::new(sp.x - r, sp.y - r * 0.5), + Size::new(r * 2.0, r), + ); + frame.stroke(&outer, stroke.clone()); + frame.stroke(&inner, stroke); + } + SnapType::Perpendicular => { + // Right-angle hook in the lower-left quadrant. + let r = 6.0_f32; + let p = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + }); + let foot = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y)); + b.line_to(Point::new(sp.x, sp.y)); + b.line_to(Point::new(sp.x, sp.y + r)); + }); + frame.stroke(&p, stroke.clone()); + frame.stroke(&foot, stroke); + } + SnapType::Tangent => { + // Circle with a tangent bar across the top. + let r = 5.5_f32; + let c = canvas::Path::circle(sp, r); + frame.stroke(&c, stroke.clone()); + let bar = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + }); + frame.stroke(&bar, stroke); + } + SnapType::Nearest => { + // Bowtie / hourglass — two opposed triangles meeting at sp. + let r = 5.5_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + b.close(); + }); + frame.stroke(&path, stroke); + } + SnapType::Extension => { + // Three dots strung along a tracked direction. + let r = 1.4_f32; + for k in [-7.0_f32, 0.0, 7.0] { + let dot = canvas::Path::circle(Point::new(sp.x + k, sp.y), r); + frame.fill(&dot, yellow); + } + } + SnapType::Parallel => { + // Two short parallel diagonal bars. + let r = 6.0_f32; + let off = 3.0_f32; + let b1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r - off, sp.y + r)); + b.line_to(Point::new(sp.x + r - off, sp.y - r)); + }); + let b2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r + off, sp.y + r)); + b.line_to(Point::new(sp.x + r + off, sp.y - r)); + }); + frame.stroke(&b1, stroke.clone()); + frame.stroke(&b2, stroke); + } SnapType::Grid => { let arm = 4.0_f32; let h = canvas::Path::new(|b| { @@ -519,18 +662,6 @@ impl canvas::Program for SelectionCanvas { frame.stroke(&h, stroke.clone()); frame.stroke(&v, stroke); } - // Other snap types use the same diamond marker. - _ => { - let r = 5.0_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y)); - b.line_to(Point::new(sp.x, sp.y + r)); - b.line_to(Point::new(sp.x - r, sp.y)); - b.close(); - }); - frame.stroke(&path, stroke); - } } }