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 11 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
14 changes: 14 additions & 0 deletions arcs/src/components/bounding_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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)]
Expand All @@ -11,6 +13,16 @@ pub struct BoundingBox {
top_right: Point,
}

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() as f32, bb.height() as f32))
}
}

impl BoundingBox {
/// Create a new [`BoundingBox`] around two points.
pub fn new(first: Point, second: Point) -> Self {
Expand Down Expand Up @@ -73,6 +85,8 @@ impl BoundingBox {

pub fn diagonal(self) -> Vector { self.top_right - self.bottom_left }

pub fn center(self) -> Vector { self.bottom_left() + self.diagonal() * 0.5 }

/// Merge two [`BoundingBox`]es.
pub fn merge(left: BoundingBox, right: BoundingBox) -> BoundingBox {
BoundingBox::new(left.bottom_left, right.top_right)
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
145 changes: 145 additions & 0 deletions arcs/src/components/spatial_entity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use crate::{
components::BoundingBox,
Vector,
primitives::{Arc},
algorithms::{Bounded},
};
use specs::{Entity, world::Index};
use aabb_quadtree::{QuadTree, Spatial, ItemId};
use quadtree_euclid::{TypedRect, TypedPoint2D, TypedSize2D};
use std::collections::HashMap;

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)]
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() as f32, bb.height() 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<Index, ItemId>
}

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

impl Space {
// FIXME: Hard-code is bad-bad
const WORLD_RADIUS: f64 = 1_000_000.0;

// FIXME: We need to supply this in *DrawingUnits*
// as 1[meter] f.e. becomes meaningless when zoomed far in/out
const QUERY_POINT_RADIUS: f64 = 1.0;

fn default_tree() -> SpatialTree{
// Initialize quadtree
let size = BoundingBox::new(
Vector::new(-Self::WORLD_RADIUS, -Self::WORLD_RADIUS),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a massive fan of telling the SpatialTree the size of the world up-front... Will the code panic if we choose the wrong number and try to place something outside of the space? And what happens if the space is a lot bigger than it needs to be, will we consume an unnecessary amount of memory?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As i commented here, the trees bounding_box is hard-coded on initialization. If we try to insert something outside of those bounds afterwards it will panic.
I'm not a fan of how the aabb_quadtree handles this and I only see 3 options:

  • Check all insertions in our API and rebuild the tree with a big enough bounding box every time (seems really inefficient)
  • Allocate a very big world size (also inefficient)
  • Use another Quadtree implementation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some more thinking on this:

  • Space needs to implement Default so we can use it as a Resource, this inevitely means we have to define some default starting parameters, as we can not supply arguments to Space::default().
  • Memory-wise we should still be quite efficient, as very large empty space in a quad-tree should take the same amount of memory as small, filled spaces
  • To be extra-safe I could write a wrapper around aabb_quadtree.insert() which detects objects outside the bounds of the tree-root and resize the tree if needed
  • Resizing basically means re-building the whole tree again, but with a large enough BoundingBox so the newly added SpatialEntity fits in

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space needs to implement Default so we can use it as a Resource, this inevitely means we have to define some default starting parameters, as we can not supply arguments to Space::default().

Could we add Space as a resource in the bookkeeping system's setup() method then use ReadExpect when trying to access it?

Resizing basically means re-building the whole tree again, but with a large enough BoundingBox so the newly added SpatialEntity fits in

That's annoying.

I was kinda hoping you could implement a resize operation by taking the quad tree's "head" node and wrapping it in a larger node, almost like the tree equivalent of adding an item to the front of a linked list. You'd be left with a really unbalanced tree (the entire previous tree is in one quadrant of the tree's head node), but it turns a resize into an O(1) operation where we just copy some pointers around.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add Space as a resource in the bookkeeping system's setup() method then use ReadExpect when trying to access it?

This seems like an unnecessary mix of two unrelated systems. So I'd rather stick to Space being it's own system

almost like the tree equivalent of adding an item to the front of a linked list.
There is nothing in the API that woudl allow for this.

It really looks like we need a better backend for our Space

Vector::new(Self::WORLD_RADIUS, Self::WORLD_RADIUS)
).aabb();
let quadtree: SpatialTree = QuadTree::new(
size,
true,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably pull these out into constants, because true, 4, 16, 8, 4 doesn't really mean anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

4,
16,
8,
4
);

quadtree
}

pub fn insert(&mut self, spatial: SpatialEntity) {
let entity_id = spatial.entity.id();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using id() here instead of the full Entity? An Entity also contains a Generation so you can't get the equivalent of a use-after-free when an entity gets deleted and specs allocates the same Index to an entity created later on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually that makes more sense!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if let Some(id) = self.quadtree.insert(spatial) {
self.ids.insert(entity_id, id);
}
}

pub fn modify(&mut self, spatial: SpatialEntity) {
let entity_id = spatial.entity.id();
if self.ids.contains_key(&entity_id) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a thought, but could we merge modify() and insert() by using the entry API?

Also, should it be an error to modify a SpatialEntity which doesn't already exist in self.ids?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind this is that we can't insert the same entity twice? If it already exists we modify it instead?

If we keep two different methods I would agree on throwing an error when trying to modify a SpatialEntity which is not already present.

It would be less explicit but probably more usage-friendly to merge those two methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I merged the two methods

let item_id = self.ids[&entity_id];

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

// Add modified
self.insert(spatial);
}
}

pub fn remove_by_id(&mut self, id: Index) {
if self.ids.contains_key(&id) {
let item_id = self.ids[&id];

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

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

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

pub fn query_point(&self, point: Vector) -> Option<Vec<Entity>> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... Should this accept a radius parameter instead of assuming the user always wants QUERY_POINT_RADIUS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's even more complicated then that:
This should accept a radius in DrawingUnit so the selection size never changes regardless of the * Zoom-Factor*.

I don't have a good idea at the moment where to wrap this functionality

let cursor_circle = Arc::from_centre_radius(point, Self::QUERY_POINT_RADIUS, 0.0, 2.0 * std::f64::consts::PI);
self.query_region(cursor_circle.bounding_box())
}

pub fn query_region(&self, region: BoundingBox) -> Option<Vec<Entity>> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the underlying quad tree provide an API for searching lazily (i.e. by returning an iterator) instead of greedily collecting into a Vec?

From my experience, a lot of the time you're only interested in the first result matching a predicate so it's a waste of time/memory to populate a vector with all objects within the region. Returning an impl Iterator<Item = SpatialEntity> would also feel a bit more idiomatic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the query function is hardcoded to return a small-vec, maybe it would be possible using the custom_query method, I can look into that.

Also +1 on impl Iterator<Item = SpatialEntity>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed return type to impl Iterator<Item = SpatialEntity>

let query = self.quadtree.query(region.aabb());

if query.is_empty() {
None
}
else {
let query_result: Vec<_> = query.iter().map(|q| q.0.entity).collect();
Some(query_result)
}
}

pub fn clear(&mut self) {
self.quadtree = Self::default_tree();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does self.quadtree have a clear() method so we can reuse any underlying allocations?

Copy link
Contributor Author

@DerLando DerLando Feb 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it has not, which is why I opted for this approach.
Is there a more efficient way?

self.ids.clear();
}
}
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, &[])
}
Loading