perf(render): skip exploding hatch-free blocks in hatch build
Opening an xref-heavy drawing froze for ~22s before the first frame. `synced_hatch_models` walked every INSERT and called `explode_from_document()` to scan the result for hatch fills — but explode CLONES every child of the block, including 3D solids that each carry tens of KB of SAB geometry. Clinic.dwg's five xref INSERTs each reference a discipline block of thousands of solids and no hatch, so this materialized ~25000 solid clones (~1 GB) on every open just to find zero hatches. Add a memoized `block_contains_hatch`: a read-only scan of the block's entity types (recursing nested INSERTs, cycle-guarded) that skips the explode for blocks containing no hatch, transitively. Blocks that do hold a hatch still explode exactly as before, so the hatch set is unchanged. Hatch build for Clinic.dwg drops from ~21.7s to ~0.02s, cutting the total open-to-first-frame time from ~25s to ~5s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c91593ab9c
commit
0ce918da68
1 changed files with 69 additions and 0 deletions
|
|
@ -409,6 +409,55 @@ impl Scene {
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a block definition holds at least one HATCH, directly or through
|
||||||
|
/// a nested INSERT. Memoized by block-record handle — `None` marks a block
|
||||||
|
/// currently being visited so a cyclic block reference resolves to `false`
|
||||||
|
/// instead of recursing forever. Lets `synced_hatch_models` skip the costly
|
||||||
|
/// explode of hatch-free blocks (a read-only type scan, no entity clones).
|
||||||
|
fn block_contains_hatch(
|
||||||
|
&self,
|
||||||
|
block_handle: Handle,
|
||||||
|
memo: &mut HashMap<Handle, Option<bool>>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(v) = memo.get(&block_handle) {
|
||||||
|
return v.unwrap_or(false);
|
||||||
|
}
|
||||||
|
memo.insert(block_handle, None);
|
||||||
|
let mut has = false;
|
||||||
|
if let Some(br) = self
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.handle == block_handle)
|
||||||
|
{
|
||||||
|
for &h in &br.entity_handles {
|
||||||
|
match self.document.get_entity(h) {
|
||||||
|
Some(EntityType::Hatch(_)) => {
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(EntityType::Insert(nins)) => {
|
||||||
|
let nbh = self
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.name.eq_ignore_ascii_case(&nins.block_name))
|
||||||
|
.map(|b| b.handle);
|
||||||
|
if let Some(nbh) = nbh {
|
||||||
|
if self.block_contains_hatch(nbh, memo) {
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memo.insert(block_handle, Some(has));
|
||||||
|
has
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn synced_hatch_models(&self) -> Vec<HatchModel> {
|
pub(super) fn synced_hatch_models(&self) -> Vec<HatchModel> {
|
||||||
let layout_block = self.current_layout_block_handle();
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
|
||||||
|
|
@ -495,6 +544,15 @@ impl Scene {
|
||||||
} else {
|
} else {
|
||||||
self.bg_color
|
self.bg_color
|
||||||
};
|
};
|
||||||
|
// Exploding an INSERT materializes (clones) every child of its block —
|
||||||
|
// including 3D solids that each carry megabytes of SAB geometry — just
|
||||||
|
// to scan the result for hatch fills. On xref-heavy drawings whose
|
||||||
|
// blocks hold thousands of solids and NO hatches, that cloned ~all block
|
||||||
|
// content on every open for nothing (~20 s on a 5-xref BIM aggregate).
|
||||||
|
// Pre-scan each block's entity types (no clone) and skip the explode for
|
||||||
|
// blocks that contain no hatch, transitively. Blocks that DO contain a
|
||||||
|
// hatch still explode exactly as before, so the output is unchanged.
|
||||||
|
let mut hatch_block_memo: HashMap<Handle, Option<bool>> = HashMap::default();
|
||||||
for entity in self.document.entities() {
|
for entity in self.document.entities() {
|
||||||
let EntityType::Insert(ins) = entity else {
|
let EntityType::Insert(ins) = entity else {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -509,6 +567,17 @@ impl Scene {
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if let Some(bh) = self
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.name.eq_ignore_ascii_case(&ins.block_name))
|
||||||
|
.map(|b| b.handle)
|
||||||
|
{
|
||||||
|
if !self.block_contains_hatch(bh, &mut hatch_block_memo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
let selected = self.selected.contains(&ins.common.handle);
|
let selected = self.selected.contains(&ins.common.handle);
|
||||||
// XCLIP: clip this insert's exploded hatch fills to the boundary,
|
// XCLIP: clip this insert's exploded hatch fills to the boundary,
|
||||||
// matching how the line geometry is clipped in expand_insert.
|
// matching how the line geometry is clipped in expand_insert.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue