forked from 1b-agent/opencadstudio-upstream
Cut the cost of finding what a selection box contains
A window over a whole drawing spent 407 ms gathering candidates and 259 ms testing them, of 825 ms total, at 186 468 entities. **Gathering** ran eight spatial queries because that is what hover and snapping need between them. An area selection reads five: the wires and their segments, plus the fill triangles, pick triangles and glyphs that `indexed_box_crossing_hits` uses to catch hatches and text inside the box. The three that exist only for snapping are skipped. That was worth 148 ms. The rest came from the queries themselves. `SpatialGrid::query` sorts its result twice — once over the entry indices, to remove the repeats an entry collects for spanning several cells, and once over the values. Over a box covering the drawing that is two O(n log n) passes across 4.4 M elements to remove a handful of duplicates. `query_unordered` returns the same set in cell order with the duplicates left in, and reserves the space it needs instead of growing a 17 MB vector in steps. Gathering went from 515 ms to 69 ms on a synthetic set at that size, and from 407 ms to 27-40 ms in the application. Everything an area selection does with the result already dedups and does not read the order. Click picking keeps the sorted `query`, where scan order decides which of two overlapping entities a click lands on. **Testing** projected both endpoints of every segment in the box, then threw the result away for any wire already selected. A circle or a polyline contributes one segment per tessellated span, so at 24 spans per wire that is 23 projection pairs out of 24 doing nothing. Skipping a wire once it is in took a 4.4 M-segment scan from roughly a second to 42 ms. The overlay path — active after any edit, so most of a working session — needed the same treatment, including its own merge: `extend_indexed` sorts and dedups every category, which would have put back the sorts this removes. `extend_indexed_area` keeps the dedup on `wire_indices`, and must: the window branch of `box_hit` walks it directly, so a wire left in twice would be selected twice. Tests cover the equivalence in both directions: the area query matches the full one as a set in every category an area selection reads, and the merge dedups `wire_indices` and nothing else.
This commit is contained in:
1 parent
d02ce08349
commit
d5bf919414
4 files changed
+547
-20
No files matched your search
+35
-8
@@ -7865,11 +7865,18 @@ impl Scene {
|
||||
(Arc::new(wires), base_slots, changed_slots)
|
||||
}
|
||||
|
||||
/// `area_only` skips the snap-only categories and gathers the rest without
|
||||
/// sorting; see [`InteractionIndex::query_xy_area`]. Both the overlay path
|
||||
/// and the plain indexed path honour it — the overlay runs whenever the
|
||||
/// drawing has been edited since the index was built, which is most of a
|
||||
/// working session, so a saving that stopped there would stop the first
|
||||
/// time the user drew something.
|
||||
fn indexed_interaction_candidates_xy(
|
||||
&self,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
aabb: [f64; 4],
|
||||
allow_pending_empty: bool,
|
||||
area_only: bool,
|
||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
|
||||
let perf = crate::perf::enabled();
|
||||
@@ -7888,11 +7895,20 @@ impl Scene {
|
||||
);
|
||||
let local_ms = t_local.elapsed().as_secs_f64() * 1000.0;
|
||||
let t_remap = iced::time::Instant::now();
|
||||
let mut result =
|
||||
base.query_remapped_xy(Arc::clone(&local), &base_slots, aabb);
|
||||
result.extend_indexed(
|
||||
changed_index.query_remapped_xy(local, &changed_slots, aabb),
|
||||
);
|
||||
let mut result = if area_only {
|
||||
base.query_remapped_xy_area(Arc::clone(&local), &base_slots, aabb)
|
||||
} else {
|
||||
base.query_remapped_xy(Arc::clone(&local), &base_slots, aabb)
|
||||
};
|
||||
if area_only {
|
||||
result.extend_indexed_area(
|
||||
changed_index.query_remapped_xy_area(local, &changed_slots, aabb),
|
||||
);
|
||||
} else {
|
||||
result.extend_indexed(
|
||||
changed_index.query_remapped_xy(local, &changed_slots, aabb),
|
||||
);
|
||||
}
|
||||
let remap_ms = t_remap.elapsed().as_secs_f64() * 1000.0;
|
||||
let total_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf && total_ms >= 50.0 {
|
||||
@@ -7910,7 +7926,11 @@ impl Scene {
|
||||
return result;
|
||||
}
|
||||
if let Some(index) = self.cached_interaction_index(&wires) {
|
||||
index.query_xy(wires, aabb)
|
||||
if area_only {
|
||||
index.query_xy_area(wires, aabb)
|
||||
} else {
|
||||
index.query_xy(wires, aabb)
|
||||
}
|
||||
} else if allow_pending_empty
|
||||
&& self.interaction_index_pending_key.get()
|
||||
== Some((self.geometry_epoch, Arc::as_ptr(&wires) as usize))
|
||||
@@ -8233,12 +8253,18 @@ impl Scene {
|
||||
cursor.x + radius,
|
||||
cursor.y + radius,
|
||||
];
|
||||
self.indexed_interaction_candidates_xy(wires, query, allow_pending_empty)
|
||||
self.indexed_interaction_candidates_xy(wires, query, allow_pending_empty, false)
|
||||
}
|
||||
|
||||
/// Shared rectangular broad phase for box/lasso/fence and command windows.
|
||||
/// Flat orthographic views query world XY; tilted/perspective views query
|
||||
/// projected 3D bounds using the supplied screen rectangle.
|
||||
/// Candidates for an area selection: box, crossing, fence and lasso.
|
||||
///
|
||||
/// Every caller feeds the result to the box/fence hit tests and to
|
||||
/// `interaction_candidate_handles`, none of which snap, so the flat-ortho
|
||||
/// path asks for the reduced category set. A caller that needs snapping
|
||||
/// belongs on `interaction_candidates_near` instead.
|
||||
pub fn interaction_candidates_in_aabb(
|
||||
&self,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
@@ -8275,6 +8301,7 @@ impl Scene {
|
||||
wires,
|
||||
[aabb[0] - pad, aabb[1] - pad, aabb[2] + pad, aabb[3] + pad],
|
||||
false,
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
self.indexed_interaction_candidates_screen(
|
||||
@@ -8330,7 +8357,7 @@ impl Scene {
|
||||
|
||||
pub fn interaction_handles_in_world_aabb(&self, aabb: [f64; 4]) -> HashSet<Handle> {
|
||||
let wires = self.hit_test_wires();
|
||||
let candidates = self.indexed_interaction_candidates_xy(wires, aabb, false);
|
||||
let candidates = self.indexed_interaction_candidates_xy(wires, aabb, false, false);
|
||||
let mut handles: HashSet<Handle> = candidates
|
||||
.iter()
|
||||
.filter_map(|wire| Self::handle_from_wire_name(&wire.name))
|
||||
|
||||
Reference in new issue
Block a user