perf: viewport wire cache, Arc hit_test_wires, snap world-space pre-rejection (I+J+K)
Option I: add per-viewport Arc wire cache so paper-space viewport rendering no longer re-tessellates model content every frame. Option J: hit_test_wires() returns Arc<Vec<WireModel>> — O(1) pointer copy in model space instead of a full Vec clone on every mouse move during commands. paper_canvas_hatches/wipeouts() now route through the epoch caches added in F. Option K: add world_snap_r + wire_in_range() pre-check to Snapper::snap(). All Endpoint/Midpoint/Nearest/Perpendicular/Intersection loops skip wires whose chord sphere is outside the snap circle using O(1) scalar comparisons, eliminating O(entities × vertices) matrix multiply cost when zoomed in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
81db3c37d0
commit
2da9301a2b
6 changed files with 149 additions and 32 deletions
|
|
@ -112,8 +112,66 @@ extruded polylines). Navigation stays free regardless of model complexity.
|
|||
|
||||
---
|
||||
|
||||
## Option I — Per-viewport wire cache for paper space
|
||||
|
||||
**Status:** Done
|
||||
|
||||
Added `viewport_wire_cache: RefCell<HashMap<Handle, (u64, Arc<Vec<WireModel>>)>>` to `Scene`.
|
||||
`model_wires_for_viewport_arc(vp_handle)` checks the cache first; on a hit it returns an
|
||||
Arc clone (O(1)). On a miss it tessellates, stores the result, and returns it.
|
||||
|
||||
`build_viewport_primitive()` in `render.rs` now calls `model_wires_for_viewport_arc()` instead
|
||||
of `model_wires_for_viewport()`, so paper-space viewport rendering no longer re-tessellates
|
||||
model-space content every frame.
|
||||
|
||||
**Impact:** Eliminates per-frame tessellation when viewing paper space layouts. Before this fix,
|
||||
every navigation frame in paper space re-tessellated the entire model-space entity set for each
|
||||
visible viewport — the same bug Options A/B fixed for model space.
|
||||
|
||||
---
|
||||
|
||||
## Option J — Arc-return `hit_test_wires()` and `paper_canvas_*`
|
||||
|
||||
**Status:** Done
|
||||
|
||||
`hit_test_wires()` now returns `Arc<Vec<WireModel>>` instead of `Vec<WireModel>`.
|
||||
In the model-space case it returns `entity_wires_arc()` directly — O(1), no Vec clone.
|
||||
In the paper-space case it still builds a Vec (no suitable cache for those paths), but the
|
||||
interface is consistent.
|
||||
|
||||
`paper_canvas_hatches()` and `paper_canvas_wipeouts()` now return `Arc<Vec<HatchModel>>`
|
||||
via the epoch caches added in Option F, eliminating per-frame hatch rebuilds in the paper
|
||||
canvas widget.
|
||||
|
||||
**Impact:** Every `ViewportMove` message during a command (grip drag, line drawing, etc.)
|
||||
called `hit_test_wires()` up to twice. In model space this was a full Vec clone of all
|
||||
tessellated wires — potentially MB-scale. Now it is a pointer copy.
|
||||
|
||||
---
|
||||
|
||||
## Option K — Snap world-space wire pre-rejection
|
||||
|
||||
**Status:** Done
|
||||
|
||||
Added `world_snap_r` (snap radius in world units, derived from `view_proj`) and a
|
||||
`wire_in_range()` closure to `Snapper::snap()`. Before iterating a wire's vertices for
|
||||
Endpoint, Midpoint, Nearest, Perpendicular, Intersection, and ApparentIntersection,
|
||||
the closure checks whether the wire's first↔last chord sphere overlaps the snap search
|
||||
circle. Wires that fail the check are skipped with `continue` — no vertex projection,
|
||||
no matrix multiplies.
|
||||
|
||||
Closed wires (first ≈ last, e.g. tessellated circles) are always passed through since
|
||||
their chord radius is ~0.
|
||||
|
||||
**Impact:** When zoomed in on a small region of a large drawing, the vast majority of
|
||||
wires are outside the snap circle and are rejected in O(1) scalar comparisons each. The
|
||||
per-mouse-move snap cost drops from O(entities × vertices) to O(entities) for the
|
||||
pre-check plus O(nearby_vertices) for the actual snap work.
|
||||
|
||||
---
|
||||
|
||||
## Implementation order
|
||||
|
||||
F → G → H. Each is independent; all three can be done in a single pass since the pattern
|
||||
is identical. F has the most complex rebuild logic (Insert explosion); G has the highest
|
||||
raw byte impact; H is the simplest but matters for 3D-heavy files.
|
||||
F → G → H → I → J → K (all implemented).
|
||||
Each option is independent; the pattern (epoch-keyed Arc cache or a cheap pre-rejection
|
||||
guard) is the same throughout.
|
||||
|
|
|
|||
|
|
@ -1177,9 +1177,9 @@ impl H7CAD {
|
|||
self.tabs[i].snap_result = if needs_entity || is_gathering {
|
||||
None
|
||||
} else if needs_tan {
|
||||
self.snapper.snap_tangent_only(cursor_world, p, &all_wires, view_proj, bounds)
|
||||
self.snapper.snap_tangent_only(cursor_world, p, &all_wires[..], view_proj, bounds)
|
||||
} else {
|
||||
self.snapper.snap(cursor_world, p, &all_wires, view_proj, bounds)
|
||||
self.snapper.snap(cursor_world, p, &all_wires[..], view_proj, bounds)
|
||||
};
|
||||
|
||||
// Object Snap Tracking: update dwell and override snap if tracking.
|
||||
|
|
@ -1224,7 +1224,7 @@ impl H7CAD {
|
|||
|
||||
let mut previews = if needs_entity {
|
||||
let hover_handle =
|
||||
scene::hit_test::click_hit(p, &all_wires, view_proj, bounds)
|
||||
scene::hit_test::click_hit(p, &all_wires[..], view_proj, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.unwrap_or(acadrust::Handle::NULL);
|
||||
self.tabs[i].active_cmd.as_mut()
|
||||
|
|
@ -1395,9 +1395,9 @@ impl H7CAD {
|
|||
let snap_hit = if needs_entity_click {
|
||||
None
|
||||
} else if needs_tan {
|
||||
self.snapper.snap_tangent_only(raw, p, &all_wires, vp_mat, bounds)
|
||||
self.snapper.snap_tangent_only(raw, p, &all_wires[..], vp_mat, bounds)
|
||||
} else {
|
||||
self.snapper.snap(raw, p, &all_wires, vp_mat, bounds)
|
||||
self.snapper.snap(raw, p, &all_wires[..], vp_mat, bounds)
|
||||
};
|
||||
// snap.world is in paper-space (projected wire coords in MSPACE);
|
||||
// convert to model-space so commands receive consistent coordinates.
|
||||
|
|
@ -1424,7 +1424,7 @@ impl H7CAD {
|
|||
{
|
||||
let vp_mat2 = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let all_wires2 = self.tabs[i].scene.hit_test_wires();
|
||||
let hit = scene::hit_test::click_hit(p, &all_wires2, vp_mat2, bounds)
|
||||
let hit = scene::hit_test::click_hit(p, &all_wires2[..], vp_mat2, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s));
|
||||
if let Some(handle) = hit {
|
||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_entity_pick(handle, world_pt));
|
||||
|
|
@ -1514,7 +1514,7 @@ impl H7CAD {
|
|||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let mut handles: Vec<Handle> = scene::hit_test::box_hit(
|
||||
a, p, crossing, &all_wires, vp_mat, bounds,
|
||||
a, p, crossing, &all_wires[..], vp_mat, bounds,
|
||||
).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect();
|
||||
handles.extend(scene::hit_test::box_hit_hatch(
|
||||
a, p, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds,
|
||||
|
|
@ -1534,7 +1534,7 @@ impl H7CAD {
|
|||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let mut handles: Vec<Handle> = scene::hit_test::poly_hit(
|
||||
&poly_pts, crossing, &all_wires, vp_mat, bounds,
|
||||
&poly_pts, crossing, &all_wires[..], vp_mat, bounds,
|
||||
).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect();
|
||||
handles.extend(scene::hit_test::poly_hit_hatch(
|
||||
&poly_pts, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds,
|
||||
|
|
@ -1555,7 +1555,7 @@ impl H7CAD {
|
|||
if box_anchor.is_none() {
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
||||
let hit = scene::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.or_else(|| scene::hit_test::click_hit_hatch(
|
||||
p, &self.tabs[i].scene.hatches, vp_mat, bounds,
|
||||
|
|
@ -1579,7 +1579,7 @@ impl H7CAD {
|
|||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let mut handles: Vec<Handle> = scene::hit_test::box_hit(
|
||||
a, p, crossing, &all_wires, vp_mat, bounds,
|
||||
a, p, crossing, &all_wires[..], vp_mat, bounds,
|
||||
).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect();
|
||||
handles.extend(scene::hit_test::box_hit_hatch(
|
||||
a, p, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds,
|
||||
|
|
@ -1639,7 +1639,7 @@ impl H7CAD {
|
|||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
||||
let hit = scene::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s));
|
||||
if let Some(handle) = hit {
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity(handle) {
|
||||
|
|
@ -1686,7 +1686,7 @@ impl H7CAD {
|
|||
let hit_vp: Option<acadrust::Handle> = {
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
||||
scene::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.and_then(|h| {
|
||||
if let Some(AcadEntityType::Viewport(vp)) =
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@ pub struct Scene {
|
|||
image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,
|
||||
/// Cached mesh models, keyed by geometry_epoch.
|
||||
mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshModel>>)>>,
|
||||
/// Per-viewport wire cache for paper-space rendering.
|
||||
/// Maps vp_handle → (geometry_epoch, Arc<Vec<WireModel>>).
|
||||
viewport_wire_cache: RefCell<HashMap<Handle, (u64, Arc<Vec<WireModel>>)>>,
|
||||
/// Active layout name — "Model" or a paper space layout name.
|
||||
pub current_layout: String,
|
||||
/// GPU render data for hatch fills, keyed by the DXF entity Handle.
|
||||
|
|
@ -124,6 +127,7 @@ impl Scene {
|
|||
wipeout_cache: RefCell::new(None),
|
||||
image_cache: RefCell::new(None),
|
||||
mesh_cache: RefCell::new(None),
|
||||
viewport_wire_cache: RefCell::new(HashMap::new()),
|
||||
current_layout: "Model".to_string(),
|
||||
hatches: HashMap::new(),
|
||||
meshes: HashMap::new(),
|
||||
|
|
@ -472,19 +476,15 @@ impl Scene {
|
|||
/// viewport content is NOT interactive.
|
||||
/// - MSPACE (active viewport set): model-space content of the active viewport
|
||||
/// only — paper-space entities are NOT interactive.
|
||||
pub fn hit_test_wires(&self) -> Vec<WireModel> {
|
||||
pub fn hit_test_wires(&self) -> Arc<Vec<WireModel>> {
|
||||
if self.current_layout == "Model" {
|
||||
return self.entity_wires();
|
||||
return self.entity_wires_arc();
|
||||
}
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
match self.active_viewport {
|
||||
None => {
|
||||
// PSPACE: only paper-space entities (viewport borders, title blocks…)
|
||||
self.wires_for_block(layout_block)
|
||||
}
|
||||
None => Arc::new(self.wires_for_block(layout_block)),
|
||||
Some(vp_handle) => {
|
||||
// MSPACE: only model content visible through the active viewport
|
||||
self.viewport_content_wires(layout_block, Some(vp_handle), None)
|
||||
Arc::new(self.viewport_content_wires(layout_block, Some(vp_handle), None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2277,13 +2277,13 @@ impl Scene {
|
|||
}
|
||||
|
||||
/// Hatch fills for the paper-space canvas.
|
||||
pub fn paper_canvas_hatches(&self) -> Vec<HatchModel> {
|
||||
self.synced_hatch_models()
|
||||
pub fn paper_canvas_hatches(&self) -> Arc<Vec<HatchModel>> {
|
||||
self.hatch_models_arc()
|
||||
}
|
||||
|
||||
/// Wipeout (opaque background fill) models for the paper-space canvas.
|
||||
pub fn paper_canvas_wipeouts(&self) -> Vec<HatchModel> {
|
||||
self.wipeout_models()
|
||||
pub fn paper_canvas_wipeouts(&self) -> Arc<Vec<HatchModel>> {
|
||||
self.wipeout_models_arc()
|
||||
}
|
||||
|
||||
pub(super) fn paper_sheet_wires(&self) -> Vec<WireModel> {
|
||||
|
|
@ -2380,6 +2380,22 @@ impl Scene {
|
|||
.flat_map(|e| self.tessellate_one(e))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn model_wires_for_viewport_arc(&self, vp_handle: Handle) -> Arc<Vec<WireModel>> {
|
||||
{
|
||||
let cache = self.viewport_wire_cache.borrow();
|
||||
if let Some((cached_epoch, ref arc)) = cache.get(&vp_handle) {
|
||||
if *cached_epoch == self.geometry_epoch {
|
||||
return Arc::clone(arc);
|
||||
}
|
||||
}
|
||||
}
|
||||
let arc = Arc::new(self.model_wires_for_viewport(vp_handle));
|
||||
self.viewport_wire_cache
|
||||
.borrow_mut()
|
||||
.insert(vp_handle, (self.geometry_epoch, Arc::clone(&arc)));
|
||||
arc
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Scene {
|
||||
|
|
|
|||
|
|
@ -83,12 +83,12 @@ impl<'a> canvas::Program<Message> for PaperCanvas<'a> {
|
|||
}
|
||||
|
||||
// ── Wipeout fills (rendered before wires, cover background) ──────────
|
||||
for hatch in &self.scene.paper_canvas_wipeouts() {
|
||||
for hatch in self.scene.paper_canvas_wipeouts().iter() {
|
||||
draw_hatch(&mut frame, hatch, &to_px);
|
||||
}
|
||||
|
||||
// ── Hatch fills ───────────────────────────────────────────────────────
|
||||
for hatch in &self.scene.paper_canvas_hatches() {
|
||||
for hatch in self.scene.paper_canvas_hatches().iter() {
|
||||
draw_hatch(&mut frame, hatch, &to_px);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -291,11 +291,11 @@ impl Scene {
|
|||
None => return self.build_primitive(hover_region, bounds, false),
|
||||
};
|
||||
|
||||
let base_wires = self.model_wires_for_viewport(vp_handle);
|
||||
let base_arc = self.model_wires_for_viewport_arc(vp_handle);
|
||||
let all_wires = if self.interim_wire.is_none() && self.preview_wires.is_empty() {
|
||||
Arc::new(base_wires)
|
||||
base_arc
|
||||
} else {
|
||||
let mut v = base_wires;
|
||||
let mut v = (*base_arc).clone();
|
||||
if let Some(iw) = &self.interim_wire {
|
||||
v.push(iw.clone());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,6 +281,41 @@ impl Snapper {
|
|||
let mut best: Option<SnapResult> = None;
|
||||
let mut best_d2 = self.snap_radius_px * self.snap_radius_px;
|
||||
|
||||
// World-space snap radius — derived from the view scale so wires whose
|
||||
// entire extent is clearly outside the snap circle can be skipped cheaply
|
||||
// before projecting any of their vertices to screen space.
|
||||
// view_proj col-0 x = 2*zoom / viewport_width for an orthographic camera,
|
||||
// so scale_x * (width/2) = pixels per world unit.
|
||||
let world_snap_r = {
|
||||
let s = view_proj.col(0).x.abs() * bounds.width * 0.5;
|
||||
if s > 1e-6 { self.snap_radius_px / s } else { f32::MAX }
|
||||
};
|
||||
|
||||
// Returns false when the wire's chord sphere is definitely outside the
|
||||
// snap circle — safe to skip all vertex work for this wire.
|
||||
// Uses first/last point of key_vertices (if any) or tessellated points.
|
||||
// Closed wires (first ≈ last) are always passed through because the
|
||||
// chord radius would be ~0 and their arc can still pass near the cursor.
|
||||
let wire_in_range = |wire: &WireModel| -> bool {
|
||||
let pts: &[[f32; 3]] = if !wire.key_vertices.is_empty() {
|
||||
&wire.key_vertices
|
||||
} else {
|
||||
&wire.points
|
||||
};
|
||||
let (Some(&f), Some(&l)) = (pts.first(), pts.last()) else {
|
||||
return false;
|
||||
};
|
||||
let dx = l[0] - f[0];
|
||||
let dy = l[1] - f[1];
|
||||
let half_chord = (dx * dx + dy * dy).sqrt() * 0.5;
|
||||
if half_chord < 1e-4 {
|
||||
return true; // closed or degenerate — can't prune
|
||||
}
|
||||
let cx = (f[0] + l[0]) * 0.5 - cursor_world.x;
|
||||
let cy = (f[1] + l[1]) * 0.5 - cursor_world.y;
|
||||
cx * cx + cy * cy <= (world_snap_r + half_chord) * (world_snap_r + half_chord)
|
||||
};
|
||||
|
||||
let mut try_pt = |world: Vec3, snap_type: SnapType| {
|
||||
let screen = world_to_screen(world, view_proj, bounds);
|
||||
let d2 = dist2(screen, cursor_screen);
|
||||
|
|
@ -313,6 +348,7 @@ impl Snapper {
|
|||
// ── Endpoint ───────────────────────────────────────────────────────
|
||||
if self.is_on(SnapType::Endpoint) {
|
||||
for wire in wires {
|
||||
if !wire_in_range(wire) { continue; }
|
||||
if !wire.key_vertices.is_empty() {
|
||||
// Use explicit vertices (Line, LwPolyline): every vertex is an endpoint.
|
||||
for &p in &wire.key_vertices {
|
||||
|
|
@ -335,6 +371,7 @@ impl Snapper {
|
|||
// ── Midpoint ───────────────────────────────────────────────────────
|
||||
if self.is_on(SnapType::Midpoint) {
|
||||
for wire in wires {
|
||||
if !wire_in_range(wire) { continue; }
|
||||
if !wire.key_vertices.is_empty() {
|
||||
// Use explicit vertices for accurate per-segment midpoints.
|
||||
for seg in wire.key_vertices.windows(2) {
|
||||
|
|
@ -361,6 +398,7 @@ impl Snapper {
|
|||
// ── Nearest — closest point on any segment (clamped) ──────────────
|
||||
if self.is_on(SnapType::Nearest) {
|
||||
for wire in wires {
|
||||
if !wire_in_range(wire) { continue; }
|
||||
for seg in wire.points.windows(2) {
|
||||
let p =
|
||||
nearest_on_segment(cursor_world, Vec3::from(seg[0]), Vec3::from(seg[1]));
|
||||
|
|
@ -372,6 +410,7 @@ impl Snapper {
|
|||
// ── Perpendicular — foot of perpendicular from cursor (unclamped) ──
|
||||
if self.is_on(SnapType::Perpendicular) {
|
||||
for wire in wires {
|
||||
if !wire_in_range(wire) { continue; }
|
||||
for seg in wire.points.windows(2) {
|
||||
if let Some(foot) =
|
||||
perp_foot(cursor_world, Vec3::from(seg[0]), Vec3::from(seg[1]))
|
||||
|
|
@ -386,7 +425,9 @@ impl Snapper {
|
|||
if self.is_on(SnapType::Intersection) {
|
||||
let r_world = self.snap_radius_px * 0.5; // rough world-space cull
|
||||
for i in 0..wires.len() {
|
||||
if !wire_in_range(&wires[i]) { continue; }
|
||||
for j in (i + 1)..wires.len() {
|
||||
if !wire_in_range(&wires[j]) { continue; }
|
||||
for seg_a in wires[i].points.windows(2) {
|
||||
for seg_b in wires[j].points.windows(2) {
|
||||
let a0 = Vec3::from(seg_a[0]);
|
||||
|
|
@ -455,7 +496,9 @@ impl Snapper {
|
|||
// ── Apparent Intersection — screen-space intersections ─────────────
|
||||
if self.is_on(SnapType::ApparentIntersection) {
|
||||
for i in 0..wires.len() {
|
||||
if !wire_in_range(&wires[i]) { continue; }
|
||||
for j in (i + 1)..wires.len() {
|
||||
if !wire_in_range(&wires[j]) { continue; }
|
||||
for seg_a in wires[i].points.windows(2) {
|
||||
for seg_b in wires[j].points.windows(2) {
|
||||
let sa0 = world_to_screen(Vec3::from(seg_a[0]), view_proj, bounds);
|
||||
|
|
|
|||
Loading…
Reference in a new issue