4
\$\begingroup\$

I am learning Rust, coming from a Java background, and I was hoping to get some critique on my simple QuadTree implementation in Rust. This is as a learning exercise for me.

Are there tips on how to make it more idiomatic? Are there features of Rust I am overlooking?

// common.rs
#[derive(Copy, Clone, Debug)]
pub struct Point2D {
    pub x: f32,
    pub y: f32
}

impl Point2D {

    #[wasm_bindgen(constructor)]
    pub fn new(x: f32, y: f32) -> Self {
        Point2D { x, y }
    }

    pub fn zero() -> Self {
        Point2D::new(0.0, 0.0 )
    }

    pub fn offset(&self, dx: f32, dy: f32) -> Self {
        Point2D::new(self.x + dx, self.y + dy)
    }
}

#[derive(Debug)]
pub struct Rectangle {
    pub p0: Point2D,
    pub width: f32,
    pub height: f32
}

impl Rectangle {

    #[wasm_bindgen(constructor)]
    pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
        Rectangle {
            p0: Point2D::new(x, y),
            width,
            height
        }
    }

    pub fn contains(&self, point: &Point2D) -> bool {
        if point.x < self.p0.x || point.x > self.p0.x + self.width {
            return false
        }
        if point.y < self.p0.y || point.y > self.p0.y + self.height {
            return false
        }
        true
    }

    pub fn intersects(&self, other: &Rectangle) -> bool {
        let l1 = self.p0;
        let r1 = self.p0.offset(self.width, self.height);
        let l2 = other.p0;
        let r2 = other.p0.offset(other.width, other.height);

        if l1.x > r2.x || l2.x > r1.x {
            return false
        }

        if l1.y > r2.y || l2.y > r1.y {
            return false
        }
        return true
    }
}
// quad_tree.rs
use std::mem;
use crate::common::*;

#[derive(Debug, Eq, PartialEq)]
pub enum QuadTreeResult {
    Ok,
    Err
}

pub struct QuadTree {
    boundary: Rectangle,
    points: Vec<Point2D>,
    north_east: Option<Box<QuadTree>>,
    south_east: Option<Box<QuadTree>>,
    south_west: Option<Box<QuadTree>>,
    north_west: Option<Box<QuadTree>>
}

impl QuadTree {

    const MAX_CAPACITY: usize = 4;

    pub fn new(p0: Point2D, width: f32, height: f32) -> Self {
        QuadTree {
            boundary: Rectangle {
                p0,
                width,
                height
            },
            points: Vec::new(),
            north_east: None,
            south_east: None,
            south_west: None,
            north_west: None
        }
    }

    pub fn insert(&mut self, point: Point2D) -> QuadTreeResult {
        if !self.boundary.contains(&point) {
            return QuadTreeResult::Err
        }

        if self.points.len() < QuadTree::MAX_CAPACITY && self.is_leaf() {
            self.points.push(point);
            return QuadTreeResult::Ok
        }

        if self.points.len() >= QuadTree::MAX_CAPACITY || !self.is_leaf() {
            self.subdivide();

            if self.north_east.as_mut().unwrap().boundary.contains(&point) {
                return self.north_east.as_mut().unwrap().insert(point)
            } else if self.south_east.as_mut().unwrap().boundary.contains(&point) {
                return self.south_east.as_mut().unwrap().insert(point)
            } else if self.south_west.as_mut().unwrap().boundary.contains(&point) {
                return self.south_west.as_mut().unwrap().insert(point)
            } else {
                return self.north_west.as_mut().unwrap().insert(point)
            }
        }
        QuadTreeResult::Err
    }

    fn subdivide(&mut self) -> QuadTreeResult {
        if self.is_leaf() {
            let p0 = &self.boundary.p0;
            let new_width = self.boundary.width / 2.0;
            let new_height = self.boundary.height / 2.0;

            self.north_east = Some(Box::new(QuadTree::new(p0.offset(new_width, 0.0), new_width, new_height)));
            self.south_east = Some(Box::new(QuadTree::new(p0.offset(new_width, new_height), new_width, new_height)));
            self.south_west = Some(Box::new(QuadTree::new(p0.offset(0.0, new_height), new_width, new_height)));
            self.north_west = Some(Box::new(QuadTree::new(p0.offset(0.0, 0.0), new_width, new_height)));

            let old_points = mem::replace(&mut self.points, Vec::new());
            for p in old_points {
                if let QuadTreeResult::Err = self.insert(p) {
                    return QuadTreeResult::Err
                }
            }
        }
        QuadTreeResult::Ok
    }

    pub fn query(&self, range: &Rectangle) -> Vec<Point2D> {
        let mut result = Vec::new();

        if !self.boundary.intersects(range) {
            return result;
        }

        if self.is_leaf() {
            for p in &self.points {
                if range.contains(p) {
                    result.push(*p)
                }
            }
        } else {
            result.extend(self.north_east.as_ref().unwrap().query(range));
            result.extend(self.south_east.as_ref().unwrap().query(range));
            result.extend(self.south_west.as_ref().unwrap().query(range));
            result.extend(self.north_west.as_ref().unwrap().query(range));
        }
        return result
    }

    fn is_leaf(&self) -> bool {
        return self.north_east.is_none() && self.south_east.is_none() && self.south_west.is_none() && self.north_west.is_none()
    }
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Just a very general observation: the position of a rectangle is not fundamental to the rectangle itself, but rather a to the instantiation of a rectangle in a scene. In your implementation making it a property of Rectangle might make sense, but pulling it out makes the implementation more flexible and I suspect might make it easier to read when it grows. For one thing, a rectangle might be placed in a 3D scene, a non-Euclidean space, or might be used without reference to any particular reference frame (such as when comparing the area of two rectangles). Pulling out the position also makes it possible to reuse an instance in a bunch of places. In the following image, for example, all the equally sized squares could refer to the same struct, saving memory in case of large trees.

quadtree example

Also, I don't think QuadTreeResult is helpful - it doesn't add anything to Result, and I don't think being able to distinguish them in your code is useful. It's a bit like trivially subclassing java.lang.Boolean (as opposed to trivially subclassing java.lang.RuntimeException, which is idiomatic).

\$\endgroup\$
2
  • \$\begingroup\$ Thanks! Nice observations and they make a lot of sense. I will change my implementation. \$\endgroup\$ Sep 13, 2019 at 14:15
  • \$\begingroup\$ I am also learning Rust and was wondering about quadtrees. I don't completely understand what you mean with "pulling it out" i.e. the position of a rectangle of its implementation. Do you mean that each quadtree also saves the positions of each child-rectangle, but not the child-rectangle itself? But then the rectangles would still have to save their sizes, right? \$\endgroup\$
    – exocortex
    Feb 10, 2022 at 12:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.