forked from 1b-agent/opencadstudio-upstream
Merge PR #1098: scale bulk selections
This commit is contained in:
commit
bba46f1c9c
5 files changed
+424
-42
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user