forked from 1b-agent/opencadstudio-upstream
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]>
This commit is contained in:
1 parent
b4745efb24
commit
416b0d4dc3
2 files changed
+201
-1
No files matched your search
+30
-1
@@ -99,9 +99,14 @@ impl QuadTree {
|
||||
.push(Item { handle, aabb });
|
||||
self.locator.insert(handle, (node_idx, item_idx));
|
||||
|
||||
// Split if the leaf overflowed and we still have depth budget.
|
||||
// Split only LEAF nodes. Internal nodes accumulate straddlers
|
||||
// (items whose AABB crosses a child boundary); calling split on
|
||||
// an internal node would overwrite its `children` pointer and
|
||||
// orphan the entire existing subtree. Straddlers can't fit
|
||||
// smaller children anyway, so splitting is futile here.
|
||||
if self.nodes[node_idx as usize].items.len() > LEAF_CAPACITY
|
||||
&& self.nodes[node_idx as usize].depth < MAX_DEPTH
|
||||
&& self.nodes[node_idx as usize].children.is_none()
|
||||
{
|
||||
self.split(node_idx);
|
||||
}
|
||||
@@ -336,6 +341,30 @@ mod tests {
|
||||
assert_eq!(far, vec![h(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_straddlers_dont_orphan_subtree() {
|
||||
// Regression: previously, when straddlers piled up at an
|
||||
// internal node and exceeded LEAF_CAPACITY, split() was called
|
||||
// again — overwriting the node's `children` and orphaning every
|
||||
// descendant. Symptom: thousands of items inserted, only ~5%
|
||||
// reachable from root via a huge_query.
|
||||
let mut t = QuadTree::new([0.0, 0.0, 100.0, 100.0]);
|
||||
// First, fill one leaf so it splits.
|
||||
for i in 0..40u64 {
|
||||
t.insert(h(i), [1.0, 1.0, 2.0, 2.0]);
|
||||
}
|
||||
// Now flood the root with straddlers (AABBs that cross center).
|
||||
for i in 100..200u64 {
|
||||
t.insert(h(i), [10.0, 10.0, 90.0, 90.0]);
|
||||
}
|
||||
// After the regression-trigger inserts, every handle must still
|
||||
// come back from a huge query.
|
||||
let huge = [-1e9, -1e9, 1e9, 1e9];
|
||||
let hits = t.query_rect(huge);
|
||||
let expected = 40 + 100;
|
||||
assert_eq!(hits.len(), expected, "lost items: tree.len={} hits.len={}", t.len(), hits.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn straddler_stays_at_parent() {
|
||||
let mut t = QuadTree::new([0.0, 0.0, 100.0, 100.0]);
|
||||
|
||||
Reference in new issue
Block a user