diff --git a/src/entities/point.rs b/src/entities/point.rs index fecb7b0a0..02f24a6e6 100644 --- a/src/entities/point.rs +++ b/src/entities/point.rs @@ -51,7 +51,7 @@ fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity { object: RenderObject::Lines(vec![]), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], - key_vertices: vec![[wx, wy, wz]], + key_vertices: vec![], fill_tris: vec![], }; } @@ -63,7 +63,8 @@ fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity { key_vertices: if pt.thickness.abs() > 1.0e-10 { vec![[wx, wy, wz], [top.x, top.y, top.z]] } else { - vec![[wx, wy, wz]] + // Snaps as a Node only, never as an endpoint. + vec![] }, fill_tris: vec![], } diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 4358d1e5c..233ee854d 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -744,6 +744,11 @@ impl WireModel { pub fn is_display_only(&self) -> bool { self.snap_only && self.display_visible } + /// A POINT's wire: the Node is its only snap; the glyph strokes are drawn + /// but offer no endpoints or segments to snap to. + pub fn is_node_marker(&self) -> bool { + self.key_vertices.is_empty() && matches!(self.snap_pts.as_slice(), [(_, SnapHint::Node)]) + } /// Sentinel AABB that never rejects any snap query. pub const UNBOUNDED_AABB: [f32; 4] = [ f32::NEG_INFINITY, diff --git a/src/scene/pick/interaction_index.rs b/src/scene/pick/interaction_index.rs index efc62a2cd..47f1bc15b 100644 --- a/src/scene/pick/interaction_index.rs +++ b/src/scene/pick/interaction_index.rs @@ -699,7 +699,7 @@ fn append_wire_index_entries(wire_idx: u32, wire: &WireModel, batch: &mut WireIn ); } - if wire.point_marker.is_none() { + if wire.point_marker.is_none() && !wire.is_node_marker() { for start in 0..wire.points.len().saturating_sub(1) { let Some(aabb) = points_aabb3([ wire_point(wire, start), diff --git a/src/snap.rs b/src/snap.rs index 39917f1a6..94e10234a 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -1346,10 +1346,17 @@ impl Snapper { const MAX_PAIRWISE_POINTS: usize = 3_000; let mut in_range_pts = 0usize; let mut in_range_wires = InRangeWires::new(); + // POINT wires offer only their Node; their glyph strokes stay out of + // every geometric snap below. + let mut in_range_nodes: Vec<&WireModel> = Vec::new(); let unindexed = wires.segments().is_none(); if unindexed { for w in wires.iter() { - if !w.is_display_only() && wire_in_range(w) { + if w.is_node_marker() { + if wire_in_range(w) { + in_range_nodes.push(w); + } + } else if !w.is_display_only() && wire_in_range(w) { in_range_pts += w.points.len(); in_range_wires.push(w); } @@ -1361,7 +1368,11 @@ impl Snapper { // Early out: if no unindexed wires overlap the cursor aperture and // no persistent tracking points exist, discrete/continuous object snaps // cannot hit anything. Avoid running all subsequent pass setups and loops. - if unindexed && in_range_wires.is_empty() && self.tracking_points.is_empty() { + if unindexed + && in_range_wires.is_empty() + && in_range_nodes.is_empty() + && self.tracking_points.is_empty() + { return best; } @@ -1460,7 +1471,7 @@ impl Snapper { try_snap_hint(world, hint, wire_source(wire)); } } else { - for wire in in_range_wires.iter() { + for wire in in_range_wires.iter().chain(in_range_nodes.iter().copied()) { let src = wire_source(wire); for &(world, hint) in &wire.snap_pts { try_snap_hint(world, hint, src); @@ -1489,7 +1500,10 @@ impl Snapper { // Tessellated open curves have no key-vertex set. Their only // endpoints are first/last, so testing candidate wires remains // O(local wires), independent of tessellation density. - for wire in wires.iter().filter(|wire| wire.key_vertices.is_empty()) { + for wire in wires + .iter() + .filter(|wire| wire.key_vertices.is_empty() && !wire.is_node_marker()) + { let closed = wire .snap_pts .iter() @@ -1516,7 +1530,7 @@ impl Snapper { for &point in &wire.key_vertices { try_pt(DVec3::from_array(point), SnapType::Endpoint, src, None); } - } else { + } else if !wire.is_node_marker() { let closed = wire .snap_pts .iter()