nationtech-website/src/space_colonization/space_colonization.rs

327 lines
11 KiB
Rust

use super::math::calculate_new_node_position;
use super::Attraction;
use super::{Attractor, Node, Point};
use log::info;
use rand::thread_rng;
use rand::Rng;
use std::collections::HashMap;
pub struct SpaceColonization {
max_point: Point,
/// When a node grows within kill_distance of an attractor, the attractor is killed
kill_distance: f64,
/// Maximum distance between an attractor and a node for the node to
/// be affected by the attractor.
///
/// Must be greater than 2 * sqrt((density)**2 + (density)**2) + kill_distance
/// The edge case that must be covered is :
///
/// - There are two attractors and one node like this :
/// ```text
/// ---------------
/// |A | |
/// | | |
/// | | |
/// ---------------
/// | | |
/// | | |
/// | | A|
/// --------------- N
/// ```
///
/// - An attractor is at the top-left corner of its cell
/// - The other attractor is at the bottom-right corner of its cell
/// - The Node is placed at the bottom-right limit of the kill distance of the bottom-right
/// attractor
/// - On the next iteration, the bottom-right attractor will be dead so the nearest attractor
/// is ( 2 * cell\_diagonal + kill\_distance ) away.
attraction_distance: i32,
segment_length: u16,
/// Size of the cells on which attractors are placed.
///
/// If density is 10, then there will be an average distance of 10 between attractors
density: i32,
pub attractors: Vec<Attractor>,
}
impl SpaceColonization {
pub fn new(width: i32, height: i32) -> SpaceColonization {
let attractors = Vec::new();
let mut sc = SpaceColonization {
max_point: Point {
x: width,
y: height,
},
kill_distance: 5.0,
attraction_distance: 100,
segment_length: 5,
density: 30,
attractors,
};
sc.place_attractors();
return sc;
}
#[cfg(test)]
pub fn new_for_tests(width: i32, height: i32, attractors: Vec<Attractor>) -> SpaceColonization {
SpaceColonization {
max_point: Point {
x: width,
y: height,
},
kill_distance: 5.0,
attraction_distance: 12,
segment_length: 3,
density: 3,
attractors,
}
}
pub fn render_nodes<F>(&self, nodes: &Vec<Node>, render_id: f64, render_fn: F)
where
F: Copy + Fn(&Node, &Node),
{
info!("Rendering {} nodes", nodes.len());
for n in nodes.iter() {
n.render(render_id, render_fn);
}
}
fn place_attractors(&mut self) {
let mut x_pos = 0;
let mut y_pos = 0;
while x_pos < self.max_point.x {
while y_pos < self.max_point.y {
self.attractors.push(Attractor::new(
self.get_random_point(x_pos.into(), y_pos.into()),
));
y_pos += self.density;
}
x_pos += self.density;
y_pos = 0;
}
}
fn get_random_point(&self, x_pos: i32, y_pos: i32) -> Point {
let half_density: i32 = (self.density / 2).into();
let mut x_min = x_pos - half_density;
if x_min < 0 {
x_min = 0;
}
let mut y_min = y_pos - half_density;
if y_min < 0 {
y_min = 0;
}
Point {
x: thread_rng()
.gen_range(x_min..x_pos + half_density)
.try_into()
.unwrap(),
y: thread_rng()
.gen_range(y_min..y_pos + half_density)
.try_into()
.unwrap(),
}
}
pub fn grow<'a>(&mut self, mut nodes: &'a mut Vec<Node>) {
// TODO
// [x] Find a clean API that will be stable across refactoring
// [ ] Write the test against this api including performance
// [x] Find a way to make a compile-time safe datastructure that will ensure that
// - I can store my attractors and their state (remove them or update them when dead)
// - I can store my nodes and their state
// - I can efficiently render my nodes on a canvas
// - I use as little memory as possible
// - I can update my nodes
self.grow_nodes(&mut nodes)
}
pub fn grow_nodes(&mut self, nodes: &mut Vec<Node>) {
// iterate through attractors
// find closest node within attraction range
// build a map of nodes to affecting attractors
// attractors within the attraction range that this node is the closest to
//
// calculate new node position
// ------------ START OF BLOCK ----------
// DO NOT MODIFY THE NODES VEC AFTER THIS
// We are taking raw pointers to Node to be dereferenced later, if the Vec of nodes is
// modified it will cause wrong behavior or segmentation faults and crash
let mut attractor_to_closest_node: HashMap<*mut Attractor, Attraction> = HashMap::new();
for n in nodes.iter_mut() {
self.build_attractor_to_closest_node(&mut attractor_to_closest_node, n);
}
let mut node_to_attractors: HashMap<*mut Node, Vec<*mut Attractor>> = HashMap::new();
attractor_to_closest_node
.drain()
.for_each(|(attractor, attraction)| {
let mut node_attractors = match node_to_attractors.remove(&attraction.node) {
Some(node_a) => node_a,
None => Vec::new(),
};
node_attractors.push(attractor);
node_to_attractors.insert(attraction.node, node_attractors);
});
let mut dead_attractors: Vec<*mut Attractor> = Vec::new();
node_to_attractors.iter().for_each(|(node, attractor)| {
// Unsafe is used here for two main reasons :
//
// PERFORMANCE : Using unsafe here allows to store multiple mutable references to a
// Node and save a few bytes of memory and cpu cycles to store a struct that holds
// a fake reference to the Node, such as the path in the tree, and then resolve it
// handling the Option<> every step of the way.
//
// Using a raw pointer we can Oh I actually just realised having a raw pointer deep
// in a tree of Vec that are getting pushed into might very well cause the pointer
// to become invalid when its parent gets pushed into and moved to another memory
// space
//
// Using raw fixed length arrays would solve that but its a fine line between too
// large memory usage and enough children nodes
unsafe {
let new_node = Node::new(calculate_new_node_position(
&(**node),
attractor,
self.segment_length,
));
attractor.iter().for_each(|a| {
if (**a).position.distance(&new_node.position) <= self.kill_distance {
dead_attractors.push(*a);
(**a).dead = true;
}
});
(**node).children.push(new_node);
}
});
}
fn build_attractor_to_closest_node<'a>(
&'a mut self,
mut attractor_to_closest_node: &mut HashMap<*mut Attractor, Attraction>,
n: &mut Node,
) {
if !n.growing {
return;
}
let attractors_in_range = self.find_attractors_in_range(&n);
if attractors_in_range.is_empty() {
n.growing = false;
return;
}
for a in attractors_in_range {
if let Some(closest) = attractor_to_closest_node.get(&a.0) {
if a.1 < closest.distance {
attractor_to_closest_node.insert(a.0, Attraction::new(n, a.1));
}
} else {
attractor_to_closest_node.insert(a.0, Attraction::new(n, a.1));
}
}
for child in n.children.iter_mut() {
self.build_attractor_to_closest_node(&mut attractor_to_closest_node, child);
}
}
fn find_attractors_in_range(&mut self, n: &Node) -> Vec<(*mut Attractor, f64)> {
let mut attractors_in_range = Vec::new();
for a in self.attractors.iter_mut() {
let distance = n.position.distance(&a.position);
if distance < self.attraction_distance as f64 {
attractors_in_range.push((a as *mut Attractor, distance));
}
}
attractors_in_range
}
}
#[cfg(test)]
mod test {
use std::cell::RefCell;
use super::*;
fn assert_vertices(
sc: &SpaceColonization,
nodes: &Vec<Node>,
mut expected_nodes: Vec<(Point, Point)>,
) {
let rendered_nodes = RefCell::new(Vec::new());
sc.render_nodes(&nodes, 0.0, |n1, n2| {
rendered_nodes.borrow_mut().push((n1.position, n2.position));
});
let sort_points = |line1: &(Point, Point), line2: &(Point, Point)| {
if line1.0 != line2.0 {
return line1.0.cmp(&line2.0);
}
return line1.1.cmp(&line2.1);
};
rendered_nodes.borrow_mut().sort_by(sort_points);
expected_nodes.sort_by(sort_points);
let rendered_nodes = rendered_nodes.take();
assert_eq!(rendered_nodes, expected_nodes);
}
#[test]
fn grow_should_reach_single_attractor_and_die() {
let mut nodes = Vec::new();
nodes.push(Node::new(Point::new((0, 0))));
let mut attractors = Vec::new();
attractors.push(Attractor::new(Point::new((10, 0))));
let mut sc = SpaceColonization::new_for_tests(100, 100, attractors);
assert_eq!(sc.attractors.len(), 1);
assert!(sc.attractors.iter().find(|a| a.dead == true).is_none());
println!("before grow");
dbg!(&nodes);
sc.grow(&mut nodes);
println!("after grow 1");
dbg!(&nodes);
assert!(sc.attractors.iter().find(|a| a.dead == true).is_none());
sc.grow(&mut nodes);
println!("after grow 2");
dbg!(&nodes);
// TODO assert nodes 3,0 and 6,0
assert_vertices(
&sc,
&nodes,
Vec::from([
(Point::new((0, 0)), Point::new((3, 0))),
(Point::new((3, 0)), Point::new((6, 0))),
]),
);
assert_eq!(
sc.attractors
.iter()
.filter(|a| a.dead == true)
.collect::<Vec<&Attractor>>()
.len(),
1
);
}
}