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
|
||||
|
||||
+242
-100
@@ -1032,6 +1032,10 @@ pub struct Scene {
|
||||
Arc<crate::scene::pick::interaction_index::InteractionIndex>,
|
||||
)>,
|
||||
>,
|
||||
/// Latest large source requested for off-thread preparation. A matching
|
||||
/// cache miss returns no candidates temporarily instead of scanning the
|
||||
/// entire scene on the UI thread.
|
||||
interaction_index_pending_key: std::cell::Cell<Option<(u64, usize)>>,
|
||||
/// Large resident interaction index kept as an immutable base across small
|
||||
/// entity edits. Geometry-journal handles form a tombstone/delta overlay;
|
||||
/// the exact index is rebuilt only after the overlay grows too large.
|
||||
@@ -1063,9 +1067,6 @@ pub struct Scene {
|
||||
Arc<crate::scene::pick::interaction_index::InteractionHandleIndex>,
|
||||
)>,
|
||||
>,
|
||||
/// Estimated hatch/mesh/Insert interaction work, cached per geometry epoch
|
||||
/// so the index threshold itself stays O(1) on pointer-move events.
|
||||
interaction_aux_work_cache: std::cell::Cell<Option<(u64, u64, usize)>>,
|
||||
/// Index built from every SortEntitiesTable in the document.
|
||||
/// Maps block_handle → (entity_handle.value() → sort_handle.value()).
|
||||
/// Replaces the O(objects) linear scan inside `wires_for_block()` with an O(1) lookup.
|
||||
@@ -1357,10 +1358,10 @@ impl Scene {
|
||||
selection_generation: 0,
|
||||
wire_cache: RefCell::new(None),
|
||||
interaction_index_cache: RefCell::new(Vec::new()),
|
||||
interaction_index_pending_key: std::cell::Cell::new(None),
|
||||
interaction_base_index_cache: RefCell::new(None),
|
||||
interaction_overlay_index_cache: RefCell::new(None),
|
||||
interaction_handle_index_cache: RefCell::new(None),
|
||||
interaction_aux_work_cache: std::cell::Cell::new(None),
|
||||
sort_cache: RefCell::new(None),
|
||||
draw_depth_cache: RefCell::new(None),
|
||||
hatch_cache: RefCell::new(None),
|
||||
@@ -4791,107 +4792,137 @@ impl Scene {
|
||||
const INTERACTION_INDEX_MIN_WORK: usize = 20_000;
|
||||
|
||||
fn interaction_index_worthwhile(&self, wires: &[WireModel]) -> bool {
|
||||
if wires.len() >= Self::INTERACTION_INDEX_MIN_WIRES
|
||||
wires.len() >= Self::INTERACTION_INDEX_MIN_WIRES
|
||||
|| crate::scene::pick::interaction_index::InteractionIndex::estimated_work(wires)
|
||||
>= Self::INTERACTION_INDEX_MIN_WORK
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let space_key = self.interaction_space_key();
|
||||
let aux_work = self
|
||||
.interaction_aux_work_cache
|
||||
.get()
|
||||
.filter(|(epoch, space, _)| *epoch == self.geometry_epoch && *space == space_key)
|
||||
.map(|(_, _, work)| work)
|
||||
.unwrap_or_else(|| {
|
||||
let hatch_work = self.hatches.iter().fold(0usize, |total, (&handle, hatch)| {
|
||||
if self.hatch_visible_for_interaction(handle) {
|
||||
total.saturating_add(hatch.boundary.len())
|
||||
} else {
|
||||
total
|
||||
}
|
||||
});
|
||||
let insert_work = self
|
||||
.insert_hatches_for_click()
|
||||
.values()
|
||||
.flatten()
|
||||
.fold(0usize, |total, hatch| {
|
||||
total.saturating_add(hatch.boundary.len())
|
||||
});
|
||||
let mesh_work = self
|
||||
.interaction_meshes_arc()
|
||||
.iter()
|
||||
.fold(0usize, |total, set| {
|
||||
total.saturating_add(set.lods.first().map_or(0, |mesh| mesh.verts.len()))
|
||||
});
|
||||
let work = hatch_work
|
||||
.saturating_add(mesh_work)
|
||||
.saturating_add(insert_work);
|
||||
self.interaction_aux_work_cache
|
||||
.set(Some((self.geometry_epoch, space_key, work)));
|
||||
work
|
||||
});
|
||||
aux_work >= Self::INTERACTION_INDEX_MIN_WORK
|
||||
|| self.hatches.len() >= Self::INTERACTION_INDEX_MIN_WIRES
|
||||
|| self.meshes.len().saturating_add(self.block_meshes.len())
|
||||
>= Self::INTERACTION_INDEX_MIN_WIRES
|
||||
|| self
|
||||
.hatches
|
||||
.values()
|
||||
.any(|hatch| hatch.boundary.len() >= Self::INTERACTION_INDEX_MIN_WORK)
|
||||
|| self
|
||||
.meshes
|
||||
.values()
|
||||
.chain(self.block_meshes.values())
|
||||
.any(|set| {
|
||||
set.lods
|
||||
.first()
|
||||
.is_some_and(|mesh| mesh.verts.len() >= Self::INTERACTION_INDEX_MIN_WORK)
|
||||
})
|
||||
}
|
||||
|
||||
fn interaction_index(
|
||||
fn cached_interaction_index(
|
||||
&self,
|
||||
wires: &Arc<Vec<WireModel>>,
|
||||
) -> Arc<crate::scene::pick::interaction_index::InteractionIndex> {
|
||||
const MAX_CACHED_SOURCES: usize = 4;
|
||||
) -> Option<Arc<crate::scene::pick::interaction_index::InteractionIndex>> {
|
||||
let source = Arc::as_ptr(wires) as usize;
|
||||
{
|
||||
let mut cache = self.interaction_index_cache.borrow_mut();
|
||||
cache.retain(|(epoch, _, weak, _)| {
|
||||
*epoch == self.geometry_epoch && weak.strong_count() > 0
|
||||
});
|
||||
if let Some(position) = cache.iter().position(|(_, ptr, weak, _)| {
|
||||
*ptr == source
|
||||
&& weak
|
||||
.upgrade()
|
||||
.is_some_and(|cached| Arc::ptr_eq(&cached, wires))
|
||||
}) {
|
||||
let entry = cache.remove(position);
|
||||
let index = Arc::clone(&entry.3);
|
||||
cache.push(entry);
|
||||
if self.current_layout == "Model" {
|
||||
*self.interaction_base_index_cache.borrow_mut() = Some((
|
||||
self.geometry_epoch,
|
||||
self.interaction_space_key(),
|
||||
Arc::clone(&index),
|
||||
));
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
let started = iced::time::Instant::now();
|
||||
let index = Arc::new(crate::scene::pick::interaction_index::InteractionIndex::build(wires));
|
||||
let build_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
if std::env::var_os("OCS_PERF").is_some() && build_ms >= 50.0 {
|
||||
eprintln!(
|
||||
"[perf] interaction-index {:>7.1}ms wires={}",
|
||||
build_ms,
|
||||
wires.len(),
|
||||
);
|
||||
}
|
||||
let mut cache = self.interaction_index_cache.borrow_mut();
|
||||
cache.retain(|(epoch, _, weak, _)| {
|
||||
*epoch == self.geometry_epoch && weak.strong_count() > 0
|
||||
});
|
||||
if let Some(position) = cache.iter().position(|(_, ptr, weak, _)| {
|
||||
*ptr == source
|
||||
&& weak
|
||||
.upgrade()
|
||||
.is_some_and(|cached| Arc::ptr_eq(&cached, wires))
|
||||
}) {
|
||||
let entry = cache.remove(position);
|
||||
let index = Arc::clone(&entry.3);
|
||||
cache.push(entry);
|
||||
if self.current_layout == "Model" {
|
||||
*self.interaction_base_index_cache.borrow_mut() = Some((
|
||||
self.geometry_epoch,
|
||||
self.interaction_space_key(),
|
||||
Arc::clone(&index),
|
||||
));
|
||||
}
|
||||
return Some(index);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn cache_interaction_index(
|
||||
&self,
|
||||
epoch: u64,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
index: Arc<crate::scene::pick::interaction_index::InteractionIndex>,
|
||||
) {
|
||||
const MAX_CACHED_SOURCES: usize = 4;
|
||||
let source = Arc::as_ptr(&wires) as usize;
|
||||
let mut cache = self.interaction_index_cache.borrow_mut();
|
||||
cache.retain(|(cached_epoch, ptr, weak, _)| {
|
||||
*cached_epoch == self.geometry_epoch
|
||||
&& weak.strong_count() > 0
|
||||
&& !(*cached_epoch == epoch && *ptr == source)
|
||||
});
|
||||
cache.push((
|
||||
self.geometry_epoch,
|
||||
epoch,
|
||||
source,
|
||||
Arc::downgrade(wires),
|
||||
Arc::downgrade(&wires),
|
||||
Arc::clone(&index),
|
||||
));
|
||||
if cache.len() > MAX_CACHED_SOURCES {
|
||||
cache.remove(0);
|
||||
}
|
||||
if self.current_layout == "Model" {
|
||||
if epoch == self.geometry_epoch && self.current_layout == "Model" {
|
||||
*self.interaction_base_index_cache.borrow_mut() = Some((
|
||||
self.geometry_epoch,
|
||||
epoch,
|
||||
self.interaction_space_key(),
|
||||
Arc::clone(&index),
|
||||
index,
|
||||
));
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
/// Return a stable build key when this source needs its large
|
||||
/// interaction index. The app uses it to deduplicate background builds.
|
||||
pub fn interaction_index_build_key(
|
||||
&self,
|
||||
wires: &Arc<Vec<WireModel>>,
|
||||
screen_height_px: f32,
|
||||
) -> Option<(u64, usize)> {
|
||||
let key = (self.geometry_epoch, Arc::as_ptr(wires) as usize);
|
||||
if !self.interaction_source_is_resident(wires, screen_height_px.max(1.0))
|
||||
|| !self.interaction_index_worthwhile(wires)
|
||||
|| self.interaction_overlay_base().is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if self.cached_interaction_index(wires).is_some() {
|
||||
if self.interaction_index_pending_key.get() == Some(key) {
|
||||
self.interaction_index_pending_key.set(None);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Some(key)
|
||||
}
|
||||
|
||||
pub fn mark_interaction_index_pending(&self, epoch: u64, source: usize) {
|
||||
self.interaction_index_pending_key
|
||||
.set(Some((epoch, source)));
|
||||
}
|
||||
|
||||
/// Install a fully prepared background index only if its geometry/source
|
||||
/// still belongs to this scene.
|
||||
pub fn install_prepared_interaction_index(
|
||||
&self,
|
||||
epoch: u64,
|
||||
source: usize,
|
||||
wires: std::sync::Weak<Vec<WireModel>>,
|
||||
index: Arc<crate::scene::pick::interaction_index::InteractionIndex>,
|
||||
) -> bool {
|
||||
if self.interaction_index_pending_key.get() == Some((epoch, source)) {
|
||||
self.interaction_index_pending_key.set(None);
|
||||
}
|
||||
let Some(wires) = wires.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
if epoch != self.geometry_epoch || Arc::as_ptr(&wires) as usize != source {
|
||||
return false;
|
||||
}
|
||||
self.cache_interaction_index(epoch, wires, index);
|
||||
true
|
||||
}
|
||||
|
||||
const INTERACTION_OVERLAY_MAX_HANDLES: usize = 2_048;
|
||||
@@ -5018,6 +5049,7 @@ impl Scene {
|
||||
&self,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
aabb: [f64; 4],
|
||||
allow_pending_empty: bool,
|
||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
@@ -5057,7 +5089,16 @@ impl Scene {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
self.interaction_index(&wires).query_xy(wires, aabb)
|
||||
if let Some(index) = self.cached_interaction_index(&wires) {
|
||||
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))
|
||||
{
|
||||
crate::scene::pick::interaction_index::InteractionCandidates::pending(wires)
|
||||
} else {
|
||||
crate::scene::pick::interaction_index::InteractionCandidates::all(wires)
|
||||
}
|
||||
}
|
||||
|
||||
fn indexed_interaction_candidates_screen(
|
||||
@@ -5067,6 +5108,7 @@ impl Scene {
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
allow_pending_empty: bool,
|
||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
@@ -5117,8 +5159,16 @@ impl Scene {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
self.interaction_index(&wires)
|
||||
.query_screen(wires, screen_rect, view_rot, eye, bounds)
|
||||
if let Some(index) = self.cached_interaction_index(&wires) {
|
||||
index.query_screen(wires, screen_rect, view_rot, eye, bounds)
|
||||
} else if allow_pending_empty
|
||||
&& self.interaction_index_pending_key.get()
|
||||
== Some((self.geometry_epoch, Arc::as_ptr(&wires) as usize))
|
||||
{
|
||||
crate::scene::pick::interaction_index::InteractionCandidates::pending(wires)
|
||||
} else {
|
||||
crate::scene::pick::interaction_index::InteractionCandidates::all(wires)
|
||||
}
|
||||
}
|
||||
|
||||
fn indexed_interaction_pick_radius(
|
||||
@@ -5131,7 +5181,8 @@ impl Scene {
|
||||
self.interaction_overlay_changed_index(base_epoch, &changes);
|
||||
base.pick_radius_px(changed.pick_radius_px(base_radius_px))
|
||||
} else {
|
||||
self.interaction_index(wires).pick_radius_px(base_radius_px)
|
||||
self.cached_interaction_index(wires)
|
||||
.map_or(base_radius_px, |index| index.pick_radius_px(base_radius_px))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5141,7 +5192,7 @@ impl Scene {
|
||||
screen_height_px: f32,
|
||||
) -> bool {
|
||||
if self.current_layout == "Model" {
|
||||
return true;
|
||||
return Arc::ptr_eq(&self.entity_wires_arc(), wires);
|
||||
}
|
||||
match self.active_viewport {
|
||||
Some(viewport) => {
|
||||
@@ -5237,7 +5288,7 @@ impl Scene {
|
||||
radius_px: f32,
|
||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||
self.interaction_candidates_near_impl(
|
||||
wires, cursor, view_rot, eye, bounds, radius_px, false,
|
||||
wires, cursor, view_rot, eye, bounds, radius_px, false, false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5258,6 +5309,28 @@ impl Scene {
|
||||
bounds,
|
||||
radius_px,
|
||||
self.document.header.lineweight_display,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn interaction_hover_candidates_near(
|
||||
&self,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
cursor: glam::DVec3,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
radius_px: f32,
|
||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||
self.interaction_candidates_near_impl(
|
||||
wires,
|
||||
cursor,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
radius_px,
|
||||
self.document.header.lineweight_display,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5270,6 +5343,7 @@ impl Scene {
|
||||
bounds: iced::Rectangle,
|
||||
radius_px: f32,
|
||||
include_line_weight: bool,
|
||||
allow_pending_empty: bool,
|
||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||
if !self.interaction_source_is_resident(&wires, bounds.height)
|
||||
|| !self.interaction_index_worthwhile(&wires)
|
||||
@@ -5306,6 +5380,7 @@ impl Scene {
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
allow_pending_empty,
|
||||
);
|
||||
}
|
||||
let world_x_px = ((view_rot.x_axis.x * bounds.width * 0.5).powi(2)
|
||||
@@ -5325,7 +5400,7 @@ impl Scene {
|
||||
cursor.x + radius,
|
||||
cursor.y + radius,
|
||||
];
|
||||
self.indexed_interaction_candidates_xy(wires, query)
|
||||
self.indexed_interaction_candidates_xy(wires, query, allow_pending_empty)
|
||||
}
|
||||
|
||||
/// Shared rectangular broad phase for box/lasso/fence and command windows.
|
||||
@@ -5349,9 +5424,16 @@ impl Scene {
|
||||
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires);
|
||||
}
|
||||
if flat_ortho {
|
||||
self.indexed_interaction_candidates_xy(wires, aabb)
|
||||
self.indexed_interaction_candidates_xy(wires, aabb, false)
|
||||
} else {
|
||||
self.indexed_interaction_candidates_screen(wires, screen_rect, view_rot, eye, bounds)
|
||||
self.indexed_interaction_candidates_screen(
|
||||
wires,
|
||||
screen_rect,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5363,6 +5445,12 @@ impl Scene {
|
||||
.iter()
|
||||
.filter_map(|wire| Self::handle_from_wire_name(&wire.name))
|
||||
.collect();
|
||||
if candidates.is_indexed()
|
||||
&& candidates.query_aabb().is_none()
|
||||
&& candidates.screen_query().is_none()
|
||||
{
|
||||
return Some(handles);
|
||||
}
|
||||
if let Some(aabb) = candidates.query_aabb() {
|
||||
let index = self.entity_index();
|
||||
handles.extend(index.tree.query_rect(aabb));
|
||||
@@ -5386,7 +5474,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);
|
||||
let candidates = self.indexed_interaction_candidates_xy(wires, aabb, false);
|
||||
let mut handles: HashSet<Handle> = candidates
|
||||
.iter()
|
||||
.filter_map(|wire| Self::handle_from_wire_name(&wire.name))
|
||||
@@ -5543,6 +5631,46 @@ impl Scene {
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
candidate_handles: Option<&HashSet<Handle>>,
|
||||
) -> Option<Handle> {
|
||||
self.solid_hit(
|
||||
cursor,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Hover may use the coarsest cached solid LOD. Click selection keeps the
|
||||
/// full-resolution mesh, while rollover only needs a stable parent handle
|
||||
/// and must stay within an interactive frame budget.
|
||||
pub fn solid_hover_hit(
|
||||
&self,
|
||||
cursor: iced::Point,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
candidate_handles: Option<&HashSet<Handle>>,
|
||||
) -> Option<Handle> {
|
||||
self.solid_hit(
|
||||
cursor,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
fn solid_hit(
|
||||
&self,
|
||||
cursor: iced::Point,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
candidate_handles: Option<&HashSet<Handle>>,
|
||||
coarse_lod: bool,
|
||||
) -> Option<Handle> {
|
||||
// Reuse the renderer's expanded mesh set (top-level solids + per-INSERT
|
||||
// block instances), cached per geometry epoch — so hover no longer
|
||||
@@ -5581,10 +5709,24 @@ impl Scene {
|
||||
.then_some((handle, set))
|
||||
}));
|
||||
}
|
||||
let candidates = sets
|
||||
.into_iter()
|
||||
.filter_map(|(handle, set)| set.lods.first().map(|mesh| (handle, mesh)));
|
||||
pick::hit_test::mesh_click_hit(cursor, candidates, view_rot, eye, bounds)
|
||||
if coarse_lod {
|
||||
return pick::hit_test::mesh_click_hit(
|
||||
cursor,
|
||||
sets.iter()
|
||||
.filter_map(|(handle, set)| set.lods.last().map(|mesh| (*handle, mesh))),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
}
|
||||
pick::hit_test::mesh_click_hit(
|
||||
cursor,
|
||||
sets.iter()
|
||||
.filter_map(|(handle, set)| set.lods.first().map(|mesh| (*handle, mesh))),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
}
|
||||
|
||||
/// Parent INSERT handles whose block-internal solid meshes fall in a
|
||||
|
||||
@@ -671,6 +671,24 @@ pub fn mesh_click_hit<'a>(
|
||||
let v = &mesh.verts;
|
||||
let idx = &mesh.indices;
|
||||
let lo = &mesh.verts_low;
|
||||
// Indexed meshes reuse most vertices across several triangles.
|
||||
// Project once per vertex instead of three matrix transforms per
|
||||
// triangle; dense solid misses are otherwise the worst-case hover.
|
||||
let projected: Vec<(Point, f32)> = v
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &vertex)| {
|
||||
let ndc =
|
||||
view_rot.project_point3((mesh_vert(vertex, lo, i) - eye).as_vec3());
|
||||
(
|
||||
Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
),
|
||||
ndc.z,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut t = 0;
|
||||
while t + 2 < idx.len() {
|
||||
let tri = [idx[t] as usize, idx[t + 1] as usize, idx[t + 2] as usize];
|
||||
@@ -678,12 +696,9 @@ pub fn mesh_click_hit<'a>(
|
||||
let mut sp = [Point::ORIGIN; 3];
|
||||
let mut depth = 0.0f32;
|
||||
for (j, &k) in tri.iter().enumerate() {
|
||||
let ndc = view_rot.project_point3((mesh_vert(v[k], lo, k) - eye).as_vec3());
|
||||
sp[j] = Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
);
|
||||
depth += ndc.z;
|
||||
let (point, z) = projected[k];
|
||||
sp[j] = point;
|
||||
depth += z;
|
||||
}
|
||||
if point_in_polygon(cursor, &sp) {
|
||||
let d = depth / 3.0;
|
||||
|
||||
@@ -140,12 +140,12 @@ fn build_bvh2_node<T>(
|
||||
return node_idx;
|
||||
}
|
||||
let axis = usize::from(bounds[3] - bounds[1] > bounds[2] - bounds[0]);
|
||||
order.sort_unstable_by(|&a, &b| {
|
||||
let mid = order.len() / 2;
|
||||
order.select_nth_unstable_by(mid, |&a, &b| {
|
||||
let aa = entry_aabb2(&entries[a as usize]);
|
||||
let bb = entry_aabb2(&entries[b as usize]);
|
||||
(aa[axis] + aa[axis + 2]).total_cmp(&(bb[axis] + bb[axis + 2]))
|
||||
});
|
||||
let mid = order.len() / 2;
|
||||
let (left_order, right_order) = order.split_at_mut(mid);
|
||||
let left = build_bvh2_node(entries, left_order, base, nodes);
|
||||
let right = build_bvh2_node(entries, right_order, base + mid, nodes);
|
||||
@@ -253,12 +253,12 @@ fn build_bvh3_node<T>(
|
||||
axis = candidate;
|
||||
}
|
||||
}
|
||||
order.sort_unstable_by(|&a, &b| {
|
||||
let mid = order.len() / 2;
|
||||
order.select_nth_unstable_by(mid, |&a, &b| {
|
||||
let aa = entries[a as usize].aabb;
|
||||
let bb = entries[b as usize].aabb;
|
||||
(aa[axis] + aa[axis + 3]).total_cmp(&(bb[axis] + bb[axis + 3]))
|
||||
});
|
||||
let mid = order.len() / 2;
|
||||
let (left_order, right_order) = order.split_at_mut(mid);
|
||||
let left = build_bvh3_node(entries, left_order, base, nodes);
|
||||
let right = build_bvh3_node(entries, right_order, base + mid, nodes);
|
||||
@@ -520,6 +520,11 @@ impl<T: Copy + Ord + Sync> SpatialSet<T> {
|
||||
self.xy.query(&self.entries, aabb)
|
||||
}
|
||||
|
||||
fn prepare_screen(&self) {
|
||||
self.xyz
|
||||
.get_or_init(|| SpatialBvh3::build(&self.entries));
|
||||
}
|
||||
|
||||
fn query_screen(
|
||||
&self,
|
||||
screen_rect: [f32; 4],
|
||||
@@ -951,6 +956,52 @@ impl InteractionIndex {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build every projected 3D broad phase before the index reaches the UI
|
||||
/// thread. Perspective/orbit hover must never pay this one-time cost.
|
||||
pub fn prepare_screen(&self) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
rayon::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
|| rayon::join(|| self.wires.prepare_screen(), || self.segments.prepare_screen()),
|
||||
|| {
|
||||
rayon::join(
|
||||
|| self.snap_points.prepare_screen(),
|
||||
|| self.key_vertices.prepare_screen(),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
|| self.key_segments.prepare_screen(),
|
||||
|| self.fill_triangles.prepare_screen(),
|
||||
)
|
||||
},
|
||||
|| {
|
||||
rayon::join(
|
||||
|| self.pick_triangles.prepare_screen(),
|
||||
|| self.glyphs.prepare_screen(),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.wires.prepare_screen();
|
||||
self.segments.prepare_screen();
|
||||
self.snap_points.prepare_screen();
|
||||
self.key_vertices.prepare_screen();
|
||||
self.key_segments.prepare_screen();
|
||||
self.fill_triangles.prepare_screen();
|
||||
self.pick_triangles.prepare_screen();
|
||||
self.glyphs.prepare_screen();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pick_radius_px(&self, base_radius_px: f32) -> f32 {
|
||||
base_radius_px.max(self.max_line_half_width_px)
|
||||
}
|
||||
@@ -1241,6 +1292,17 @@ impl InteractionIndex {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for InteractionIndex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("InteractionIndex")
|
||||
.field("wires", &self.wire_handles.len())
|
||||
.field("segments", &self.segments.entries.len())
|
||||
.field("fill_triangles", &self.fill_triangles.entries.len())
|
||||
.field("pick_triangles", &self.pick_triangles.entries.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InteractionCandidates {
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
wire_indices: Option<Vec<u32>>,
|
||||
@@ -1302,6 +1364,30 @@ impl InteractionCandidates {
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty but explicitly indexed candidate set. Used while a large source
|
||||
/// is still being prepared off-thread so UI input never falls back to a
|
||||
/// full-scene scan.
|
||||
pub fn pending(wires: Arc<Vec<WireModel>>) -> Self {
|
||||
Self {
|
||||
wires,
|
||||
wire_indices: Some(Vec::new()),
|
||||
segments: Some(Vec::new()),
|
||||
snap_points: Some(Vec::new()),
|
||||
key_vertices: Some(Vec::new()),
|
||||
key_segments: Some(Vec::new()),
|
||||
fill_triangles: Some(Vec::new()),
|
||||
pick_triangles: Some(Vec::new()),
|
||||
glyphs: Some(Vec::new()),
|
||||
query_aabb: None,
|
||||
screen_rect: None,
|
||||
screen_view: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_indexed(&self) -> bool {
|
||||
self.wire_indices.is_some()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> WireIter<'_> {
|
||||
match &self.wire_indices {
|
||||
Some(indices) => WireIter::Indexed {
|
||||
|
||||
Reference in new issue
Block a user