Skip to content

Commit a955dd1

Browse files
committed
refactor: Simplify TileQuadtree and Node methods to use BBox directly
1 parent 9de2bdb commit a955dd1

7 files changed

Lines changed: 188 additions & 268 deletions

File tree

versatiles_core/src/types/tile_quadtree/constructors.rs

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ impl TileQuadtree {
3939
return TileQuadtree::new_empty(level).unwrap();
4040
};
4141

42-
let root = Node::build_node(level, (0, 0), 1u64 << level, &bbox);
42+
let root = Node::build_node(&BBox::root(level), &bbox);
4343
TileQuadtree { level, root }
4444
}
4545

@@ -73,8 +73,7 @@ impl TileQuadtree {
7373
let mut codes: Vec<u64> = tiles.iter().map(|&(x, y)| morton(u64::from(x), u64::from(y))).collect();
7474
codes.sort_unstable();
7575
codes.dedup();
76-
let size = 1u64 << level;
77-
let root = Node::from_morton_sorted(&codes, 0, 0, size);
76+
let root = Node::from_morton_sorted(&codes, &BBox::root(level));
7877
Ok(TileQuadtree { level, root })
7978
}
8079

@@ -91,59 +90,44 @@ impl TileQuadtree {
9190
}
9291

9392
impl Node {
94-
/// Recursively build a quadtree node for the cell covering
95-
/// `[x_off, x_off+size) × [y_off, y_off+size)` against the bbox
96-
/// `[bbox.x_min, bbox.x_max) × [bbox.y_min, bbox.y_max)` (all exclusive on max side).
97-
pub fn build_node(depth: u8, (x_off, y_off): (u64, u64), size: u64, bbox: &BBox) -> Node {
98-
// Intersection of bbox with this cell
99-
let ix_min = bbox.x_min.max(x_off);
100-
let iy_min = bbox.y_min.max(y_off);
101-
let ix_max = bbox.x_max.min(x_off + size);
102-
let iy_max = bbox.y_max.min(y_off + size);
103-
104-
if ix_min >= ix_max || iy_min >= iy_max {
93+
/// Recursively build a quadtree node for `cell` against `bbox` (both using
94+
/// exclusive max coordinates).
95+
pub fn build_node(cell: &BBox, bbox: &BBox) -> Node {
96+
let Some(clip) = cell.intersection(bbox) else {
10597
return Node::Empty;
106-
}
107-
108-
if ix_min == x_off && iy_min == y_off && ix_max == x_off + size && iy_max == y_off + size {
98+
};
99+
if clip.covers(cell) {
109100
return Node::Full;
110101
}
111-
112-
if depth == 0 {
113-
// We've reached leaf level — any intersection means Full
102+
// At size == 1, overlap implies coverage for tile-aligned bboxes,
103+
// so this branch is defensive — it should never fire in practice.
104+
if cell.size() == 1 {
114105
return Node::Full;
115106
}
116-
117-
let half = size / 2;
118-
let mid_x = x_off + half;
119-
let mid_y = y_off + half;
120-
Node::new_partial([
121-
Node::build_node(depth - 1, (x_off, y_off), half, bbox),
122-
Node::build_node(depth - 1, (mid_x, y_off), half, bbox),
123-
Node::build_node(depth - 1, (x_off, mid_y), half, bbox),
124-
Node::build_node(depth - 1, (mid_x, mid_y), half, bbox),
125-
])
107+
let quads = cell.quadrants();
108+
Node::new_partial(std::array::from_fn(|i| Node::build_node(&quads[i], bbox)))
126109
}
127110

128111
/// Build a `Node` from a sorted, deduplicated slice of Morton codes.
129112
///
130-
/// The slice must only contain codes for tiles within the cell
131-
/// `[x_off, x_off+size) × [y_off, y_off+size)` and be sorted ascending.
132-
/// Each quadrant split uses `partition_point` with a trivial `u64 <`
133-
/// comparison — no Morton recomputation per tile.
134-
pub(super) fn from_morton_sorted(codes: &[u64], x_off: u64, y_off: u64, size: u64) -> Node {
113+
/// The slice must only contain codes for tiles within `cell` and be sorted
114+
/// ascending. Each quadrant split uses `partition_point` with a trivial
115+
/// `u64 <` comparison — no Morton recomputation per tile.
116+
pub(super) fn from_morton_sorted(codes: &[u64], cell: &BBox) -> Node {
135117
if codes.is_empty() {
136118
return Node::Empty;
137119
}
120+
let size = cell.size();
138121
if codes.len() as u64 == size * size {
139122
return Node::Full;
140123
}
141124
if size == 1 {
142125
return Node::Full;
143126
}
144-
let half = size / 2;
145-
let mid_x = x_off + half;
146-
let mid_y = y_off + half;
127+
let quads = cell.quadrants();
128+
let (x_off, y_off) = (cell.x_min, cell.y_min);
129+
let mid_x = quads[0].x_max;
130+
let mid_y = quads[0].y_max;
147131
// Morton code of the first tile in each quadrant.
148132
let ne_base = morton(mid_x, y_off);
149133
let sw_base = morton(x_off, mid_y);
@@ -154,10 +138,10 @@ impl Node {
154138
let se_start = codes.partition_point(|&c| c < se_base);
155139

156140
Node::new_partial([
157-
Node::from_morton_sorted(&codes[..ne_start], x_off, y_off, half), // NW
158-
Node::from_morton_sorted(&codes[ne_start..sw_start], mid_x, y_off, half), // NE
159-
Node::from_morton_sorted(&codes[sw_start..se_start], x_off, mid_y, half), // SW
160-
Node::from_morton_sorted(&codes[se_start..], mid_x, mid_y, half), // SE
141+
Node::from_morton_sorted(&codes[..ne_start], &quads[0]),
142+
Node::from_morton_sorted(&codes[ne_start..sw_start], &quads[1]),
143+
Node::from_morton_sorted(&codes[sw_start..se_start], &quads[2]),
144+
Node::from_morton_sorted(&codes[se_start..], &quads[3]),
161145
])
162146
}
163147
}

versatiles_core/src/types/tile_quadtree/convert.rs

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ impl TileQuadtree {
88
pub fn to_bbox(&self) -> TileBBox {
99
self
1010
.root
11-
.bounds((0, 0), 1u64 << self.level)
11+
.bounds(&BBox::root(self.level))
1212
.map_or_else(|| TileBBox::new_empty(self.level).unwrap(), |b| b.into_bbox(self.level))
1313
}
1414

@@ -20,27 +20,19 @@ impl TileQuadtree {
2020
}
2121

2222
impl Node {
23-
/// Returns the bounding box `(x_min, y_min, x_max_excl, y_max_excl)` of non-empty tiles.
24-
pub fn bounds(&self, (x_off, y_off): (u64, u64), size: u64) -> Option<BBox> {
23+
/// Returns the bounding box of non-empty tiles within `cell`, or `None` if
24+
/// this subtree is empty.
25+
pub fn bounds(&self, cell: &BBox) -> Option<BBox> {
2526
match self {
2627
Node::Empty => None,
27-
Node::Full => Some(BBox::new(x_off, y_off, x_off + size, y_off + size)),
28+
Node::Full => Some(*cell),
2829
Node::Partial(children) => {
29-
let half = size / 2;
30-
let mid_x = x_off + half;
31-
let mid_y = y_off + half;
32-
let child_offsets = [(x_off, y_off), (mid_x, y_off), (x_off, mid_y), (mid_x, mid_y)];
33-
let mut result: Option<BBox> = None;
34-
for (i, child) in children.iter().enumerate() {
35-
let (cx, cy) = child_offsets[i];
36-
if let Some(b) = child.bounds((cx, cy), half) {
37-
result = Some(match result {
38-
None => b,
39-
Some(r) => r.union(b),
40-
});
41-
}
42-
}
43-
result
30+
let quads = cell.quadrants();
31+
children
32+
.iter()
33+
.zip(&quads)
34+
.filter_map(|(child, q)| child.bounds(q))
35+
.reduce(BBox::union)
4436
}
4537
}
4638
}

versatiles_core/src/types/tile_quadtree/include.rs

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ impl TileQuadtree {
1111
assert_eq!(self.level, coord.level);
1212
self
1313
.root
14-
.includes_coord((0, 0), 1u64 << self.level, (u64::from(coord.x), u64::from(coord.y)))
14+
.includes_coord(&BBox::root(self.level), (u64::from(coord.x), u64::from(coord.y)))
1515
}
1616

1717
/// Returns `true` if every tile in `bbox` is covered by this quadtree.
@@ -26,7 +26,7 @@ impl TileQuadtree {
2626
let Some(bbox) = BBox::from_bbox(bbox) else {
2727
return true;
2828
};
29-
self.root.includes_bbox((0, 0), 1u64 << self.level, bbox)
29+
self.root.includes_bbox(&BBox::root(self.level), &bbox)
3030
}
3131

3232
/// Returns `true` if every tile in `tree` is also covered by `self`.
@@ -61,54 +61,32 @@ impl Node {
6161

6262
/// Returns `true` if tile `(tx, ty)` is covered by this subtree.
6363
///
64-
/// `(x_off, y_off)` and `size` describe the tile-space region this node
65-
/// covers.
66-
pub fn includes_coord(&self, (x_off, y_off): (u64, u64), size: u64, (tx, ty): (u64, u64)) -> bool {
64+
/// `cell` is the tile-space region this node covers.
65+
pub fn includes_coord(&self, cell: &BBox, (tx, ty): (u64, u64)) -> bool {
6766
match self {
6867
Node::Empty => false,
6968
Node::Full => true,
7069
Node::Partial(children) => {
71-
let (idx, cx, cy, half) = Node::child_quadrant((x_off, y_off), size, (tx, ty));
72-
children[idx].includes_coord((cx, cy), half, (tx, ty))
70+
let (idx, child_cell) = Node::child_quadrant(cell, (tx, ty));
71+
children[idx].includes_coord(&child_cell, (tx, ty))
7372
}
7473
}
7574
}
7675

7776
/// Returns `true` if every tile in `bbox` is covered by this subtree.
7877
///
79-
/// `(x_off, y_off)` and `size` describe this node's tile-space region;
80-
/// `bbox` uses exclusive max coordinates.
81-
pub fn includes_bbox(&self, (x_off, y_off): (u64, u64), size: u64, bbox: BBox) -> bool {
78+
/// `cell` is this node's tile-space region; `bbox` uses exclusive max
79+
/// coordinates.
80+
pub fn includes_bbox(&self, cell: &BBox, bbox: &BBox) -> bool {
8281
match self {
8382
Node::Empty => false,
8483
Node::Full => true,
8584
Node::Partial(children) => {
86-
let half = size / 2;
87-
let mid_x = x_off + half;
88-
let mid_y = y_off + half;
89-
let child_offsets = [(x_off, y_off), (mid_x, y_off), (x_off, mid_y), (mid_x, mid_y)];
90-
for (i, child) in children.iter().enumerate() {
91-
let (cx, cy) = child_offsets[i];
92-
let cx_max = cx + half;
93-
let cy_max = cy + half;
94-
// Clip bbox against this child's region
95-
let ix_min = bbox.x_min.max(cx);
96-
let iy_min = bbox.y_min.max(cy);
97-
let ix_max = bbox.x_max.min(cx_max);
98-
let iy_max = bbox.y_max.min(cy_max);
99-
if ix_min < ix_max && iy_min < iy_max {
100-
let child_bbox = BBox {
101-
x_min: ix_min,
102-
y_min: iy_min,
103-
x_max: ix_max,
104-
y_max: iy_max,
105-
};
106-
if !child.includes_bbox((cx, cy), half, child_bbox) {
107-
return false;
108-
}
109-
}
110-
}
111-
true
85+
let quads = cell.quadrants();
86+
children
87+
.iter()
88+
.zip(&quads)
89+
.all(|(child, q)| q.intersection(bbox).is_none() || child.includes_bbox(q, bbox))
11290
}
11391
}
11492
}

0 commit comments

Comments
 (0)