Merge PR #442: cut mesh-import wire-set memory ~35% (#358)

entity_wires() returns the memoized Arc instead of a per-call deep copy;
MESH entities over 50k vertices ship no per-vertex snap/key tables
(matching PolyfaceMesh/PolygonMesh); the Face3D/other render split
caches a None marker and uses the base set directly when there is no
Face3D wire; test fix for the depth-map BlockCache::build signature.

Reviewed: correctness sound — wire AABB comes from entity_aabb (not
key_vertices) so large meshes stay selectable; snap consumers are
empty-guarded; the Arc/strong_count reasoning for try_resident_patch
holds. One consistent pre-existing quirk noted (auto_fit_viewport
centroid excludes tableless meshes, as Poly*Mesh already do).
This commit is contained in:
Hakan Seven 2026-07-23 10:25:27 +03:00
commit 1803f10264
6 changed files with 70 additions and 34 deletions

View file

@ -1069,9 +1069,15 @@ pub struct Scene {
/// [`WIRE_CONTENT_GEN`] id. `split_face3d_wires` is an O(N) per-wire
/// handle lookup + clone that otherwise re-runs every frame. A map, not a
/// slot: a paper frame walks several sources (sheet + viewports).
///
/// `None` for the second element means "no Face3D wire in this set — use
/// the base set itself". The base `Arc` is deliberately NOT stored here:
/// `try_resident_patch` can only move a resident set out for an
/// incremental patch while its `Arc` is uniquely held, so pinning it in
/// this cache would silently force a full rebuild on every edit (#358).
#[allow(clippy::type_complexity)]
split_cache:
RefCell<HashMap<u64, (Arc<Vec<WireModel>>, Arc<Vec<WireModel>>)>>,
RefCell<HashMap<u64, (Arc<Vec<WireModel>>, Option<Arc<Vec<WireModel>>>)>>,
/// Cached `selected ∪ hover` handle set for the GPU xray overlay, keyed by
/// `selection_generation`. Rebuilt only when the selection changes so
/// `build_primitive` doesn't clone the set every frame.
@ -2870,9 +2876,9 @@ impl Scene {
/// Collect closed polygon outlines (world XY) from the current layout.
pub fn closed_outlines(&self) -> Vec<Vec<[f32; 2]>> {
self.entity_wires()
.into_iter()
.iter()
.filter_map(|wire| {
let pts = wire.points;
let pts = &wire.points;
if pts.len() < 4 {
return None;
}
@ -2891,7 +2897,7 @@ impl Scene {
// boundary commands) see one vertex per corner — not the doubled
// ring that otherwise shows two grips at every corner.
let mut ring: Vec<[f32; 2]> = Vec::with_capacity(pts.len());
for p in &pts {
for p in pts {
if !p[0].is_finite() || !p[1].is_finite() {
continue;
}
@ -3361,8 +3367,12 @@ impl Scene {
}
/// Build WireModels from all document entities + optional preview wire.
pub fn entity_wires(&self) -> Vec<WireModel> {
(*self.entity_wires_arc()).clone()
///
/// Returns the memoized set by `Arc` — callers only ever iterate it, and
/// the previous per-call deep clone was a full copy of every wire buffer
/// (hundreds of MB on large mesh imports, #358).
pub fn entity_wires(&self) -> Arc<Vec<WireModel>> {
self.entity_wires_arc()
}
/// Per-entity normalized draw-order depth, keyed by entity handle value.

View file

@ -1251,9 +1251,18 @@ impl Scene {
// each frame — for every source, since all ids are stable now.
let (face3d_wires, other_arc) = {
let cached = { self.split_cache.borrow().get(&wire_content_id).cloned() };
cached.unwrap_or_else(|| {
let (f, o) = split_face3d_wires(&base_arc, &self.document);
let (fa, oa) = (Arc::new(f), Arc::new(o));
let (fa, oa) = cached.unwrap_or_else(|| {
// No Face3D wire at all (pure 2-D drawings, mesh imports):
// "others" would be a wire-for-wire copy of the base set —
// mark it `None` and use the base set directly instead of
// duplicating it (#358). The base Arc itself must not be
// stored in the cache (see the `split_cache` field docs).
let (fa, oa) = if base_arc.iter().any(|w| is_face3d_wire(w, &self.document)) {
let (f, o) = split_face3d_wires(&base_arc, &self.document);
(Arc::new(f), Some(Arc::new(o)))
} else {
(Arc::new(Vec::new()), None)
};
let mut c = self.split_cache.borrow_mut();
// Ids change on rebuild, so old keys die naturally; the cap
// just bounds pathological churn.
@ -1262,7 +1271,8 @@ impl Scene {
}
c.insert(wire_content_id, (fa.clone(), oa.clone()));
(fa, oa)
})
});
(fa, oa.unwrap_or_else(|| Arc::clone(&base_arc)))
};
// Base wire set — the cached `other` Arc directly, never cloned to
// append overlays. Preview / interim wires ride in their own small
@ -1569,9 +1579,19 @@ pub(crate) fn resolve_pattern(
(pat_len, pat)
}
/// Whether a wire belongs to a Face3D entity, by document handle lookup —
/// so no changes to WireModel are needed.
fn is_face3d_wire(w: &WireModel, document: &acadrust::CadDocument) -> bool {
w.name
.parse::<u64>()
.ok()
.and_then(|v| document.get_entity(Handle::new(v)))
.map(|e| matches!(e, EntityType::Face3D(_)))
.unwrap_or(false)
}
/// Partition a wire list into (face3d_wires, other_wires).
///
/// Uses a document handle lookup so no changes to WireModel are needed.
/// O(N) per geometry epoch — acceptable since it runs once per epoch.
fn split_face3d_wires(
wires: &[WireModel],
@ -1580,14 +1600,7 @@ fn split_face3d_wires(
let mut face3d = Vec::new();
let mut others = Vec::new();
for w in wires {
let is_face3d = w
.name
.parse::<u64>()
.ok()
.and_then(|v| document.get_entity(Handle::new(v)))
.map(|e| matches!(e, EntityType::Face3D(_)))
.unwrap_or(false);
if is_face3d {
if is_face3d_wire(w, document) {
face3d.push(w.clone());
} else {
others.push(w.clone());