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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-05 02:03:16 +03:00
1 parent c91593ab9c
commit 0ce918da68
1 file changed
+69
+69
View File
@@ -409,6 +409,55 @@ impl Scene {
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> {
let layout_block = self.current_layout_block_handle();
@@ -495,6 +544,15 @@ impl Scene {
} else {
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() {
let EntityType::Insert(ins) = entity else {
continue;
@@ -509,6 +567,17 @@ impl Scene {
) {
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);
// XCLIP: clip this insert's exploded hatch fills to the boundary,
// matching how the line geometry is clipped in expand_insert.