fix(osnap): a POINT snaps as a Node only

A point carried a key vertex, so Endpoint (which outranks Node) claimed
it, and an absolute-size glyph's strokes were indexed for Nearest and
Intersection. Points now offer only their Node, in the indexed and the
unindexed snap paths alike.

Fixes #1570

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-04 20:44:08 +03:00
1 parent d6dfd0d46d
commit fd01a36ad3
4 files changed
+28 -8

No files matched your search

+3 -2
View File
@@ -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![],
}
+5
View File
@@ -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,
+1 -1
View File
@@ -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),
+19 -5
View File
@@ -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()