diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 87a49987f8..c76a5186ab 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Wojciech Danilo "] edition = "2018" [lib] -crate-type = ["cdylib"] +crate-type = ["rlib", "cdylib"] [features] default = ["console_error_panic_hook"] @@ -29,7 +29,11 @@ console_error_panic_hook = { version = "0.1.1", optional = true } version = "0.3.4" features = [ 'Document', + 'Node', 'Element', + 'HtmlElement', + 'HtmlCollection', + 'CssStyleDeclaration', 'HtmlCanvasElement', 'WebGlBuffer', 'WebGlRenderingContext', diff --git a/lib/core/src/data/mod.rs b/lib/core/src/data/mod.rs index efcb8e32e1..994dfd5630 100644 --- a/lib/core/src/data/mod.rs +++ b/lib/core/src/data/mod.rs @@ -1 +1,2 @@ pub mod opt_vec; +pub mod types; diff --git a/lib/core/src/data/opt_vec.rs b/lib/core/src/data/opt_vec.rs index e61ba17cb4..c7717bee09 100644 --- a/lib/core/src/data/opt_vec.rs +++ b/lib/core/src/data/opt_vec.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use super::types::Index; // ============== // === OptVec === @@ -11,21 +12,25 @@ use crate::prelude::*; pub struct OptVec { #[shrinkwrap(main_field)] pub items: Vec>, - pub free_ixs: Vec, + pub free_ixs: Vec, +} + +impl Default for OptVec { + fn default() -> Self { + let items = Default::default(); + let free_ixs = Default::default(); + Self { items, free_ixs } + } } impl OptVec { /// Constructs a new, empty `Vec`. It will not allocate until elements /// are pushed onto it. - pub const fn new() -> Self { - let items = Vec::new(); - let free_ixs = Vec::new(); - Self { items, free_ixs } - } + pub fn new() -> Self { default() } /// Finds a free index and inserts the element. The index is re-used in case /// the array is sparse or is added in case of no free places. - pub fn insert T>(&mut self, f: F) -> usize { + pub fn insert T>(&mut self, f: F) -> Index { match self.free_ixs.pop() { None => { let ix = self.items.len(); @@ -42,11 +47,48 @@ impl OptVec { /// Removes the element at provided index and marks the index to be reused. /// Does nothing if the index was already empty. Panics if the index was out /// of bounds. - pub fn remove(&mut self, ix: usize) -> Option { + pub fn remove(&mut self, ix: Index) -> Option { let item = self.items[ix].take(); item.iter().for_each(|_| self.free_ixs.push(ix)); item } + + /// Returns the number of elements in the vector, also referred to as its 'length'. + pub fn len(&self) -> usize { + self.items.len() - self.free_ixs.len() + } + + /// Returns true if vector contains no element. + pub fn is_empty(&self) -> bool { + self.items.len() == self.free_ixs.len() + } +} + +// ============ +// === Iter === +// ============ + +/// We can use Iter to iterate over OptVec similarly to Vec. +pub struct Iter<'a, T> { + iter : std::slice::Iter<'a, Option> +} + +impl<'a, T> Iterator for Iter<'a, T> { + type Item = &'a T; + fn next(&mut self) -> Option { + if let Some(opt_item) = self.iter.next() { + // If opt_item has some item, we return it, else we try the next one + if let Some(item) = opt_item { Some(item) } else { self.next() } + } else { None } + } +} + +impl<'a, T> IntoIterator for &'a OptVec { + type Item = &'a T; + type IntoIter = Iter<'a, T>; + fn into_iter(self) -> Self::IntoIter { + Iter { iter : (&self.items).iter() } + } } #[cfg(test)] @@ -56,14 +98,51 @@ mod tests { #[test] fn test_add() { let mut v = OptVec::new(); + assert!(v.is_empty(), "OptVec should be created empty"); + let ix1 = v.insert(|_| 1); + assert_eq!(ix1, 0, "ix1 should be indexed at 0"); + assert_eq!(v.len(), 1, "OptVec should have 1 item now"); + assert!(!v.is_empty(), "OptVec is no longer empty now"); + let ix2 = v.insert(|_| 2); - v.remove(ix1); + assert_eq!(ix2, 1, "ix2 should be indexed at 1"); + assert_eq!(v.len(), 2); + + v.remove(ix1); // remove ix1 (0) and make 0 index free + assert_eq!(v.len(), 1); // removing should decrease len by 1 + + v.remove(ix2); // remove ix2 (1) and make 1 index free + assert_eq!(v.len(), 0); + assert!(v.is_empty(), "OptVec should be empty now"); + let ix3 = v.insert(|_| 3); + assert_eq!(v.len(), 1); let ix4 = v.insert(|_| 4); - assert_eq!(ix1, 0); - assert_eq!(ix2, 1); - assert_eq!(ix3, 0); - assert_eq!(ix4, 2); + assert_eq!(ix3, 1, "ix3 should be the first freed index"); + assert_eq!(ix4, 0, "ix4 should be the second freed index"); + assert_eq!(v.len(), 2); + } + + #[test] + fn test_iter() { + 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"); + + for (i, value) in v.into_iter().enumerate() { + assert_eq!(i, *value); + } + + v.remove(ix1); + assert_eq!(v.len(), 2, "OptVec should have 2 items"); + for (i, value) in v.into_iter().enumerate() { + // we add + 1, because the fisrt item is 1 now. + assert_eq!(i + 1, *value); + } } } diff --git a/lib/core/src/data/types.rs b/lib/core/src/data/types.rs new file mode 100644 index 0000000000..07432f7b46 --- /dev/null +++ b/lib/core/src/data/types.rs @@ -0,0 +1 @@ +pub type Index = usize; diff --git a/lib/core/src/display/mod.rs b/lib/core/src/display/mod.rs index a7c2532e25..6a6d7a1bfc 100644 --- a/lib/core/src/display/mod.rs +++ b/lib/core/src/display/mod.rs @@ -1,3 +1,4 @@ +pub mod rendering; pub mod symbol; pub mod workspace; pub mod world; diff --git a/lib/core/src/display/rendering/camera.rs b/lib/core/src/display/rendering/camera.rs new file mode 100644 index 0000000000..252ff5eb29 --- /dev/null +++ b/lib/core/src/display/rendering/camera.rs @@ -0,0 +1,49 @@ +use crate::prelude::*; + +use super::Object; + +use nalgebra::base::Matrix4; +use nalgebra::geometry::Perspective3; +use std::f32::consts::PI; + +// ============== +// === Camera === +// ============== + +/// A 3D camera representation with its own 3D `Transform` and +/// projection matrix. +#[derive(Shrinkwrap, Debug)] +#[shrinkwrap(mutable)] +pub struct Camera { + #[shrinkwrap(main_field)] + pub object : Object, + pub projection : Matrix4, +} + +impl Camera { + /// Creates a Camera with perspective projection. + 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 } + } +} + +#[cfg(test)] +mod test { + #[test] + fn perspective() { + use super::Camera; + use nalgebra::Matrix4; + let camera = Camera::perspective(45.0, 1920.0 / 1080.0, 1.0, 1000.0); + let expected = Matrix4::new + ( 1.357995, 0.0, 0.0, 0.0 + , 0.0 , 2.4142134, 0.0, 0.0 + , 0.0 , 0.0, -1.002002, -2.002002 + , 0.0 , 0.0, -1.0, 0.0 + ); + assert_eq!(camera.projection, expected); + } +} diff --git a/lib/core/src/display/rendering/htmlobject.rs b/lib/core/src/display/rendering/htmlobject.rs new file mode 100644 index 0000000000..99de612e19 --- /dev/null +++ b/lib/core/src/display/rendering/htmlobject.rs @@ -0,0 +1,67 @@ +use crate::prelude::*; + +use super::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; + +// ================== +// === HTMLObject === +// ================== + +/// A structure for representing a 3D HTMLElement in a `HTMLScene`. +#[derive(Shrinkwrap, Debug)] +#[shrinkwrap(mutable)] +pub struct HTMLObject { + #[shrinkwrap(main_field)] + pub object : Object, + pub element : HtmlElement, + pub 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)) + } + + /// 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(); + let dimensions = Vector2::new(0.0, 0.0); + Self { object, element, dimensions } + } + + /// Creates a HTMLObject from a HTML string. + pub fn from_html_string(html_string: T) -> Result + where T : AsRef { + let element = create_element("div")?; + element.set_inner_html(html_string.as_ref()); + match element.first_element_child() { + Some(element) => Ok(Self::from_element(dyn_into(element)?)), + None => Err(Error::missing("valid HTML")), + } + } + + /// 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)); + } + + /// Gets the underlying HtmlElement dimension. + pub fn get_dimensions(&self) -> &Vector2 { + &self.dimensions + } +} diff --git a/lib/core/src/display/rendering/htmlrenderer.rs b/lib/core/src/display/rendering/htmlrenderer.rs new file mode 100644 index 0000000000..62bc17b81a --- /dev/null +++ b/lib/core/src/display/rendering/htmlrenderer.rs @@ -0,0 +1,53 @@ +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/lib/core/src/display/rendering/htmlscene.rs b/lib/core/src/display/rendering/htmlscene.rs new file mode 100644 index 0000000000..fd93d948d9 --- /dev/null +++ b/lib/core/src/display/rendering/htmlscene.rs @@ -0,0 +1,74 @@ +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; +use crate::data::types::Index; + +// ================= +// === 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, +} + +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/lib/core/src/display/rendering/mod.rs b/lib/core/src/display/rendering/mod.rs new file mode 100644 index 0000000000..e29cc8e12c --- /dev/null +++ b/lib/core/src/display/rendering/mod.rs @@ -0,0 +1,19 @@ +mod camera; +mod object; +mod renderer; +mod scene; +mod transform; + +mod htmlobject; +mod htmlrenderer; +mod htmlscene; + +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; diff --git a/lib/core/src/display/rendering/object.rs b/lib/core/src/display/rendering/object.rs new file mode 100644 index 0000000000..fa0ec10d1d --- /dev/null +++ b/lib/core/src/display/rendering/object.rs @@ -0,0 +1,59 @@ +use super::Transform; +use crate::prelude::*; + +// ============== +// === 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, +} + +impl Object { + /// Creates a Default Object. + pub fn new() -> Object { default() } + + /// Sets the object's position. + pub fn set_position(&mut self, x: f32, y: f32, z: f32) { + self.transform.set_translation(x, y, z) + } + + /// Sets the object's rotation in YXZ (yaw -> roll -> pitch) order. + pub fn set_rotation(&mut self, roll: f32, pitch: f32, yaw: f32) { + self.transform.set_rotation(roll, pitch, yaw) + } + + /// Sets the object's scale. + pub fn set_scale(&mut self, x: f32, y: f32, z: f32) { + self.transform.set_scale(x, y, z); + } +} + +#[cfg(test)] +mod test { + #[test] + fn set_transform() { + use super::Object; + use nalgebra::Vector3; + use nalgebra::Quaternion; + use std::f32::consts::PI; + + let mut object = Object::new(); + object.set_position(1.0, 2.0, 3.0); + 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)); + + let expected = Quaternion::new + ( 0.00000009272586 + , -0.7071068 + , -0.7071068 + , -0.000000030908623 ); + assert_eq!(*object.transform.rotation.quaternion(), expected); + } +} diff --git a/lib/core/src/display/rendering/renderer.rs b/lib/core/src/display/rendering/renderer.rs new file mode 100644 index 0000000000..07403ddaba --- /dev/null +++ b/lib/core/src/display/rendering/renderer.rs @@ -0,0 +1,10 @@ +// 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/lib/core/src/display/rendering/scene.rs b/lib/core/src/display/rendering/scene.rs new file mode 100644 index 0000000000..6555a41fdb --- /dev/null +++ b/lib/core/src/display/rendering/scene.rs @@ -0,0 +1,24 @@ +use crate::system::web::{get_element_by_id, dyn_into, Result}; +use web_sys::HtmlElement; +use nalgebra::Vector2; + +/// A collection for holding 3D `Object`s. +#[derive(Debug)] +pub struct Scene { + pub container : HtmlElement, +} + +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 }) + } + + /// 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) + } +} diff --git a/lib/core/src/display/rendering/transform.rs b/lib/core/src/display/rendering/transform.rs new file mode 100644 index 0000000000..faf34b3665 --- /dev/null +++ b/lib/core/src/display/rendering/transform.rs @@ -0,0 +1,149 @@ +use crate::prelude::*; + +use nalgebra::Matrix4; +use nalgebra::Quaternion; +use nalgebra::UnitQuaternion; +use nalgebra::Vector3; + +// ============= +// === Utils === +// ============= + +// This constructs a Quaternion with rotation order Pitch -> Roll -> Yaw: +fn from_euler_angles_pry(roll:f32, pitch:f32, yaw:f32) -> UnitQuaternion { + let (s1, c1): (f32, f32) = (roll * 0.5 as f32).sin_cos(); + let (s2, c2): (f32, f32) = (pitch * 0.5 as f32).sin_cos(); + let (s3, c3): (f32, f32) = (yaw * 0.5 as f32).sin_cos(); + UnitQuaternion::from_quaternion(Quaternion::new( + c1 * c2 * c3 - s1 * s2 * s3, + s1 * c2 * c3 + c1 * s2 * s3, + c1 * s2 * c3 - s1 * c2 * s3, + c1 * c2 * s3 + s1 * s2 * c3, + )) +} + +// ================= +// === Transform === +// ================= + +/// A structure representing 3D Position, Rotation and Scale. +#[derive(Debug)] +pub struct Transform { + pub translation : Vector3, + pub rotation : UnitQuaternion, + pub scale : Vector3, +} + +impl Default for Transform { + fn default() -> Self { + let translation = Vector3::new(0.0, 0.0, 0.0); + let rotation = UnitQuaternion::identity(); + let scale = Vector3::new(1.0, 1.0, 1.0); + Self { translation, rotation, scale } + } +} + +impl Transform { + /// Creates an identity transform. + pub fn identity() -> Self { default() } + + /// Sets Transform's translation. + 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) { + 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) { + self.rotation = from_euler_angles_pry(roll, pitch, yaw); + } + + /// 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 + pub fn to_homogeneous(&self) -> Matrix4 { + let rx = self.rotation.coords.x; + let ry = self.rotation.coords.y; + let rz = self.rotation.coords.z; + let rw = self.rotation.coords.w; + + let (x2, y2, z2) = (rx + rx , ry + ry , rz + rz); + let (xx, xy, xz) = (rx * x2 , rx * y2 , rx * z2); + let (yy, yz, zz) = (ry * y2 , ry * z2 , rz * z2); + let (wx, wy, wz) = (rw * x2 , rw * y2 , rw * z2); + let (sx, sy, sz) = (self.scale.x, self.scale.y, self.scale.z); + + let m00 = (1.0 - (yy + zz)) * sx; + let m01 = (xy + wz) * sx; + let m02 = (xz - wy) * sx; + let m03 = 0.0; + + let m10 = (xy - wz) * sy; + let m11 = (1.0 - (xx + zz)) * sy; + let m12 = (yz + wx) * sy; + let m13 = 0.0; + + let m20 = (xz + wy) * sz; + let m21 = (yz - wx) * sz; + let m22 = (1.0 - (xx + yy)) * sz; + let m23 = 0.0; + + let m30 = self.translation.x; + let m31 = self.translation.y; + let m32 = self.translation.z; + let m33 = 1.0; + + Matrix4::new + ( m00, m10, m20, m30 + , m01, m11, m21, m31 + , m02, m12, m22, m32 + , m03, m13, m23, m33 + ) + } +} + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod test { + #[test] + fn identity() { + use super::Transform; + use nalgebra::Vector3; + 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()); + } + + #[test] + fn set_transform() { + use super::Transform; + use nalgebra::Vector3; + use nalgebra::Quaternion; + use std::f32::consts::PI; + + let mut transform = Transform::identity(); + transform.set_translation(1.0, 2.0, 3.0); + 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)); + + let expected = Quaternion::new + ( 0.00000009272586 + , -0.7071068 + , -0.7071068 + , -0.000000030908623 ); + assert_eq!(*transform.rotation.quaternion(), expected); + } +} diff --git a/lib/core/src/display/world.rs b/lib/core/src/display/world.rs index 23b8a9a24c..715800fab4 100644 --- a/lib/core/src/display/world.rs +++ b/lib/core/src/display/world.rs @@ -46,7 +46,7 @@ impl Default for World { impl World { pub fn new() -> Self { - Default::default() + default() } pub fn started(&self) -> bool { @@ -107,7 +107,7 @@ impl Default for WorldData { impl WorldData { pub fn new() -> Self { - Default::default() + default() } pub fn add_workspace(&mut self, name: &str) -> workspace::ID { diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index 8b8bc69dd8..a4d34f8972 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -11,6 +11,7 @@ // ================================= pub mod data; +pub mod math; pub mod dirty; pub mod display; pub use basegl_prelude as prelude; diff --git a/lib/core/src/math/mod.rs b/lib/core/src/math/mod.rs new file mode 100644 index 0000000000..b5614dd823 --- /dev/null +++ b/lib/core/src/math/mod.rs @@ -0,0 +1 @@ +pub mod utils; diff --git a/lib/core/src/math/utils.rs b/lib/core/src/math/utils.rs new file mode 100644 index 0000000000..ca4073d546 --- /dev/null +++ b/lib/core/src/math/utils.rs @@ -0,0 +1,58 @@ +use nalgebra::Matrix4; +use nalgebra::RealField; + +// ====================== +// === Matrix Printer === +// ====================== + +pub trait IntoCSSMatrix { + fn into_css_matrix(&self) -> String; +} + +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)); + format!("matrix3d({})", ret) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn into_css_matrix() { + use nalgebra::Matrix4; + use super::IntoCSSMatrix; + + 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 column_major = matrix.into_css_matrix(); + let expected = "matrix3d(1, 2, 3, 4, \ + 5, 6, 7, 8, \ + 9, 10, 11, 12, \ + 13, 14, 15, 16)"; + assert_eq!(column_major, expected); + } +} + +// ============= +// === Misc === +// ============= + +// eps is used to round very small values to 0.0 for numerical stability +pub fn eps(value: f32) -> f32 { + if value.abs() < 1e-10 { 0.0 } else { value } +} + +// Inverts Matrix Y coordinates. +// It's equivalent to scaling by (1.0, -1.0, 1.0). +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 +} diff --git a/lib/core/tests/common.rs b/lib/core/tests/common.rs new file mode 100644 index 0000000000..d8e0906fc0 --- /dev/null +++ b/lib/core/tests/common.rs @@ -0,0 +1,89 @@ +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/lib/core/tests/htmlrenderer.rs b/lib/core/tests/htmlrenderer.rs new file mode 100644 index 0000000000..3cd2cceba2 --- /dev/null +++ b/lib/core/tests/htmlrenderer.rs @@ -0,0 +1,126 @@ +//! 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/lib/prelude/Cargo.toml b/lib/prelude/Cargo.toml index 4cf92283bb..4b12c57904 100644 --- a/lib/prelude/Cargo.toml +++ b/lib/prelude/Cargo.toml @@ -11,3 +11,7 @@ failure = "0.1.5" derive_more = "0.15.0" shrinkwraprs = "0.2.1" itertools = "0.8" +derivative = "1.0.3" +num = "0.2.0" +boolinator = "2.4.0" +paste = "0.1" \ No newline at end of file diff --git a/lib/prelude/src/lib.rs b/lib/prelude/src/lib.rs index ce680b64f9..8b6079c863 100644 --- a/lib/prelude/src/lib.rs +++ b/lib/prelude/src/lib.rs @@ -1,8 +1,146 @@ +#![feature(trait_alias)] + +pub use boolinator::Boolinator; pub use core::any::type_name; +pub use core::fmt::Debug; +pub use derivative::Derivative; pub use derive_more::*; pub use failure::Fail; pub use itertools::Itertools; +pub use num::Num; +pub use paste; pub use shrinkwraprs::Shrinkwrap; +pub use std::cell::Ref; pub use std::cell::RefCell; +pub use std::collections::HashMap; +pub use std::collections::HashSet; +pub use std::convert::identity; +pub use std::convert::TryFrom; +pub use std::convert::TryInto; +pub use std::fmt::Display; +pub use std::fmt; +pub use std::hash::Hash; +pub use std::iter; +pub use std::iter::FromIterator; +pub use std::marker::PhantomData; pub use std::ops::Deref; +pub use std::ops::DerefMut; +pub use std::ops::Index; +pub use std::ops::IndexMut; pub use std::rc::Rc; +pub use std::rc::Weak; +pub use std::slice; +pub use std::slice::SliceIndex; + +pub trait Str = AsRef; + +pub fn default() -> T { + Default::default() +} + +pub type PhantomData2 = PhantomData<(PhantomData , PhantomData)>; +pub type PhantomData3 = PhantomData2, PhantomData>; +pub type PhantomData4 = PhantomData2, PhantomData>; +pub type PhantomData5 = PhantomData2, PhantomData>; +pub type PhantomData6 = PhantomData2, PhantomData>; +pub type PhantomData7 = PhantomData2, PhantomData>; +pub type PhantomData8 = PhantomData2, PhantomData>; +pub type PhantomData9 = PhantomData2, PhantomData>; + + +/// Surprisingly useful function. Consider the following code: +/// +/// ```compile_fail +/// fn init(self) -> Self { +/// let mut data = self.borrow_mut(); +/// ... +/// self +/// } +/// ``` +/// +/// It may not compile telling that the last line moves self out, however, +/// borrow might be used there, when `data` is dropped and runs the destructor. +/// +/// We can usethis function to narrow-down the lifetimes. The following code +/// compiles just fine: +/// +/// ```compile_fail +/// fn init(self) -> Self { +/// with(self.borrow_mut(), |mut data| { +/// ... +/// }); +/// self +/// } +/// ``` +pub fn with Out, Out>(t: T, f: F) -> Out { f(t) } + + +/// This is a very unsafe function, use it with caution please. There are few +/// legitimate use cases listed below. You are not allowed to use this function +/// for any other use case. If you discover a new possibly legitimate case, +/// confirm it with Luna Rust Core team and add its description below. +/// +/// In long-run, the below use cases should be replaced with safe-versions +/// implemented as macros. +/// +/// 1. Keeping mutually connected fields in a single structure. Especially +/// useful when defining iterators for wrappers keeping containers behind +/// a shared `Rc>` gate. An example: +/// +/// ```compile_fail +/// use std::rc::Rc; +/// use core::cell::RefCell; +/// use core::cell::Ref; +/// +/// pub struct SharedDirtyFlag { +/// data: Rc> +/// } +/// +/// impl SharedDirtyFlag +/// where for<'t> &'t T: IntoIterator { +/// pub fn iter(&self) -> SharedDirtyFlagIter { +/// let borrow = self.data.borrow(); +/// let reference = unsafe { drop_lifetime(&borrow) }; +/// let iter = reference.into_iter(); +/// SharedDirtyFlagIter { iter, borrow } +/// } +/// } +/// +/// // CAUTION !!! +/// // Please keep the fields in the correct order. They will be dropped +/// // in order. Moreover, keep the borrow field private. +/// pub struct SharedDirtyFlagIter<'t,T> +/// where &'t T: IntoIterator { +/// pub iter : <&'t T as IntoIterator>::IntoIter, +/// borrow : Ref<'t,T> +/// } +/// ``` + +// from clippy: error: unsafe function's docs miss `# Safety` section +// pub unsafe fn drop_lifetime<'a,'b,T>(t: &'a T) -> &'b T { +// std::mem::transmute(t) +// } + + +// =================== +// === WithPhantom === +// =================== + +/// A wrapper adding a phantom type to a structure. +#[derive(Derivative)] +#[derive(Shrinkwrap)] +#[shrinkwrap(mutable)] +#[derivative(Clone (bound="T: Clone"))] +#[derivative(Default (bound="T: Default"))] +pub struct WithPhantom { + #[shrinkwrap(main_field)] + pub without_phantom: T, + phantom: PhantomData

+} + +impl WithPhantom { + pub fn new(without_phantom: T) -> Self { + let phantom = PhantomData; + Self { without_phantom, phantom } + } +} diff --git a/lib/system/web/src/lib.rs b/lib/system/web/src/lib.rs index 4dcbdb8c65..dd2b95ac24 100644 --- a/lib/system/web/src/lib.rs +++ b/lib/system/web/src/lib.rs @@ -9,6 +9,8 @@ use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; use web_sys::WebGlRenderingContext; +use web_sys::Node; +use std::fmt::Debug; pub use web_sys::console; @@ -16,7 +18,7 @@ pub use web_sys::console; // === Error === // ============= -type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug, Fail)] pub enum Error { @@ -40,6 +42,15 @@ impl Error { } } +// =================== +// === JS Bindings === +// =================== + +#[macro_export] +macro_rules! console_log { + ($($t:tt)*) => ($crate::console::log_1(&format_args!($($t)*).to_string().into())) +} + // ============== // === LogMsg === // ============== @@ -130,6 +141,16 @@ macro_rules! fmt { // =================== // === DOM Helpers === // =================== + +pub fn dyn_into(obj : T) -> Result +where T : wasm_bindgen::JsCast + Debug, + U : wasm_bindgen::JsCast +{ + let expected = type_name::(); + let got = format!("{:?}", obj); + obj.dyn_into().map_err(|_| Error::type_mismatch(&expected, &got)) +} + pub fn window() -> Result { web_sys::window().ok_or_else(|| Error::missing("window")) } @@ -139,14 +160,19 @@ pub fn document() -> 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 { let elem = get_element_by_id(id)?; - let expected = type_name::(); - let got = format!("{:?}", elem); - elem.dyn_into().map_err(|_| Error::type_mismatch(&expected, &got)) + dyn_into(elem) +} +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 { - get_element_by_id_as(id) + dyn_into(get_element_by_id(id)?) } pub fn get_webgl_context( canvas: &HtmlCanvasElement, @@ -163,3 +189,75 @@ 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")) } + +// =================== +// === Other Helpers === +// =================== + +pub trait AttributeSetter { + 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) + 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) + .unwrap_or_else(|_| panic!("Failed to set attribute {}", values)); + } +} + +pub trait StyleSetter { + 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) + 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)); + } +} + +pub trait NodeAppender { + fn append_child_or_panic(&self, 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 + ) + ); + } +} + +pub trait NodeRemover { + 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 + ) + ); + } +}