forked from 1b-agent/opencadstudio-upstream
perf: expand spatial interaction indexing
Share subprimitive broad-phase queries across snap, hover, selection, hatch, and mesh paths while preserving exact hit tests.\n\nRefs #433
This commit is contained in:
1 parent
4db763ef27
commit
90714d34b2
5 files changed
+2640
-691
No files matched your search
+83
-16
@@ -776,6 +776,7 @@ impl OpenCADStudio {
|
||||
all_wires,
|
||||
raw,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
);
|
||||
@@ -941,6 +942,7 @@ impl OpenCADStudio {
|
||||
all_wires,
|
||||
snap_cursor,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
);
|
||||
@@ -1547,6 +1549,7 @@ impl OpenCADStudio {
|
||||
all_wires,
|
||||
raw,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
);
|
||||
@@ -2034,6 +2037,7 @@ impl OpenCADStudio {
|
||||
all_wires,
|
||||
snap_cursor,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
);
|
||||
@@ -2206,10 +2210,11 @@ impl OpenCADStudio {
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let (view_rot2, eye2, all_wires2) = self.pick_view(i, &edit_cam, bounds);
|
||||
let click_candidates = self.tabs[i].scene.interaction_candidates_near(
|
||||
let click_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
|
||||
all_wires2,
|
||||
world_pt,
|
||||
view_rot2,
|
||||
eye2,
|
||||
bounds,
|
||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
||||
);
|
||||
@@ -2445,9 +2450,14 @@ impl OpenCADStudio {
|
||||
aabb
|
||||
},
|
||||
);
|
||||
let area_candidates = self.tabs[i]
|
||||
.scene
|
||||
.interaction_candidates_in_aabb(all_wires, world_aabb, view_rot);
|
||||
let area_candidates = self.tabs[i].scene.interaction_candidates_in_aabb(
|
||||
all_wires,
|
||||
world_aabb,
|
||||
[a.x.min(p.x), a.y.min(p.y), a.x.max(p.x), a.y.max(p.y)],
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
let candidate_handles = self.tabs[i]
|
||||
.scene
|
||||
.interaction_candidate_handles(&area_candidates);
|
||||
@@ -2475,6 +2485,16 @@ impl OpenCADStudio {
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(scene::pick::hit_test::box_hit_insert_hatch(
|
||||
a,
|
||||
p,
|
||||
crossing,
|
||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(self.tabs[i].scene.mesh_box_hit(
|
||||
a,
|
||||
p,
|
||||
@@ -2548,9 +2568,28 @@ impl OpenCADStudio {
|
||||
aabb
|
||||
},
|
||||
);
|
||||
let area_candidates = self.tabs[i]
|
||||
.scene
|
||||
.interaction_candidates_in_aabb(all_wires, world_aabb, view_rot);
|
||||
let area_candidates = self.tabs[i].scene.interaction_candidates_in_aabb(
|
||||
all_wires,
|
||||
world_aabb,
|
||||
poly_pts.iter().fold(
|
||||
[
|
||||
f32::INFINITY,
|
||||
f32::INFINITY,
|
||||
f32::NEG_INFINITY,
|
||||
f32::NEG_INFINITY,
|
||||
],
|
||||
|mut rect, point| {
|
||||
rect[0] = rect[0].min(point.x);
|
||||
rect[1] = rect[1].min(point.y);
|
||||
rect[2] = rect[2].max(point.x);
|
||||
rect[3] = rect[3].max(point.y);
|
||||
rect
|
||||
},
|
||||
),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
let candidate_handles = self.tabs[i]
|
||||
.scene
|
||||
.interaction_candidate_handles(&area_candidates);
|
||||
@@ -2576,6 +2615,15 @@ impl OpenCADStudio {
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(scene::pick::hit_test::poly_hit_insert_hatch(
|
||||
&poly_pts,
|
||||
crossing,
|
||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(self.tabs[i].scene.mesh_poly_hit(
|
||||
&poly_pts,
|
||||
crossing,
|
||||
@@ -2630,10 +2678,11 @@ impl OpenCADStudio {
|
||||
if box_anchor.is_none() {
|
||||
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
|
||||
let click_world = self.cursor_model_point(i, &edit_cam, p, bounds);
|
||||
let click_candidates = self.tabs[i].scene.interaction_candidates_near(
|
||||
let click_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
|
||||
all_wires,
|
||||
click_world,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
||||
);
|
||||
@@ -2695,7 +2744,7 @@ impl OpenCADStudio {
|
||||
// parent Insert (AutoCAD behaviour).
|
||||
scene::pick::hit_test::click_hit_insert_hatch(
|
||||
p,
|
||||
&self.tabs[i].scene.insert_hatches_for_click()[..],
|
||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
@@ -2801,9 +2850,14 @@ impl OpenCADStudio {
|
||||
aabb
|
||||
},
|
||||
);
|
||||
let area_candidates = self.tabs[i]
|
||||
.scene
|
||||
.interaction_candidates_in_aabb(all_wires, world_aabb, view_rot);
|
||||
let area_candidates = self.tabs[i].scene.interaction_candidates_in_aabb(
|
||||
all_wires,
|
||||
world_aabb,
|
||||
[a.x.min(p.x), a.y.min(p.y), a.x.max(p.x), a.y.max(p.y)],
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
let candidate_handles = self.tabs[i]
|
||||
.scene
|
||||
.interaction_candidate_handles(&area_candidates);
|
||||
@@ -2831,6 +2885,16 @@ impl OpenCADStudio {
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(scene::pick::hit_test::box_hit_insert_hatch(
|
||||
a,
|
||||
p,
|
||||
crossing,
|
||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(self.tabs[i].scene.mesh_box_hit(
|
||||
a,
|
||||
p,
|
||||
@@ -2937,10 +3001,11 @@ impl OpenCADStudio {
|
||||
};
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let click_world = self.cursor_model_point(i, &edit_cam, p, bounds);
|
||||
let click_candidates = self.tabs[i].scene.interaction_candidates_near(
|
||||
let click_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
|
||||
all_wires,
|
||||
click_world,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
||||
);
|
||||
@@ -3058,10 +3123,11 @@ impl OpenCADStudio {
|
||||
};
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let click_world = self.cursor_model_point(i, &edit_cam, p, bounds);
|
||||
let click_candidates = self.tabs[i].scene.interaction_candidates_near(
|
||||
let click_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
|
||||
all_wires,
|
||||
click_world,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
||||
);
|
||||
@@ -3384,10 +3450,11 @@ impl OpenCADStudio {
|
||||
.map(|(cam, _)| cam);
|
||||
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
|
||||
let hover_world = self.cursor_model_point(i, &edit_cam, p, bounds);
|
||||
let hover_candidates = self.tabs[i].scene.interaction_candidates_near(
|
||||
let hover_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
|
||||
all_wires,
|
||||
hover_world,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
||||
);
|
||||
@@ -3421,7 +3488,7 @@ impl OpenCADStudio {
|
||||
.or_else(|| {
|
||||
scene::pick::hit_test::click_hit_insert_hatch(
|
||||
p,
|
||||
&self.tabs[i].scene.insert_hatches_for_click()[..],
|
||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
|
||||
+644
-261
File diff suppressed because it is too large.
Load diff
+817
-199
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+253
-132
@@ -90,7 +90,7 @@ pub struct OtrackHit {
|
||||
|
||||
// ── Snapper ───────────────────────────────────────────────────────────────
|
||||
|
||||
use rustc_hash::FxHashSet as HashSet;
|
||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||
|
||||
pub struct Snapper {
|
||||
/// Global snap on/off toggle. When false, all snapping is bypassed
|
||||
@@ -926,7 +926,7 @@ impl Snapper {
|
||||
}
|
||||
}
|
||||
}
|
||||
let allow_pairwise = wires.segments().is_some() || in_range_pts <= MAX_PAIRWISE_POINTS;
|
||||
let allow_unindexed_pairwise = in_range_pts <= MAX_PAIRWISE_POINTS;
|
||||
let local_segments = indexed_segments(wires);
|
||||
|
||||
let mut try_pt = |world: glam::DVec3, snap_type: SnapType| {
|
||||
@@ -959,56 +959,87 @@ impl Snapper {
|
||||
};
|
||||
|
||||
// ── Pre-baked snap points (Center, Node, Quadrant, Insertion) ──────
|
||||
for wire in wires.iter() {
|
||||
for &(world, hint) in &wire.snap_pts {
|
||||
let snap_type = match hint {
|
||||
SnapHint::Center => SnapType::Center,
|
||||
SnapHint::Node => SnapType::Node,
|
||||
SnapHint::Quadrant => SnapType::Quadrant,
|
||||
SnapHint::Insertion => SnapType::Insertion,
|
||||
SnapHint::Midpoint => SnapType::Midpoint,
|
||||
let mut try_snap_hint = |world: DVec3, hint: SnapHint| {
|
||||
let snap_type = match hint {
|
||||
SnapHint::Center => SnapType::Center,
|
||||
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);
|
||||
}
|
||||
};
|
||||
if let Some(points) = wires.snap_points() {
|
||||
for point_ref in points {
|
||||
let Some(&(world, hint)) = wires
|
||||
.source_wire(point_ref.wire)
|
||||
.and_then(|wire| wire.snap_pts.get(point_ref.index as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if self.is_on(snap_type) {
|
||||
try_pt(world, snap_type);
|
||||
try_snap_hint(world, hint);
|
||||
}
|
||||
} else {
|
||||
for wire in wires.iter() {
|
||||
for &(world, hint) in &wire.snap_pts {
|
||||
try_snap_hint(world, hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(try_snap_hint);
|
||||
|
||||
// ── Endpoint ───────────────────────────────────────────────────────
|
||||
if self.is_on(SnapType::Endpoint) {
|
||||
for wire in wires.iter() {
|
||||
if !wire_in_range(wire) {
|
||||
continue;
|
||||
if let Some(vertices) = wires.key_vertices() {
|
||||
for vertex_ref in vertices {
|
||||
let Some(&point) = wires
|
||||
.source_wire(vertex_ref.wire)
|
||||
.and_then(|wire| wire.key_vertices.get(vertex_ref.index as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
try_pt(DVec3::from_array(point), SnapType::Endpoint);
|
||||
}
|
||||
if !wire.key_vertices.is_empty() {
|
||||
// Use explicit vertices (Line, LwPolyline): every vertex is an endpoint.
|
||||
for &p in &wire.key_vertices {
|
||||
try_pt(glam::DVec3::new(p[0], p[1], p[2]), SnapType::Endpoint);
|
||||
}
|
||||
} else {
|
||||
// Tessellated curves (Circle, Arc, Ellipse): only an OPEN
|
||||
// one (an arc) has real endpoints. A full circle / ellipse is
|
||||
// closed — its tessellation's first/last is a seam point, not
|
||||
// an endpoint — and is the only tessellated curve that carries
|
||||
// Quadrant snap hints (arcs never do), so emit no Endpoint for
|
||||
// those (#275).
|
||||
// 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()) {
|
||||
let closed = wire
|
||||
.snap_pts
|
||||
.iter()
|
||||
.any(|(_, h)| matches!(h, SnapHint::Quadrant));
|
||||
if !closed {
|
||||
if let Some(&p) = wire.points.first() {
|
||||
try_pt(
|
||||
glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64),
|
||||
SnapType::Endpoint,
|
||||
);
|
||||
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant));
|
||||
if closed {
|
||||
continue;
|
||||
}
|
||||
if !wire.points.is_empty() {
|
||||
try_pt(wp_f64(wire, 0), SnapType::Endpoint);
|
||||
}
|
||||
if wire.points.len() > 1 {
|
||||
try_pt(wp_f64(wire, wire.points.len() - 1), SnapType::Endpoint);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for wire in wires.iter() {
|
||||
if !wire_in_range(wire) {
|
||||
continue;
|
||||
}
|
||||
if !wire.key_vertices.is_empty() {
|
||||
for &point in &wire.key_vertices {
|
||||
try_pt(DVec3::from_array(point), SnapType::Endpoint);
|
||||
}
|
||||
if wire.points.len() > 1 {
|
||||
if let Some(&p) = wire.points.last() {
|
||||
try_pt(
|
||||
glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64),
|
||||
SnapType::Endpoint,
|
||||
);
|
||||
} else {
|
||||
let closed = wire
|
||||
.snap_pts
|
||||
.iter()
|
||||
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant));
|
||||
if !closed {
|
||||
if !wire.points.is_empty() {
|
||||
try_pt(wp_f64(wire, 0), SnapType::Endpoint);
|
||||
}
|
||||
if wire.points.len() > 1 {
|
||||
try_pt(wp_f64(wire, wire.points.len() - 1), SnapType::Endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1017,20 +1048,32 @@ 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.
|
||||
// Tessellated curves contribute their pre-baked Midpoint above.
|
||||
if self.is_on(SnapType::Midpoint) {
|
||||
for wire in wires.iter() {
|
||||
if !wire_in_range(wire) {
|
||||
continue;
|
||||
if let Some(segments) = wires.key_segments() {
|
||||
for segment_ref in segments {
|
||||
let Some(wire) = wires.source_wire(segment_ref.wire) else {
|
||||
continue;
|
||||
};
|
||||
let start = segment_ref.start as usize;
|
||||
let Some((&a, &b)) = wire
|
||||
.key_vertices
|
||||
.get(start)
|
||||
.zip(wire.key_vertices.get(start + 1))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let a = DVec3::from_array(a);
|
||||
let b = DVec3::from_array(b);
|
||||
if a.distance_squared(b) > 1e-12 {
|
||||
try_pt((a + b) * 0.5, SnapType::Midpoint);
|
||||
}
|
||||
}
|
||||
if !wire.key_vertices.is_empty() {
|
||||
for seg in wire.key_vertices.windows(2) {
|
||||
let a = glam::DVec3::new(seg[0][0], seg[0][1], seg[0][2]);
|
||||
let b = glam::DVec3::new(seg[1][0], seg[1][1], seg[1][2]);
|
||||
} else {
|
||||
for wire in wires.iter().filter(|wire| wire_in_range(wire)) {
|
||||
for segment in wire.key_vertices.windows(2) {
|
||||
let a = DVec3::from_array(segment[0]);
|
||||
let b = DVec3::from_array(segment[1]);
|
||||
if a.distance_squared(b) > 1e-12 {
|
||||
try_pt((a + b) * 0.5, SnapType::Midpoint);
|
||||
}
|
||||
@@ -1095,30 +1138,35 @@ impl Snapper {
|
||||
}
|
||||
|
||||
// ── Intersection — segment-segment intersections (pairwise, gated) ──
|
||||
if self.is_on(SnapType::Intersection) && allow_pairwise {
|
||||
if self.is_on(SnapType::Intersection)
|
||||
&& (local_segments.is_some() || allow_unindexed_pairwise)
|
||||
{
|
||||
if let Some(segments) = &local_segments {
|
||||
// Cursor-local segment bounds make this set small. In a
|
||||
// pathological dense crossing retain closest segments instead
|
||||
// of disabling Intersection for the whole aperture.
|
||||
const MAX_LOCAL_INTERSECTION_SEGMENTS: usize = 512;
|
||||
// Exact cursor-local sweep: never discard a valid intersection
|
||||
// in dense geometry. Min-X ordering plus Y overlap avoids
|
||||
// comparing segment pairs whose bounds cannot meet.
|
||||
let mut local: Vec<&IndexedSegment> = segments.iter().collect();
|
||||
local.sort_by(|a, b| {
|
||||
segment_aabb_distance2(a, cursor_world)
|
||||
.total_cmp(&segment_aabb_distance2(b, cursor_world))
|
||||
});
|
||||
local.truncate(MAX_LOCAL_INTERSECTION_SEGMENTS);
|
||||
local.sort_by(|a, b| a.min_x().total_cmp(&b.min_x()));
|
||||
for i in 0..local.len() {
|
||||
let a = local[i];
|
||||
for &b in local.iter().skip(i + 1) {
|
||||
if b.min_x() > a.max_x() {
|
||||
break;
|
||||
}
|
||||
if a.wire == b.wire || a.max_y() < b.min_y() || a.min_y() > b.max_y() {
|
||||
continue;
|
||||
}
|
||||
if let Some(pt) = seg_intersect_3d(a.a, a.b, b.a, b.b) {
|
||||
try_pt(pt, SnapType::Intersection);
|
||||
if indexed_sweep_within_budget(&local) {
|
||||
'intersection_sweep: for i in 0..local.len() {
|
||||
let a = local[i];
|
||||
for &b in local.iter().skip(i + 1) {
|
||||
if b.min_x() > a.max_x() {
|
||||
break;
|
||||
}
|
||||
if a.wire == b.wire || a.max_y() < b.min_y() || a.min_y() > b.max_y() {
|
||||
continue;
|
||||
}
|
||||
if let Some(pt) = seg_intersect_3d(a.a, a.b, b.a, b.b) {
|
||||
let exact_cursor = dist2(
|
||||
world_to_screen(pt, view_rot, eye, bounds),
|
||||
cursor_screen,
|
||||
) <= f32::EPSILON;
|
||||
try_pt(pt, SnapType::Intersection);
|
||||
if exact_cursor {
|
||||
break 'intersection_sweep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1237,39 +1285,57 @@ impl Snapper {
|
||||
|
||||
// ── Apparent Intersection — screen-space intersections (pairwise, gated) ──
|
||||
// L: pre-project each in-range wire's points to screen once, not once per segment pair.
|
||||
if self.is_on(SnapType::ApparentIntersection) && allow_pairwise {
|
||||
if self.is_on(SnapType::ApparentIntersection)
|
||||
&& (local_segments.is_some() || allow_unindexed_pairwise)
|
||||
{
|
||||
if let Some(segments) = &local_segments {
|
||||
const MAX_LOCAL_APPARENT_SEGMENTS: usize = 512;
|
||||
let mut local: Vec<&IndexedSegment> = segments.iter().collect();
|
||||
local.sort_by(|a, b| {
|
||||
segment_aabb_distance2(a, cursor_world)
|
||||
.total_cmp(&segment_aabb_distance2(b, cursor_world))
|
||||
});
|
||||
local.truncate(MAX_LOCAL_APPARENT_SEGMENTS);
|
||||
let projected: Vec<(Point, Point)> = local
|
||||
// Project once, then run an exact screen-AABB sweep. Unlike the
|
||||
// previous 512-item cap this cannot drop the true apparent
|
||||
// intersection in a dense aperture.
|
||||
let mut projected: Vec<(&IndexedSegment, Point, Point, [f32; 4])> = segments
|
||||
.iter()
|
||||
.map(|seg| {
|
||||
let a = world_to_screen(seg.a, view_rot, eye, bounds);
|
||||
let b = world_to_screen(seg.b, view_rot, eye, bounds);
|
||||
(
|
||||
world_to_screen(seg.a, view_rot, eye, bounds),
|
||||
world_to_screen(seg.b, view_rot, eye, bounds),
|
||||
seg,
|
||||
a,
|
||||
b,
|
||||
[a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for i in 0..local.len() {
|
||||
for j in (i + 1)..local.len() {
|
||||
if local[i].wire == local[j].wire {
|
||||
continue;
|
||||
}
|
||||
if let Some((ta, _)) = seg_intersect_2d(
|
||||
projected[i].0,
|
||||
projected[i].1,
|
||||
projected[j].0,
|
||||
projected[j].1,
|
||||
) {
|
||||
try_pt(
|
||||
local[i].a + ta as f64 * (local[i].b - local[i].a),
|
||||
SnapType::ApparentIntersection,
|
||||
);
|
||||
projected.sort_by(|a, b| a.3[0].total_cmp(&b.3[0]));
|
||||
if projected_sweep_within_budget(&projected) {
|
||||
'apparent_sweep: for i in 0..projected.len() {
|
||||
let (segment_a, screen_a0, screen_a1, aabb_a) = projected[i];
|
||||
for &(segment_b, screen_b0, screen_b1, aabb_b) in
|
||||
projected.iter().skip(i + 1)
|
||||
{
|
||||
if aabb_b[0] > aabb_a[2] {
|
||||
break;
|
||||
}
|
||||
if segment_a.wire == segment_b.wire
|
||||
|| aabb_a[3] < aabb_b[1]
|
||||
|| aabb_a[1] > aabb_b[3]
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some((ta, _)) =
|
||||
seg_intersect_2d(screen_a0, screen_a1, screen_b0, screen_b1)
|
||||
{
|
||||
let apparent_screen = Point::new(
|
||||
screen_a0.x + ta * (screen_a1.x - screen_a0.x),
|
||||
screen_a0.y + ta * (screen_a1.y - screen_a0.y),
|
||||
);
|
||||
try_pt(
|
||||
segment_a.a + ta as f64 * (segment_a.b - segment_a.a),
|
||||
SnapType::ApparentIntersection,
|
||||
);
|
||||
if dist2(apparent_screen, cursor_screen) <= f32::EPSILON {
|
||||
break 'apparent_sweep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1450,27 +1516,15 @@ impl Snapper {
|
||||
// the cursor is to the curve. Runs here, after `try_pt`'s borrow ends,
|
||||
// so it can update the candidate state directly. (#152)
|
||||
if self.is_on(SnapType::Center) {
|
||||
for wire in wires.iter() {
|
||||
if !wire_in_range(wire) {
|
||||
continue;
|
||||
}
|
||||
// Only tessellated curves carry a pre-baked Center hint; reuse
|
||||
// it as the snap target. Lines / polylines have none → skip.
|
||||
let mut offer = |wire: &WireModel, curve_d2: f32| {
|
||||
let Some(center) = wire
|
||||
.snap_pts
|
||||
.iter()
|
||||
.find(|(_, h)| matches!(h, SnapHint::Center))
|
||||
.find(|(_, hint)| matches!(hint, SnapHint::Center))
|
||||
.map(|&(c, _)| c)
|
||||
else {
|
||||
continue;
|
||||
return;
|
||||
};
|
||||
// Nearest screen distance from the cursor to the curve itself.
|
||||
let mut curve_d2 = f32::INFINITY;
|
||||
for i in 0..wire.points.len().saturating_sub(1) {
|
||||
let p = nearest_on_segment(cursor_world, wp_f64(wire, i), wp_f64(wire, i + 1));
|
||||
let sp = world_to_screen(p, view_rot, eye, bounds);
|
||||
curve_d2 = curve_d2.min(dist2(sp, cursor_screen));
|
||||
}
|
||||
let screen = world_to_screen(center, view_rot, eye, bounds);
|
||||
// Rim-hover Center: the cursor is on the CURVE, not near the
|
||||
// centre, so its distance metric (curve_d2 ≈ 0 anywhere on the
|
||||
@@ -1496,6 +1550,51 @@ impl Snapper {
|
||||
extension_base2: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
if let Some(segments) = &local_segments {
|
||||
let mut distances: HashMap<u32, f32> = HashMap::default();
|
||||
for segment in segments {
|
||||
let Some(wire) = wires.source_wire(segment.wire) else {
|
||||
continue;
|
||||
};
|
||||
if !wire
|
||||
.snap_pts
|
||||
.iter()
|
||||
.any(|(_, hint)| matches!(hint, SnapHint::Center))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let nearest = nearest_on_segment(cursor_world, segment.a, segment.b);
|
||||
let distance = dist2(
|
||||
world_to_screen(nearest, view_rot, eye, bounds),
|
||||
cursor_screen,
|
||||
);
|
||||
distances
|
||||
.entry(segment.wire)
|
||||
.and_modify(|best| *best = best.min(distance))
|
||||
.or_insert(distance);
|
||||
}
|
||||
for (wire_idx, distance) in distances {
|
||||
if let Some(wire) = wires.source_wire(wire_idx) {
|
||||
offer(wire, distance);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for wire in wires.iter().filter(|wire| wire_in_range(wire)) {
|
||||
let mut curve_d2 = f32::INFINITY;
|
||||
for index in 0..wire.points.len().saturating_sub(1) {
|
||||
let nearest = nearest_on_segment(
|
||||
cursor_world,
|
||||
wp_f64(wire, index),
|
||||
wp_f64(wire, index + 1),
|
||||
);
|
||||
curve_d2 = curve_d2.min(dist2(
|
||||
world_to_screen(nearest, view_rot, eye, bounds),
|
||||
cursor_screen,
|
||||
));
|
||||
}
|
||||
offer(wire, curve_d2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1683,6 +1782,46 @@ impl IndexedSegment {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_INDEXED_PAIRWISE_WORK: usize = 500_000;
|
||||
|
||||
fn indexed_sweep_within_budget(segments: &[&IndexedSegment]) -> bool {
|
||||
let mut work = 0usize;
|
||||
for (index, &a) in segments.iter().enumerate() {
|
||||
for &b in segments.iter().skip(index + 1) {
|
||||
if b.min_x() > a.max_x() {
|
||||
break;
|
||||
}
|
||||
work += 1;
|
||||
if work > MAX_INDEXED_PAIRWISE_WORK {
|
||||
return false;
|
||||
}
|
||||
if a.wire == b.wire || a.max_y() < b.min_y() || a.min_y() > b.max_y() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn projected_sweep_within_budget(segments: &[(&IndexedSegment, Point, Point, [f32; 4])]) -> bool {
|
||||
let mut work = 0usize;
|
||||
for (index, &(a, _, _, aabb_a)) in segments.iter().enumerate() {
|
||||
for &(b, _, _, aabb_b) in segments.iter().skip(index + 1) {
|
||||
if aabb_b[0] > aabb_a[2] {
|
||||
break;
|
||||
}
|
||||
work += 1;
|
||||
if work > MAX_INDEXED_PAIRWISE_WORK {
|
||||
return false;
|
||||
}
|
||||
if a.wire == b.wire || aabb_a[3] < aabb_b[1] || aabb_a[1] > aabb_b[3] {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn indexed_segments<W: WireSource + ?Sized>(wires: &W) -> Option<Vec<IndexedSegment>> {
|
||||
let refs = wires.segments()?;
|
||||
Some(
|
||||
@@ -1706,24 +1845,6 @@ fn indexed_segments<W: WireSource + ?Sized>(wires: &W) -> Option<Vec<IndexedSegm
|
||||
)
|
||||
}
|
||||
|
||||
fn segment_aabb_distance2(segment: &IndexedSegment, point: DVec3) -> f64 {
|
||||
let dx = if point.x < segment.min_x() {
|
||||
segment.min_x() - point.x
|
||||
} else if point.x > segment.max_x() {
|
||||
point.x - segment.max_x()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let dy = if point.y < segment.min_y() {
|
||||
segment.min_y() - point.y
|
||||
} else if point.y > segment.max_y() {
|
||||
point.y - segment.max_y()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
dx * dx + dy * dy
|
||||
}
|
||||
|
||||
/// Reconstruct the absolute f64 position of wire vertex `i` from its
|
||||
/// double-single high/low pair. At UTM-scale coordinates the `points` (high)
|
||||
/// f32 alone is ~0.5 m off; adding the low residual restores f64 precision so
|
||||
|
||||
Reference in new issue
Block a user