forked from 1b-agent/opencadstudio-upstream
fix(hover): defer rollover pick until the cursor stops
The rollover hit-test now sweeps wires, hatches, block-internal hatches and shaded solid bodies, which is O(N) per cursor frame and was freezing the crosshair on large drawings. Each idle move now clears the live highlight and arms a dwell timer; HoverDwellTick runs the pick once the cursor has been still for HOVER_DWELL_MS (120 ms), using the active tile's bounds so the deferred pick matches the entity actually under the cursor in tiled viewports. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
f860fb39c2
commit
082018756e
3 files changed
+105
-35
No files matched your search
@@ -48,6 +48,23 @@ pub struct GripHover {
|
|||||||
pub started: iced::time::Instant,
|
pub started: iced::time::Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cursor dwell awaiting a rollover hit-test. Refreshed on every idle
|
||||||
|
/// move; `HoverDwellTick` runs the pick once `last_move_at.elapsed()`
|
||||||
|
/// crosses `HOVER_DWELL_MS`. `point` and `tile_size` are tile-local so
|
||||||
|
/// the deferred pick uses the same projection the move handler would
|
||||||
|
/// have — picking with the full canvas bounds in a tiled layout matches
|
||||||
|
/// the wrong entity under the cursor.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct HoverDwell {
|
||||||
|
pub last_move_at: iced::time::Instant,
|
||||||
|
pub point: iced::Point,
|
||||||
|
pub tile_size: (f32, f32),
|
||||||
|
pub tab: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How long the cursor must sit still before the idle rollover pick runs.
|
||||||
|
pub const HOVER_DWELL_MS: u128 = 120;
|
||||||
|
|
||||||
/// Open multi-functional-grip popup state.
|
/// Open multi-functional-grip popup state.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct GripPopup {
|
pub struct GripPopup {
|
||||||
@@ -259,6 +276,12 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// updates only the overlay rather than re-tessellating the whole model.
|
/// updates only the overlay rather than re-tessellating the whole model.
|
||||||
/// Committed (un-hidden + one re-tess) when the drag ends. `None` = idle.
|
/// Committed (un-hidden + one re-tess) when the drag ends. `None` = idle.
|
||||||
grip_preview_handle: Option<acadrust::Handle>,
|
grip_preview_handle: Option<acadrust::Handle>,
|
||||||
|
/// Pending rollover hit-test. Each idle cursor move stashes
|
||||||
|
/// `(last_move_at, point, tab)` here and clears the live highlight;
|
||||||
|
/// `HoverDwellTick` runs the pick once the cursor has been still for
|
||||||
|
/// `HOVER_DWELL_MS`. Skipping the pick mid-stroke avoids the per-frame
|
||||||
|
/// O(N) wire+hatch+mesh sweep that froze the cursor on large drawings.
|
||||||
|
hover_dwell: Option<HoverDwell>,
|
||||||
/// Snapshot of the edited entity taken at the start of a grip drag, used to
|
/// Snapshot of the edited entity taken at the start of a grip drag, used to
|
||||||
/// restore it if the user presses Esc to cancel the drag. The drag mutates
|
/// restore it if the user presses Esc to cancel the drag. The drag mutates
|
||||||
/// the document live (so grips / properties track), so cancel reverts from
|
/// the document live (so grips / properties track), so cancel reverts from
|
||||||
@@ -943,6 +966,9 @@ pub enum Message {
|
|||||||
/// Timer pulse while the cursor is dwelling on a grip; drives the
|
/// Timer pulse while the cursor is dwelling on a grip; drives the
|
||||||
/// dwell-to-popup transition without requiring further mouse motion.
|
/// dwell-to-popup transition without requiring further mouse motion.
|
||||||
GripDwellTick,
|
GripDwellTick,
|
||||||
|
/// Timer pulse while a rollover hit-test is queued; fires when the
|
||||||
|
/// cursor has been still long enough to safely run the pick.
|
||||||
|
HoverDwellTick,
|
||||||
WindowResized(f32, f32),
|
WindowResized(f32, f32),
|
||||||
/// Enter pressed globally — finalises the active command (no text-input involvement).
|
/// Enter pressed globally — finalises the active command (no text-input involvement).
|
||||||
CommandFinalize,
|
CommandFinalize,
|
||||||
@@ -1584,6 +1610,7 @@ impl OpenCADStudio {
|
|||||||
visibility_popup: None,
|
visibility_popup: None,
|
||||||
grip_add_provisional: None,
|
grip_add_provisional: None,
|
||||||
grip_preview_handle: None,
|
grip_preview_handle: None,
|
||||||
|
hover_dwell: None,
|
||||||
grip_original: None,
|
grip_original: None,
|
||||||
qselect: None,
|
qselect: None,
|
||||||
show_ucs_icon: true,
|
show_ucs_icon: true,
|
||||||
|
|||||||
+69
-35
@@ -2709,45 +2709,25 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].last_cursor_world = world;
|
self.tabs[i].last_cursor_world = world;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rollover highlight: when idle (no active command, no drag),
|
// Rollover highlight: when idle (no active command, no
|
||||||
// highlight the entity under the cursor. Decoupling selection
|
// drag), defer the pick until the cursor stops. The full
|
||||||
// from tessellation makes this cheap — it only refreshes the GPU
|
// pick (wires + hatches + block hatches + shaded meshes) is
|
||||||
// xray overlay via `bump_selection`, never re-tessellates.
|
// O(N) per frame and stalls the cursor on large drawings,
|
||||||
|
// so each move clears the current highlight and resets the
|
||||||
|
// dwell timer — `HoverDwellTick` runs the hit-test only
|
||||||
|
// once the cursor has been still for `HOVER_DWELL_MS`.
|
||||||
if !dragging && self.tabs[i].active_cmd.is_none() {
|
if !dragging && self.tabs[i].active_cmd.is_none() {
|
||||||
let bounds = iced::Rectangle {
|
self.tabs[i].scene.set_hover_highlight(None);
|
||||||
x: 0.0,
|
self.hover_dwell = Some(super::HoverDwell {
|
||||||
y: 0.0,
|
last_move_at: Instant::now(),
|
||||||
width: vp_size.0,
|
point: p,
|
||||||
height: vp_size.1,
|
tile_size: vp_size,
|
||||||
};
|
tab: i,
|
||||||
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
});
|
||||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
|
||||||
// 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(p, &all_wires[..], view_proj, bounds)
|
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
|
||||||
.or_else(|| {
|
|
||||||
scene::pick::hit_test::click_hit_hatch(
|
|
||||||
p,
|
|
||||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
|
||||||
view_proj,
|
|
||||||
bounds,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.or_else(|| {
|
|
||||||
scene::pick::hit_test::click_hit_insert_hatch(
|
|
||||||
p,
|
|
||||||
&self.tabs[i].scene.insert_hatches_for_click(),
|
|
||||||
view_proj,
|
|
||||||
bounds,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.or_else(|| self.tabs[i].scene.mesh_click_hit(p, view_proj, bounds));
|
|
||||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
|
||||||
} else {
|
} else {
|
||||||
// Suppress the rollover during a command or a drag.
|
// Suppress the rollover during a command or a drag.
|
||||||
self.tabs[i].scene.set_hover_highlight(None);
|
self.tabs[i].scene.set_hover_highlight(None);
|
||||||
|
self.hover_dwell = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tabs[i].active_cmd.is_some() {
|
if self.tabs[i].active_cmd.is_some() {
|
||||||
@@ -4330,6 +4310,60 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::HoverDwellTick => {
|
||||||
|
let Some(dwell) = self.hover_dwell.clone() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if Instant::now()
|
||||||
|
.duration_since(dwell.last_move_at)
|
||||||
|
.as_millis()
|
||||||
|
< super::HOVER_DWELL_MS
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let i = dwell.tab;
|
||||||
|
// Re-check the gate — drag / command may have started
|
||||||
|
// between the move that armed the dwell and this tick.
|
||||||
|
if i >= self.tabs.len() || self.tabs[i].active_cmd.is_some() {
|
||||||
|
self.hover_dwell = None;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let bounds = iced::Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: dwell.tile_size.0,
|
||||||
|
height: dwell.tile_size.1,
|
||||||
|
};
|
||||||
|
let p = dwell.point;
|
||||||
|
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||||
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||||
|
// 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(p, &all_wires[..], view_proj, bounds)
|
||||||
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
|
.or_else(|| {
|
||||||
|
scene::pick::hit_test::click_hit_hatch(
|
||||||
|
p,
|
||||||
|
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||||
|
view_proj,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
scene::pick::hit_test::click_hit_insert_hatch(
|
||||||
|
p,
|
||||||
|
&self.tabs[i].scene.insert_hatches_for_click(),
|
||||||
|
view_proj,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.or_else(|| self.tabs[i].scene.mesh_click_hit(p, view_proj, bounds));
|
||||||
|
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||||
|
self.hover_dwell = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::VisibilityPick(idx) => {
|
Message::VisibilityPick(idx) => {
|
||||||
if let Some(popup) = self.visibility_popup.take() {
|
if let Some(popup) = self.visibility_popup.take() {
|
||||||
self.apply_visibility_state(popup.insert_handle, idx);
|
self.apply_visibility_state(popup.insert_handle, idx);
|
||||||
|
|||||||
@@ -1623,6 +1623,14 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
Subscription::none()
|
Subscription::none()
|
||||||
};
|
};
|
||||||
|
// While a rollover pick is queued, drive ticks so it fires the
|
||||||
|
// moment the cursor has been still for the dwell window — without
|
||||||
|
// this `ViewportMove` alone never re-fires once the user stops.
|
||||||
|
let hover_dwell = if self.hover_dwell.is_some() {
|
||||||
|
window::frames().map(|_| Message::HoverDwellTick)
|
||||||
|
} else {
|
||||||
|
Subscription::none()
|
||||||
|
};
|
||||||
// Blink the MText preview caret while the editor is open.
|
// Blink the MText preview caret while the editor is open.
|
||||||
let caret_blink = if self.mtext_editor.is_some() {
|
let caret_blink = if self.mtext_editor.is_some() {
|
||||||
iced::time::every(std::time::Duration::from_millis(530))
|
iced::time::every(std::time::Duration::from_millis(530))
|
||||||
@@ -1642,6 +1650,7 @@ impl OpenCADStudio {
|
|||||||
frames,
|
frames,
|
||||||
history_tick,
|
history_tick,
|
||||||
grip_dwell,
|
grip_dwell,
|
||||||
|
hover_dwell,
|
||||||
caret_blink,
|
caret_blink,
|
||||||
web_fonts,
|
web_fonts,
|
||||||
event::listen_with(|ev, status, win_id| {
|
event::listen_with(|ev, status, win_id| {
|
||||||
|
|||||||
Reference in new issue
Block a user