From d6df7dd1567ed88238bc214d4c103b5b8f8e119e Mon Sep 17 00:00:00 2001 From: Danilo Guanabara Date: Fri, 6 Dec 2019 13:11:17 -0300 Subject: [PATCH] HTMLRenderer Benchmarks (https://github.com/enso-org/ide/pull/58) * Custom benchmark solution * Optimized HTMLRenderer Original commit: https://github.com/enso-org/ide/commit/8a147bf9d4f6cd4e33e221109480ac1303626955 --- gui/lib/core/Cargo.toml | 1 + gui/lib/core/js/html_renderer.js | 21 ++ gui/lib/core/src/data/opt_vec.rs | 31 +++ gui/lib/core/src/display/mod.rs | 2 +- gui/lib/core/src/display/rendering/camera.rs | 6 +- .../src/display/rendering/dom_container.rs | 99 +++++++ .../display/rendering/graphics_renderer.rs | 42 +++ .../{htmlobject.rs => html/html_object.rs} | 30 +-- .../display/rendering/html/html_renderer.rs | 184 +++++++++++++ .../core/src/display/rendering/html/mod.rs | 5 + .../src/display/rendering/htmlrenderer.rs | 53 ---- .../core/src/display/rendering/htmlscene.rs | 76 ------ gui/lib/core/src/display/rendering/mod.rs | 26 +- gui/lib/core/src/display/rendering/object.rs | 34 ++- .../core/src/display/rendering/renderer.rs | 10 - gui/lib/core/src/display/rendering/scene.rs | 63 +++-- .../core/src/display/rendering/transform.rs | 37 ++- gui/lib/core/src/math/utils.rs | 83 +++++- gui/lib/core/tests/bench_test.js | 6 + gui/lib/core/tests/common.rs | 89 ------- gui/lib/core/tests/html_renderer.rs | 251 ++++++++++++++++++ gui/lib/core/tests/htmlrenderer.rs | 126 --------- gui/lib/system/web/Cargo.toml | 3 +- gui/lib/system/web/src/animationframeloop.rs | 60 +++++ gui/lib/system/web/src/lib.rs | 143 ++++++---- gui/lib/web-test-proc-macro/Cargo.toml | 14 + gui/lib/web-test-proc-macro/src/lib.rs | 109 ++++++++ gui/lib/web-test/Cargo.toml | 19 ++ gui/lib/web-test/src/bench_container.rs | 62 +++++ gui/lib/web-test/src/bencher.rs | 133 ++++++++++ gui/lib/web-test/src/container.rs | 56 ++++ gui/lib/web-test/src/group.rs | 57 ++++ gui/lib/web-test/src/lib.rs | 21 ++ gui/tests/web.rs | 13 - 34 files changed, 1464 insertions(+), 501 deletions(-) create mode 100644 gui/lib/core/js/html_renderer.js create mode 100644 gui/lib/core/src/display/rendering/dom_container.rs create mode 100644 gui/lib/core/src/display/rendering/graphics_renderer.rs rename gui/lib/core/src/display/rendering/{htmlobject.rs => html/html_object.rs} (66%) create mode 100644 gui/lib/core/src/display/rendering/html/html_renderer.rs create mode 100644 gui/lib/core/src/display/rendering/html/mod.rs delete mode 100644 gui/lib/core/src/display/rendering/htmlrenderer.rs delete mode 100644 gui/lib/core/src/display/rendering/htmlscene.rs delete mode 100644 gui/lib/core/src/display/rendering/renderer.rs create mode 100644 gui/lib/core/tests/bench_test.js delete mode 100644 gui/lib/core/tests/common.rs create mode 100644 gui/lib/core/tests/html_renderer.rs delete mode 100644 gui/lib/core/tests/htmlrenderer.rs create mode 100644 gui/lib/system/web/src/animationframeloop.rs create mode 100644 gui/lib/web-test-proc-macro/Cargo.toml create mode 100644 gui/lib/web-test-proc-macro/src/lib.rs create mode 100644 gui/lib/web-test/Cargo.toml create mode 100644 gui/lib/web-test/src/bench_container.rs create mode 100644 gui/lib/web-test/src/bencher.rs create mode 100644 gui/lib/web-test/src/container.rs create mode 100644 gui/lib/web-test/src/group.rs create mode 100644 gui/lib/web-test/src/lib.rs delete mode 100644 gui/tests/web.rs diff --git a/gui/lib/core/Cargo.toml b/gui/lib/core/Cargo.toml index 2f3f67ce0f..70d77a871c 100644 --- a/gui/lib/core/Cargo.toml +++ b/gui/lib/core/Cargo.toml @@ -57,3 +57,4 @@ features = [ [dev-dependencies] wasm-bindgen-test = "0.3.3" +web-test = { version = "0.1.0" , path = "../web-test" } diff --git a/gui/lib/core/js/html_renderer.js b/gui/lib/core/js/html_renderer.js new file mode 100644 index 0000000000..b2a805b5e6 --- /dev/null +++ b/gui/lib/core/js/html_renderer.js @@ -0,0 +1,21 @@ +function arr_to_css_matrix3d(a) { + return "matrix3d(" + a.join(',') + ")" +} + +export function set_object_transform(dom, matrix_array) { + let css = arr_to_css_matrix3d(matrix_array); + dom.style.transform = "translate(-50%, -50%)" + css; +} + +export function setup_perspective(dom, znear) { + dom.style.perspective = znear + "px"; +} + +export function setup_camera_transform +(dom, znear, half_width, half_height, matrix_array) { + let translateZ = "translateZ(" + znear + "px)"; + let matrix3d = arr_to_css_matrix3d(matrix_array); + let translate2d = "translate(" + half_width + "px, " + half_height + "px)"; + let transform = translateZ + matrix3d + translate2d; + dom.style.transform = transform; +} diff --git a/gui/lib/core/src/data/opt_vec.rs b/gui/lib/core/src/data/opt_vec.rs index dfbd445238..c1768f6ea4 100644 --- a/gui/lib/core/src/data/opt_vec.rs +++ b/gui/lib/core/src/data/opt_vec.rs @@ -115,6 +115,14 @@ impl<'a, T> IntoIterator for &'a OptVec { } } +impl<'a, T> IntoIterator for &'a mut OptVec { + type Item = &'a mut T; + type IntoIter = IterMut<'a, T>; + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + #[cfg(test)] mod tests { use super::*; @@ -169,4 +177,27 @@ mod tests { assert_eq!(i + 1, *value); } } + + #[test] + fn test_iter_mut() { + let mut v = OptVec::new(); + + let ix1 = v.insert(0); + let _ix2 = v.insert(1); + let _ix3 = v.insert(2); + + assert_eq!(v.len(), 3, "OptVec should have 3 items"); + + v.remove(ix1); + + assert_eq!(v.len(), 2, "OptVec should have 2 items"); + + for value in &mut v { + *value *= 2; + } + + for (i, value) in v.into_iter().enumerate() { + assert_eq!((i + 1) * 2, *value); + } + } } diff --git a/gui/lib/core/src/display/mod.rs b/gui/lib/core/src/display/mod.rs index ae010b2d33..e4b7259c7c 100644 --- a/gui/lib/core/src/display/mod.rs +++ b/gui/lib/core/src/display/mod.rs @@ -1,5 +1,5 @@ pub mod mesh_registry; -pub mod rendering; pub mod symbol; pub mod workspace; pub mod world; +pub mod rendering; \ No newline at end of file diff --git a/gui/lib/core/src/display/rendering/camera.rs b/gui/lib/core/src/display/rendering/camera.rs index 252ff5eb29..74fc95b2b9 100644 --- a/gui/lib/core/src/display/rendering/camera.rs +++ b/gui/lib/core/src/display/rendering/camera.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -use super::Object; +use crate::display::rendering::Object; use nalgebra::base::Matrix4; use nalgebra::geometry::Perspective3; @@ -22,13 +22,15 @@ pub struct Camera { impl Camera { /// Creates a Camera with perspective projection. - pub fn perspective(fov: f32, aspect: f32, z_near: f32, z_far: f32) -> Self { + pub fn perspective(fov:f32, aspect:f32, z_near:f32, z_far:f32) -> Self { let fov = fov / 180.0 * PI; let projection = Perspective3::new(aspect, fov, z_near, z_far); let projection = *projection.as_matrix(); let object = default(); Self { object, projection } } + + pub fn get_y_scale(&self) -> f32 { self.projection.m11 } } #[cfg(test)] diff --git a/gui/lib/core/src/display/rendering/dom_container.rs b/gui/lib/core/src/display/rendering/dom_container.rs new file mode 100644 index 0000000000..4e33296672 --- /dev/null +++ b/gui/lib/core/src/display/rendering/dom_container.rs @@ -0,0 +1,99 @@ +use crate::prelude::*; + +use crate::system::web::get_element_by_id; +use crate::system::web::dyn_into; +use crate::system::web::Result; +use crate::system::web::StyleSetter; +use crate::system::web::resize_observer::ResizeObserver; + +use wasm_bindgen::prelude::Closure; +use web_sys::HtmlElement; +use nalgebra::Vector2; +use std::cell::RefCell; +use std::rc::Rc; + + +// ====================== +// === ResizeCallback === +// ====================== + +type ResizeCallback = Box)>; +pub trait ResizeCallbackFn = where Self: Fn(&Vector2) + 'static; + +// ======================== +// === DOMContainerData === +// ======================== + +#[derive(Derivative)] +#[derivative(Debug)] +pub struct DOMContainerData { + dimensions : Vector2, + #[derivative(Debug="ignore")] + resize_callbacks : Vec +} + +impl DOMContainerData { + pub fn new(dimensions : Vector2) -> Self { + let resize_callbacks = default(); + Self { dimensions, resize_callbacks } + } +} + + +// ==================== +// === DOMContainer === +// ==================== + +/// A collection for holding 3D `Object`s. +#[derive(Debug)] +pub struct DOMContainer { + pub dom : HtmlElement, + resize_observer : Option, + data : Rc>, +} + +impl DOMContainer { + pub fn new(dom_id:&str) -> Result { + let dom : HtmlElement = dyn_into(get_element_by_id(dom_id)?)?; + + let width = dom.client_width() as f32; + let height = dom.client_height() as f32; + let dimensions = Vector2::new(width, height); + let data = Rc::new(RefCell::new(DOMContainerData::new(dimensions))); + let resize_observer = None; + let mut ret = Self { dom, resize_observer, data }; + + ret.init_listeners(); + Ok(ret) + } + + fn init_listeners(&mut self) { + let data = self.data.clone(); + let resize_closure = Closure::new(move |width, height| { + let mut data = data.borrow_mut(); + data.dimensions = Vector2::new(width as f32, height as f32); + for callback in &data.resize_callbacks { + callback(&data.dimensions); + } + }); + self.resize_observer = Some(ResizeObserver::new(&self.dom, resize_closure)); + } + + /// Sets the Scene DOM's dimensions. + pub fn set_dimensions(&mut self, dimensions:Vector2) { + self.dom.set_property_or_panic("width" , format!("{}px", dimensions.x)); + self.dom.set_property_or_panic("height", format!("{}px", dimensions.y)); + self.data.borrow_mut().dimensions = dimensions; + } + + /// Gets the Scene DOM's dimensions. + pub fn dimensions(&self) -> Vector2 { + self.data.borrow().dimensions + } + + /// Adds a ResizeCallback. + pub fn add_resize_callback(&mut self, callback:T) + where T : ResizeCallbackFn { + self.data.borrow_mut().resize_callbacks.push(Box::new(callback)); + } +} \ No newline at end of file diff --git a/gui/lib/core/src/display/rendering/graphics_renderer.rs b/gui/lib/core/src/display/rendering/graphics_renderer.rs new file mode 100644 index 0000000000..589773b8b2 --- /dev/null +++ b/gui/lib/core/src/display/rendering/graphics_renderer.rs @@ -0,0 +1,42 @@ +// use super::Camera; +use crate::prelude::*; +use super::DOMContainer; +use super::ResizeCallbackFn; +use crate::system::web::Result; +use crate::system::web::StyleSetter; + +use nalgebra::Vector2; + +// ======================== +// === GraphicsRenderer === +// ======================== + +/// Base structure for our Renderers. +#[derive(Debug)] +pub struct GraphicsRenderer { + pub container : DOMContainer +} + +impl GraphicsRenderer { + pub fn new(dom_id: &str) -> Result { + let container = DOMContainer::new(dom_id)?; + container.dom.set_property_or_panic("overflow", "hidden"); + Ok(Self { container }) + } + + /// Sets the Scene Renderer's dimensions. + pub fn set_dimensions(&mut self, dimensions : Vector2) { + self.container.set_dimensions(dimensions); + } + + /// Gets the Scene Renderer's dimensions. + pub fn dimensions(&self) -> Vector2 { + self.container.dimensions() + } + + /// Adds a ResizeCallback. + pub fn add_resize_callback(&mut self, callback : T) + where T : ResizeCallbackFn { + self.container.add_resize_callback(callback); + } +} diff --git a/gui/lib/core/src/display/rendering/htmlobject.rs b/gui/lib/core/src/display/rendering/html/html_object.rs similarity index 66% rename from gui/lib/core/src/display/rendering/htmlobject.rs rename to gui/lib/core/src/display/rendering/html/html_object.rs index 99de612e19..e45e389578 100644 --- a/gui/lib/core/src/display/rendering/htmlobject.rs +++ b/gui/lib/core/src/display/rendering/html/html_object.rs @@ -1,12 +1,12 @@ use crate::prelude::*; -use super::Object; - +use crate::display::rendering::Object; use crate::system::web::create_element; use crate::system::web::dyn_into; use crate::system::web::Result; use crate::system::web::Error; use crate::system::web::StyleSetter; + use nalgebra::Vector2; use web_sys::HtmlElement; @@ -20,26 +20,26 @@ use web_sys::HtmlElement; pub struct HTMLObject { #[shrinkwrap(main_field)] pub object : Object, - pub element : HtmlElement, - pub dimensions : Vector2, + pub dom : HtmlElement, + dimensions : Vector2, } impl HTMLObject { /// Creates a HTMLObject from element name. pub fn new(dom_name: &str) -> Result { - let element = dyn_into(create_element(dom_name)?)?; - Ok(Self::from_element(element)) + let dom = dyn_into(create_element(dom_name)?)?; + Ok(Self::from_element(dom)) } /// Creates a HTMLObject from a web_sys::HtmlElement. pub fn from_element(element: HtmlElement) -> Self { - element.set_property_or_panic("transform-style", "preserve-3d"); - element.set_property_or_panic("position" , "absolute"); - element.set_property_or_panic("width" , "0px"); - element.set_property_or_panic("height" , "0px"); - let object = default(); + element.set_property_or_panic("position", "absolute"); + element.set_property_or_panic("width" , "0px"); + element.set_property_or_panic("height" , "0px"); + let dom = element; + let object = default(); let dimensions = Vector2::new(0.0, 0.0); - Self { object, element, dimensions } + Self { object, dom, dimensions } } /// Creates a HTMLObject from a HTML string. @@ -56,12 +56,12 @@ impl HTMLObject { /// Sets the underlying HtmlElement dimension. pub fn set_dimensions(&mut self, width: f32, height: f32) { self.dimensions = Vector2::new(width, height); - self.element.set_property_or_panic("width", format!("{}px", width)); - self.element.set_property_or_panic("height", format!("{}px", height)); + self.dom.set_property_or_panic("width", format!("{}px", width)); + self.dom.set_property_or_panic("height", format!("{}px", height)); } /// Gets the underlying HtmlElement dimension. - pub fn get_dimensions(&self) -> &Vector2 { + pub fn dimensions(&self) -> &Vector2 { &self.dimensions } } diff --git a/gui/lib/core/src/display/rendering/html/html_renderer.rs b/gui/lib/core/src/display/rendering/html/html_renderer.rs new file mode 100644 index 0000000000..64b131207d --- /dev/null +++ b/gui/lib/core/src/display/rendering/html/html_renderer.rs @@ -0,0 +1,184 @@ +use crate::prelude::*; + +use crate::display::rendering::GraphicsRenderer; +use crate::display::rendering::Scene; +use crate::display::rendering::Camera; +use crate::display::rendering::html::HTMLObject; +use crate::math::utils::IntoFloat32ArrayView; +use crate::math::utils::eps; +use crate::math::utils::invert_y; +use crate::system::web::Result; +use crate::system::web::create_element; +use crate::system::web::dyn_into; +use crate::system::web::NodeInserter; +use crate::system::web::StyleSetter; + +use nalgebra::Vector2; +use nalgebra::Matrix4; + +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; +use web_sys::HtmlElement; + +mod js { + use super::*; + #[wasm_bindgen(module = "/js/html_renderer.js")] + extern "C" { + pub fn set_object_transform(dom: &JsValue, matrix_array: &JsValue); + pub fn setup_perspective(dom: &JsValue, znear: &JsValue); + pub fn setup_camera_transform + ( dom : &JsValue + , znear : &JsValue + , half_width : &JsValue + , half_height : &JsValue + , matrix_array : &JsValue + ); + } +} + +fn set_object_transform(dom: &JsValue, matrix: &Matrix4) { + // Views to WASM memory are only valid as long the backing buffer isn't + // resized. Check documentation of IntoFloat32ArrayView trait for more + // details. + unsafe { + let matrix_array = matrix.into_float32_array_view(); + js::set_object_transform(&dom, &matrix_array); + } +} + +fn setup_camera_transform +( dom : &JsValue +, near : f32 +, half_width : f32 +, half_height : f32 +, matrix : &Matrix4 +) { + + // Views to WASM memory are only valid as long the backing buffer isn't + // resized. Check documentation of IntoFloat32ArrayView trait for more + // details. + unsafe { + let matrix_array = matrix.into_float32_array_view(); + js::setup_camera_transform( + &dom, + &near.into(), + &half_width.into(), + &half_height.into(), + &matrix_array + ) + } +} + +// ======================== +// === HTMLRendererData === +// ======================== + +#[derive(Debug)] +pub struct HTMLRendererData { + pub div : HtmlElement, + pub camera : HtmlElement +} + +impl HTMLRendererData { + pub fn new(div : HtmlElement, camera : HtmlElement) -> Self { + Self { div, camera } + } + + pub fn set_dimensions(&self, dimensions : Vector2) { + let width = format!("{}px", dimensions.x); + let height = format!("{}px", dimensions.y); + self.div .set_property_or_panic("width" , &width); + self.div .set_property_or_panic("height", &height); + self.camera.set_property_or_panic("width" , &width); + self.camera.set_property_or_panic("height", &height); + } +} + +// ==================== +// === HTMLRenderer === +// ==================== + +/// A renderer for `HTMLObject`s. +#[derive(Shrinkwrap, Debug)] +pub struct HTMLRenderer { + #[shrinkwrap(main_field)] + pub renderer : GraphicsRenderer, + pub data : Rc +} + +impl HTMLRenderer { + /// Creates a HTMLRenderer. + pub fn new(dom_id: &str) -> Result { + let renderer = GraphicsRenderer::new(dom_id)?; + let div : HtmlElement = dyn_into(create_element("div")?)?; + let camera : HtmlElement = dyn_into(create_element("div")?)?; + + div .set_property_or_panic("width" , "100%"); + div .set_property_or_panic("height" , "100%"); + camera.set_property_or_panic("width" , "100%"); + camera.set_property_or_panic("height" , "100%"); + camera.set_property_or_panic("transform-style", "preserve-3d"); + + renderer.container.dom.append_or_panic(&div); + div .append_or_panic(&camera); + + let data = Rc::new(HTMLRendererData::new(div, camera)); + let mut htmlrenderer = Self { renderer, data }; + + htmlrenderer.init_listeners(); + Ok(htmlrenderer) + } + + fn init_listeners(&mut self) { + let dimensions = self.renderer.dimensions(); + let data = self.data.clone(); + self.renderer.add_resize_callback(move |dimensions:&Vector2| { + data.set_dimensions(*dimensions); + }); + self.set_dimensions(dimensions); + } + + /// Renders the `Scene` from `Camera`'s point of view. + pub fn render(&self, camera: &mut Camera, scene: &Scene) { + let trans_cam = camera.transform.to_homogeneous().try_inverse(); + let trans_cam = trans_cam.expect("Camera's matrix is not invertible."); + let trans_cam = trans_cam.map(eps); + let trans_cam = invert_y(trans_cam); + + // Note [znear from projection matrix] + let half_dim = self.renderer.container.dimensions() / 2.0; + let y_scale = camera.get_y_scale(); + let near = y_scale * half_dim.y; + + js::setup_perspective(&self.data.div, &near.into()); + setup_camera_transform( + &self.data.camera, + near, + half_dim.x, + half_dim.y, + &trans_cam + ); + + let scene : &Scene = &scene; + for object in &mut scene.into_iter() { + let mut transform = object.transform.to_homogeneous(); + transform.iter_mut().for_each(|a| *a = eps(*a)); + + let parent_node = object.dom.parent_node(); + if !self.data.camera.is_same_node(parent_node.as_ref()) { + self.data.camera.append_or_panic(&object.dom); + } + + set_object_transform(&object.dom, &transform); + } + } + + pub fn set_dimensions(&mut self, dimensions : Vector2) { + self.renderer.set_dimensions(dimensions); + self.data.set_dimensions(dimensions); + } +} + +// Note [znear from projection matrix] +// =================================== +// https://github.com/mrdoob/three.js/blob/22ed6755399fa180ede84bf18ff6cea0ad66f6c0/examples/js/renderers/CSS3DRenderer.js#L275 diff --git a/gui/lib/core/src/display/rendering/html/mod.rs b/gui/lib/core/src/display/rendering/html/mod.rs new file mode 100644 index 0000000000..6249da157d --- /dev/null +++ b/gui/lib/core/src/display/rendering/html/mod.rs @@ -0,0 +1,5 @@ +mod html_object; +mod html_renderer; + +pub use html_object::*; +pub use html_renderer::*; diff --git a/gui/lib/core/src/display/rendering/htmlrenderer.rs b/gui/lib/core/src/display/rendering/htmlrenderer.rs deleted file mode 100644 index 62bc17b81a..0000000000 --- a/gui/lib/core/src/display/rendering/htmlrenderer.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::prelude::*; - -use super::Camera; -use super::HTMLScene; -use crate::math::utils::IntoCSSMatrix; -use crate::math::utils::eps; -use crate::math::utils::invert_y; - -use crate::system::web::StyleSetter; - -// ==================== -// === HTMLRenderer === -// ==================== - -/// A renderer for `HTMLObject`s. -#[derive(Default, Debug)] -pub struct HTMLRenderer {} - -impl HTMLRenderer { - /// Creates a HTMLRenderer. - pub fn new() -> Self { default() } - - /// Renders the `Scene` from `Camera`'s point of view. - pub fn render(&self, camera: &mut Camera, scene: &HTMLScene) { - // Note [znear from projection matrix] - let half_dim = scene.get_dimensions() / 2.0; - let expr = camera.projection[(1, 1)]; - let near = format!("{}px", expr * half_dim.y); - let trans_cam = camera.transform.to_homogeneous().try_inverse(); - let trans_cam = trans_cam.expect("Camera's matrix is not invertible."); - let trans_cam = trans_cam.map(eps); - let trans_cam = invert_y(trans_cam); - let trans_z = format!("translateZ({})", near); - let matrix3d = trans_cam.into_css_matrix(); - let trans = format!("translate({}px,{}px)", half_dim.x, half_dim.y); - let css = format!("{} {} {}", trans_z, matrix3d, trans); - - scene.div .element.set_property_or_panic("perspective", near); - scene.camera.element.set_property_or_panic("transform" , css); - - for object in &scene.objects { - let mut transform = object.transform.to_homogeneous(); - transform.iter_mut().for_each(|a| *a = eps(*a)); - let matrix3d = transform.into_css_matrix(); - let css = format!("translate(-50%, -50%) {}", matrix3d); - object.element.set_property_or_panic("transform", css); - } - } -} - -// Note [znear from projection matrix] -// ================================= -// https://github.com/mrdoob/three.js/blob/22ed6755399fa180ede84bf18ff6cea0ad66f6c0/examples/js/renderers/CSS3DRenderer.js#L275 diff --git a/gui/lib/core/src/display/rendering/htmlscene.rs b/gui/lib/core/src/display/rendering/htmlscene.rs deleted file mode 100644 index b673906aec..0000000000 --- a/gui/lib/core/src/display/rendering/htmlscene.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::prelude::*; - -use super::HTMLObject; -use super::Scene; -use crate::data::opt_vec::OptVec; -use crate::system::web::Result; -use crate::system::web::StyleSetter; -use crate::system::web::NodeAppender; -use crate::system::web::NodeRemover; - - -// ================= -// === HTMLScene === -// ================= - -/// A collection for holding 3D `HTMLObject`s. -#[derive(Shrinkwrap, Debug)] -#[shrinkwrap(mutable)] -pub struct HTMLScene { - #[shrinkwrap(main_field)] - pub scene : Scene, - pub div : HTMLObject, - pub camera : HTMLObject, - pub objects : OptVec, -} - -pub type Index = usize; - -impl HTMLScene { - /// Searches for a HtmlElement identified by id and appends to it. - pub fn new(dom_id: &str) -> Result { - let scene = Scene::new(dom_id)?; - let view_dim = scene.get_dimensions(); - let width = format!("{}px", view_dim.x); - let height = format!("{}px", view_dim.y); - let div = HTMLObject::new("div")?; - let camera = HTMLObject::new("div")?; - let objects = default(); - - scene.container.set_property_or_panic("overflow", "hidden"); - scene.container.append_child_or_panic(&div.element); - div.element.append_child_or_panic(&camera.element); - div .element.set_property_or_panic("width" , &width); - div .element.set_property_or_panic("height" , &height); - camera.element.set_property_or_panic("width" , &width); - camera.element.set_property_or_panic("height" , &height); - - Ok(Self { scene, div, camera, objects }) - } - - /// Moves a HTMLObject to the Scene and returns an index to it. - pub fn add(&mut self, object: HTMLObject) -> Index { - self.camera.element.append_child_or_panic(&object.element); - self.objects.insert(object) - } - - /// Removes and retrieves a HTMLObject based on the index provided by - pub fn remove(&mut self, index: usize) -> Option { - let result = self.objects.remove(index); - result.iter().for_each(|object| { - self.camera.element.remove_child_or_panic(&object.element); - }); - result - } - - /// Returns the number of `Object`s in the Scene, - /// also referred to as its 'length'. - pub fn len(&self) -> usize { - self.objects.len() - } - - /// Returns true if the Scene contains no `Object`s. - pub fn is_empty(&self) -> bool { - self.objects.is_empty() - } -} diff --git a/gui/lib/core/src/display/rendering/mod.rs b/gui/lib/core/src/display/rendering/mod.rs index e29cc8e12c..6cd9b90df1 100644 --- a/gui/lib/core/src/display/rendering/mod.rs +++ b/gui/lib/core/src/display/rendering/mod.rs @@ -1,19 +1,17 @@ -mod camera; mod object; -mod renderer; mod scene; + +pub use object::*; +pub use scene::*; + +mod camera; mod transform; +mod graphics_renderer; +mod dom_container; -mod htmlobject; -mod htmlrenderer; -mod htmlscene; +pub use camera::*; +pub use transform::*; +pub use graphics_renderer::*; +pub use dom_container::*; -pub use camera::Camera; -pub use object::Object; -pub use renderer::Renderer; -pub use scene::Scene; -pub use transform::Transform; - -pub use htmlobject::HTMLObject; -pub use htmlrenderer::HTMLRenderer; -pub use htmlscene::HTMLScene; +pub mod html; diff --git a/gui/lib/core/src/display/rendering/object.rs b/gui/lib/core/src/display/rendering/object.rs index fa0ec10d1d..0d89fb5bc8 100644 --- a/gui/lib/core/src/display/rendering/object.rs +++ b/gui/lib/core/src/display/rendering/object.rs @@ -1,15 +1,18 @@ -use super::Transform; use crate::prelude::*; +use crate::display::rendering::Transform; + +use nalgebra::UnitQuaternion; +use nalgebra::Vector3; + // ============== // === Object === // ============== -// FIXME: You should derive Debug on every structure whenever its possible. /// Base structure for representing a 3D object in a `Scene`. #[derive(Default, Debug)] pub struct Object { - pub transform : Transform, + pub transform : Transform } impl Object { @@ -17,19 +20,34 @@ impl Object { pub fn new() -> Object { default() } /// Sets the object's position. - pub fn set_position(&mut self, x: f32, y: f32, z: f32) { + pub fn set_position(&mut self, x:f32, y:f32, z:f32) { self.transform.set_translation(x, y, z) } + /// Gets the object's position. + pub fn position(&self) -> &Vector3 { + self.transform.translation() + } + /// Sets the object's rotation in YXZ (yaw -> roll -> pitch) order. - pub fn set_rotation(&mut self, roll: f32, pitch: f32, yaw: f32) { + pub fn set_rotation(&mut self, roll:f32, pitch:f32, yaw:f32) { self.transform.set_rotation(roll, pitch, yaw) } + /// Gets the object's rotation UnitQuaternion. + pub fn rotation(&self) -> &UnitQuaternion { + self.transform.rotation() + } + /// Sets the object's scale. pub fn set_scale(&mut self, x: f32, y: f32, z: f32) { self.transform.set_scale(x, y, z); } + + /// Gets the object's scale. + pub fn scale(&self) -> &Vector3 { + self.transform.scale() + } } #[cfg(test)] @@ -46,14 +64,14 @@ mod test { object.set_scale(3.0, 2.0, 1.0); object.set_rotation(PI * 2.0, PI, PI / 2.0); - assert_eq!(object.transform.translation, Vector3::new(1.0, 2.0, 3.0)); - assert_eq!(object.transform.scale, Vector3::new(3.0, 2.0, 1.0)); + assert_eq!(*object.position(), Vector3::new(1.0, 2.0, 3.0)); + assert_eq!(*object.scale(), Vector3::new(3.0, 2.0, 1.0)); let expected = Quaternion::new ( 0.00000009272586 , -0.7071068 , -0.7071068 , -0.000000030908623 ); - assert_eq!(*object.transform.rotation.quaternion(), expected); + assert_eq!(*object.rotation().quaternion(), expected); } } diff --git a/gui/lib/core/src/display/rendering/renderer.rs b/gui/lib/core/src/display/rendering/renderer.rs deleted file mode 100644 index 07403ddaba..0000000000 --- a/gui/lib/core/src/display/rendering/renderer.rs +++ /dev/null @@ -1,10 +0,0 @@ -// use super::Camera; -use crate::prelude::*; - -/// Base structure for our Renderers. -#[derive(Default, Debug)] -pub struct Renderer {} - -impl Renderer { - pub fn new() -> Self { default() } -} diff --git a/gui/lib/core/src/display/rendering/scene.rs b/gui/lib/core/src/display/rendering/scene.rs index 6555a41fdb..ffe2e7819d 100644 --- a/gui/lib/core/src/display/rendering/scene.rs +++ b/gui/lib/core/src/display/rendering/scene.rs @@ -1,24 +1,57 @@ -use crate::system::web::{get_element_by_id, dyn_into, Result}; -use web_sys::HtmlElement; -use nalgebra::Vector2; +use crate::prelude::*; +use crate::data::opt_vec::*; + +type Index = usize; + +// ============= +// === Scene === +// ============= /// A collection for holding 3D `Object`s. -#[derive(Debug)] -pub struct Scene { - pub container : HtmlElement, +#[derive(Derivative)] +#[derivative(Default(bound = ""))] +pub struct Scene { + objects : OptVec } -impl Scene { +impl Scene { /// Searches for a HtmlElement identified by id and appends to it. - pub fn new(dom_id: &str) -> Result { - let container = dyn_into(get_element_by_id(dom_id)?)?; - Ok(Self { container }) + pub fn new() -> Self { default() } + + /// Moves a HTMLObject to the Scene and returns an index to it. + pub fn add(&mut self, object: T) -> Index { + self.objects.insert(object) } - /// Gets the HtmlElement container's dimensions. - pub fn get_dimensions(&self) -> Vector2 { - let width = self.container.client_width() as f32; - let height = self.container.client_height() as f32; - Vector2::new(width, height) + /// Removes and retrieves a HTMLObject based on the index provided by + pub fn remove(&mut self, index: usize) -> Option { + self.objects.remove(index) + } + + /// Returns the number of `Object`s in the Scene, + /// also referred to as its 'length'. + pub fn len(&self) -> usize { + self.objects.len() + } + + /// Returns true if the Scene contains no `Object`s. + pub fn is_empty(&self) -> bool { + self.objects.is_empty() + } +} + +impl<'a, T> IntoIterator for &'a Scene { + type Item = &'a T; + type IntoIter = Iter<'a, T>; + fn into_iter(self) -> Self::IntoIter { + self.objects.into_iter() + } +} + +impl<'a, T> IntoIterator for &'a mut Scene { + type Item = &'a mut T; + type IntoIter = IterMut<'a, T>; + fn into_iter(self) -> Self::IntoIter { + (&mut self.objects).into_iter() } } diff --git a/gui/lib/core/src/display/rendering/transform.rs b/gui/lib/core/src/display/rendering/transform.rs index faf34b3665..bf96bd44ce 100644 --- a/gui/lib/core/src/display/rendering/transform.rs +++ b/gui/lib/core/src/display/rendering/transform.rs @@ -48,20 +48,35 @@ impl Transform { pub fn identity() -> Self { default() } /// Sets Transform's translation. - pub fn set_translation(&mut self, x: f32, y: f32, z: f32) { + pub fn set_translation(&mut self, x:f32, y:f32, z:f32) { self.translation = Vector3::new(x, y, z); } - /// Set Transform's scale. - pub fn set_scale(&mut self, x: f32, y: f32, z: f32) { + /// Gets Transform's translation. + pub fn translation(&self) -> &Vector3 { + &self.translation + } + + /// Sets Transform's scale. + pub fn set_scale(&mut self, x:f32, y:f32, z:f32) { self.scale = Vector3::new(x, y, z); } - /// Set Transform's rotation from Euler angles in radians. - pub fn set_rotation(&mut self, roll: f32, pitch: f32, yaw: f32) { + /// Gets Transform's scale. + pub fn scale(&self) -> &Vector3 { + &self.scale + } + + /// Sets Transform's rotation from Euler angles in radians. + pub fn set_rotation(&mut self, roll:f32, pitch:f32, yaw:f32) { self.rotation = from_euler_angles_pry(roll, pitch, yaw); } + /// Gets Transform's rotation UnitQuaternion + pub fn rotation(&self) -> &UnitQuaternion { + &self.rotation + } + /// Gets a homogeneous transform Matrix4. The rotation order is YXZ (pitch, /// roll, yaw). Based on: // https://github.com/mrdoob/three.js/blob/master/src/math/Matrix4.js#L732 @@ -119,9 +134,9 @@ mod test { use nalgebra::UnitQuaternion; let transform = Transform::identity(); - assert_eq!(transform.translation, Vector3::new(0.0, 0.0, 0.0)); - assert_eq!(transform.scale , Vector3::new(1.0, 1.0, 1.0)); - assert_eq!(transform.rotation , UnitQuaternion::identity()); + assert_eq!(*transform.translation(), Vector3::new(0.0, 0.0, 0.0)); + assert_eq!(*transform.scale(), Vector3::new(1.0, 1.0, 1.0)); + assert_eq!(*transform.rotation(), UnitQuaternion::identity()); } #[test] @@ -136,14 +151,14 @@ mod test { transform.set_scale(3.0, 2.0, 1.0); transform.set_rotation(PI * 2.0, PI, PI / 2.0); - assert_eq!(transform.translation, Vector3::new(1.0, 2.0, 3.0)); - assert_eq!(transform.scale, Vector3::new(3.0, 2.0, 1.0)); + assert_eq!(*transform.translation(), Vector3::new(1.0, 2.0, 3.0)); + assert_eq!(*transform.scale(), Vector3::new(3.0, 2.0, 1.0)); let expected = Quaternion::new ( 0.00000009272586 , -0.7071068 , -0.7071068 , -0.000000030908623 ); - assert_eq!(*transform.rotation.quaternion(), expected); + assert_eq!(*transform.rotation().quaternion(), expected); } } diff --git a/gui/lib/core/src/math/utils.rs b/gui/lib/core/src/math/utils.rs index ca4073d546..c5625e7d72 100644 --- a/gui/lib/core/src/math/utils.rs +++ b/gui/lib/core/src/math/utils.rs @@ -1,9 +1,14 @@ +use crate::prelude::*; + use nalgebra::Matrix4; use nalgebra::RealField; +use js_sys::Float32Array; +use std::marker::PhantomData; -// ====================== -// === Matrix Printer === -// ====================== + +// ===================== +// === IntoCSSMatrix === +// ===================== pub trait IntoCSSMatrix { fn into_css_matrix(&self) -> String; @@ -12,18 +17,51 @@ pub trait IntoCSSMatrix { impl IntoCSSMatrix for Matrix4 { fn into_css_matrix(&self) -> String { let mut iter = self.iter(); - let item = iter.next().expect("Matrix4 should have the first item"); - let acc = format!("{}", item); - let ret = iter.fold(acc, |acc, item| format!("{}, {}", acc, item)); + let item = iter.next().expect("Matrix4 should have the first item"); + let acc = format!("{}", item); + let ret = iter.fold(acc, |acc, item| format!("{}, {}", acc, item)); format!("matrix3d({})", ret) } } + +// ======================== +// === Float32ArrayView === +// ======================== + +/// A Float32Array view created from `IntoFloat32ArrayView`. +#[derive(Shrinkwrap)] +pub struct Float32ArrayView<'a> { + #[shrinkwrap(main_field)] + array : Float32Array, + phantom : PhantomData<&'a Float32Array> +} + +pub trait IntoFloat32ArrayView { + /// # Safety + /// Views into WebAssembly memory are only valid so long as the backing buffer isn't resized in + /// JS. Once this function is called any future calls to Box::new (or malloc of any form) may + /// cause the returned value here to be invalidated. Use with caution! + /// + /// Additionally the returned object can be safely mutated but the input slice isn't guaranteed + /// to be mutable. + unsafe fn into_float32_array_view(&self) -> Float32ArrayView<'_>; +} + +impl IntoFloat32ArrayView for Matrix4 { + unsafe fn into_float32_array_view(&self) -> Float32ArrayView<'_> { + let matrix = matrix4_to_array(&self); + let array = Float32Array::view(matrix); + let phantom = PhantomData; + Float32ArrayView { array, phantom } + } +} + #[cfg(test)] mod tests { #[test] fn into_css_matrix() { - use nalgebra::Matrix4; + use super::Matrix4; use super::IntoCSSMatrix; let matrix = Matrix4::new @@ -38,11 +76,36 @@ mod tests { 13, 14, 15, 16)"; assert_eq!(column_major, expected); } + + #[test] + fn matrix4_memory_layout() { + use super::Matrix4; + use super::matrix4_to_array; + + let matrix = Matrix4::::new + ( 1.0, 5.0, 9.0, 13.0 + , 2.0, 6.0, 10.0, 14.0 + , 3.0, 7.0, 11.0, 15.0 + , 4.0, 8.0, 12.0, 16.0 ); + + let layout = matrix4_to_array(&matrix); + for (i, n) in layout.iter().enumerate() { + assert_eq!((i + 1) as f32, *n); + } + } } -// ============= +// ============ // === Misc === -// ============= +// ============ + +pub fn matrix4_to_array(matrix:&Matrix4) -> &[f32; 16] { + // To interpret [[f32; 4]; 4] as [f32; 16]. + // The memory layout is the same, so this operation is safe. + unsafe { + &*(matrix.as_ref() as *const [[f32; 4]; 4] as *const [f32; 16]) + } +} // eps is used to round very small values to 0.0 for numerical stability pub fn eps(value: f32) -> f32 { @@ -55,4 +118,4 @@ pub fn invert_y(mut m: Matrix4) -> Matrix4 { // Negating the second column to invert Y. m.row_part_mut(1, 4).iter_mut().for_each(|a| *a = -*a); m -} +} \ No newline at end of file diff --git a/gui/lib/core/tests/bench_test.js b/gui/lib/core/tests/bench_test.js new file mode 100644 index 0000000000..69649b3e61 --- /dev/null +++ b/gui/lib/core/tests/bench_test.js @@ -0,0 +1,6 @@ +export function set_gradient_bg(dom, r, g, b) { + let components = r + "," + g + "," + b; + let css = "radial-gradient(rgb(" + components + ") 50%," + + "rgba(" + components + ", 0.0))"; + dom.style.backgroundImage = css; +} diff --git a/gui/lib/core/tests/common.rs b/gui/lib/core/tests/common.rs deleted file mode 100644 index d8e0906fc0..0000000000 --- a/gui/lib/core/tests/common.rs +++ /dev/null @@ -1,89 +0,0 @@ -use basegl::display::rendering::HTMLObject; -use basegl::system::web::document; -use basegl::system::web::dyn_into; -use basegl::system::web::create_element; -use basegl::system::web::get_element_by_id; -use basegl::system::web::AttributeSetter; -use basegl::system::web::StyleSetter; -use basegl::system::web::NodeAppender; -use web_sys::HtmlElement; - -// ================= -// === TestGroup === -// ================= - -pub struct TestGroup { - pub div : HtmlElement, -} - -impl TestGroup { - pub fn new() -> Self { - let div : HtmlElement = match get_element_by_id("testgroup") { - // If id="testgroup" exists, we use it. - Ok(div) => dyn_into(div).expect("div should be a HtmlElement"), - // If it doesn't exist, we create a new element. - Err(_) => { - let div = create_element("div") - .expect("TestGroup failed to create div"); - - let div : HtmlElement = dyn_into(div).expect("HtmlElement"); - div.set_attribute_or_panic("id", "testgroup"); - div.set_property_or_panic("display", "flex"); - div.set_property_or_panic("flex-wrap", "wrap"); - document() - .expect("Document is not present") - .body() - .expect("Body is not present") - .append_child_or_panic(&div); - div - }, - }; - Self { div } - } -} - -// ===================== -// === TestContainer === -// ===================== - -pub struct TestContainer { - div: HTMLObject, -} - -impl TestContainer { - pub fn new(name: &str, width: f32, height: f32) -> Self { - let mut div = HTMLObject::new("div").expect("div"); - div.set_dimensions(width, height + 16.0); - - div.element.set_property_or_panic("border", "1px solid black"); - div.element.set_property_or_panic("position", "relative"); - div.element.set_property_or_panic("margin", "10px"); - - let html_string = format!("
{}
", name); - let mut header = HTMLObject::from_html_string(html_string) - .expect("TestContainer should have a header"); - header.set_dimensions(width, 16.0); - - let border_bottom = "1px solid black"; - header.element.set_property_or_panic("border-bottom", border_bottom); - header.element.set_property_or_panic("position", "relative"); - - div.element.append_child_or_panic(&header.element); - - let mut container = HTMLObject::new("div") - .expect("TestContainer's div not created"); - - container.set_dimensions(width, height); - container.element.set_attribute_or_panic("id", name); - container.element.set_property_or_panic("position", "relative"); - - div.element.append_child_or_panic(&container.element); - - TestGroup::new().div.append_child_or_panic(&div.element); - Self { div } - } - - pub fn append_child(&mut self, element: &HtmlElement) { - self.div.element.append_child_or_panic(&element); - } -} diff --git a/gui/lib/core/tests/html_renderer.rs b/gui/lib/core/tests/html_renderer.rs new file mode 100644 index 0000000000..b8ec0d5f9d --- /dev/null +++ b/gui/lib/core/tests/html_renderer.rs @@ -0,0 +1,251 @@ +//! Test suite for the Web and headless browsers. +#![cfg(target_arch = "wasm32")] + +use web_test::web_configure; +web_configure!(run_in_browser); + +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; + +#[wasm_bindgen(module = "/tests/bench_test.js")] +extern "C" { + fn set_gradient_bg( + dom : &JsValue, + red : &JsValue, + green : &JsValue, + blue : &JsValue); +} + +#[cfg(test)] +mod tests { + use basegl::display::rendering::Scene; + use basegl::display::rendering::Camera; + use basegl::display::rendering::html::HTMLObject; + use basegl::display::rendering::html::HTMLRenderer; + use basegl::system::web::StyleSetter; + use basegl::system::web::get_performance; + use web_test::*; + use web_sys::Performance; + + #[web_test(no_container)] + fn invalid_container() { + let renderer = HTMLRenderer::new("nonexistent_id"); + assert!(renderer.is_err(), "nonexistent_id should not exist"); + } + + #[web_test] + fn object_behind_camera() { + let mut scene : Scene = Scene::new(); + let renderer = HTMLRenderer::new("object_behind_camera") + .expect("Renderer couldn't be created"); + assert_eq!(scene.len(), 0, "Scene should be empty"); + + let view_dim = renderer.dimensions(); + assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); + + let mut object = HTMLObject::new("div").unwrap(); + object.set_position(0.0, 0.0, 0.0); + object.dom.set_property_or_panic("background-color", "black"); + object.set_dimensions(100.0, 100.0); + scene.add(object); + + let aspect_ratio = view_dim.x / view_dim.y; + let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); + // We move the Camera behind the object so we don't see it. + camera.set_position(0.0, 0.0, -100.0); + + renderer.render(&mut camera, &scene); + } + + fn create_scene(renderer : &HTMLRenderer) -> Scene { + let mut scene : Scene = Scene::new(); + assert_eq!(scene.len(), 0); + + renderer.container.dom.set_property_or_panic("background-color", "black"); + + // Iterate over 3 axes. + for axis in vec![(1, 0, 0), (0, 1, 0), (0, 0, 1)] { + // Creates 10 HTMLObjects per axis. + for i in 0 .. 10 { + let mut object = HTMLObject::new("div").unwrap(); + object.set_dimensions(1.0, 1.0); + + // Using axis for masking. + // For instance, the axis (0, 1, 0) creates: + // (x, y, z) = (0, 0, 0) .. (0, 9, 0) + let x = (i * axis.0) as f32; + let y = (i * axis.1) as f32; + let z = (i * axis.2) as f32; + object.set_position(x, y, z); + + // Creates a gradient color based on the axis. + let r = (x * 25.5) as u8; + let g = (y * 25.5) as u8; + let b = (z * 25.5) as u8; + let color = format!("rgba({}, {}, {}, {})", r, g, b, 1.0); + + object.dom.set_property_or_panic("background-color", color); + scene.add(object); + } + } + assert_eq!(scene.len(), 30, "We should have 30 HTMLObjects"); + scene + } + + #[web_test] + fn rhs_coordinates() { + let renderer = HTMLRenderer::new("rhs_coordinates") + .expect("Renderer couldn't be created"); + let scene = create_scene(&renderer); + + let view_dim = renderer.dimensions(); + assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); + + let aspect_ratio = view_dim.x / view_dim.y; + let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); + + // We move the Camera 29 units away from the center. + camera.set_position(0.0, 0.0, 29.0); + + renderer.render(&mut camera, &scene); + } + + #[web_test] + fn rhs_coordinates_from_back() { + use std::f32::consts::PI; + + let renderer = HTMLRenderer::new("rhs_coordinates_from_back") + .expect("Renderer couldn't be created"); + let scene = create_scene(&renderer); + + let view_dim = renderer.dimensions(); + assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); + + let aspect_ratio = view_dim.x / view_dim.y; + let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); + + // We move the Camera -29 units away from the center. + camera.set_position(0.0, 0.0, -29.0); + // We rotate it 180 degrees so we can see the center of the scene + // from behind. + camera.set_rotation(0.0, PI, 0.0); + + renderer.render(&mut camera, &scene); + } + + #[web_bench] + fn camera_movement(b: &mut Bencher) { + let renderer = HTMLRenderer::new("camera_movement") + .expect("Renderer couldn't be created"); + let scene = create_scene(&renderer); + + let view_dim = renderer.dimensions(); + assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); + + let aspect_ratio = view_dim.x / view_dim.y; + let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); + let performance = get_performance() + .expect("Couldn't get performance obj"); + + b.iter(move || { + let t = (performance.now() / 1000.0) as f32; + // We move the Camera 29 units away from the center. + camera.set_position(t.sin() * 5.0, t.cos() * 5.0, 29.0); + + renderer.render(&mut camera, &scene); + }) + } + + fn make_sphere(mut scene : &mut Scene, performance : &Performance) { + use super::set_gradient_bg; + + let t = (performance.now() / 1000.0) as f32; + let length = scene.len() as f32; + let mut scene : &mut Scene = &mut scene; + for (i, object) in (&mut scene).into_iter().enumerate() { + let i = i as f32; + let d = (i / length - 0.5) * 2.0; + + let mut y = d; + let r = (1.0 - y * y).sqrt(); + let mut x = (y * 100.0 + t).cos() * r; + let mut z = (y * 100.0 + t).sin() * r; + + x += (y * 1.25 + t * 2.50).cos() * 0.5; + y += (z * 1.25 + t * 2.00).cos() * 0.5; + z += (x * 1.25 + t * 3.25).cos() * 0.5; + object.set_position(x * 5.0, y * 5.0, z * 5.0); + + let faster_t = t * 100.0; + let r = (i + 0.0 + faster_t) as u8 % 255; + let g = (i + 85.0 + faster_t) as u8 % 255; + let b = (i + 170.0 + faster_t) as u8 % 255; + set_gradient_bg(&object.dom, &r.into(), &g.into(), &b.into()); + } + } + + #[web_bench] + fn object_x1000(b: &mut Bencher) { + let mut scene : Scene = Scene::new(); + let renderer = HTMLRenderer::new("object_x1000") + .expect("Renderer couldn't be created"); + renderer.container.dom.set_property_or_panic("background-color", "black"); + + for _ in 0..1000 { + let mut object = HTMLObject::new("div") + .expect("Failed to create object"); + object.set_dimensions(1.0, 1.0); + object.set_scale(0.5, 0.5, 0.5); + scene.add(object); + } + + let view_dim = renderer.dimensions(); + assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); + + let aspect_ratio = view_dim.x / view_dim.y; + let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); + let performance = get_performance() + .expect("Couldn't get performance obj"); + + // We move the Camera 29 units away from the center. + camera.set_position(0.0, 0.0, 29.0); + + make_sphere(&mut scene, &performance); + + b.iter(move || { + renderer.render(&mut camera, &scene); + }) + } + + #[web_bench] + fn object_x400_update(b: &mut Bencher) { + let renderer = HTMLRenderer::new("object_x400_update") + .expect("Renderer couldn't be created"); + let mut scene : Scene = Scene::new(); + renderer.container.dom.set_property_or_panic("background-color", "black"); + + for _ in 0..400 { + let mut object = HTMLObject::new("div") + .expect("Failed to create object"); + object.set_dimensions(1.0, 1.0); + object.set_scale(0.5, 0.5, 0.5); + scene.add(object); + } + + let view_dim = renderer.dimensions(); + assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); + + let aspect_ratio = view_dim.x / view_dim.y; + let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); + let performance = get_performance() + .expect("Couldn't get performance obj"); + + // We move the Camera 29 units away from the center. + camera.set_position(0.0, 0.0, 29.0); + + b.iter(move || { + make_sphere(&mut scene, &performance); + renderer.render(&mut camera, &scene); + }) + } +} diff --git a/gui/lib/core/tests/htmlrenderer.rs b/gui/lib/core/tests/htmlrenderer.rs deleted file mode 100644 index 3cd2cceba2..0000000000 --- a/gui/lib/core/tests/htmlrenderer.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Test suite for the Web and headless browsers. - -#![cfg(target_arch = "wasm32")] - -extern crate wasm_bindgen_test; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(run_in_browser); - -pub mod common; - -#[cfg(test)] -mod tests { - use crate::common::TestContainer; - use basegl::display::rendering::*; - use basegl::system::web::StyleSetter; - use wasm_bindgen_test::*; - - #[wasm_bindgen_test] - fn invalid_container() { - let scene = HTMLScene::new("nonexistent_id"); - assert!(scene.is_err(), "nonexistent_id should not exist"); - } - - #[wasm_bindgen_test] - fn object_behind_camera() { - TestContainer::new("object_behind_camera", 320.0, 240.0); - let mut scene = HTMLScene::new("object_behind_camera") - .expect("Failed to create HTMLScene"); - assert_eq!(scene.len(), 0, "Scene should be empty"); - - let view_dim = scene.get_dimensions(); - assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); - - let mut object = HTMLObject::new("div").unwrap(); - object.set_position(0.0, 0.0, 0.0); - object.element.set_property_or_panic("background-color", "black"); - object.set_dimensions(100.0, 100.0); - scene.add(object); - - let aspect_ratio = view_dim.x / view_dim.y; - let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); - // We move the Camera behind the object so we don't see it. - camera.set_position(0.0, 0.0, -100.0); - - let renderer = HTMLRenderer::new(); - renderer.render(&mut camera, &scene); - } - - fn create_scene(dom_id : &str) -> HTMLScene { - let mut scene = HTMLScene::new(dom_id) - .expect("Failed to create HTMLScene"); - assert_eq!(scene.len(), 0); - - scene.container.set_property_or_panic("background-color", "black"); - - // Iterate over 3 axes. - for axis in vec![(1, 0, 0), (0, 1, 0), (0, 0, 1)] { - // Creates 10 HTMLObjects per axis. - for i in 0 .. 10 { - let mut object = HTMLObject::new("div").unwrap(); - object.set_dimensions(1.0, 1.0); - - // Using axis for masking. - // For instance, the axis (0, 1, 0) creates: - // (x, y, z) = (0, 0, 0) .. (0, 9, 0) - let x = (i * axis.0) as f32; - let y = (i * axis.1) as f32; - let z = (i * axis.2) as f32; - object.set_position(x, y, z); - - // Creates a gradient color based on the axis. - let r = (x * 25.5) as u8; - let g = (y * 25.5) as u8; - let b = (z * 25.5) as u8; - let color = format!("rgba({}, {}, {}, {})", r, g, b, 1.0); - - object.element.set_property_or_panic("background-color", color); - scene.add(object); - } - } - assert_eq!(scene.len(), 30, "We should have 30 HTMLObjects"); - scene - } - - #[wasm_bindgen_test] - fn rhs_coordinates() { - TestContainer::new("rhs_coordinates", 320.0, 240.0); - let scene = create_scene("rhs_coordinates"); - - let view_dim = scene.get_dimensions(); - assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); - - let aspect_ratio = view_dim.x / view_dim.y; - let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); - - // We move the Camera 29 units away from the center. - camera.set_position(0.0, 0.0, 29.0); - - let renderer = HTMLRenderer::new(); - renderer.render(&mut camera, &scene); - } - - #[wasm_bindgen_test] - fn rhs_coordinates_from_back() { - use std::f32::consts::PI; - - TestContainer::new("rhs_coordinates_from_back", 320.0, 240.0); - let scene = create_scene("rhs_coordinates_from_back"); - - let view_dim = scene.get_dimensions(); - assert_eq!((view_dim.x, view_dim.y), (320.0, 240.0)); - - let aspect_ratio = view_dim.x / view_dim.y; - let mut camera = Camera::perspective(45.0, aspect_ratio, 1.0, 1000.0); - - // We move the Camera -29 units away from the center. - camera.set_position(0.0, 0.0, -29.0); - // We rotate it 180 degrees so we can see the center of the scene - // from behind. - camera.set_rotation(0.0, PI, 0.0); - - let renderer = HTMLRenderer::new(); - renderer.render(&mut camera, &scene); - } -} diff --git a/gui/lib/system/web/Cargo.toml b/gui/lib/system/web/Cargo.toml index 856f5ae5e6..d2af3748a9 100644 --- a/gui/lib/system/web/Cargo.toml +++ b/gui/lib/system/web/Cargo.toml @@ -32,7 +32,8 @@ features = [ 'WebGlProgram', 'WebGlShader', 'Window', - 'console' + 'console', + 'Performance' ] [dev-dependencies] diff --git a/gui/lib/system/web/src/animationframeloop.rs b/gui/lib/system/web/src/animationframeloop.rs new file mode 100644 index 0000000000..d43d741470 --- /dev/null +++ b/gui/lib/system/web/src/animationframeloop.rs @@ -0,0 +1,60 @@ +use super::request_animation_frame; + +use std::rc::Rc; +use std::cell::RefCell; +use wasm_bindgen::prelude::Closure; + + +// ========================== +// === AnimationFrameData === +// ========================== + +struct AnimationFrameData { + run : bool +} + +pub struct AnimationFrameLoop { + forget : bool, + data : Rc> +} + + +// ========================== +// === AnimationFrameLoop === +// ========================== + +impl AnimationFrameLoop { + pub fn new(mut func:Box) -> Self { + let nop_func = Box::new(|| ()) as Box; + let nop_closure = Closure::once(nop_func); + let callback = Rc::new(RefCell::new(nop_closure)); + let run = true; + let data = Rc::new(RefCell::new(AnimationFrameData { run })); + let callback_clone = callback.clone(); + let data_clone = data.clone(); + + *callback.borrow_mut() = Closure::wrap(Box::new(move || { + if data_clone.borrow().run { + func(); + let clb = &callback_clone.borrow(); + request_animation_frame(&clb).expect("Request Animation Frame"); + } + }) as Box); + request_animation_frame(&callback.borrow()).unwrap(); + + let forget = false; + AnimationFrameLoop{forget,data} + } + + pub fn forget(mut self) { + self.forget = true; + } +} + +impl Drop for AnimationFrameLoop { + fn drop(&mut self) { + if !self.forget { + self.data.borrow_mut().run = false; + } + } +} diff --git a/gui/lib/system/web/src/lib.rs b/gui/lib/system/web/src/lib.rs index 43d6c9b5f4..dc8a361bc3 100644 --- a/gui/lib/system/web/src/lib.rs +++ b/gui/lib/system/web/src/lib.rs @@ -1,6 +1,8 @@ #![feature(trait_alias)] pub mod resize_observer; +mod animationframeloop; +pub use animationframeloop::AnimationFrameLoop; use basegl_prelude::*; @@ -9,11 +11,13 @@ use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; use web_sys::WebGlRenderingContext; +use web_sys::Performance; use web_sys::Node; use std::fmt::Debug; pub use web_sys::console; + // ============= // === Error === // ============= @@ -30,47 +34,51 @@ pub enum Error { NoWebGL { version: u32 }, } impl Error { - pub fn missing(name: &str) -> Error { + pub fn missing(name:&str) -> Error { let name = name.to_string(); Error::Missing { name } } - pub fn type_mismatch(expected: &str, got: &str) -> Error { + pub fn type_mismatch(expected:&str, got:&str) -> Error { let expected = expected.to_string(); let got = got.to_string(); Error::TypeMismatch { expected, got } } } + // =================== // === JS Bindings === // =================== #[macro_export] macro_rules! console_log { - ($($t:tt)*) => ($crate::console::log_1(&format_args!($($t)*).to_string().into())) + ($($t:tt)*) => ($crate::console::log_1(&format_args!($($t)*) + .to_string().into())) } + // ============== // === LogMsg === // ============== pub trait LogMsg { - fn with_log_msg T, T>(&self, f: F) -> T; + fn with_log_msg T, T>(&self, f:F) -> T; } impl LogMsg for &str { - fn with_log_msg T, T>(&self, f: F) -> T { + fn with_log_msg T, T>(&self, f:F) -> T { f(self) } } impl S, S: AsRef> LogMsg for F { - fn with_log_msg T, T>(&self, f: G) -> T { + fn with_log_msg T, T>(&self, f:G) -> T { f(self().as_ref()) } } + // ============== // === Logger === // ============== @@ -80,7 +88,7 @@ pub struct Logger { pub path: String, } impl Logger { - pub fn new>(path: T) -> Self { + pub fn new>(path:T) -> Self { let path = path.as_ref().to_string(); Self { path } } @@ -90,29 +98,29 @@ impl Logger { Self::new("") } - pub fn sub>(&self, path: T) -> Self { + pub fn sub>(&self, path:T) -> Self { Self::new(format!("{}.{}", self.path, path.as_ref())) } - pub fn trace(&self, msg: M) { + pub fn trace(&self, msg:M) { console::debug_1(&self.format(msg)); } - pub fn info(&self, msg: M) { + pub fn info(&self, msg:M) { // console::info_1(&self.format(msg)); console::group_1(&self.format(msg)); console::group_end(); } - pub fn warning(&self, msg: M) { + pub fn warning(&self, msg:M) { console::warn_1(&self.format(msg)); } - pub fn error(&self, msg: M) { + pub fn error(&self, msg:M) { console::error_1(&self.format(msg)); } - pub fn group_begin(&self, msg: M) { + pub fn group_begin(&self, msg:M) { console::group_1(&self.format(msg)); } @@ -120,14 +128,14 @@ impl Logger { console::group_end(); } - pub fn group T>(&self, msg: M, f: F) -> T { + pub fn group T>(&self, msg:M, f:F) -> T { self.group_begin(msg); let out = f(); self.group_end(); out } - fn format(&self, msg: M) -> JsValue { + fn format(&self, msg:M) -> JsValue { msg.with_log_msg(|s| format!("[{}] {}", self.path, s)).into() } } @@ -138,6 +146,7 @@ impl Default for Logger { } } + // ==================== // === Logger Utils === // ==================== @@ -170,7 +179,7 @@ macro_rules! group { // === DOM Helpers === // =================== -pub fn dyn_into(obj : T) -> Result +pub fn dyn_into(obj :T) -> Result where T : wasm_bindgen::JsCast + Debug, U : wasm_bindgen::JsCast { @@ -192,23 +201,23 @@ pub fn document() -> Result { window()?.document().ok_or_else(|| Error::missing("document")) } -pub fn get_element_by_id(id: &str) -> Result { +pub fn get_element_by_id(id:&str) -> Result { document()?.get_element_by_id(id).ok_or_else(|| Error::missing(id)) } #[deprecated(note = "Use get_element_by_id with dyn_into instead")] -pub fn get_element_by_id_as(id: &str) -> Result { +pub fn get_element_by_id_as(id:&str) -> Result { let elem = get_element_by_id(id)?; dyn_into(elem) } -pub fn create_element(id: &str) -> Result { +pub fn create_element(id:&str) -> Result { match document()?.create_element(id) { Ok(element) => Ok(element), Err(_) => Err(Error::missing(id)), } } -pub fn get_canvas(id: &str) -> Result { +pub fn get_canvas(id:&str) -> Result { dyn_into(get_element_by_id(id)?) } @@ -218,90 +227,110 @@ pub fn get_webgl_context( ) -> Result { let no_webgl = || Error::NoWebGL { version }; - let name_sfx = if version == 1 { "".to_string() } else { version.to_string() }; + let name_sfx = if version == 1 { + "".to_string() + } else { + version.to_string() + }; let name = &format!("webgl{}", &name_sfx); - let context = canvas.get_context(name).map_err(|_| no_webgl())?.ok_or_else(no_webgl)?; + let context = canvas.get_context(name) + .map_err(|_| no_webgl())?.ok_or_else(no_webgl)?; context.dyn_into().map_err(|_| no_webgl()) } -pub fn request_animation_frame(f: &Closure) -> Result { +pub fn request_animation_frame(f:&Closure) -> Result { let req = window()?.request_animation_frame(f.as_ref().unchecked_ref()); req.map_err(|_| Error::missing("requestAnimationFrame")) } -pub fn cancel_animation_frame(id: i32) -> Result<()> { +pub fn cancel_animation_frame(id:i32) -> Result<()> { let req = window()?.cancel_animation_frame(id); req.map_err(|_| Error::missing("cancel_animation_frame")) } +pub fn get_performance() -> Result { + window()?.performance().ok_or_else(|| Error::missing("performance")) +} + + // ===================== // === Other Helpers === // ===================== pub trait AttributeSetter { - fn set_attribute_or_panic(&self, name : T, value : U) + fn set_attribute_or_panic(&self, name:T, value:U) where T : AsRef, U : AsRef; } impl AttributeSetter for web_sys::HtmlElement { - fn set_attribute_or_panic(&self, name : T, value : U) + fn set_attribute_or_panic(&self, name:T, value:U) where T : AsRef, U : AsRef { - let name = name.as_ref(); - let value = value.as_ref(); - let values = format!("\"{}\" = \"{}\" on \"{:?}\"", name, value, self); - self.set_attribute(name, value) + let name = name.as_ref(); + let value = value.as_ref(); + let values = format!("\"{}\" = \"{}\" on \"{:?}\"",name,value,self); + self.set_attribute(name,value) .unwrap_or_else(|_| panic!("Failed to set attribute {}", values)); } } pub trait StyleSetter { - fn set_property_or_panic(&self, name : T, value : U) + fn set_property_or_panic(&self, name:T, value:U) where T : AsRef, U : AsRef; } impl StyleSetter for web_sys::HtmlElement { - fn set_property_or_panic(&self, name : T, value : U) + fn set_property_or_panic(&self, name:T, value:U) where T : AsRef, U : AsRef { - let name = name.as_ref(); - let value = value.as_ref(); - let values = format!("\"{}\" = \"{}\" on \"{:?}\"", name, value, self); - self.style().set_property(name, value) - .unwrap_or_else(|_| panic!("Failed to set style {}", values)); + let name = name.as_ref(); + let value = value.as_ref(); + let values = format!("\"{}\" = \"{}\" on \"{:?}\"",name,value,self); + let panic_msg = |_| panic!("Failed to set style {}",values); + self.style().set_property(name, value).unwrap_or_else(panic_msg); } } -pub trait NodeAppender { - fn append_child_or_panic(&self, node : &Node); +pub trait NodeInserter { + fn append_or_panic (&self, node:&Node); + fn prepend_or_panic(&self, node:&Node); + fn insert_before_or_panic(&self,node:&Node,reference_node:&Node); } -impl NodeAppender for Node { - fn append_child_or_panic(&self, node : &Node) { - self.append_child(node) - .unwrap_or_else(|_| - panic!("Failed to append child \"{:?}\" to \"{:?}\"", - node, - self - ) - ); +impl NodeInserter for Node { + fn append_or_panic(&self, node:&Node) { + let panic_msg = |_| + panic!("Failed to append child {:?} to {:?}",node,self); + self.append_child(node).unwrap_or_else(panic_msg); + } + + fn prepend_or_panic(&self, node : &Node) { + let panic_msg = |_| + panic!("Failed to prepend child \"{:?}\" to \"{:?}\"",node,self); + + let first_c = self.first_child(); + self.insert_before(node, first_c.as_ref()).unwrap_or_else(panic_msg); + } + fn insert_before_or_panic(&self, node:&Node, ref_node:&Node) { + let panic_msg = |_| + panic!("Failed to insert {:?} before {:?} in {:?}", + node, + ref_node, + self); + self.insert_before(node, Some(ref_node)).unwrap_or_else(panic_msg); } } pub trait NodeRemover { - fn remove_child_or_panic(&self, node : &Node); + fn remove_child_or_panic(&self, node:&Node); } impl NodeRemover for Node { - fn remove_child_or_panic(&self, node : &Node) { - self.remove_child(node) - .unwrap_or_else(|_| - panic!("Failed to remove child \"{:?}\" from \"{:?}\"", - node, - self - ) - ); + fn remove_child_or_panic(&self, node:&Node) { + let panic_msg = |_| + panic!("Failed to remove child {:?} from {:?}",node,self); + self.remove_child(node).unwrap_or_else(panic_msg); } } diff --git a/gui/lib/web-test-proc-macro/Cargo.toml b/gui/lib/web-test-proc-macro/Cargo.toml new file mode 100644 index 0000000000..40815f7bc3 --- /dev/null +++ b/gui/lib/web-test-proc-macro/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "web-test-proc-macro" +version = "0.1.0" +authors = ["Enso Team "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.1" +syn = { version = "1.0.2", features = [ "full" ] } +quote = "1.0" +wasm-bindgen-test = "0.3.3" diff --git a/gui/lib/web-test-proc-macro/src/lib.rs b/gui/lib/web-test-proc-macro/src/lib.rs new file mode 100644 index 0000000000..5b43b46922 --- /dev/null +++ b/gui/lib/web-test-proc-macro/src/lib.rs @@ -0,0 +1,109 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use syn::*; +use quote::quote; + +// FIXME: Parse proc_macro args to read the following info: +// #[web_test(dimensions(320.0, 240.0)) and +// #[web_test(no_container)]. + +// =================== +// === #[web_test] === +// =================== + +/// #[web_test] creates a [320.0, 240.0] div with id = fn_name and appends it +/// into the document. +/// # Example +/// ```rust,compile_fail +/// use web_test::web_test; +/// use web_test::web_configure; +/// use basegl::system::web::get_element_by_id; +/// +/// web_configure!(run_in_browser); +/// +/// #[web_test] +/// fn get_identified_element() { +/// // #[web_test] creates a
+/// assert!(get_element_by_id("get_identified_element").is_ok()); +/// } +/// ``` +#[proc_macro_attribute] +pub fn web_test(_args:TokenStream, input:TokenStream) -> TokenStream { + if let Ok(mut parsed) = syn::parse::(input.clone()) { + let fn_string = format!("{}", parsed.sig.ident); + let code = format!("Container::new(\"Tests\", \"{}\", 320.0, 240.0);", + fn_string); + + if let Ok(stmt) = parse_str::(&code) { + // We insert Container::new("Tests", fn_name, 320.0, 240.0) + // at the beginning of the function block. + parsed.block.stmts.insert(0, stmt); + + let output = quote! { + #[wasm_bindgen_test] + #parsed + }; + output.into() + } else { + input + } + } else { + input + } +} + + +// ==================== +// === #[web_bench] === +// ==================== + +/// #[web_bench] creates a benchmark div with a toggle button. +/// # Example +/// ```rust,compile_fail +/// use web_test::web_bench; +/// use web_test::web_configure; +/// use basegl::system::web::get_element_by_id; +/// use basegl::system::web::dyn_into; +/// use web_sys::HtmlElement; +/// +/// web_configure!(run_in_browser); +/// +/// #[web_bench] +/// fn test_performance(b: &mut Bencher) { +/// let element = get_element_by_id("test_performance").expect("div"); +/// let element : HtmlElement = dyn_into(element).expect("HtmlElement"); +/// +/// let numbers : Vec<_> = (1 ..= 1000).collect(); +/// b.iter(move || { +/// let ans = numbers.iter().fold(0, |acc, x| acc + x); +/// element.set_inner_html(&format!("Answer: {}", ans)); +/// }) +/// } +/// ``` +#[proc_macro_attribute] +pub fn web_bench(_args:TokenStream, input:TokenStream) -> TokenStream { + + if let Ok(parsed) = parse::(input.clone()) { + use proc_macro2::*; + let input : TokenStream = input.into(); + + let fn_ident = parsed.sig.ident; + let fn_string = format!("{}", fn_ident); + let fn_benchmark_str = format!("{}_benchmark", fn_string); + let fn_benchmark = Ident::new(&fn_benchmark_str, Span::call_site()); + + let output = quote! { + #[wasm_bindgen_test] + fn #fn_benchmark() { + let container = BenchContainer::new(#fn_string, 320.0, 240.0); + let mut b = Bencher::new(container); + #fn_ident(&mut b); + } + #input + }; + output.into() + } else { + input + } +} diff --git a/gui/lib/web-test/Cargo.toml b/gui/lib/web-test/Cargo.toml new file mode 100644 index 0000000000..bd3af67e7f --- /dev/null +++ b/gui/lib/web-test/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "web-test" +version = "0.1.0" +authors = ["Enso Team "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +wasm-bindgen-test = "0.3.3" +web-test-proc-macro = { version = "0.1.0" , path = "../web-test-proc-macro" } +basegl-prelude = { version = "0.1.0" , path = "../prelude" } +basegl-system-web = { version = "0.1.0" , path = "../system/web" } +wasm-bindgen = { version = "^0.2" , features = ["nightly"] } +js-sys = { version = "0.3.28" } +shrinkwraprs = { version = "0.2.1" } + +[dependencies.web-sys] +version = "0.3.4" diff --git a/gui/lib/web-test/src/bench_container.rs b/gui/lib/web-test/src/bench_container.rs new file mode 100644 index 0000000000..3bd7ae322c --- /dev/null +++ b/gui/lib/web-test/src/bench_container.rs @@ -0,0 +1,62 @@ +use crate::prelude::*; + +use super::Container; +use crate::system::web::create_element; +use crate::system::web::dyn_into; +use crate::system::web::NodeInserter; +use crate::system::web::StyleSetter; + +use web_sys::HtmlElement; + + +// ====================== +// === BenchContainer === +// ====================== + +/// Html container displaying benchmark results. +#[derive(Shrinkwrap)] +pub struct BenchContainer { + #[shrinkwrap(main_field)] + container : Container, + pub measurement : HtmlElement, + pub time : HtmlElement, + pub iter : HtmlElement, + pub button : HtmlElement +} + +impl BenchContainer { + /// Creates an identificable container with provided dimensions. + pub fn new(name:&str, width:f32, height:f32) -> Self { + let div = create_element("div").expect("div"); + let div : HtmlElement = dyn_into(div).expect("HtmlElement"); + + div.set_property_or_panic("margin" , "0px 2px"); + div.set_property_or_panic("height" , "24px"); + div.set_property_or_panic("bottom-border" , "1px solid black"); + div.set_property_or_panic("display" , "flex"); + div.set_property_or_panic("justify-content", "space-between"); + div.set_property_or_panic("align-items" , "center"); + + div.set_inner_html("
00.00ms
\ +
0 iterations
\ + "); + + let children = div.children(); + let time = children.item(0).expect("time div"); + let iter = children.item(1).expect("iter div"); + let button = children.item(2).expect("button div"); + let time : HtmlElement = dyn_into(time).expect("time HtmlElement"); + let iter : HtmlElement = dyn_into(iter).expect("iter HtmlElement"); + let button : HtmlElement = dyn_into(button).expect("buttn HtmlElement"); + + let container = Container::new("Benchmarks", name, width, height); + let header_height = 17.0; + let height = format!("{}px", height + header_height + 25.0); + + container.div.set_property_or_panic("height", height); + container.div.insert_before_or_panic(&div, &container.container); + + let measurement = div; + Self { container,measurement,time,iter,button } + } +} diff --git a/gui/lib/web-test/src/bencher.rs b/gui/lib/web-test/src/bencher.rs new file mode 100644 index 0000000000..f87a050deb --- /dev/null +++ b/gui/lib/web-test/src/bencher.rs @@ -0,0 +1,133 @@ +use crate::prelude::*; + +use super::BenchContainer; +pub use crate::system::web::get_performance; +pub use crate::system::web::AnimationFrameLoop; + +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use std::rc::Rc; +use std::cell::RefCell; + + +// =================== +// === BencherCell === +// =================== + +/// Cell, used to hold Bencher's data +pub struct BencherCell { + func : Box, + container : BenchContainer, + iterations : usize, + total_time : f64, + anim_loop : Option +} + +impl BencherCell { + pub fn new(f:Box, container:BenchContainer) -> Self { + let func = f; + let iterations = 0; + let total_time = 0.0; + let anim_loop = None; + Self { func,container,iterations,total_time,anim_loop } + } + + /// Adds the duration of the next iteration and updates the UI. + pub fn add_iteration_time(&mut self, time : f64) { + self.iterations += 1; + self.total_time += time; + let iterations = format!("{} iterations", self.iterations); + let average = self.total_time / self.iterations as f64; + let display = format!("{:.2}ms", average); + + self.container.iter.set_inner_html(&iterations); + self.container.time.set_inner_html(&display); + } +} + + +// =================== +// === BencherData === +// =================== + +#[derive(Shrinkwrap)] +pub struct BencherData { + cell : RefCell +} + +impl BencherData { + pub fn new(f:Box, container:BenchContainer) -> Rc { + let cell = RefCell::new(BencherCell::new(f, container)); + Rc::new(Self { cell }) + } + + /// Starts the benchmarking loop. + fn start(self:&Rc) { + let data_clone = self.clone(); + let performance = get_performance().expect("Performance object"); + let mut t0 = performance.now(); + let anim_loop = AnimationFrameLoop::new(Box::new(move || { + let mut data = data_clone.borrow_mut(); + + (&mut data.func)(); + + let t1 = performance.now(); + let dt = t1 - t0; + t0 = t1; + + data.add_iteration_time(dt); + })); + self.borrow_mut().anim_loop = Some(anim_loop); + } + + /// Stops the benchmarking loop. + fn stop(self:&Rc) { + self.borrow_mut().anim_loop = None; + } + + /// Check if the loop is running. + fn is_running(self:&Rc) -> bool { + self.borrow().anim_loop.is_some() + } +} + +// =============== +// === Bencher === +// =============== + +/// The Bencher struct with an API compatible to Rust's test Bencher. +pub struct Bencher { + data : Rc +} + +impl Bencher { + /// Creates a Bencher with a html test container. + pub fn new(container:BenchContainer) -> Self { + let func = Box::new(|| ()); + let data = BencherData::new(func, container); + + let data_clone = data.clone(); + let closure = Box::new(move || { + if data_clone.is_running() { + data_clone.stop(); + } else { + data_clone.start(); + } + }) as Box; + + { + let closure = Closure::wrap(closure); + let cell = data.cell.borrow(); + let measurement = &cell.container.measurement; + measurement.set_onclick(Some(closure.as_ref().unchecked_ref())); + closure.forget(); + } + + Self { data } + } + + /// Callback for benchmark functions to run in their body. + pub fn iter T + 'static>(&mut self, mut func:F) { + self.data.borrow_mut().func = Box::new(move || { func(); }); + } +} diff --git a/gui/lib/web-test/src/container.rs b/gui/lib/web-test/src/container.rs new file mode 100644 index 0000000000..1178904f85 --- /dev/null +++ b/gui/lib/web-test/src/container.rs @@ -0,0 +1,56 @@ +use super::Group; +use crate::system::web::create_element; +use crate::system::web::dyn_into; +use crate::system::web::AttributeSetter; +use crate::system::web::StyleSetter; +use crate::system::web::NodeInserter; + +use web_sys::HtmlElement; + + +// ================= +// === Container === +// ================= + +/// A container to hold tests in `wasm-pack test`. +pub struct Container { + pub div : HtmlElement, + pub header : HtmlElement, + pub container : HtmlElement +} + +impl Container { + /// Creates an identificable container with provided dimensions. + pub fn new(group:&str, name:&str, width:f32, height:f32) -> Self { + let div = create_element("div").expect("div"); + let div = dyn_into::<_, HtmlElement>(div).expect("HtmlElement"); + let width = format!("{}px", width); + let header = create_element("center").expect("div"); + let header = dyn_into::<_, HtmlElement>(header).expect("HtmlElement"); + + div.set_property_or_panic("width" , &width); + div.set_property_or_panic("height" , format!("{}px", height + 17.0)); + div.set_property_or_panic("border" , "1px solid black"); + div.set_property_or_panic("position", "relative"); + div.set_property_or_panic("margin" , "10px"); + header.set_inner_html(name); + header.set_property_or_panic("width" , &width); + header.set_property_or_panic("height", format!("{}px", 16.0)); + header.set_property_or_panic("border-bottom", "1px solid black"); + header.set_property_or_panic("position", "relative"); + div.append_or_panic(&header); + + let container = create_element("div").expect("div"); + let container : HtmlElement = dyn_into(container).expect("HtmlElement"); + + container.set_property_or_panic("width" , width); + container.set_property_or_panic("height", format!("{}px", height)); + container.set_attribute_or_panic("id", name); + container.set_property_or_panic("position", "relative"); + + div.append_or_panic(&container); + + Group::new(group).div.append_or_panic(&div); + Self { div, header, container } + } +} diff --git a/gui/lib/web-test/src/group.rs b/gui/lib/web-test/src/group.rs new file mode 100644 index 0000000000..c722d62fe9 --- /dev/null +++ b/gui/lib/web-test/src/group.rs @@ -0,0 +1,57 @@ +use crate::system::web::document; +use crate::system::web::dyn_into; +use crate::system::web::create_element; +use crate::system::web::get_element_by_id; +use crate::system::web::AttributeSetter; +use crate::system::web::StyleSetter; +use crate::system::web::NodeInserter; + +use web_sys::HtmlElement; + + +// ============= +// === Group === +// ============= + +/// Helper to group test containers +pub struct Group { + pub div : HtmlElement, +} + +impl Group { + pub fn new(name:&str) -> Self { + let div:HtmlElement = match get_element_by_id(name) { + // If id=name exists, we use it. + Ok(div) => dyn_into(div).expect("div should be a HtmlElement"), + // If it doesn't exist, we create a new element. + Err(_) => { + let div = create_element("div"); + let div = div.expect("TestGroup failed to create div"); + let div = dyn_into::<_, HtmlElement>(div).expect("HtmlElement"); + + div.set_attribute_or_panic("id" , name); + div.set_property_or_panic ("display" , "flex"); + div.set_property_or_panic ("flex-wrap" , "wrap"); + div.set_property_or_panic ("border" , "1px solid black"); + div.set_property_or_panic ("margin-bottom", "10px"); + + let header = create_element("center"); + let header = header.expect("TestGroup failed to create header"); + let header = dyn_into::<_, HtmlElement>(header); + let header = header.expect("HtmlElement"); + let border = "1px solid black"; + + header.set_inner_html(name); + header.set_property_or_panic("border-bottom", border); + header.set_property_or_panic("width" , "100%"); + div.append_or_panic(&header); + + let document = document().expect("Document is not present"); + let body = document.body().expect("Body is not present"); + body.append_or_panic(&div); + div + }, + }; + Self { div } + } +} diff --git a/gui/lib/web-test/src/lib.rs b/gui/lib/web-test/src/lib.rs new file mode 100644 index 0000000000..e99311dae5 --- /dev/null +++ b/gui/lib/web-test/src/lib.rs @@ -0,0 +1,21 @@ +#![feature(arbitrary_self_types)] + +mod system { + pub use basegl_system_web as web; +} + +use basegl_prelude as prelude; + +pub use wasm_bindgen_test::wasm_bindgen_test_configure as web_configure; +pub use web_test_proc_macro::*; +pub use wasm_bindgen_test::wasm_bindgen_test; + +mod bencher; +mod group; +mod container; +mod bench_container; + +pub use bencher::Bencher; +pub use group::Group; +pub use container::Container; +pub use bench_container::BenchContainer; diff --git a/gui/tests/web.rs b/gui/tests/web.rs deleted file mode 100644 index de5c1dafef..0000000000 --- a/gui/tests/web.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Test suite for the Web and headless browsers. - -#![cfg(target_arch = "wasm32")] - -extern crate wasm_bindgen_test; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(run_in_browser); - -#[wasm_bindgen_test] -fn pass() { - assert_eq!(1 + 1, 2); -}