Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements first draft of quadtree lookup implemented via *BoundingBox* #12

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
068ec7d
Implements first draft of quadtree lookup implemented via *BoundingBox*
DerLando Jan 30, 2020
87b3c44
Adds *SpatialEntity*, a new struct which implements *Spatial*
DerLando Jan 31, 2020
81d23e0
Adds first draft of *SpatialRelation* System to populate a quadtree
DerLando Jan 31, 2020
636b502
Starts refactoring of *Space* into its own resource
DerLando Feb 1, 2020
424fea0
Starts implementing *unimplemented* in *Space*
DerLando Feb 1, 2020
b369bed
Implements *Space.query_point()*, updates tests for *SpatialRelation*
DerLando Feb 1, 2020
cae53ff
Removes test from *Space* As we do the testing inside of the *Spatial…
DerLando Feb 1, 2020
9245876
Implements *Space.query_region()*
DerLando Feb 1, 2020
8d3e831
Makes *clippy* happy
DerLando Feb 1, 2020
879109d
Preemptively circumvents newer versions of *euclid* being added in th…
DerLando Feb 1, 2020
274f1a5
Merge branch 'master' into implement-quadtree-lookup
DerLando Feb 1, 2020
29af58c
Fixes merge conflicts
DerLando Feb 1, 2020
d69af7a
Fices merge conflict for *euclid* dependency in *Cargo.toml*
DerLando Feb 1, 2020
fffac42
Reworks *query_point* and *query_region* methods of *space* into ret…
DerLando Feb 1, 2020
db32c56
Updated tests for *Space*
DerLando Feb 1, 2020
404e15f
Makes arguments to *aabb_quadtree* constructor more explicit as const…
DerLando Feb 1, 2020
bd07cd7
Merged *Space.insert()* and *Space.modify()* into one method,
DerLando Feb 1, 2020
d8597be
Changes *ReadStorage* of *SpatialRelation* to subscribe to *BoundingB…
DerLando Feb 1, 2020
1c2cb7d
Merge branch 'master' of https://github.com/Michael-F-Bryan/arcs into…
DerLando Feb 2, 2020
3133b74
Added a tree constructor which takes a size to *Space*
DerLando Feb 2, 2020
be73c1e
Added documentation for *Space*
DerLando Feb 2, 2020
c62794d
Update Tests for *Space*, fixed bug in deleting of *SpatialEntity* fr…
DerLando Feb 3, 2020
0fa5f7d
Added *Resize* method to *Space* to allow insertion of items
DerLando Feb 3, 2020
76ca85c
Merge branch 'master' of https://github.com/Michael-F-Bryan/arcs into…
DerLando Feb 3, 2020
0e44b3b
Fixed small un-needed .copy() inside of *Space.remove_by_id()*
DerLando Feb 3, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions arcs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ kurbo = "0.5"
shred = "0.9"
cgmath = "0.17.0"
lazy_static = "1"
aabb-quadtree = "0.2.0"
quadtree_euclid = { package = "euclid", version = "0.19.8" }
euclid = "0.20.7"

[dev-dependencies]
Expand Down
20 changes: 17 additions & 3 deletions arcs/src/components/bounding_box.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
use crate::{algorithms::Bounded, DrawingSpace, Length, Point, Vector};
use euclid::{num::Zero, Size2D};
use specs::prelude::*;
use specs_derive::Component;
use aabb_quadtree::{Spatial};
use quadtree_euclid::{TypedRect, TypedPoint2D, TypedSize2D};

/// An axis-aligned bounding box.
#[derive(Debug, Copy, Clone, PartialEq, Component)]
#[storage(DenseVecStorage)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct BoundingBox {
bottom_left: Point,
top_right: Point,
}

impl Component for BoundingBox {
type Storage = FlaggedStorage<Self, DenseVecStorage<Self>>;
}

impl Spatial<f64> for BoundingBox {
fn aabb(&self) -> TypedRect<f32, f64> {
let bb = self;
TypedRect::<f32, f64>::new(
// TypedRects have their origin at the bottom left corner (this is undocumented!)
TypedPoint2D::new(bb.bottom_left().x as f32, bb.bottom_left().y as f32),
TypedSize2D::new(bb.width().0 as f32, bb.height().0 as f32))
}
}

impl BoundingBox {
/// Create a new [`BoundingBox`] around two points.
pub fn new(first: Point, second: Point) -> Self {
Expand Down
2 changes: 2 additions & 0 deletions arcs/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod name;
mod styles;
mod viewport;
mod vtable;
mod spatial_entity;

pub use bounding_box::BoundingBox;
pub use dimension::Dimension;
Expand All @@ -17,6 +18,7 @@ pub use name::{Name, NameTable};
pub use styles::{LineStyle, PointStyle, WindowStyle};
pub use viewport::Viewport;
pub(crate) use vtable::ComponentVtable;
pub use spatial_entity::{SpatialEntity, Space};

use specs::World;

Expand Down
247 changes: 247 additions & 0 deletions arcs/src/components/spatial_entity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
use crate::{
components::BoundingBox,
{Point, Arc},
algorithms::{Bounded},
};
use specs::{Entity, world::Index};
use aabb_quadtree::{QuadTree, Spatial, ItemId};
use quadtree_euclid::{TypedRect, TypedPoint2D, TypedSize2D};
use std::collections::HashMap;
use euclid::Angle;

pub(crate) type SpatialTree = QuadTree<SpatialEntity, f64, [(ItemId, TypedRect<f32, f64>); 0]>;

/// A intermediate struct that maps an [`Entity`] to its [`BoundingBox`]
///
/// This is used to populate an efficient spatial lookup structure like a `QuadTree`
#[derive(Debug, Copy, Clone)]
pub struct SpatialEntity {
pub bounds: BoundingBox,
pub entity: Entity
}

impl Spatial<f64> for SpatialEntity {
fn aabb(&self) -> TypedRect<f32, f64> {
let bb = self.bounds;
TypedRect::<f32, f64>::new(
// TypedRects have their origin at the bottom left corner (this is undocumented!)
TypedPoint2D::new(bb.bottom_left().x as f32, bb.bottom_left().y as f32),
TypedSize2D::new(bb.width().0 as f32, bb.height().0 as f32))
}
}

impl SpatialEntity {
pub fn new(bounds: BoundingBox, entity: Entity) -> SpatialEntity {
SpatialEntity {
bounds,
entity
}
}
}

/// A global [`Resource`] for looking up which [`Entity`]s inhabit
/// a given spatial point or region
#[derive(Debug)]
pub struct Space {
quadtree: SpatialTree,
ids: HashMap<Entity, ItemId>
}

impl Default for Space {
fn default() -> Self {
Space {
quadtree: Self::default_tree(),
ids: HashMap::new()
}
}
}

impl Space {
// FIXME: Hard-code is bad-bad
pub const WORLD_RADIUS: f64 = 1_000_000.0;
const TREE_ALLOW_DUPLICATES: bool = true;
const TREE_MIN_CHILDREN: usize = 4;
const TREE_MAX_CHILDREN: usize = 16;
const TREE_MAX_DEPTH: usize = 8;
const TREE_SIZE_HINT: usize = 4;

fn default_tree() -> SpatialTree{
// Initialize quadtree
let size = BoundingBox::new(
Point::new(-Self::WORLD_RADIUS, -Self::WORLD_RADIUS),
Point::new(Self::WORLD_RADIUS, Self::WORLD_RADIUS)
).aabb();
let quadtree: SpatialTree = QuadTree::new(
size,
Self::TREE_ALLOW_DUPLICATES,
Self::TREE_MIN_CHILDREN,
Self::TREE_MAX_CHILDREN,
Self::TREE_MAX_DEPTH,
Self::TREE_SIZE_HINT,
);

quadtree
}

fn tree_with_world_size(size: impl Spatial<f64>) -> SpatialTree {
let quadtree: SpatialTree = QuadTree::new(
size.aabb(),
Self::TREE_ALLOW_DUPLICATES,
Self::TREE_MIN_CHILDREN,
Self::TREE_MAX_CHILDREN,
Self::TREE_MAX_DEPTH,
Self::TREE_SIZE_HINT,
);

quadtree
}

/// Modifies the spatial position of the given [`SpatialEntity`] inside of [`Space`]
/// If the [`SpatialEntity`] is not already inside of [`Space`] it will be inserted.
pub fn modify(&mut self, spatial: SpatialEntity) {
if !self.quadtree.bounding_box().contains_rect(&spatial.bounds.aabb()) {
self.resize(spatial.bounds);
}
let id = if self.ids.contains_key(&spatial.entity) {
self.modify_entity(spatial)
}
else {
self.insert_entity(spatial)
};
// Update hashmap
self.ids.entry(spatial.entity).or_insert(id);
}

fn insert_entity(&mut self, spatial: SpatialEntity) -> ItemId {
if let Some(id) = self.quadtree.insert(spatial) {
id
}
else {
panic!("ERROR: Failed to insert {:?} into Space!", self)
}
}

fn modify_entity(&mut self, spatial: SpatialEntity) -> ItemId {
let item_id = self.ids[&spatial.entity];
// remove old item
self.quadtree.remove(item_id);

// Add modified
self.insert_entity(spatial)
}

/// Removes the given [`Entity`] from this [`Space`]
pub fn remove(&mut self, entity: Entity) {
if self.ids.contains_key(&entity) {
let item_id = self.ids[&entity];

// remove old item
self.quadtree.remove(item_id);
self.ids.remove(&entity);
}
}

/// Removes an [`Entity`] from this [`Space`] given its [`Index`]
pub fn remove_by_id(&mut self, id: Index) {
let filter = move |(ent, _item_id): (&Entity, &ItemId)| {
if ent.id() == id {
Some(*ent)
} else {
None
}
};

if let Some(ent) = self.ids.iter().filter_map(filter).next() {
self.remove(ent);
}
}

/// Returns an iterator over all [`SpatialEntity`] in this [`Space`]
pub fn iter<'this>(
&'this self,
) -> impl Iterator<Item = SpatialEntity> + 'this {
self.quadtree.iter().map(|(_, (ent, _))| *ent)
}

pub fn len(&self) -> usize {
self.quadtree.len()
}

pub fn is_empty(&self) -> bool {
self.quadtree.is_empty()
}

// FIXME: radius in CanvasSpace in method signature
/// Performs a spatial query in an radius around a given [`Point`]
/// Returns an iterator with all [`SpatialEntity`] inhabiting the [`Space`]
/// close to the given point
/// The returned iterator can be empty
pub fn query_point<'this>(
&'this self, point: Point, radius: f64
) -> impl Iterator<Item = SpatialEntity> + 'this {
let cursor_circle = Arc::from_centre_radius(
point,
radius,
Angle::radians(0.0),
Angle::radians(2.0 * std::f64::consts::PI)
);
self.query_region(cursor_circle.bounding_box())
}

/// Performs a spatial query for a given [`BoundingBox`]
/// Returns an iterator with all [`SpatialEntity`] inhabiting the [`Space`]
/// of the given BoundingBox
/// The returned iterator can be empty
pub fn query_region<'this>(
&'this self, region: BoundingBox
) -> impl Iterator<Item = SpatialEntity> + 'this {
self.quadtree.query(region.aabb()).into_iter().map(|q| *q.0)
}

/// Clears the [`Space`] of all [`SpatialEntity`]
pub fn clear(&mut self) {
// Re-use old size
let size = self.quadtree.bounding_box();
self.quadtree = Self::tree_with_world_size(size);
self.ids.clear();
}

/// Resizes the inner quadtree to the given **bigger** size
///
/// # Panics
/// Panics if the size given is not bigger then the initial bounding_box of the [`Space`]
pub fn resize(&mut self, size: impl Spatial<f64>) {
if self.quadtree.bounding_box().contains_rect(&size.aabb()) {
panic!("Space.resize() ERROR: Size to resize to is smaller then the tree!")
}
let spatial_entities: Vec<_> = self.iter().collect();

self.clear();

self.quadtree = Self::tree_with_world_size(size);
for spatial_entity in spatial_entities {
let item_id = self.insert_entity(spatial_entity);
self.ids.insert(spatial_entity.entity, item_id);
}
}
}

#[cfg(test)]
mod tests {
use crate::{
components::{BoundingBox, Space},
Point,
};

#[test]
fn space_should_resize() {
let mut space = Space::default();
assert_eq!(space.quadtree.bounding_box().max_x() as f64, Space::WORLD_RADIUS);
let new_radius = 2_000_000.0;
let new_size = BoundingBox::new(
Point::new(-new_radius, -new_radius),
Point::new(new_radius, new_radius));
space.resize(new_size);
assert_eq!(space.quadtree.bounding_box().max_x() as f64, new_radius);
}
}
2 changes: 1 addition & 1 deletion arcs/src/systems/bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl<'world> System<'world> for SyncBounds {
bounds
.insert(ent, drawing_object.geometry.bounding_box())
.unwrap();
}
}

for (ent, _) in (&entities, &self.removed).join() {
bounds.remove(ent);
Expand Down
3 changes: 3 additions & 0 deletions arcs/src/systems/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

mod bounds;
mod name_table_bookkeeping;
mod spatial_relation;

pub use bounds::SyncBounds;
pub use name_table_bookkeeping::NameTableBookkeeping;
pub use spatial_relation::SpatialRelation;

use specs::{DispatcherBuilder, World};

Expand All @@ -20,4 +22,5 @@ pub fn register_background_tasks<'a, 'b>(
&[],
)
.with(SyncBounds::new(world), SyncBounds::NAME, &[])
.with(SpatialRelation::new(world), SpatialRelation::NAME, &[SyncBounds::NAME])
}
Loading