fix(purge): drop orphaned blocks, anonymous blocks, and stale draw-order
PURGE left behind three classes of dead data that AutoCAD removes, so an OCS-purged drawing stayed far larger than AutoCAD's (a 23 MB test file purged to 3.5 MB in OCS vs 0.58 MB in AutoCAD): - Anonymous blocks (`*U`/`*D`/`*T`/dynamic-block variants) were skipped wholesale. Leftover unreferenced ones now purge like AutoCAD's, held live only when actually referenced. - Block liveness now comes from REACHABILITY: a block is kept only if it is reached from a real layout container by following block references (INSERT names, dimension *D, table *T / cell blocks, MultiLeader content). The old flat "named by some INSERT" scan could not free a mutually-referencing dead subgraph — e.g. the dependent blocks a detached xref leaves behind, whose head carried a DANGLING layout handle that `is_layout()` mistook for a real layout and shielded. That subgraph (here 467k lines) is now purged; live xrefs, reached from the layout that inserts them, stay. The used-set also gains the by-handle block references (table/MultiLeader/dimstyle-arrow) so purging anonymous blocks can't delete an in-use block no INSERT names. - SortEntitiesTable objects (per-block draw order) were never removed, so each purged block's table lingered still listing the handle of every deleted entity — 341k stale handles / ~2.7 MB of dead weight the save re-emitted. Drop every table whose owning block is gone. Net on the test file: 3.5 MB -> 0.81 MB (AutoCAD 0.58 MB). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1f4bdfa741
commit
3326c5f0c8
1 changed files with 169 additions and 35 deletions
|
|
@ -258,37 +258,112 @@ impl OpenCADStudio {
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut used_blocks: rustc_hash::FxHashSet<String> = self.tabs[i]
|
// Which block definitions are LIVE — reachable from a real
|
||||||
.scene
|
// layout container by following every block reference. A
|
||||||
.document
|
// block nothing reachable inserts is dead and purges, even a
|
||||||
.entities()
|
// whole mutually-referencing subgraph (e.g. the dependent
|
||||||
.filter_map(|e| match e {
|
// blocks a detached xref leaves behind: they insert one
|
||||||
acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
// another but hang off no layout, so a flat "is it named by
|
||||||
acadrust::EntityType::Dimension(d) => {
|
// any INSERT" scan keeps them alive forever). Roots:
|
||||||
Some(d.base().block_name.clone())
|
// • *Model_Space / *Paper_Space(N),
|
||||||
}
|
// • any block whose layout handle resolves to a real Layout
|
||||||
_ => None,
|
// object — a DANGLING layout handle, as orphaned xref
|
||||||
})
|
// blocks carry, does NOT count, so is_layout() alone can't
|
||||||
.filter(|s| !s.is_empty())
|
// shield the dead subgraph it heads,
|
||||||
.collect();
|
// • dimension-style arrowhead blocks (referenced by handle).
|
||||||
// Dimension-style arrowheads reference blocks by handle
|
// A block's out-edges: its members' INSERT names, dimension
|
||||||
// (DIMBLK/DIMBLK1/DIMBLK2/DIMLDRBLK) — keep those too.
|
// *D blocks, table *T / cell blocks, and MultiLeader content.
|
||||||
{
|
let live_blocks: rustc_hash::FxHashSet<String> = {
|
||||||
let doc = &self.tabs[i].scene.document;
|
let doc = &self.tabs[i].scene.document;
|
||||||
let arrow_handles: Vec<acadrust::Handle> = doc
|
let by_handle: rustc_hash::FxHashMap<acadrust::Handle, String> = doc
|
||||||
.dim_styles
|
.block_records
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|ds| [ds.dimblk, ds.dimblk1, ds.dimblk2, ds.dimldrblk])
|
.map(|br| (br.handle, br.name.clone()))
|
||||||
.filter(|h| !h.is_null())
|
|
||||||
.collect();
|
.collect();
|
||||||
if !arrow_handles.is_empty() {
|
let is_real_layout = |br: &acadrust::BlockRecord| -> bool {
|
||||||
for br in doc.block_records.iter() {
|
let up = br.name.to_ascii_uppercase();
|
||||||
if arrow_handles.contains(&br.handle) {
|
up.starts_with("*MODEL_SPACE")
|
||||||
used_blocks.insert(br.name.clone());
|
|| up.starts_with("*PAPER_SPACE")
|
||||||
|
|| matches!(
|
||||||
|
doc.objects.get(&br.layout),
|
||||||
|
Some(acadrust::objects::ObjectType::Layout(_))
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let children = |name: &str| -> Vec<String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let Some(br) = doc.block_records.get(name) else {
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
for &h in &br.entity_handles {
|
||||||
|
match doc.get_entity(h) {
|
||||||
|
Some(acadrust::EntityType::Insert(ins))
|
||||||
|
if !ins.block_name.is_empty() =>
|
||||||
|
{
|
||||||
|
out.push(ins.block_name.clone());
|
||||||
|
}
|
||||||
|
Some(acadrust::EntityType::Dimension(d))
|
||||||
|
if !d.base().block_name.is_empty() =>
|
||||||
|
{
|
||||||
|
out.push(d.base().block_name.clone());
|
||||||
|
}
|
||||||
|
Some(acadrust::EntityType::Table(t)) => {
|
||||||
|
if let Some(bh) = t.block_record_handle {
|
||||||
|
if let Some(n) = by_handle.get(&bh) {
|
||||||
|
out.push(n.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for row in &t.rows {
|
||||||
|
for cell in &row.cells {
|
||||||
|
for c in &cell.contents {
|
||||||
|
if let Some(bh) = c.block_handle {
|
||||||
|
if let Some(n) = by_handle.get(&bh) {
|
||||||
|
out.push(n.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(acadrust::EntityType::MultiLeader(ml)) => {
|
||||||
|
if let Some(bh) = ml.block_content_handle {
|
||||||
|
if let Some(n) = by_handle.get(&bh) {
|
||||||
|
out.push(n.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
let mut live = rustc_hash::FxHashSet::default();
|
||||||
|
let mut stack: Vec<String> = doc
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.filter(|&br| is_real_layout(br))
|
||||||
|
.map(|br| br.name.clone())
|
||||||
|
.collect();
|
||||||
|
for ds in doc.dim_styles.iter() {
|
||||||
|
for h in [ds.dimblk, ds.dimblk1, ds.dimblk2, ds.dimldrblk] {
|
||||||
|
if !h.is_null() {
|
||||||
|
if let Some(n) = by_handle.get(&h) {
|
||||||
|
stack.push(n.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
while let Some(n) = stack.pop() {
|
||||||
|
if !live.insert(n.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for c in children(&n) {
|
||||||
|
if !live.contains(&c) {
|
||||||
|
stack.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
live
|
||||||
|
};
|
||||||
|
|
||||||
// Build removal lists (still immutable)
|
// Build removal lists (still immutable)
|
||||||
let layer_remove: Vec<String> = if do_layers {
|
let layer_remove: Vec<String> = if do_layers {
|
||||||
|
|
@ -334,9 +409,14 @@ impl OpenCADStudio {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Blocks: definitions with no INSERT/Dimension reference.
|
// Blocks: everything the layout-reachability walk above did
|
||||||
// Skip anonymous (*Model_Space/*Paper_Space/*U*/*D*…), layout,
|
// NOT mark live. That covers unreferenced named blocks,
|
||||||
// and xref blocks — those are system- or reference-managed.
|
// leftover anonymous blocks (*U hatch/unnamed, orphaned *D
|
||||||
|
// dimension, *T table, dynamic-block variants — AutoCAD drops
|
||||||
|
// these too), AND whole dead subgraphs a detached xref leaves
|
||||||
|
// behind. Live xrefs stay (an attached xref's blocks are
|
||||||
|
// reached from the layout that inserts them); the *Model_Space
|
||||||
|
// / *Paper_Space containers are never removed.
|
||||||
let block_remove: Vec<String> = if do_blocks {
|
let block_remove: Vec<String> = if do_blocks {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -344,11 +424,16 @@ impl OpenCADStudio {
|
||||||
.block_records
|
.block_records
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|br| {
|
.filter(|br| {
|
||||||
!br.is_anonymous()
|
let up = br.name.to_ascii_uppercase();
|
||||||
&& !br.is_layout()
|
// Never PURGE an xref *definition* — that is a
|
||||||
&& !br.flags.is_xref
|
// detach, not a purge. (Orphaned dependent blocks
|
||||||
|
// a past detach left behind are is_xref=false and
|
||||||
|
// still fall to the reachability test.)
|
||||||
|
!br.flags.is_xref
|
||||||
&& !br.flags.is_xref_overlay
|
&& !br.flags.is_xref_overlay
|
||||||
&& !used_blocks.contains(&br.name)
|
&& !up.starts_with("*MODEL_SPACE")
|
||||||
|
&& !up.starts_with("*PAPER_SPACE")
|
||||||
|
&& !live_blocks.contains(&br.name)
|
||||||
})
|
})
|
||||||
.map(|br| br.name.clone())
|
.map(|br| br.name.clone())
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -406,8 +491,54 @@ impl OpenCADStudio {
|
||||||
n_blocks += block_remove.len();
|
n_blocks += block_remove.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw-order cleanup. A SortEntitiesTable lists a block's
|
||||||
|
// entities in draw order; purging the block drops the entities
|
||||||
|
// and the block record, but this object lingers, still holding
|
||||||
|
// the (now dangling) handle of every deleted entity. On a
|
||||||
|
// detached-xref purge that is hundreds of thousands of stale
|
||||||
|
// handles — megabytes of dead weight the save re-emits, which is
|
||||||
|
// why an OCS-purged file stayed far larger than AutoCAD's.
|
||||||
|
// Drop every SortEntitiesTable whose owning block is gone
|
||||||
|
// (AutoCAD's "orphaned data").
|
||||||
|
let mut n_sortents = 0usize;
|
||||||
|
if do_blocks {
|
||||||
|
let live_blocks: rustc_hash::FxHashSet<acadrust::Handle> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.map(|br| br.handle)
|
||||||
|
.collect();
|
||||||
|
let orphans: Vec<acadrust::Handle> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(h, o)| match o {
|
||||||
|
acadrust::objects::ObjectType::SortEntitiesTable(s)
|
||||||
|
if !live_blocks.contains(&s.block_owner_handle) =>
|
||||||
|
{
|
||||||
|
Some(*h)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !orphans.is_empty() {
|
||||||
|
// `snapshot_pushed` is this purge's last use — no
|
||||||
|
// re-assignment needed (a block purge already snapshotted;
|
||||||
|
// a snapshot here only matters when nothing else changed).
|
||||||
|
if !snapshot_pushed {
|
||||||
|
self.push_undo_snapshot(i, "PURGE");
|
||||||
|
}
|
||||||
|
for h in &orphans {
|
||||||
|
self.tabs[i].scene.document.objects.remove(h);
|
||||||
|
}
|
||||||
|
n_sortents = orphans.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let purged = n_layers + n_styles + n_lts + n_blocks;
|
let purged = n_layers + n_styles + n_lts + n_blocks;
|
||||||
if purged > 0 {
|
if purged > 0 || n_sortents > 0 {
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// Rebuild the layer/style/linetype panel + ribbon caches so
|
// Rebuild the layer/style/linetype panel + ribbon caches so
|
||||||
// the removed definitions disappear from the UI immediately.
|
// the removed definitions disappear from the UI immediately.
|
||||||
|
|
@ -426,9 +557,12 @@ impl OpenCADStudio {
|
||||||
if n_blocks > 0 {
|
if n_blocks > 0 {
|
||||||
parts.push(format!("{n_blocks} block(s)"));
|
parts.push(format!("{n_blocks} block(s)"));
|
||||||
}
|
}
|
||||||
|
if n_sortents > 0 {
|
||||||
|
parts.push(format!("{n_sortents} stale draw-order table(s)"));
|
||||||
|
}
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"PURGE: {} definition(s) removed — {}.",
|
"PURGE: {} item(s) removed — {}.",
|
||||||
purged,
|
purged + n_sortents,
|
||||||
parts.join(", ")
|
parts.join(", ")
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue