Skip to content

Commit 21a06dd

Browse files
authored
added support for overlap between trees (#8)
1 parent 0d409e3 commit 21a06dd

File tree

5 files changed

+160
-64
lines changed

5 files changed

+160
-64
lines changed

aabbtree.py

Lines changed: 135 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -431,39 +431,86 @@ def does_overlap(self, aabb, method='DFS'):
431431
This function checks if the limits overlap any leaf nodes in the tree.
432432
It returns true if there is an overlap.
433433
434+
*New in version 2.6.0*
435+
436+
This method also supports overlap checks with another instance of the
437+
AABBTree class.
438+
434439
Args:
435-
aabb (AABB): The AABB to check.
440+
aabb (AABB or AABBTree): The AABB or AABBTree to check.
436441
method (str): {'DFS'|'BFS'} Method for traversing the tree.
437442
Setting 'DFS' performs a depth-first search and 'BFS' performs
438443
a breadth-first search. Defaults to 'DFS'.
439444
440445
Returns:
441446
bool: True if overlaps with a leaf node of tree.
442447
"""
448+
if isinstance(aabb, AABB):
449+
tree = AABBTree(aabb=aabb)
450+
else:
451+
tree = aabb
452+
443453
if method == 'DFS':
454+
if self.is_leaf and tree.is_leaf:
455+
return self.aabb.overlaps(tree.aabb)
456+
444457
if self.is_leaf:
445-
return self.aabb.overlaps(aabb)
458+
left_over = tree.left.aabb.overlaps(self.aabb)
459+
right_over = tree.right.aabb.overlaps(self.aabb)
460+
461+
if left_over and tree.left.does_overlap(self, method):
462+
return True
463+
if right_over and tree.right.does_overlap(self, method):
464+
return True
465+
return False
466+
if tree.is_leaf:
467+
left_over = self.left.aabb.overlaps(tree.aabb)
468+
right_over = self.right.aabb.overlaps(tree.aabb)
469+
470+
if left_over and self.left.does_overlap(tree, method):
471+
return True
472+
if right_over and self.right.does_overlap(tree, method):
473+
return True
474+
return False
475+
476+
# If both `self` and `tree` are trees
477+
if not self.aabb.overlaps(tree.aabb):
478+
return False
446479

447-
left_aabb_over = self.left.aabb.overlaps(aabb)
448-
right_aabb_over = self.right.aabb.overlaps(aabb)
480+
left_left = self.left.aabb.overlaps(tree.left.aabb)
481+
left_right = self.left.aabb.overlaps(tree.right.aabb)
482+
right_left = self.right.aabb.overlaps(tree.left.aabb)
483+
right_right = self.right.aabb.overlaps(tree.right.aabb)
449484

450-
if left_aabb_over and self.left.does_overlap(aabb):
485+
if left_left and self.left.does_overlap(tree.left, method):
451486
return True
452-
if right_aabb_over and self.right.does_overlap(aabb):
487+
if left_right and self.left.does_overlap(tree.right, method):
488+
return True
489+
if right_left and self.right.does_overlap(tree.left, method):
490+
return True
491+
if right_right and self.right.does_overlap(tree.right, method):
453492
return True
454493
return False
455494

456495
if method == 'BFS':
457496
q = deque()
458-
q.append(self)
497+
q.append((self, tree))
459498
while len(q) > 0:
460-
node = q.popleft()
461-
overlaps = node.aabb.overlaps(aabb)
462-
if overlaps and node.is_leaf:
499+
s_node, t_node = q.popleft()
500+
overlaps = s_node.aabb.overlaps(t_node.aabb)
501+
if overlaps and s_node.is_leaf and t_node.is_leaf:
463502
return True
464-
if overlaps:
465-
q.append(node.left)
466-
q.append(node.right)
503+
if overlaps and s_node.is_leaf:
504+
q.append((s_node, t_node.left))
505+
q.append((s_node, t_node.right))
506+
elif overlaps and t_node.is_leaf:
507+
q.append((s_node.left, t_node))
508+
q.append((s_node.right, t_node))
509+
elif overlaps:
510+
q.append((s_node.left, t_node.left))
511+
q.append((s_node.left, t_node.right))
512+
q.append((s_node.right, t_node.left))
513+
q.append((s_node.right, t_node.right))
467514
return False
468515

469516
e_str = "method should be 'DFS' or 'BFS', not " + str(method)
@@ -474,88 +521,115 @@ def overlap_aabbs(self, aabb, method='DFS'):
474521
475522
This function gets each overlapping AABB.
476523
524+
*New in version 2.6.0*
525+
526+
This method also supports overlap checks with another instance of the
527+
AABBTree class.
528+
477529
Args:
478-
aabb (AABB): The AABB to check.
530+
aabb (AABB or AABBTree): The AABB or AABBTree to check.
479531
method (str): {'DFS'|'BFS'} Method for traversing the tree.
480532
Setting 'DFS' performs a depth-first search and 'BFS' performs
481533
a breadth-first search. Defaults to 'DFS'.
482534
483535
Returns:
484536
list: AABB objects in AABBTree that overlap with the input.
485537
"""
486-
aabbs = []
487-
488-
if method == 'DFS':
489-
is_leaf = self.is_leaf
490-
if is_leaf and self.does_overlap(aabb):
491-
aabbs.append(self.aabb)
492-
elif is_leaf:
493-
pass
494-
else:
495-
if self.left.aabb.overlaps(aabb):
496-
aabbs.extend(self.left.overlap_aabbs(aabb))
497-
498-
if self.right.aabb.overlaps(aabb):
499-
aabbs.extend(self.right.overlap_aabbs(aabb))
500-
elif method == 'BFS':
501-
q = deque()
502-
q.append(self)
503-
while len(q) > 0:
504-
node = q.popleft()
505-
if node.aabb.overlaps(aabb):
506-
if node.is_leaf:
507-
aabbs.append(node.aabb)
508-
else:
509-
q.append(node.left)
510-
q.append(node.right)
511-
else:
512-
e_str = "method should be 'DFS' or 'BFS', not " + str(method)
513-
raise ValueError(e_str)
514-
return aabbs
538+
pairs = self._overlap_pairs(aabb, method)
539+
if len(pairs) == 0:
540+
return []
541+
boxes, _ = zip(*pairs)
542+
return list(boxes)
515543

516544
def overlap_values(self, aabb, method='DFS'):
517545
"""Get values of overlapping AABBs
518546
519547
This function gets the value field of each overlapping AABB.
520548
549+
*New in version 2.6.0*
550+
551+
This method also supports overlap checks with another instance of the
552+
AABBTree class.
553+
521554
Args:
522-
aabb (AABB): The AABB to check.
555+
aabb (AABB or AABBTree): The AABB or AABBTree to check.
523556
method (str): {'DFS'|'BFS'} Method for traversing the tree.
524557
Setting 'DFS' performs a depth-first search and 'BFS' performs
525558
a breadth-first search. Defaults to 'DFS'.
526559
527560
Returns:
528561
list: Value fields of each node that overlaps.
529562
"""
530-
values = []
563+
pairs = self._overlap_pairs(aabb, method)
564+
if len(pairs) == 0:
565+
return []
566+
_, values = zip(*pairs)
567+
return list(values)
568+
569+
def _overlap_pairs(self, aabb, method='DFS'):
570+
"""Get overlapping AABBs and values in (AABB, value) pairs
571+
572+
*New in version 2.6.0*
573+
574+
This function gets each overlapping AABB and its value.
575+
576+
Args:
577+
aabb (AABB or AABBTree): The AABB or AABBTree to check.
578+
method (str): {'DFS'|'BFS'} Method for traversing the tree.
579+
Setting 'DFS' performs a depth-first search and 'BFS' performs
580+
a breadth-first search. Defaults to 'DFS'.
581+
582+
Returns:
583+
list: (AABB, value) pairs in AABBTree that overlap with the input.
584+
"""
585+
if isinstance(aabb, AABB):
586+
tree = AABBTree(aabb=aabb)
587+
else:
588+
tree = aabb
589+
590+
pairs = []
531591

532592
if method == 'DFS':
533-
is_leaf = self.is_leaf
534-
if is_leaf and self.does_overlap(aabb):
535-
values.append(self.value)
536-
elif is_leaf:
593+
if self.is_leaf and self.does_overlap(tree, method):
594+
pairs.append((self.aabb, self.value))
595+
elif self.is_leaf:
537596
pass
597+
elif tree.is_leaf:
598+
for branch in (self.left, self.right):
599+
pairs.extend(branch._overlap_pairs(tree, method))
538600
else:
539-
if self.left.aabb.overlaps(aabb):
540-
values.extend(self.left.overlap_values(aabb))
601+
for s_branch in (self.left, self.right):
602+
for t_branch in (tree.left, tree.right):
603+
pairs.extend(s_branch._overlap_pairs(t_branch, method))
541604

542-
if self.right.aabb.overlaps(aabb):
543-
values.extend(self.right.overlap_values(aabb))
544605
elif method == 'BFS':
545606
q = deque()
546-
q.append(self)
607+
q.append((self, tree))
547608
while len(q) > 0:
548-
node = q.popleft()
549-
if node.aabb.overlaps(aabb):
550-
if node.is_leaf:
551-
values.append(node.value)
609+
s_node, t_node = q.popleft()
610+
if s_node.aabb.overlaps(t_node.aabb):
611+
if s_node.is_leaf and t_node.is_leaf:
612+
pairs.append((s_node.aabb, s_node.value))
613+
elif s_node.is_leaf:
614+
q.append((s_node, t_node.left))
615+
q.append((s_node, t_node.right))
616+
elif t_node.is_leaf:
617+
q.append((s_node.left, t_node))
618+
q.append((s_node.right, t_node))
552619
else:
553-
q.append(node.left)
554-
q.append(node.right)
620+
q.append((s_node.left, t_node.left))
621+
q.append((s_node.left, t_node.right))
622+
q.append((s_node.right, t_node.left))
623+
q.append((s_node.right, t_node.right))
555624
else:
556625
e_str = "method should be 'DFS' or 'BFS', not " + str(method)
557626
raise ValueError(e_str)
558-
return values
627+
628+
if len(pairs) < 2:
629+
return pairs
630+
boxes, _ = zip(*pairs)
631+
u_pairs = [p for i, p in enumerate(pairs) if p[0] not in boxes[:i]]
632+
return u_pairs
559633

560634

561635
def _merge(lims1, lims2):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def read(fname):
1515

1616
setup(
1717
name='aabbtree',
18-
version='2.5.0',
18+
version='2.6.0',
1919
license='MIT',
2020
description='Pure Python implementation of d-dimensional AABB tree.',
2121
long_description=read('README.rst'),

tests/test_aabb.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,3 @@ def test_corners():
123123

124124
for c in out_corners:
125125
assert c in aabb_corners
126-

tests/test_aabbtree.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,21 +115,34 @@ def test_does_overlap():
115115
aabb6 = AABB([(0, 1), (5, 6)])
116116
aabb7 = AABB([(6.5, 6.5), (5.5, 5.5)])
117117

118+
not_tree = AABBTree()
119+
not_tree.add(aabb6)
120+
not_tree.add(aabb7)
121+
118122
for aabb in (aabb5, aabb6, aabb7):
119123
for m in ('DFS', 'BFS'):
120124
assert not AABBTree().does_overlap(aabb, method=m)
121125

122126
aabbs = standard_aabbs()
123127
for indices in itertools.permutations(range(4)):
124128
tree = AABBTree()
125-
for i in indices:
129+
alt_tree = AABBTree()
130+
for i_ind, i in enumerate(indices):
126131
tree.add(aabbs[i])
132+
alt_tree.add(aabbs[i_ind])
127133

128134
for m in ('DFS', 'BFS'):
135+
assert tree.does_overlap(tree, method=m)
136+
assert alt_tree.does_overlap(tree, method=m)
137+
assert tree.does_overlap(alt_tree, method=m)
138+
129139
assert tree.does_overlap(aabb5, method=m)
130140
assert not tree.does_overlap(aabb6, method=m)
131141
assert not tree.does_overlap(aabb7, method=m)
132142

143+
assert not tree.does_overlap(not_tree, method=m)
144+
assert not not_tree.does_overlap(tree, method=m)
145+
133146

134147
def test_overlap_aabbs():
135148
aabbs = standard_aabbs()
@@ -139,12 +152,18 @@ def test_overlap_aabbs():
139152
aabb6 = AABB([(0, 1), (5, 6)])
140153
aabb7 = AABB([(6.5, 6.5), (5.5, 5.5)])
141154

155+
not_tree = AABBTree()
156+
not_tree.add(aabb6)
157+
not_tree.add(aabb7)
158+
142159
for indices in itertools.permutations(range(4)):
143160
tree = AABBTree()
144161
for i in indices:
145162
tree.add(aabbs[i], values[i])
146163

147164
for m in ('DFS', 'BFS'):
165+
assert all([box in tree.overlap_aabbs(tree, method=m)
166+
for box in aabbs])
148167
aabbs5 = tree.overlap_aabbs(aabb5, method=m)
149168
assert len(aabbs5) == 2
150169
for aabb in aabbs5:
@@ -153,6 +172,9 @@ def test_overlap_aabbs():
153172
assert tree.overlap_aabbs(aabb6) == []
154173
assert tree.overlap_aabbs(aabb7) == []
155174

175+
assert tree.overlap_aabbs(not_tree, method=m) == []
176+
assert not_tree.overlap_aabbs(tree, method=m) == []
177+
156178
for m in ('DFS', 'BFS'):
157179
assert AABBTree(aabb5).overlap_aabbs(aabb7, method=m) == []
158180

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ commands =
3535

3636

3737
[testenv:docs]
38+
basepython = python3.7
3839
deps =
3940
matplotlib
4041
sphinx

0 commit comments

Comments
 (0)