feat(space colonization): Only render new node when it is created, should improve performance instead of rerendering every node on every tick

This commit is contained in:
jeangab 2023-08-11 11:55:48 -04:00
parent 97cf5b21d9
commit c4cbef68d9
3 changed files with 90 additions and 51 deletions

View File

@ -25,19 +25,7 @@ pub fn Background(cx: Scope, class: &'static str) -> impl IntoView {
let window_height = u32::try_from(height).unwrap(); let window_height = u32::try_from(height).unwrap();
canvas.set_width(window_width); canvas.set_width(window_width);
canvas.set_height(window_height); canvas.set_height(window_height);
let mut sc = SpaceColonization::new(width.try_into().unwrap(), height.try_into().unwrap());
let nodes = Rc::new(RefCell::new(Vec::new()));
nodes.borrow_mut().push(Node::new(Point::new((
(window_width / 2) as i32,
(window_height / 2) as i32,
))));
// TODO Resize on window resize
log!(
"TODO resize on window resize canvas parent size = {} {}",
canvas_parent.client_width(),
canvas_parent.client_height()
);
log!("in canvas");
let context = canvas let context = canvas
.get_context("2d") .get_context("2d")
.ok() .ok()
@ -46,17 +34,54 @@ pub fn Background(cx: Scope, class: &'static str) -> impl IntoView {
.unchecked_into::<web_sys::CanvasRenderingContext2d>(); .unchecked_into::<web_sys::CanvasRenderingContext2d>();
log!("context = {:#?}", context); log!("context = {:#?}", context);
context.set_stroke_style(&JsValue::from("white")); context.set_stroke_style(&JsValue::from("white"));
let context_to_render = context.clone();
// let render_node_fn : 'a Fn(&Node, &Node) = |n: &Node, child: &Node| {
let render_node_fn = move |n: &Node, child: &Node| {
context_to_render.move_to(n.position.x.into(), n.position.y.into());
context_to_render.line_to(child.position.x.into(), child.position.y.into());
};
let mut sc = SpaceColonization::new(width.try_into().unwrap(), height.try_into().unwrap(), render_node_fn);
let nodes = Rc::new(RefCell::new(Vec::new()));
nodes.borrow_mut().push(Node::new(Point::new((
(window_width / 3) as i32,
(window_height / 3) as i32,
))));
nodes.borrow_mut().push(Node::new(Point::new((
(window_width / 2) as i32,
(window_height / 3) as i32,
))));
nodes.borrow_mut().push(Node::new(Point::new((
(window_width / 3) as i32,
(window_height / 2) as i32,
))));
nodes.borrow_mut().push(Node::new(Point::new((
(window_width - 200) as i32,
(window_height / 3) as i32,
))));
nodes.borrow_mut().push(Node::new(Point::new((
(window_width - 100) as i32,
(window_height - 100) as i32,
))));
// TODO Resize on window resize
log!(
"TODO resize on window resize canvas parent size = {} {}",
canvas_parent.client_width(),
canvas_parent.client_height()
);
context.set_stroke_style(&JsValue::from("white"));
context.set_fill_style(&JsValue::from("yellow")); context.set_fill_style(&JsValue::from("yellow"));
log!("About to render nodes"); log!("About to render nodes");
let start_time = window().unwrap().performance().unwrap().now(); let start_time = window().unwrap().performance().unwrap().now();
context.begin_path(); context.begin_path();
/*
let render_node_fn = |n: &Node, child: &Node| { let render_node_fn = |n: &Node, child: &Node| {
context.move_to(n.position.x.into(), n.position.y.into()); context.move_to(n.position.x.into(), n.position.y.into());
context.line_to(child.position.x.into(), child.position.y.into()); context.line_to(child.position.x.into(), child.position.y.into());
}; };
let render_id = window().unwrap().performance().unwrap().now(); */
sc.render_nodes(&nodes.borrow(), render_id, render_node_fn); // let render_id = window().unwrap().performance().unwrap().now();
// sc.render_all_nodes(&nodes.borrow(), render_id, render_node_fn);
context.stroke(); context.stroke();
let end_time = window().unwrap().performance().unwrap().now(); let end_time = window().unwrap().performance().unwrap().now();
@ -68,15 +93,12 @@ pub fn Background(cx: Scope, class: &'static str) -> impl IntoView {
let context = context.clone(); let context = context.clone();
let closure = Closure::<dyn FnMut(_)>::new(move |_: web_sys::MouseEvent| { let closure = Closure::<dyn FnMut(_)>::new(move |_: web_sys::MouseEvent| {
let start_time = window().unwrap().performance().unwrap().now(); let start_time = window().unwrap().performance().unwrap().now();
let render_node_fn = |n: &Node, child: &Node| {
context.move_to(n.position.x.into(), n.position.y.into());
context.line_to(child.position.x.into(), child.position.y.into());
};
sc.grow(&mut nodes.borrow_mut()); sc.grow(&mut nodes.borrow_mut());
let render_id = window().unwrap().performance().unwrap().now(); // let render_id = window().unwrap().performance().unwrap().now();
context.begin_path(); // context.begin_path();
sc.render_nodes(&nodes.borrow(), render_id, render_node_fn); // sc.render_nodes(&nodes.borrow(), render_id, render_node_fn);
/*
context.set_fill_style(&JsValue::from("magenta")); context.set_fill_style(&JsValue::from("magenta"));
for a in sc.attractors.iter().filter(|a| a.dead) { for a in sc.attractors.iter().filter(|a| a.dead) {
context.fill_rect(a.position.x.into(), a.position.y.into(), 5.0, 5.0); context.fill_rect(a.position.x.into(), a.position.y.into(), 5.0, 5.0);
@ -85,6 +107,7 @@ pub fn Background(cx: Scope, class: &'static str) -> impl IntoView {
for a in sc.attractors.iter().filter(|a| !a.dead) { for a in sc.attractors.iter().filter(|a| !a.dead) {
context.fill_rect(a.position.x.into(), a.position.y.into(), 5.0, 5.0); context.fill_rect(a.position.x.into(), a.position.y.into(), 5.0, 5.0);
} }
*/
context.stroke(); context.stroke();
let end_time = window().unwrap().performance().unwrap().now(); let end_time = window().unwrap().performance().unwrap().now();
log!( log!(
@ -96,7 +119,10 @@ pub fn Background(cx: Scope, class: &'static str) -> impl IntoView {
let window = window().unwrap(); let window = window().unwrap();
window window
.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()) .set_interval_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
10,
)
.unwrap(); .unwrap();
closure.forget(); closure.forget();
}); });

View File

@ -62,9 +62,9 @@ impl std::hash::Hash for Node {
} }
impl Node { impl Node {
pub fn render<F>(&self, render_id: f64, render_fn: F) pub fn render<G>(&self, render_id: f64, render_fn: G)
where where
F: Copy + Fn(&Node, &Node), G: Copy + Fn(&Node, &Node),
{ {
for child in self.children.iter() { for child in self.children.iter() {
render_fn(self, &child); render_fn(self, &child);

View File

@ -6,7 +6,10 @@ use rand::thread_rng;
use rand::Rng; use rand::Rng;
use std::collections::HashMap; use std::collections::HashMap;
pub struct SpaceColonization { pub struct SpaceColonization<F>
where
F: Fn(&Node, &Node),
{
max_point: Point, max_point: Point,
/// When a node grows within kill_distance of an attractor, the attractor is killed /// When a node grows within kill_distance of an attractor, the attractor is killed
kill_distance: f64, kill_distance: f64,
@ -42,10 +45,17 @@ pub struct SpaceColonization {
/// If density is 10, then there will be an average distance of 10 between attractors /// If density is 10, then there will be an average distance of 10 between attractors
density: i32, density: i32,
pub attractors: Vec<Attractor>, pub attractors: Vec<Attractor>,
render_fn: F,
} }
impl SpaceColonization { impl<F> SpaceColonization<F>
pub fn new(width: i32, height: i32) -> SpaceColonization { where
F: Fn(&Node, &Node),
{
pub fn new(width: i32, height: i32, render_fn: F) -> SpaceColonization<F>
where
F: Fn(&Node, &Node),
{
let attractors = Vec::new(); let attractors = Vec::new();
let mut sc = SpaceColonization { let mut sc = SpaceColonization {
@ -58,6 +68,7 @@ impl SpaceColonization {
segment_length: 5, segment_length: 5,
density: 30, density: 30,
attractors, attractors,
render_fn,
}; };
sc.place_attractors(); sc.place_attractors();
@ -66,7 +77,15 @@ impl SpaceColonization {
} }
#[cfg(test)] #[cfg(test)]
pub fn new_for_tests(width: i32, height: i32, attractors: Vec<Attractor>) -> SpaceColonization { pub fn new_for_tests(
width: i32,
height: i32,
attractors: Vec<Attractor>,
render_fn: F,
) -> SpaceColonization<F>
where
F: Fn(&Node, &Node),
{
SpaceColonization { SpaceColonization {
max_point: Point { max_point: Point {
x: width, x: width,
@ -77,12 +96,13 @@ impl SpaceColonization {
segment_length: 3, segment_length: 3,
density: 3, density: 3,
attractors, attractors,
render_fn,
} }
} }
pub fn render_nodes<F>(&self, nodes: &Vec<Node>, render_id: f64, render_fn: F) pub fn render_all_nodes<G>(&self, nodes: &Vec<Node>, render_id: f64, render_fn: G)
where where
F: Copy + Fn(&Node, &Node), G: Copy + Fn(&Node, &Node),
{ {
info!("Rendering {} nodes", nodes.len()); info!("Rendering {} nodes", nodes.len());
for n in nodes.iter() { for n in nodes.iter() {
@ -129,16 +149,7 @@ impl SpaceColonization {
} }
} }
pub fn grow<'a>(&mut self, mut nodes: &'a mut Vec<Node>) { pub fn grow<'b>(&mut self, mut nodes: &'b 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
info!("Growing nodes {:?}", nodes); info!("Growing nodes {:?}", nodes);
self.grow_nodes(&mut nodes) self.grow_nodes(&mut nodes)
} }
@ -211,14 +222,15 @@ impl SpaceColonization {
(**a).dead = true; (**a).dead = true;
} }
}); });
(self.render_fn)(&**node, &new_node);
(**node).children.push(new_node); (**node).children.push(new_node);
} }
} }
}); });
} }
fn build_attractor_to_closest_node<'a>( fn build_attractor_to_closest_node<'b>(
&'a mut self, &'b mut self,
mut attractor_to_closest_node: &mut HashMap<*mut Attractor, Attraction>, mut attractor_to_closest_node: &mut HashMap<*mut Attractor, Attraction>,
n: &mut Node, n: &mut Node,
) { ) {
@ -246,7 +258,6 @@ impl SpaceColonization {
attractor_to_closest_node.insert(a.0, Attraction::new(n, a.1)); attractor_to_closest_node.insert(a.0, Attraction::new(n, a.1));
} }
} }
} }
fn find_attractors_in_range(&mut self, n: &Node) -> Vec<(*mut Attractor, f64)> { fn find_attractors_in_range(&mut self, n: &Node) -> Vec<(*mut Attractor, f64)> {
@ -267,13 +278,15 @@ mod test {
use super::*; use super::*;
fn assert_vertices( fn assert_vertices<'a, F>(
sc: &SpaceColonization, sc: &SpaceColonization<F>,
nodes: &Vec<Node>, nodes: &'a Vec<Node>,
mut expected_nodes: Vec<(Point, Point)>, mut expected_nodes: Vec<(Point, Point)>,
) { ) where
F: Copy + Fn(&Node, &Node),
{
let rendered_nodes = RefCell::new(Vec::new()); let rendered_nodes = RefCell::new(Vec::new());
sc.render_nodes(&nodes, 0.0, |n1, n2| { sc.render_all_nodes(&nodes, 0.0, |n1: &Node, n2: &Node| {
rendered_nodes.borrow_mut().push((n1.position, n2.position)); rendered_nodes.borrow_mut().push((n1.position, n2.position));
}); });
let sort_points = |line1: &(Point, Point), line2: &(Point, Point)| { let sort_points = |line1: &(Point, Point), line2: &(Point, Point)| {
@ -298,7 +311,7 @@ mod test {
let mut attractors = Vec::new(); let mut attractors = Vec::new();
attractors.push(Attractor::new(Point::new((10, 0)))); attractors.push(Attractor::new(Point::new((10, 0))));
let mut sc = SpaceColonization::new_for_tests(100, 100, attractors); let mut sc = SpaceColonization::new_for_tests(100, 100, attractors, |_, _|{});
assert_eq!(sc.attractors.len(), 1); assert_eq!(sc.attractors.len(), 1);
assert!(sc.attractors.iter().find(|a| a.dead == true).is_none()); assert!(sc.attractors.iter().find(|a| a.dead == true).is_none());
@ -344,7 +357,7 @@ mod test {
dead: true, dead: true,
}); });
let mut sc = SpaceColonization::new_for_tests(100, 100, attractors); let mut sc = SpaceColonization::new_for_tests(100, 100, attractors, |_, _|{});
assert_eq!(sc.attractors.len(), 1); assert_eq!(sc.attractors.len(), 1);
assert!(sc.attractors.iter().find(|a| a.dead == true).is_some()); assert!(sc.attractors.iter().find(|a| a.dead == true).is_some());