forked from 1b-agent/opencadstudio-upstream
perf: eliminate initial hover freeze
This commit is contained in:
1 parent
47423eae18
commit
2bc4c1c8f3
7 files changed
+554
-126
No files matched your search
@@ -735,6 +735,19 @@ pub(super) struct OpenCADStudio {
|
||||
/// drawings in a file manager produces exactly that (one process per file,
|
||||
/// all arriving at once), which makes this queue load-bearing, not polish.
|
||||
pub(super) pending_opens: std::collections::VecDeque<PathBuf>,
|
||||
/// One global interaction-index build at a time. Large drawings can each
|
||||
/// hold millions of entries, so file-open bursts must not multiply peak
|
||||
/// CPU and memory by the number of tabs.
|
||||
active_interaction_index: Option<(u64, u64, usize)>,
|
||||
/// Latest resident source requested by each waiting tab. Jobs run
|
||||
/// serially behind `active_interaction_index`.
|
||||
queued_interaction_indices: std::collections::VecDeque<(
|
||||
u64,
|
||||
u64,
|
||||
usize,
|
||||
std::sync::Arc<Vec<crate::scene::WireModel>>,
|
||||
f32,
|
||||
)>,
|
||||
|
||||
// ── Unsaved-changes dialog ────────────────────────────────────────────
|
||||
/// Set when the user tries to close a tab or quit while there are unsaved changes.
|
||||
@@ -1576,6 +1589,17 @@ pub enum Message {
|
||||
/// Timer pulse while a rollover hit-test is queued; fires when the
|
||||
/// cursor has been still long enough to safely run the pick.
|
||||
HoverDwellTick,
|
||||
/// Large projected interaction index finished preparing off the UI thread.
|
||||
InteractionIndexReady {
|
||||
tab_id: u64,
|
||||
epoch: u64,
|
||||
source: usize,
|
||||
wires: std::sync::Weak<Vec<crate::scene::WireModel>>,
|
||||
index: std::sync::Arc<
|
||||
crate::scene::pick::interaction_index::InteractionIndex,
|
||||
>,
|
||||
build_ms: f64,
|
||||
},
|
||||
WindowResized(f32, f32),
|
||||
/// Enter pressed globally — finalises the active command (no text-input involvement).
|
||||
CommandFinalize,
|
||||
@@ -2461,6 +2485,8 @@ impl OpenCADStudio {
|
||||
plot_prev: None,
|
||||
opening: None,
|
||||
pending_opens: std::collections::VecDeque::new(),
|
||||
active_interaction_index: None,
|
||||
queued_interaction_indices: std::collections::VecDeque::new(),
|
||||
pending_close: None,
|
||||
active_save_jobs: std::collections::HashMap::new(),
|
||||
save_job_serial: 0,
|
||||
|
||||
+11
-1
@@ -616,7 +616,17 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.tabs[i].dirty = false;
|
||||
self.tabs[i].history = crate::app::document::HistoryState::default();
|
||||
self.refresh_selected_grips();
|
||||
Task::batch([thumbs_task, self.drain_pending_open()])
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let interaction_task = {
|
||||
let wires = self.tabs[i].scene.hit_test_wires();
|
||||
let screen_height = self.tabs[i].scene.selection.borrow().vp_size.1;
|
||||
self.prepare_interaction_index_task(i, wires, screen_height)
|
||||
.unwrap_or_else(Task::none)
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let interaction_task = Task::none();
|
||||
let pending_open_task = self.drain_pending_open();
|
||||
Task::batch([thumbs_task, pending_open_task, interaction_task])
|
||||
}
|
||||
|
||||
pub(super) fn on_wblock_save_result_some(&mut self, block_name: String, path: std::path::PathBuf) -> Task<Message> {
|
||||
|
||||
@@ -1661,6 +1661,72 @@ impl OpenCADStudio {
|
||||
|
||||
Message::HoverDwellTick => self.on_hover_dwell_tick(),
|
||||
|
||||
Message::InteractionIndexReady {
|
||||
tab_id,
|
||||
epoch,
|
||||
source,
|
||||
wires,
|
||||
index,
|
||||
build_ms,
|
||||
} => {
|
||||
if self.active_interaction_index == Some((tab_id, epoch, source)) {
|
||||
self.active_interaction_index = None;
|
||||
}
|
||||
let installed = self
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|tab| tab.id == tab_id)
|
||||
.is_some_and(|i| {
|
||||
self.tabs[i].scene.install_prepared_interaction_index(
|
||||
epoch, source, wires, index,
|
||||
)
|
||||
});
|
||||
if std::env::var_os("OCS_PERF").is_some() {
|
||||
eprintln!(
|
||||
"[perf] interaction-index-bg {:>7.1}ms installed={installed}",
|
||||
build_ms,
|
||||
);
|
||||
}
|
||||
while let Some((
|
||||
queued_tab,
|
||||
queued_epoch,
|
||||
queued_source,
|
||||
queued_wires,
|
||||
screen_height,
|
||||
)) = self.queued_interaction_indices.pop_front()
|
||||
{
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == queued_tab) else {
|
||||
continue;
|
||||
};
|
||||
let stale = self.tabs[i].scene.geometry_epoch != queued_epoch
|
||||
|| std::sync::Arc::as_ptr(&queued_wires) as usize != queued_source;
|
||||
let (wires, screen_height) = if stale {
|
||||
(
|
||||
self.tabs[i].scene.hit_test_wires(),
|
||||
self.tabs[i].scene.selection.borrow().vp_size.1,
|
||||
)
|
||||
} else {
|
||||
(queued_wires, screen_height)
|
||||
};
|
||||
if let Some(task) = self.prepare_interaction_index_task(
|
||||
i,
|
||||
wires,
|
||||
screen_height,
|
||||
) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
if self.active_interaction_index.is_none() && !self.tabs.is_empty() {
|
||||
let i = self.active_tab.min(self.tabs.len() - 1);
|
||||
let wires = self.tabs[i].scene.hit_test_wires();
|
||||
let screen_height = self.tabs[i].scene.selection.borrow().vp_size.1;
|
||||
self.prepare_interaction_index_task(i, wires, screen_height)
|
||||
.unwrap_or_else(Task::none)
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
Message::VisibilityPick(idx) => {
|
||||
if let Some(popup) = self.visibility_popup.take() {
|
||||
self.apply_visibility_state(popup.insert_handle, idx);
|
||||
|
||||
+98
-15
@@ -19,6 +19,7 @@ use acadrust::types::Color as AcadColor;
|
||||
use acadrust::{EntityType as AcadEntityType, Handle};
|
||||
use iced::time::Instant;
|
||||
use iced::{mouse, Point, Task};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Pixel radius for grabbing a UCS-icon grip (origin dot or an axis tip).
|
||||
const UCS_GRIP_HIT_PX: f32 = 9.0;
|
||||
@@ -3484,8 +3485,17 @@ impl OpenCADStudio {
|
||||
.viewport_edit_frame(canvas_sz)
|
||||
.map(|(cam, _)| cam);
|
||||
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
|
||||
if let Some(task) =
|
||||
self.prepare_interaction_index_task(i, Arc::clone(&all_wires), bounds.height)
|
||||
{
|
||||
// Keep the dwell armed. While the index is pending later timer
|
||||
// ticks return immediately; once installed, the next tick performs
|
||||
// the exact pick at the latest cursor position.
|
||||
return task;
|
||||
}
|
||||
let hover_world = self.cursor_model_point(i, &edit_cam, p, bounds);
|
||||
let hover_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
|
||||
let candidate_started = Instant::now();
|
||||
let hover_candidates = self.tabs[i].scene.interaction_hover_candidates_near(
|
||||
all_wires,
|
||||
hover_world,
|
||||
view_rot,
|
||||
@@ -3493,13 +3503,18 @@ impl OpenCADStudio {
|
||||
bounds,
|
||||
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
|
||||
);
|
||||
let candidate_ms = candidate_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let candidate_count = hover_candidates.len();
|
||||
let handles_started = Instant::now();
|
||||
let candidate_handles = self.tabs[i]
|
||||
.scene
|
||||
.interaction_candidate_handles(&hover_candidates);
|
||||
let handles_ms = handles_started.elapsed().as_secs_f64() * 1000.0;
|
||||
// Mirror the click-selection pick order so the rollover
|
||||
// highlights every selectable object: wire → hatch →
|
||||
// block-internal hatch → shaded 3D solid body.
|
||||
let hovered = scene::pick::hit_test::click_hit(
|
||||
let wire_started = Instant::now();
|
||||
let mut hovered = scene::pick::hit_test::click_hit(
|
||||
p,
|
||||
&hover_candidates,
|
||||
view_rot,
|
||||
@@ -3507,9 +3522,14 @@ impl OpenCADStudio {
|
||||
bounds,
|
||||
self.tabs[i].scene.document.header.lineweight_display,
|
||||
)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.or_else(|| {
|
||||
scene::pick::hit_test::click_hit_hatch(
|
||||
.and_then(Scene::handle_from_wire_name);
|
||||
let wire_ms = wire_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let mut hatch_ms = 0.0;
|
||||
let mut insert_ms = 0.0;
|
||||
let mut solid_ms = 0.0;
|
||||
if hovered.is_none() {
|
||||
let started = Instant::now();
|
||||
hovered = scene::pick::hit_test::click_hit_hatch(
|
||||
p,
|
||||
&self.tabs[i]
|
||||
.scene
|
||||
@@ -3518,27 +3538,35 @@ impl OpenCADStudio {
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
scene::pick::hit_test::click_hit_insert_hatch(
|
||||
);
|
||||
hatch_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
}
|
||||
if hovered.is_none() {
|
||||
let started = Instant::now();
|
||||
hovered = scene::pick::hit_test::click_hit_insert_hatch(
|
||||
p,
|
||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
self.tabs[i]
|
||||
);
|
||||
insert_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
}
|
||||
if hovered.is_none() {
|
||||
let started = Instant::now();
|
||||
hovered = self.tabs[i]
|
||||
.scene
|
||||
.solid_click_hit(p, view_rot, eye, bounds, candidate_handles.as_ref())
|
||||
});
|
||||
.solid_hover_hit(p, view_rot, eye, bounds, candidate_handles.as_ref());
|
||||
solid_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
}
|
||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||
self.hover_dwell = None;
|
||||
let hover_ms = hover_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf && hover_ms >= 5.0 {
|
||||
eprintln!(
|
||||
"[perf] hover-detail query={candidate_ms:.1} handles={handles_ms:.1} wire={wire_ms:.1} hatch={hatch_ms:.1} insert={insert_ms:.1} solid={solid_ms:.1} candidates={candidate_count}",
|
||||
);
|
||||
eprintln!(
|
||||
"[perf] hover-dwell {:>7.1}ms wires={} hit={}",
|
||||
hover_ms,
|
||||
@@ -3549,6 +3577,61 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(in crate::app) fn prepare_interaction_index_task(
|
||||
&mut self,
|
||||
i: usize,
|
||||
wires: Arc<Vec<crate::scene::WireModel>>,
|
||||
screen_height_px: f32,
|
||||
) -> Option<Task<Message>> {
|
||||
let (epoch, source) = self.tabs[i]
|
||||
.scene
|
||||
.interaction_index_build_key(&wires, screen_height_px)?;
|
||||
let tab_id = self.tabs[i].id;
|
||||
let key = (tab_id, epoch, source);
|
||||
if let Some(active) = self.active_interaction_index {
|
||||
if active != key {
|
||||
if let Some(position) = self
|
||||
.queued_interaction_indices
|
||||
.iter()
|
||||
.position(|(queued_tab, ..)| *queued_tab == tab_id)
|
||||
{
|
||||
self.queued_interaction_indices.remove(position);
|
||||
}
|
||||
self.queued_interaction_indices.push_back((
|
||||
tab_id,
|
||||
epoch,
|
||||
source,
|
||||
wires,
|
||||
screen_height_px,
|
||||
));
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.mark_interaction_index_pending(epoch, source);
|
||||
self.active_interaction_index = Some(key);
|
||||
let weak = Arc::downgrade(&wires);
|
||||
Some(Task::perform(
|
||||
async move {
|
||||
let started = std::time::Instant::now();
|
||||
let index = Arc::new(
|
||||
crate::scene::pick::interaction_index::InteractionIndex::build(&wires),
|
||||
);
|
||||
index.prepare_screen();
|
||||
Message::InteractionIndexReady {
|
||||
tab_id,
|
||||
epoch,
|
||||
source,
|
||||
wires: weak,
|
||||
index,
|
||||
build_ms: started.elapsed().as_secs_f64() * 1000.0,
|
||||
}
|
||||
},
|
||||
|message| message,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
// A BEDIT block editor locks the active space; finish it with
|
||||
|
||||
Reference in new issue
Block a user