perf(scene): grip drag previews the entity instead of re-tessellating

Phase 2.2 (partial). Dragging an entity's grip called scene.apply_grip on
every mouse move, which bumped geometry and re-tessellated the whole model
per move (plus an O(N) clone of every wire to exclude the edited one from
snapping). On a large drawing that's the 30ms->400ms spike, per frame, for
the duration of the drag.

apply_grip no longer bumps. The drag flow now:
- first move: hide the edited entity from the base tessellation (one
  re-tess) and record it in grip_preview_handle;
- each move: apply the grip to the doc (no bump) and show the entity as a
  one-entity overlay preview (set_preview_wires) — the base is a cache hit;
- commit on release / Esc: un-hide, drop the preview, one final re-tess.

Because the edited entity is hidden, it's already absent from
hit_test_wires, so snapping runs against the cached set directly — no
per-move clone and no self-snap. Two re-tessellations per drag instead of
one per move.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-05 10:33:08 +03:00
commit 8df9bef6f8
4 changed files with 57 additions and 8 deletions

View file

@ -132,6 +132,15 @@ handles; the render path re-tessellates only those, reusing the rest.
Also useful on open: any partial cache (e.g. from block defns) can be
re-used.
**Partial (landed): grip drag.** Dragging an entity's grip called
`scene.apply_grip` every move, which `bump_geometry`'d → a full model
re-tessellation per move (plus an O(N) clone of all wires for snapping). Now
the first move hides the edited entity from the base (one re-tess) and shows
it as a one-entity overlay preview; subsequent moves only re-tessellate that
one entity (cheap) — the base stays a cache hit. Snapping runs against the
set directly (the edited entity is already hidden, so no clone and no
self-snap). The drag commits on release / Esc: un-hide + one final re-tess.
**Partial (landed): preview / interim overlays.** `set_preview_wires`,
`clear_preview_wire` and `set_interim_wire` used to `bump_geometry`, so every
rubber-band frame of a drawing command re-tessellated the whole model (the

View file

@ -231,6 +231,12 @@ pub(super) struct OpenCADStudio {
/// being placed (follows the cursor). `(entity handle, new-arrow grip id)`.
/// Esc before the placement click removes it again.
grip_add_provisional: Option<(acadrust::Handle, usize)>,
/// Handle hidden from the base tessellation during an in-progress grip
/// drag. While dragging, the edited entity is excluded from the cached
/// wire set and shown as a cheap overlay preview instead, so each move
/// updates only the overlay rather than re-tessellating the whole model.
/// Committed (un-hidden + one re-tess) when the drag ends. `None` = idle.
grip_preview_handle: Option<acadrust::Handle>,
/// Open Quick Select panel state. `None` = panel closed. Filters are
/// applied via `Message::QSelectApply`; the panel is dismissed on
/// Apply / Cancel / Esc / outside-click.
@ -1272,6 +1278,7 @@ impl OpenCADStudio {
grip_popup: None,
grip_pending: None,
grip_add_provisional: None,
grip_preview_handle: None,
qselect: None,
show_ucs_icon: true,
show_viewcube: true,

View file

@ -1313,6 +1313,14 @@ impl OpenCADStudio {
self.tabs[i].scene.bump_geometry();
self.refresh_selected_grips();
}
// Commit/cleanup an in-progress grip drag: un-hide the
// edited entity, re-tessellate once, drop the preview.
if let Some(h) = self.grip_preview_handle.take() {
let i = self.active_tab;
self.tabs[i].scene.hidden.remove(&h);
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].scene.bump_geometry();
}
self.tabs[self.active_tab].snap_result = None;
self.refresh_properties();
return Task::none();
@ -2100,14 +2108,24 @@ impl OpenCADStudio {
drop(cam);
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
let edited_name = grip.handle.value().to_string();
// First move of this drag: hide the edited entity from the
// base tessellation (one re-tess) so subsequent moves only
// refresh a cheap overlay preview instead of re-tessellating
// the whole model on every move.
if self.grip_preview_handle != Some(grip.handle) {
if let Some(prev) = self.grip_preview_handle.take() {
self.tabs[i].scene.hidden.remove(&prev);
}
self.tabs[i].scene.hidden.insert(grip.handle);
self.tabs[i].scene.bump_geometry();
self.grip_preview_handle = Some(grip.handle);
}
// The edited entity is hidden, so it's already absent from
// `hit_test_wires` — snap against the set directly, no clone
// and no self-snap.
let all_wires = self.tabs[i].scene.hit_test_wires();
let snap_wires: Vec<_> = all_wires
.iter()
.filter(|w| w.name != edited_name)
.cloned()
.collect();
let snap_hit = self.snapper.snap(raw, p, &snap_wires, vp_mat, bounds);
let snap_hit = self.snapper.snap(raw, p, &all_wires[..], vp_mat, bounds);
let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw);
self.tabs[i].snap_result = snap_hit;
if let Some(s) = self.tabs[i].snap_result.as_mut() {
@ -2142,6 +2160,10 @@ impl OpenCADStudio {
.apply_grip(grip.handle, grip.grip_id, apply);
self.tabs[i].dirty = true;
self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped;
// Overlay the moved entity (hidden from the base) — no base
// re-tessellation, just a one-entity preview tessellation.
let preview = self.tabs[i].scene.wire_models_for(&[grip.handle]);
self.tabs[i].scene.set_preview_wires(preview);
self.refresh_selected_grips();
self.refresh_properties();
return Task::none();
@ -2611,6 +2633,14 @@ impl OpenCADStudio {
return Task::none();
}
self.tabs[i].active_grip = None;
// Commit the grip drag: un-hide the edited entity and
// re-tessellate the base once with its final geometry,
// dropping the overlay preview.
if let Some(h) = self.grip_preview_handle.take() {
self.tabs[i].scene.hidden.remove(&h);
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].scene.bump_geometry();
}
// Placement confirmed — keep the just-added leader.
self.grip_add_provisional = None;
self.tabs[i].snap_result = None;

View file

@ -4890,7 +4890,10 @@ impl Scene {
}
_ => {}
}
self.bump_geometry();
// NOTE: no `bump_geometry()` here. The grip-drag caller hides the
// edited entity and previews it as an overlay during the drag (so a
// move doesn't re-tessellate the whole model), then bumps once on
// commit. Any other caller must bump geometry itself.
}
// ── Hit-test convenience: wire name → Handle ──────────────────────────