Merge PR #1098: scale bulk selections

This commit is contained in:
Hakan Seven committed 2026-09-09 13:09:18 +03:00
commit bba46f1c9c
5 files changed
+424 -42

No files matched your search

+69
View File
@@ -2426,6 +2426,11 @@ impl OpenCADStudio {
}
/// Rebuild the cached selected_grips from the current entity selection.
/// Selections larger than this get no grips, matching AutoCAD's
/// `GRIPOBJLIMIT` default. The DWG header does not carry the variable, so
/// the default stands in for it.
const GRIP_OBJECT_LIMIT: usize = 100;
pub(super) fn refresh_selected_grips(&mut self) {
let i = self.active_tab;
let locked_active_grip = self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
@@ -2453,6 +2458,22 @@ impl OpenCADStudio {
.then(|| selected[0].0);
let mut grips = Vec::new();
let mut handles = Vec::new();
// Above the limit, no grips at all — the same rule AutoCAD applies
// through GRIPOBJLIMIT, and for the same reason. Every selected
// entity contributes several grips, the whole list is projected and
// rebuilt into markers on **every frame**, and a 185 096-entity
// selection put ~3.9 s in this function and then held the view at
// ~10 fps for as long as the selection stood.
//
// Downstream this reads as "this selection has no grips": the
// consumers all iterate the list, so they simply find nothing, and
// grip editing is unavailable for a selection far past the size
// where dragging one is a sensible thing to do.
let selected = if selected.len() > Self::GRIP_OBJECT_LIMIT {
Vec::new()
} else {
selected
};
for (handle, entity) in selected {
if self.tabs[i].scene.is_layer_locked(handle) {
continue;
@@ -4027,3 +4048,51 @@ mod chprop_integration_tests {
assert_eq!(app.ribbon.active_lineweight, new_lw);
}
}
#[cfg(test)]
mod grip_limit_tests {
use super::*;
/// Every selected entity contributes grips, and the whole list is projected
/// and rebuilt into markers on every frame. A 185 096-entity selection put
/// ~3.9 s in `refresh_selected_grips` and then held the view at ~10 fps for
/// as long as the selection stood, which is what AutoCAD's `GRIPOBJLIMIT`
/// exists to prevent.
#[test]
fn a_selection_past_the_limit_gets_no_grips() {
use acadrust::entities::{EntityType, Line};
use acadrust::types::Vector3;
let select_n = |n: usize| -> usize {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
let handles: Vec<_> = (0..n)
.map(|k| {
let x = k as f64;
app.tabs[i].scene.add_entity(EntityType::Line(
Line::from_points(
Vector3::new(x, 0.0, 0.0),
Vector3::new(x + 1.0, 1.0, 0.0),
),
))
})
.collect();
for handle in handles {
app.tabs[i].scene.select_entity(handle, false);
}
app.refresh_selected_grips();
app.tabs[i].selected_grips.len()
};
assert!(
select_n(OpenCADStudio::GRIP_OBJECT_LIMIT) > 0,
"a selection at the limit must still show its grips",
);
assert_eq!(
select_n(OpenCADStudio::GRIP_OBJECT_LIMIT + 1),
0,
"one past the limit must show none at all",
);
}
}
+50 -18
View File
@@ -4093,6 +4093,10 @@ impl OpenCADStudio {
aabb
},
);
// A 186 k-entity box selection sits at ~800 ms in this
// handler and two guesses about which step owns it have
// both been wrong. Split it.
let t_sel = crate::perf::enabled().then(Instant::now);
let area_candidates = self.tabs[i].scene.interaction_candidates_in_aabb(
all_wires,
world_aabb,
@@ -4101,6 +4105,9 @@ impl OpenCADStudio {
eye,
bounds,
);
let cand_ms = t_sel
.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_hit = crate::perf::enabled().then(Instant::now);
let candidate_handles = self.tabs[i]
.scene
.interaction_candidate_handles(&area_candidates);
@@ -4161,20 +4168,30 @@ impl OpenCADStudio {
// selection, Shift+box removes the boxed
// entities. Esc / empty-space click still clears.
// PICKADD 0 (#226): a plain box REPLACES.
let hit_ms = t_hit
.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_apply = crate::perf::enabled().then(Instant::now);
if self.shift_down || self.select_remove_mode {
for h in &handles {
self.tabs[i].scene.deselect_entity(*h);
}
self.tabs[i].scene.deselect_entities(&handles);
} else {
if !selection_pick_add && !handles.is_empty() {
self.tabs[i].scene.deselect_all();
}
for h in &handles {
self.tabs[i].scene.select_entity(*h, false);
}
self.tabs[i].scene.select_entities(&handles);
self.tabs[i].scene.expand_selection_for_groups(&handles);
}
let apply_ms = t_apply
.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_props = crate::perf::enabled().then(Instant::now);
self.refresh_properties();
if crate::perf::enabled() {
crate::perf_record!(
"[perf] select-commit kind=drag-box candidates={cand_ms:.1}ms \
hit={hit_ms:.1}ms apply={apply_ms:.1}ms properties={:.1}ms picked={}",
t_props.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0),
handles.len(),
);
}
selection_just_completed = true;
}
} else {
@@ -4208,9 +4225,7 @@ impl OpenCADStudio {
// leaves the current selection untouched so a stray
// drag never discards hard-won picks.
if self.shift_down || self.select_remove_mode {
for h in &handles {
self.tabs[i].scene.deselect_entity(*h);
}
self.tabs[i].scene.deselect_entities(&handles);
} else {
// PICKADD 0 (#226): a plain marquee REPLACES
// the selection (empty results still leave it
@@ -4218,9 +4233,7 @@ impl OpenCADStudio {
if !selection_pick_add && !handles.is_empty() {
self.tabs[i].scene.deselect_all();
}
for h in &handles {
self.tabs[i].scene.select_entity(*h, false);
}
self.tabs[i].scene.select_entities(&handles);
self.tabs[i].scene.expand_selection_for_groups(&handles);
}
self.refresh_properties();
@@ -4454,6 +4467,10 @@ impl OpenCADStudio {
aabb
},
);
// This is the path a click-move-click window takes, as
// opposed to a press-drag; it is the one a large selection
// actually goes through.
let t_sel = crate::perf::enabled().then(Instant::now);
let area_candidates = self.tabs[i].scene.interaction_candidates_in_aabb(
all_wires,
world_aabb,
@@ -4462,6 +4479,9 @@ impl OpenCADStudio {
eye,
bounds,
);
let cand_ms =
t_sel.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_hit = crate::perf::enabled().then(Instant::now);
let candidate_handles = self.tabs[i]
.scene
.interaction_candidate_handles(&area_candidates);
@@ -4517,27 +4537,39 @@ impl OpenCADStudio {
bounds,
candidate_handles.as_ref(),
));
let hit_ms = t_hit.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_filter = crate::perf::enabled().then(Instant::now);
// Selection filter: keep only allowed types.
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
let filter_ms =
t_filter.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_apply = crate::perf::enabled().then(Instant::now);
// Accumulate (issue #83): a plain box adds to the
// current selection, Shift+box removes the boxed
// entities. An empty box leaves the selection alone
// so an accidental empty drag never discards it.
// PICKADD 0 (#226): a plain box REPLACES instead.
if self.shift_down || self.select_remove_mode {
for h in &handles {
self.tabs[i].scene.deselect_entity(*h);
}
self.tabs[i].scene.deselect_entities(&handles);
} else {
if !selection_pick_add && !handles.is_empty() {
self.tabs[i].scene.deselect_all();
}
for h in &handles {
self.tabs[i].scene.select_entity(*h, false);
}
self.tabs[i].scene.select_entities(&handles);
self.tabs[i].scene.expand_selection_for_groups(&handles);
}
let apply_ms =
t_apply.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
let t_props = crate::perf::enabled().then(Instant::now);
self.refresh_properties();
if crate::perf::enabled() {
crate::perf_record!(
"[perf] select-commit kind=window candidates={cand_ms:.1}ms \
hit={hit_ms:.1}ms filter={filter_ms:.1}ms apply={apply_ms:.1}ms properties={:.1}ms picked={}",
t_props.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0),
handles.len(),
);
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.box_last = Some((a, p));
sel.box_last_crossing = crossing;