Files
OneCAD-Draft/examples/dump_index_stats.rs
T
Hakan SevenandClaude Opus 4.7 416b0d4dc3 fix(quadtree): split() on internal node was orphaning the entire subtree
Symptom: open a non-UTM drawing, most lines/polylines/hatches/etc.
vanish — only ~5% of indexed entities reachable from a huge query
(walking the nodes flat showed all 2975 items present, but recursing
through `node.children` from root reached only 161).

Root cause: when straddler items piled up at an INTERNAL node and the
count exceeded LEAF_CAPACITY, `insert()` re-invoked `split()` on that
node. `split()` allocates four fresh children and assigns them to
`node.children = Some([...])` — overwriting the existing children
pointer and orphaning the entire previous subtree (the descendant
nodes still hold their items; nothing reaches them).

Splitting an internal node is futile anyway: straddlers don't fit
smaller children either. Fix: gate the split on
`children.is_none()` so only leaves split. Internal nodes accumulate
straddlers as a linear-scan list — slower per-query for big strad-
dler counts, but correct.

Adds a regression test (`many_straddlers_dont_orphan_subtree`) that
floods an internal node with straddlers and verifies a huge query
still returns every inserted handle.

Bonus: `examples/dump_index_stats.rs` — standalone tool that loads a
DWG/DXF and dumps per-type counts (indexed / unbounded / top-level /
block-internal) plus a sample of bboxes. Useful for future bug
triage; built and ran cleanly against the file that exposed this.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-17 19:28:57 +03:00

172 lines
6.1 KiB
Rust

// Diagnostic: simulate Scene::entity_index() build on a DXF/DWG file
// and report per-type stats for indexed / unbounded / unindexable
// entities, plus how many would survive a hypothetical view query.
//
// cargo run --release --example dump_index_stats -- <file>
use acadrust::EntityType;
use std::collections::BTreeMap;
fn entity_kind(e: &EntityType) -> &'static str {
match e {
EntityType::Line(_) => "Line",
EntityType::LwPolyline(_) => "LwPolyline",
EntityType::Polyline2D(_) => "Polyline2D",
EntityType::Polyline3D(_) => "Polyline3D",
EntityType::Polyline(_) => "Polyline",
EntityType::Circle(_) => "Circle",
EntityType::Arc(_) => "Arc",
EntityType::Ellipse(_) => "Ellipse",
EntityType::Spline(_) => "Spline",
EntityType::Text(_) => "Text",
EntityType::MText(_) => "MText",
EntityType::Hatch(_) => "Hatch",
EntityType::Wipeout(_) => "Wipeout",
EntityType::Insert(_) => "Insert",
EntityType::Viewport(_) => "Viewport",
EntityType::Block(_) => "Block",
EntityType::BlockEnd(_) => "BlockEnd",
EntityType::Dimension(_) => "Dimension",
EntityType::Leader(_) => "Leader",
EntityType::MLine(_) => "MLine",
EntityType::Point(_) => "Point",
EntityType::Solid(_) => "Solid",
EntityType::Face3D(_) => "Face3D",
EntityType::RasterImage(_) => "RasterImage",
EntityType::Solid3D(_) => "Solid3D",
EntityType::Region(_) => "Region",
EntityType::Body(_) => "Body",
EntityType::Ole2Frame(_) => "Ole2Frame",
EntityType::Ray(_) => "Ray",
EntityType::XLine(_) => "XLine",
_ => "Other",
}
}
fn is_unindexable(e: &EntityType) -> bool {
matches!(
e,
EntityType::Insert(_)
| EntityType::Viewport(_)
| EntityType::Block(_)
| EntityType::BlockEnd(_)
)
}
fn world_aabb_f64(e: &EntityType) -> Option<[f64; 4]> {
let bb = e.as_entity().bounding_box();
let (xmin, ymin, xmax, ymax) = (bb.min.x, bb.min.y, bb.max.x, bb.max.y);
if xmin == xmax && ymin == ymax {
return None;
}
if !xmin.is_finite() || !ymin.is_finite() || !xmax.is_finite() || !ymax.is_finite() {
return None;
}
Some([xmin, ymin, xmax, ymax])
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).expect("usage: dump_index_stats <file>");
let mut doc = if path.to_lowercase().ends_with(".dwg") {
acadrust::io::dwg::DwgReader::from_file(&path)?.read()?
} else {
acadrust::io::dxf::DxfReader::from_file(&path)?.read()?
};
let base_dir = std::path::Path::new(&path)
.parent()
.unwrap_or(std::path::Path::new("."));
let _ = H7CAD::io::xref::resolve_xrefs(&mut doc, base_dir);
let model_block = doc.header.model_space_block_handle;
println!("file: {}", path);
println!("model_space_block_handle: {:?}", model_block);
println!("total entities: {}", doc.entities().count());
println!();
let mut total: BTreeMap<&str, usize> = BTreeMap::new();
let mut indexed: BTreeMap<&str, usize> = BTreeMap::new();
let mut unbounded: BTreeMap<&str, usize> = BTreeMap::new();
let mut unindexable_count: BTreeMap<&str, usize> = BTreeMap::new();
let mut top_level: BTreeMap<&str, usize> = BTreeMap::new();
let mut block_internal: BTreeMap<&str, usize> = BTreeMap::new();
let mut union: Option<[f64; 4]> = None;
for e in doc.entities() {
let k = entity_kind(e);
*total.entry(k).or_default() += 1;
let owner = e.common().owner_handle;
if owner == model_block {
*top_level.entry(k).or_default() += 1;
} else {
*block_internal.entry(k).or_default() += 1;
}
if is_unindexable(e) {
*unindexable_count.entry(k).or_default() += 1;
continue;
}
match world_aabb_f64(e) {
Some(ab) => {
*indexed.entry(k).or_default() += 1;
union = Some(match union {
None => ab,
Some(u) => [
u[0].min(ab[0]),
u[1].min(ab[1]),
u[2].max(ab[2]),
u[3].max(ab[3]),
],
});
}
None => *unbounded.entry(k).or_default() += 1,
}
}
println!("──── per type ────────────────────────────────────────────────");
println!(
"{:<14} {:>6} {:>6} {:>6} {:>6} {:>10} {:>10}",
"Type", "total", "idx", "unb", "skip", "top-lvl", "blk-int"
);
let mut all_keys: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
all_keys.extend(total.keys());
for k in &all_keys {
println!(
"{:<14} {:>6} {:>6} {:>6} {:>6} {:>10} {:>10}",
k,
total.get(k).copied().unwrap_or(0),
indexed.get(k).copied().unwrap_or(0),
unbounded.get(k).copied().unwrap_or(0),
unindexable_count.get(k).copied().unwrap_or(0),
top_level.get(k).copied().unwrap_or(0),
block_internal.get(k).copied().unwrap_or(0),
);
}
println!();
println!("union of indexed AABBs: {:?}", union);
// Sample top-level entity bboxes by type
println!();
println!("──── sample top-level entity bboxes (first 5 per type) ────────");
let mut shown: BTreeMap<&str, usize> = BTreeMap::new();
for e in doc.entities() {
let k = entity_kind(e);
if e.common().owner_handle != model_block {
continue;
}
let c = shown.entry(k).or_insert(0);
if *c >= 5 {
continue;
}
*c += 1;
let bb = e.as_entity().bounding_box();
println!(
" {:<12} h={:?} bb=({:.3},{:.3})→({:.3},{:.3})",
k,
e.common().handle,
bb.min.x, bb.min.y, bb.max.x, bb.max.y,
);
}
Ok(())
}