* Custom benchmark solution
* Optimized HTMLRenderer

Original commit: 8a147bf9d4
This commit is contained in:
Danilo Guanabara 2019-12-06 13:11:17 -03:00 committed by GitHub
parent b80e3f29e4
commit d6df7dd156
34 changed files with 1464 additions and 501 deletions

View File

@ -57,3 +57,4 @@ features = [
[dev-dependencies]
wasm-bindgen-test = "0.3.3"
web-test = { version = "0.1.0" , path = "../web-test" }

View File

@ -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;
}

View File

@ -115,6 +115,14 @@ impl<'a, T> IntoIterator for &'a OptVec<T> {
}
}
impl<'a, T> IntoIterator for &'a mut OptVec<T> {
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);
}
}
}

View File

@ -1,5 +1,5 @@
pub mod mesh_registry;
pub mod rendering;
pub mod symbol;
pub mod workspace;
pub mod world;
pub mod rendering;

View File

@ -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)]

View File

@ -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<dyn Fn(&Vector2<f32>)>;
pub trait ResizeCallbackFn = where Self: Fn(&Vector2<f32>) + 'static;
// ========================
// === DOMContainerData ===
// ========================
#[derive(Derivative)]
#[derivative(Debug)]
pub struct DOMContainerData {
dimensions : Vector2<f32>,
#[derivative(Debug="ignore")]
resize_callbacks : Vec<ResizeCallback>
}
impl DOMContainerData {
pub fn new(dimensions : Vector2<f32>) -> 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<ResizeObserver>,
data : Rc<RefCell<DOMContainerData>>,
}
impl DOMContainer {
pub fn new(dom_id:&str) -> Result<Self> {
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<f32>) {
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<f32> {
self.data.borrow().dimensions
}
/// Adds a ResizeCallback.
pub fn add_resize_callback<T>(&mut self, callback:T)
where T : ResizeCallbackFn {
self.data.borrow_mut().resize_callbacks.push(Box::new(callback));
}
}

View File

@ -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<Self> {
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<f32>) {
self.container.set_dimensions(dimensions);
}
/// Gets the Scene Renderer's dimensions.
pub fn dimensions(&self) -> Vector2<f32> {
self.container.dimensions()
}
/// Adds a ResizeCallback.
pub fn add_resize_callback<T>(&mut self, callback : T)
where T : ResizeCallbackFn {
self.container.add_resize_callback(callback);
}
}

View File

@ -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<f32>,
pub dom : HtmlElement,
dimensions : Vector2<f32>,
}
impl HTMLObject {
/// Creates a HTMLObject from element name.
pub fn new(dom_name: &str) -> Result<Self> {
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<f32> {
pub fn dimensions(&self) -> &Vector2<f32> {
&self.dimensions
}
}

View File

@ -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<f32>) {
// 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<f32>
) {
// 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<f32>) {
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<HTMLRendererData>
}
impl HTMLRenderer {
/// Creates a HTMLRenderer.
pub fn new(dom_id: &str) -> Result<Self> {
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<f32>| {
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<HTMLObject>) {
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<HTMLObject> = &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<f32>) {
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

View File

@ -0,0 +1,5 @@
mod html_object;
mod html_renderer;
pub use html_object::*;
pub use html_renderer::*;

View File

@ -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

View File

@ -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<HTMLObject>,
}
pub type Index = usize;
impl HTMLScene {
/// Searches for a HtmlElement identified by id and appends to it.
pub fn new(dom_id: &str) -> Result<Self> {
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<HTMLObject> {
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()
}
}

View File

@ -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;

View File

@ -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<f32> {
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<f32> {
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<f32> {
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);
}
}

View File

@ -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() }
}

View File

@ -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<T> {
objects : OptVec<T>
}
impl Scene {
impl<T> Scene<T> {
/// Searches for a HtmlElement identified by id and appends to it.
pub fn new(dom_id: &str) -> Result<Self> {
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<f32> {
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<T> {
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<T> {
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<T> {
type Item = &'a mut T;
type IntoIter = IterMut<'a, T>;
fn into_iter(self) -> Self::IntoIter {
(&mut self.objects).into_iter()
}
}

View File

@ -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<f32> {
&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<f32> {
&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<f32> {
&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);
}
}

View File

@ -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<T : RealField> IntoCSSMatrix for Matrix4<T> {
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<f32> {
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::<f32>::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>) -> &[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<f32>) -> Matrix4<f32> {
// Negating the second column to invert Y.
m.row_part_mut(1, 4).iter_mut().for_each(|a| *a = -*a);
m
}
}

View File

@ -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;
}

View File

@ -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!("<center>{}</center>", 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);
}
}

View File

@ -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<HTMLObject> = 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<HTMLObject> {
let mut scene : Scene<HTMLObject> = 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<HTMLObject>, 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<HTMLObject> = &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<HTMLObject> = 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<HTMLObject> = 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);
})
}
}

View File

@ -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);
}
}

View File

@ -32,7 +32,8 @@ features = [
'WebGlProgram',
'WebGlShader',
'Window',
'console'
'console',
'Performance'
]
[dev-dependencies]

View File

@ -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<RefCell<AnimationFrameData>>
}
// ==========================
// === AnimationFrameLoop ===
// ==========================
impl AnimationFrameLoop {
pub fn new(mut func:Box<dyn FnMut()>) -> Self {
let nop_func = Box::new(|| ()) as Box<dyn FnMut()>;
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<dyn FnMut()>);
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;
}
}
}

View File

@ -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<F: FnOnce(&str) -> T, T>(&self, f: F) -> T;
fn with_log_msg<F: FnOnce(&str) -> T, T>(&self, f:F) -> T;
}
impl LogMsg for &str {
fn with_log_msg<F: FnOnce(&str) -> T, T>(&self, f: F) -> T {
fn with_log_msg<F: FnOnce(&str) -> T, T>(&self, f:F) -> T {
f(self)
}
}
impl<F: Fn() -> S, S: AsRef<str>> LogMsg for F {
fn with_log_msg<G: FnOnce(&str) -> T, T>(&self, f: G) -> T {
fn with_log_msg<G: FnOnce(&str) -> 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<T: AsRef<str>>(path: T) -> Self {
pub fn new<T: AsRef<str>>(path:T) -> Self {
let path = path.as_ref().to_string();
Self { path }
}
@ -90,29 +98,29 @@ impl Logger {
Self::new("")
}
pub fn sub<T: AsRef<str>>(&self, path: T) -> Self {
pub fn sub<T: AsRef<str>>(&self, path:T) -> Self {
Self::new(format!("{}.{}", self.path, path.as_ref()))
}
pub fn trace<M: LogMsg>(&self, msg: M) {
pub fn trace<M: LogMsg>(&self, msg:M) {
console::debug_1(&self.format(msg));
}
pub fn info<M: LogMsg>(&self, msg: M) {
pub fn info<M: LogMsg>(&self, msg:M) {
// console::info_1(&self.format(msg));
console::group_1(&self.format(msg));
console::group_end();
}
pub fn warning<M: LogMsg>(&self, msg: M) {
pub fn warning<M: LogMsg>(&self, msg:M) {
console::warn_1(&self.format(msg));
}
pub fn error<M: LogMsg>(&self, msg: M) {
pub fn error<M: LogMsg>(&self, msg:M) {
console::error_1(&self.format(msg));
}
pub fn group_begin<M: LogMsg>(&self, msg: M) {
pub fn group_begin<M: LogMsg>(&self, msg:M) {
console::group_1(&self.format(msg));
}
@ -120,14 +128,14 @@ impl Logger {
console::group_end();
}
pub fn group<M: LogMsg, T, F: FnOnce() -> T>(&self, msg: M, f: F) -> T {
pub fn group<M:LogMsg, T, F:FnOnce() -> T>(&self, msg:M, f:F) -> T {
self.group_begin(msg);
let out = f();
self.group_end();
out
}
fn format<M: LogMsg>(&self, msg: M) -> JsValue {
fn format<M: LogMsg>(&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<T, U>(obj : T) -> Result<U>
pub fn dyn_into<T,U>(obj :T) -> Result<U>
where T : wasm_bindgen::JsCast + Debug,
U : wasm_bindgen::JsCast
{
@ -192,23 +201,23 @@ pub fn document() -> Result<web_sys::Document> {
window()?.document().ok_or_else(|| Error::missing("document"))
}
pub fn get_element_by_id(id: &str) -> Result<web_sys::Element> {
pub fn get_element_by_id(id:&str) -> Result<web_sys::Element> {
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<T: wasm_bindgen::JsCast>(id: &str) -> Result<T> {
pub fn get_element_by_id_as<T:wasm_bindgen::JsCast>(id:&str) -> Result<T> {
let elem = get_element_by_id(id)?;
dyn_into(elem)
}
pub fn create_element(id: &str) -> Result<web_sys::Element> {
pub fn create_element(id:&str) -> Result<web_sys::Element> {
match document()?.create_element(id) {
Ok(element) => Ok(element),
Err(_) => Err(Error::missing(id)),
}
}
pub fn get_canvas(id: &str) -> Result<web_sys::HtmlCanvasElement> {
pub fn get_canvas(id:&str) -> Result<web_sys::HtmlCanvasElement> {
dyn_into(get_element_by_id(id)?)
}
@ -218,90 +227,110 @@ pub fn get_webgl_context(
) -> Result<WebGlRenderingContext>
{
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<dyn FnMut()>) -> Result<i32> {
pub fn request_animation_frame(f:&Closure<dyn FnMut()>) -> Result<i32> {
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<Performance> {
window()?.performance().ok_or_else(|| Error::missing("performance"))
}
// =====================
// === Other Helpers ===
// =====================
pub trait AttributeSetter {
fn set_attribute_or_panic<T, U>(&self, name : T, value : U)
fn set_attribute_or_panic<T, U>(&self, name:T, value:U)
where T : AsRef<str>,
U : AsRef<str>;
}
impl AttributeSetter for web_sys::HtmlElement {
fn set_attribute_or_panic<T, U>(&self, name : T, value : U)
fn set_attribute_or_panic<T,U>(&self, name:T, value:U)
where T : AsRef<str>,
U : AsRef<str> {
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<T, U>(&self, name : T, value : U)
fn set_property_or_panic<T,U>(&self, name:T, value:U)
where T : AsRef<str>,
U : AsRef<str>;
}
impl StyleSetter for web_sys::HtmlElement {
fn set_property_or_panic<T, U>(&self, name : T, value : U)
fn set_property_or_panic<T,U>(&self, name:T, value:U)
where T : AsRef<str>,
U : AsRef<str> {
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);
}
}

View File

@ -0,0 +1,14 @@
[package]
name = "web-test-proc-macro"
version = "0.1.0"
authors = ["Enso Team <contact@luna-lang.org>"]
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"

View File

@ -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 <div id="get_identified_element"></div>
/// 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::<ItemFn>(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::<Stmt>(&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::<ItemFn>(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
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "web-test"
version = "0.1.0"
authors = ["Enso Team <contact@luna-lang.org>"]
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"

View File

@ -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("<div>00.00ms</div>\
<div>0 iterations</div>\
<button>Toggle</button>");
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 }
}
}

View File

@ -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<dyn FnMut()>,
container : BenchContainer,
iterations : usize,
total_time : f64,
anim_loop : Option<AnimationFrameLoop>
}
impl BencherCell {
pub fn new(f:Box<dyn FnMut()>, 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<BencherCell>
}
impl BencherData {
pub fn new(f:Box<dyn FnMut()>, container:BenchContainer) -> Rc<Self> {
let cell = RefCell::new(BencherCell::new(f, container));
Rc::new(Self { cell })
}
/// Starts the benchmarking loop.
fn start(self:&Rc<Self>) {
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>) {
self.borrow_mut().anim_loop = None;
}
/// Check if the loop is running.
fn is_running(self:&Rc<Self>) -> 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<BencherData>
}
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<dyn FnMut()>;
{
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, F:FnMut() -> T + 'static>(&mut self, mut func:F) {
self.data.borrow_mut().func = Box::new(move || { func(); });
}
}

View File

@ -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 }
}
}

View File

@ -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 }
}
}

View File

@ -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;

View File

@ -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);
}