Skip to content

Commit 44bf422

Browse files
Merge pull request #12 from DerLando/implement-quadtree-lookup
Implements first draft of quadtree lookup implemented via *BoundingBox*
2 parents 19c15f6 + 0e44b3b commit 44bf422

File tree

7 files changed

+625
-4
lines changed

7 files changed

+625
-4
lines changed

arcs/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ kurbo = "0.5"
2626
shred = "0.9"
2727
cgmath = "0.17.0"
2828
lazy_static = "1"
29+
aabb-quadtree = "0.2.0"
30+
quadtree_euclid = { package = "euclid", version = "0.19.8" }
2931
euclid = "0.20.7"
3032

3133
[dev-dependencies]

arcs/src/components/bounding_box.rs

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
use crate::{algorithms::Bounded, DrawingSpace, Length, Point, Vector};
22
use euclid::{num::Zero, Size2D};
33
use specs::prelude::*;
4-
use specs_derive::Component;
4+
use aabb_quadtree::{Spatial};
5+
use quadtree_euclid::{TypedRect, TypedPoint2D, TypedSize2D};
56

67
/// An axis-aligned bounding box.
7-
#[derive(Debug, Copy, Clone, PartialEq, Component)]
8-
#[storage(DenseVecStorage)]
8+
#[derive(Debug, Copy, Clone, PartialEq)]
99
pub struct BoundingBox {
1010
bottom_left: Point,
1111
top_right: Point,
1212
}
1313

14+
impl Component for BoundingBox {
15+
type Storage = FlaggedStorage<Self, DenseVecStorage<Self>>;
16+
}
17+
18+
impl Spatial<f64> for BoundingBox {
19+
fn aabb(&self) -> TypedRect<f32, f64> {
20+
let bb = self;
21+
TypedRect::<f32, f64>::new(
22+
// TypedRects have their origin at the bottom left corner (this is undocumented!)
23+
TypedPoint2D::new(bb.bottom_left().x as f32, bb.bottom_left().y as f32),
24+
TypedSize2D::new(bb.width().0 as f32, bb.height().0 as f32))
25+
}
26+
}
27+
1428
impl BoundingBox {
1529
/// Create a new [`BoundingBox`] around two points.
1630
pub fn new(first: Point, second: Point) -> Self {

arcs/src/components/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod name;
88
mod styles;
99
mod viewport;
1010
mod vtable;
11+
mod spatial_entity;
1112

1213
pub use bounding_box::BoundingBox;
1314
pub use dimension::Dimension;
@@ -17,6 +18,7 @@ pub use name::{Name, NameTable};
1718
pub use styles::{LineStyle, PointStyle, WindowStyle};
1819
pub use viewport::Viewport;
1920
pub(crate) use vtable::ComponentVtable;
21+
pub use spatial_entity::{SpatialEntity, Space};
2022

2123
use specs::World;
2224

arcs/src/components/spatial_entity.rs

+247
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
use crate::{
2+
components::BoundingBox,
3+
{Point, Arc},
4+
algorithms::{Bounded},
5+
};
6+
use specs::{Entity, world::Index};
7+
use aabb_quadtree::{QuadTree, Spatial, ItemId};
8+
use quadtree_euclid::{TypedRect, TypedPoint2D, TypedSize2D};
9+
use std::collections::HashMap;
10+
use euclid::Angle;
11+
12+
pub(crate) type SpatialTree = QuadTree<SpatialEntity, f64, [(ItemId, TypedRect<f32, f64>); 0]>;
13+
14+
/// A intermediate struct that maps an [`Entity`] to its [`BoundingBox`]
15+
///
16+
/// This is used to populate an efficient spatial lookup structure like a `QuadTree`
17+
#[derive(Debug, Copy, Clone)]
18+
pub struct SpatialEntity {
19+
pub bounds: BoundingBox,
20+
pub entity: Entity
21+
}
22+
23+
impl Spatial<f64> for SpatialEntity {
24+
fn aabb(&self) -> TypedRect<f32, f64> {
25+
let bb = self.bounds;
26+
TypedRect::<f32, f64>::new(
27+
// TypedRects have their origin at the bottom left corner (this is undocumented!)
28+
TypedPoint2D::new(bb.bottom_left().x as f32, bb.bottom_left().y as f32),
29+
TypedSize2D::new(bb.width().0 as f32, bb.height().0 as f32))
30+
}
31+
}
32+
33+
impl SpatialEntity {
34+
pub fn new(bounds: BoundingBox, entity: Entity) -> SpatialEntity {
35+
SpatialEntity {
36+
bounds,
37+
entity
38+
}
39+
}
40+
}
41+
42+
/// A global [`Resource`] for looking up which [`Entity`]s inhabit
43+
/// a given spatial point or region
44+
#[derive(Debug)]
45+
pub struct Space {
46+
quadtree: SpatialTree,
47+
ids: HashMap<Entity, ItemId>
48+
}
49+
50+
impl Default for Space {
51+
fn default() -> Self {
52+
Space {
53+
quadtree: Self::default_tree(),
54+
ids: HashMap::new()
55+
}
56+
}
57+
}
58+
59+
impl Space {
60+
// FIXME: Hard-code is bad-bad
61+
pub const WORLD_RADIUS: f64 = 1_000_000.0;
62+
const TREE_ALLOW_DUPLICATES: bool = true;
63+
const TREE_MIN_CHILDREN: usize = 4;
64+
const TREE_MAX_CHILDREN: usize = 16;
65+
const TREE_MAX_DEPTH: usize = 8;
66+
const TREE_SIZE_HINT: usize = 4;
67+
68+
fn default_tree() -> SpatialTree{
69+
// Initialize quadtree
70+
let size = BoundingBox::new(
71+
Point::new(-Self::WORLD_RADIUS, -Self::WORLD_RADIUS),
72+
Point::new(Self::WORLD_RADIUS, Self::WORLD_RADIUS)
73+
).aabb();
74+
let quadtree: SpatialTree = QuadTree::new(
75+
size,
76+
Self::TREE_ALLOW_DUPLICATES,
77+
Self::TREE_MIN_CHILDREN,
78+
Self::TREE_MAX_CHILDREN,
79+
Self::TREE_MAX_DEPTH,
80+
Self::TREE_SIZE_HINT,
81+
);
82+
83+
quadtree
84+
}
85+
86+
fn tree_with_world_size(size: impl Spatial<f64>) -> SpatialTree {
87+
let quadtree: SpatialTree = QuadTree::new(
88+
size.aabb(),
89+
Self::TREE_ALLOW_DUPLICATES,
90+
Self::TREE_MIN_CHILDREN,
91+
Self::TREE_MAX_CHILDREN,
92+
Self::TREE_MAX_DEPTH,
93+
Self::TREE_SIZE_HINT,
94+
);
95+
96+
quadtree
97+
}
98+
99+
/// Modifies the spatial position of the given [`SpatialEntity`] inside of [`Space`]
100+
/// If the [`SpatialEntity`] is not already inside of [`Space`] it will be inserted.
101+
pub fn modify(&mut self, spatial: SpatialEntity) {
102+
if !self.quadtree.bounding_box().contains_rect(&spatial.bounds.aabb()) {
103+
self.resize(spatial.bounds);
104+
}
105+
let id = if self.ids.contains_key(&spatial.entity) {
106+
self.modify_entity(spatial)
107+
}
108+
else {
109+
self.insert_entity(spatial)
110+
};
111+
// Update hashmap
112+
self.ids.entry(spatial.entity).or_insert(id);
113+
}
114+
115+
fn insert_entity(&mut self, spatial: SpatialEntity) -> ItemId {
116+
if let Some(id) = self.quadtree.insert(spatial) {
117+
id
118+
}
119+
else {
120+
panic!("ERROR: Failed to insert {:?} into Space!", self)
121+
}
122+
}
123+
124+
fn modify_entity(&mut self, spatial: SpatialEntity) -> ItemId {
125+
let item_id = self.ids[&spatial.entity];
126+
// remove old item
127+
self.quadtree.remove(item_id);
128+
129+
// Add modified
130+
self.insert_entity(spatial)
131+
}
132+
133+
/// Removes the given [`Entity`] from this [`Space`]
134+
pub fn remove(&mut self, entity: Entity) {
135+
if self.ids.contains_key(&entity) {
136+
let item_id = self.ids[&entity];
137+
138+
// remove old item
139+
self.quadtree.remove(item_id);
140+
self.ids.remove(&entity);
141+
}
142+
}
143+
144+
/// Removes an [`Entity`] from this [`Space`] given its [`Index`]
145+
pub fn remove_by_id(&mut self, id: Index) {
146+
let filter = move |(ent, _item_id): (&Entity, &ItemId)| {
147+
if ent.id() == id {
148+
Some(*ent)
149+
} else {
150+
None
151+
}
152+
};
153+
154+
if let Some(ent) = self.ids.iter().filter_map(filter).next() {
155+
self.remove(ent);
156+
}
157+
}
158+
159+
/// Returns an iterator over all [`SpatialEntity`] in this [`Space`]
160+
pub fn iter<'this>(
161+
&'this self,
162+
) -> impl Iterator<Item = SpatialEntity> + 'this {
163+
self.quadtree.iter().map(|(_, (ent, _))| *ent)
164+
}
165+
166+
pub fn len(&self) -> usize {
167+
self.quadtree.len()
168+
}
169+
170+
pub fn is_empty(&self) -> bool {
171+
self.quadtree.is_empty()
172+
}
173+
174+
// FIXME: radius in CanvasSpace in method signature
175+
/// Performs a spatial query in an radius around a given [`Point`]
176+
/// Returns an iterator with all [`SpatialEntity`] inhabiting the [`Space`]
177+
/// close to the given point
178+
/// The returned iterator can be empty
179+
pub fn query_point<'this>(
180+
&'this self, point: Point, radius: f64
181+
) -> impl Iterator<Item = SpatialEntity> + 'this {
182+
let cursor_circle = Arc::from_centre_radius(
183+
point,
184+
radius,
185+
Angle::radians(0.0),
186+
Angle::radians(2.0 * std::f64::consts::PI)
187+
);
188+
self.query_region(cursor_circle.bounding_box())
189+
}
190+
191+
/// Performs a spatial query for a given [`BoundingBox`]
192+
/// Returns an iterator with all [`SpatialEntity`] inhabiting the [`Space`]
193+
/// of the given BoundingBox
194+
/// The returned iterator can be empty
195+
pub fn query_region<'this>(
196+
&'this self, region: BoundingBox
197+
) -> impl Iterator<Item = SpatialEntity> + 'this {
198+
self.quadtree.query(region.aabb()).into_iter().map(|q| *q.0)
199+
}
200+
201+
/// Clears the [`Space`] of all [`SpatialEntity`]
202+
pub fn clear(&mut self) {
203+
// Re-use old size
204+
let size = self.quadtree.bounding_box();
205+
self.quadtree = Self::tree_with_world_size(size);
206+
self.ids.clear();
207+
}
208+
209+
/// Resizes the inner quadtree to the given **bigger** size
210+
///
211+
/// # Panics
212+
/// Panics if the size given is not bigger then the initial bounding_box of the [`Space`]
213+
pub fn resize(&mut self, size: impl Spatial<f64>) {
214+
if self.quadtree.bounding_box().contains_rect(&size.aabb()) {
215+
panic!("Space.resize() ERROR: Size to resize to is smaller then the tree!")
216+
}
217+
let spatial_entities: Vec<_> = self.iter().collect();
218+
219+
self.clear();
220+
221+
self.quadtree = Self::tree_with_world_size(size);
222+
for spatial_entity in spatial_entities {
223+
let item_id = self.insert_entity(spatial_entity);
224+
self.ids.insert(spatial_entity.entity, item_id);
225+
}
226+
}
227+
}
228+
229+
#[cfg(test)]
230+
mod tests {
231+
use crate::{
232+
components::{BoundingBox, Space},
233+
Point,
234+
};
235+
236+
#[test]
237+
fn space_should_resize() {
238+
let mut space = Space::default();
239+
assert_eq!(space.quadtree.bounding_box().max_x() as f64, Space::WORLD_RADIUS);
240+
let new_radius = 2_000_000.0;
241+
let new_size = BoundingBox::new(
242+
Point::new(-new_radius, -new_radius),
243+
Point::new(new_radius, new_radius));
244+
space.resize(new_size);
245+
assert_eq!(space.quadtree.bounding_box().max_x() as f64, new_radius);
246+
}
247+
}

arcs/src/systems/bounds.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ impl<'world> System<'world> for SyncBounds {
5757
bounds
5858
.insert(ent, drawing_object.geometry.bounding_box())
5959
.unwrap();
60-
}
60+
}
6161

6262
for (ent, _) in (&entities, &self.removed).join() {
6363
bounds.remove(ent);

arcs/src/systems/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
33
mod bounds;
44
mod name_table_bookkeeping;
5+
mod spatial_relation;
56

67
pub use bounds::SyncBounds;
78
pub use name_table_bookkeeping::NameTableBookkeeping;
9+
pub use spatial_relation::SpatialRelation;
810

911
use specs::{DispatcherBuilder, World};
1012

@@ -20,4 +22,5 @@ pub fn register_background_tasks<'a, 'b>(
2022
&[],
2123
)
2224
.with(SyncBounds::new(world), SyncBounds::NAME, &[])
25+
.with(SpatialRelation::new(world), SpatialRelation::NAME, &[SyncBounds::NAME])
2326
}

0 commit comments

Comments
 (0)