fix(snap): correct snap symbols + drop bogus midpoints on circles (#34)
Two bugs from #34: 1. Hovering a circle showed the Center snap as a diamond instead of the conventional circle outline. 2. Hovering a circle also showed Midpoint hits (triangle marker) even though a closed curve has no midpoint. (2) came from a "tessellated curve" fallback in the Midpoint snap path that produced a midpoint for every chord of the polyline approximation. Drop that fallback — only entities with explicit `key_vertices` (Line, LwPolyline) contribute per-segment midpoints now. Curves that DO have a single well-defined midpoint (arcs, and later splines) declare it explicitly via a new `SnapHint::Midpoint`; `arc.rs` emits one at the arc-length centre. (1) — and the broader "everything past Endpoint / Midpoint / Grid was a diamond" inconsistency — fixed by giving every `SnapType` a distinct marker in `SelectionCanvas::draw`: - Center: hollow circle - Node: circle with inscribed X - Quadrant: diamond (unchanged) - Intersection: X - Apparent Intersection: X inside a square - Insertion: two overlapping rectangles (tag glyph) - Perpendicular: right-angle hook - Tangent: circle with a bar across the top - Nearest: hourglass / bowtie - Extension: three dots - Parallel: two parallel diagonal bars - Endpoint / Midpoint / Grid: unchanged Closes #34
This commit is contained in:
parent
89fcb21b46
commit
0ec6d77cb1
4 changed files with 162 additions and 28 deletions
|
|
@ -40,6 +40,10 @@ fn to_truck(arc: &Arc) -> TruckEntity {
|
||||||
};
|
};
|
||||||
|
|
||||||
let cv = Vec3::new(cwx as f32, cwy as f32, cwz as f32);
|
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 {
|
let tangent = TangentGeom::Circle {
|
||||||
center: [cwx as f32, cwy as f32, cwz as f32],
|
center: [cwx as f32, cwy as f32, cwz as f32],
|
||||||
radius: r 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]);
|
pts.push([pe.x + t * nx, pe.y + t * ny, pe.z + t * nz]);
|
||||||
return TruckEntity {
|
return TruckEntity {
|
||||||
object: TruckObject::Lines(pts),
|
object: TruckObject::Lines(pts),
|
||||||
snap_pts: vec![(cv, SnapHint::Center)],
|
snap_pts: vec![(cv, SnapHint::Center), (mv, SnapHint::Midpoint)],
|
||||||
tangent_geoms: vec![tangent],
|
tangent_geoms: vec![tangent],
|
||||||
key_vertices: vec![],
|
key_vertices: vec![],
|
||||||
fill_tris: 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);
|
let edge = builder::circle_arc(&v_start, &v_end, p_mid);
|
||||||
TruckEntity {
|
TruckEntity {
|
||||||
object: TruckObject::Curve(edge),
|
object: TruckObject::Curve(edge),
|
||||||
snap_pts: vec![(cv, SnapHint::Center)],
|
snap_pts: vec![(cv, SnapHint::Center), (mv, SnapHint::Midpoint)],
|
||||||
tangent_geoms: vec![tangent],
|
tangent_geoms: vec![tangent],
|
||||||
key_vertices: vec![],
|
key_vertices: vec![],
|
||||||
fill_tris: vec![],
|
fill_tris: vec![],
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ pub enum SnapHint {
|
||||||
Quadrant,
|
Quadrant,
|
||||||
/// Insertion point of text or block.
|
/// Insertion point of text or block.
|
||||||
Insertion,
|
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.
|
/// Geometric primitive used by the tangent-snap engine.
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,7 @@ impl Snapper {
|
||||||
SnapHint::Node => SnapType::Node,
|
SnapHint::Node => SnapType::Node,
|
||||||
SnapHint::Quadrant => SnapType::Quadrant,
|
SnapHint::Quadrant => SnapType::Quadrant,
|
||||||
SnapHint::Insertion => SnapType::Insertion,
|
SnapHint::Insertion => SnapType::Insertion,
|
||||||
|
SnapHint::Midpoint => SnapType::Midpoint,
|
||||||
};
|
};
|
||||||
if self.is_on(snap_type) {
|
if self.is_on(snap_type) {
|
||||||
try_pt(world, snap_type);
|
try_pt(world, snap_type);
|
||||||
|
|
@ -367,13 +368,17 @@ impl Snapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Midpoint ───────────────────────────────────────────────────────
|
// ── 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) {
|
if self.is_on(SnapType::Midpoint) {
|
||||||
for wire in wires {
|
for wire in wires {
|
||||||
if !wire_in_range(wire) {
|
if !wire_in_range(wire) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !wire.key_vertices.is_empty() {
|
if !wire.key_vertices.is_empty() {
|
||||||
// Use explicit vertices for accurate per-segment midpoints.
|
|
||||||
for seg in wire.key_vertices.windows(2) {
|
for seg in wire.key_vertices.windows(2) {
|
||||||
let a = Vec3::from(seg[0]);
|
let a = Vec3::from(seg[0]);
|
||||||
let b = Vec3::from(seg[1]);
|
let b = Vec3::from(seg[1]);
|
||||||
|
|
@ -381,16 +386,6 @@ impl Snapper {
|
||||||
try_pt((a + b) * 0.5, SnapType::Midpoint);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -489,10 +489,10 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
};
|
};
|
||||||
match snap_type {
|
match snap_type {
|
||||||
SnapType::Endpoint => {
|
SnapType::Endpoint => {
|
||||||
let half = 5.0_f32;
|
let h = 5.0_f32;
|
||||||
let rect = canvas::Path::rectangle(
|
let rect = canvas::Path::rectangle(
|
||||||
Point::new(sp.x - half, sp.y - half),
|
Point::new(sp.x - h, sp.y - h),
|
||||||
Size::new(half * 2.0, half * 2.0),
|
Size::new(h * 2.0, h * 2.0),
|
||||||
);
|
);
|
||||||
frame.stroke(&rect, stroke);
|
frame.stroke(&rect, stroke);
|
||||||
}
|
}
|
||||||
|
|
@ -506,6 +506,149 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
});
|
});
|
||||||
frame.stroke(&path, stroke);
|
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 => {
|
SnapType::Grid => {
|
||||||
let arm = 4.0_f32;
|
let arm = 4.0_f32;
|
||||||
let h = canvas::Path::new(|b| {
|
let h = canvas::Path::new(|b| {
|
||||||
|
|
@ -519,18 +662,6 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
frame.stroke(&h, stroke.clone());
|
frame.stroke(&h, stroke.clone());
|
||||||
frame.stroke(&v, stroke);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue