aframe/component/
register.rs

1//! Allows componenets to be registered in aframe. See the `component_def` macro for detailed docs.
2
3use crate::sys::{registerComponent, registerGeometry};
4use crate::utils::*;
5use std::{borrow::Cow, collections::HashMap};
6use serde::{Serialize};
7use wasm_bindgen::{JsCast, prelude::*};
8
9/// Top-level macro to define components. Usage resembles struct creation syntax.
10/// The `js!` macro is available for writing inline javascript, and returns a
11/// js_sys::Function object. This macro calls `into` on expressions passed into the 
12/// fields expecting function, allowing the `js!` macro to be used as a catch-all.
13/// Takes the optional fields described in the table below.
14///
15/// | field | syntax explanation | description |
16/// |-------|--------------------|-------------|
17/// | dependencies | strings separated by commas | names of components that must be initialized prior to this one |
18/// | schema | A hashmap containing string keys and ComponentProperty values. Recommend the maplit crate | Describes component properties |
19/// | multiple | boolean value | True to allow multiple components on a single entity |
20/// | init | JsValue created from a js_sys::Function() | Called on initialization |
21/// | update | JsValue created from a js_sys::Function(oldData) | Called whenever the component’s properties change |
22/// | tick | JsValue created from a js_sys::Function(time, timeDelta) | Called on each tick or frame of the scene’s render loop |
23/// | tock | JsValue created from a js_sys::Function(time, timeDelta, camera) | Identical to the tick method but invoked after the scene has rendered |
24/// | remove | JsValue created from a js_sys::Function() | Called whenever the component is detached from the entity |
25/// | pause | JsValue created from a js_sys::Function() | Called when the entity or scene pauses |
26/// | play | JsValue created from a js_sys::Function() | Called when the entity or scene resumes |
27/// | update_schema | JsValue created from a js_sys::Function(data) | if defined, is called on every update in order to check if the schema needs to be dynamically modified |
28///
29/// All parameteres are optional, although the order must be exactly as shown. 
30/// `dependencies` should be a comma-separated list of strings followed by a 
31/// semicolon. `schema` should be a HashMap with string keys and `AframeProperty` 
32/// values. `multiple` is a boolean value. The rest are strings containing 
33/// javascript code. A `js!` macro is provided to allow inline javascript code 
34/// to be included in the Rust code (See the docs for the `js!` macro for 
35/// caveats and limitations). Here's an example:
36/// ```ignore
37/// // Example: 
38/// let some_component = component_def!
39/// (
40///     dependencies: "dependency1", "dependency2", some_string,
41///     schema: hashmap!
42///     {
43///         "position" => AframeProperty::float("number", None),
44///         "text" => AframeProperty::string("string", Some(Cow::Borrowed("x"))),
45///         "autoplay" => AframeProperty::boolean("boolean", Some(true))
46///     },
47///     multiple: true,
48///     init: js!
49///     (
50///         this.radians = Math.PI * 2; 
51///         this.initalRotation = this.el.object3D.rotation.clone();
52///     ),
53///     update: js!(oldData =>> this.rotation = this.el.object3D.rotation;),
54///     tick: js!
55///     (time, delta =>>
56///         if (this.data.autoplay)
57///         {
58///             var amount = this.data.radiansPerMillisecond * delta * this.data.speedMult;
59///             if (this.data.axis.includes('x'))
60///                 this.rotation.x = (this.rotation.x + amount) % this.radians;
61///             if (this.data.axis.includes('y'))
62///                 this.rotation.y = (this.rotation.y + amount) % this.radians;
63///             if (this.data.axis.includes('z'))
64///                 this.rotation.z = (this.rotation.z + amount) % this.radians;
65///         }
66///     ),
67///     remove: js!(this.rotation.copy(this.initialRotation);),
68///     pause: js!(this.data.autoplay = false;),
69///     play: js!(this.data.autoplay = true;),
70/// );
71/// unsafe
72/// {
73///     some_component.register("component_name");
74/// }
75/// ```
76#[macro_export]
77macro_rules! component_def
78{
79    (
80        $(dependencies: $($deps:expr),*;)? 
81        $(schema: $schema:expr,)?
82        $(multiple: $mult:expr,)? 
83        $(init: $init:expr,)?
84        $(update: $update:expr,)?
85        $(tick: $tick:expr,)?
86        $(tock: $tock:expr,)?
87        $(remove: $remove:expr,)?
88        $(pause: $pause:expr,)?
89        $(play: $play:expr,)?
90        $(update_schema: $update_schema:expr,)?
91    ) => 
92    {
93        $crate::component::ComponentReg
94        {
95            $(schema: $schema,)?
96            $(dependencies: std::borrow::Cow::Borrowed(&[$(std::borrow::Cow::Borrowed($deps)),*]),)?
97            $(multiple: $mult,)?
98            $(init: $init.into(),)?
99            $(update: $update.into(),)?
100            $(tick: $tick.into(),)?
101            $(tock: $tock.into(),)?
102            $(remove: $remove.into(),)?
103            $(pause: $pause.into(),)?
104            $(play: $play.into(),)?
105            $(update_schema: $update_schema.into(),)?
106            ..$crate::component::ComponentReg::default()
107        }
108    }
109}
110
111/// Top-level macro to define custom geometries. Syntax resemles but is simpler
112/// than the `component_def!` macro.
113/// The `js!` macro is available for writing inline javascript, and returns a
114/// js_sys::Function object. This macro calls `into` on expressions passed into the 
115/// fields expecting function, allowing the `js!` macro to be used as a catch-all.
116/// Takes the optional fields described in the table below.
117///
118/// | field | syntax explanation | description |
119/// |-------|--------------------|-------------|
120/// | schema | A hashmap containing string keys and GeometryProperty values. Recommend the maplit crate | Describes custom geometry properties |
121/// | init | JsValue created from a js_sys::Function() | Called on initialization |
122///
123/// All parameteres are optional, although leaving either out may not result in 
124/// a meaningful geometry definition.
125/// ```ignore
126/// // Example (this is an exact replica of the builtin `box` geometry): 
127/// let newbox = geometry_def!
128/// {
129///     schema: hashmap!
130///     {
131///         "depth" => GeometryProperty::new(AframeVal::Float(1.0), Some(AframeVal::Float(0.0)), None, None),
132///         "height" => GeometryProperty::new(AframeVal::Float(1.0), Some(AframeVal::Float(0.0)), None, None),
133///         "width" => GeometryProperty::new(AframeVal::Float(1.0), Some(AframeVal::Float(0.0)), None, None),
134///         "segmentsHeight" => GeometryProperty::new(AframeVal::Int(1), Some(AframeVal::Int(1)), Some(AframeVal::Int(20)), Some("int")),
135///         "segmentsWidth" => GeometryProperty::new(AframeVal::Int(1), Some(AframeVal::Int(1)), Some(AframeVal::Int(20)), Some("int")),
136///         "segmentsDepth" => GeometryProperty::new(AframeVal::Int(1), Some(AframeVal::Int(1)), Some(AframeVal::Int(20)), Some("int")),
137///     },
138///     init: js!(data =>> this.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth);)
139/// };
140/// unsafe
141/// {
142///     newbox.register("newbox");
143/// }
144/// ```
145#[macro_export]
146macro_rules! geometry_def
147{
148    (
149        $(schema: $schema:expr,)?
150        $(init: $init:expr)?
151    ) => 
152    {
153        $crate::component::GeometryReg
154        {
155            $(schema: $schema,)?
156            $(init: $init.into(),)?
157            ..$crate::component::GeometryReg::default()
158        }
159    }
160}
161
162/// Component registration definition. All JsValues should be derived from [`js_sys::Function`]
163#[derive(Serialize, Clone)]
164pub struct ComponentReg
165{
166    pub schema: HashMap<&'static str, AframeProperty>,
167    pub dependencies: Cow<'static, [Cow<'static, str>]>,
168    pub multiple: bool,
169    // TODO: events: HashMap<Cow<'static, str>, Function(event)>
170    #[serde(skip)] pub init: JsValue,
171    #[serde(skip)] pub update: JsValue,
172    #[serde(skip)] pub tick: JsValue, 
173    #[serde(skip)] pub tock: JsValue,
174    #[serde(skip)] pub remove: JsValue,
175    #[serde(skip)] pub pause: JsValue,
176    #[serde(skip)] pub play: JsValue,
177    #[serde(skip)] pub update_schema: JsValue
178}
179impl Default for ComponentReg
180{
181    fn default() -> Self 
182    {
183        let empty_fn: JsValue = js_sys::Function::default().into();
184        Self
185        {
186            schema: HashMap::new(),
187            dependencies: Cow::Borrowed(&[]),
188            multiple: false,
189            init: empty_fn.clone(),
190            update: empty_fn.clone(),
191            tick: empty_fn.clone(),
192            tock: empty_fn.clone(),
193            remove: empty_fn.clone(),
194            pause: empty_fn.clone(),
195            play: empty_fn.clone(),
196            update_schema: empty_fn
197        }
198    }
199}
200impl From<&ComponentReg> for JsValue
201{
202    fn from(cmr: &ComponentReg) -> Self 
203    {
204        let js_value = serde_wasm_bindgen::to_value(cmr).expect("Failed to convert ComponentReg into JsObject");
205        define_property(js_value.unchecked_ref(), "init", (cmr.init).unchecked_ref());
206        define_property(js_value.unchecked_ref(), "update", (cmr.update).unchecked_ref());
207        define_property(js_value.unchecked_ref(), "tick", (cmr.tick).unchecked_ref());
208        define_property(js_value.unchecked_ref(), "tock", (cmr.tock).unchecked_ref());
209        define_property(js_value.unchecked_ref(), "remove", (cmr.remove).unchecked_ref());
210        define_property(js_value.unchecked_ref(), "pause", (cmr.pause).unchecked_ref());
211        define_property(js_value.unchecked_ref(), "play", (cmr.play).unchecked_ref());
212        define_property(js_value.unchecked_ref(), "update_schema", (cmr.update_schema).unchecked_ref());
213        js_value
214    }
215}
216impl ComponentReg
217{
218    /// Register a component in aframe. Warning: Aframe must be initialized before this is called.
219    pub unsafe fn register(self, name: &str)
220    {
221        registerComponent(name, (&self).into());
222    }
223}
224
225/// Geometry registration definition. The `init` JsValue should be derived from [`js_sys::Function`]
226#[derive(Serialize, Clone)]
227pub struct GeometryReg
228{
229    pub schema: HashMap<&'static str, GeometryProperty>,
230    #[serde(skip)] pub init: JsValue,
231}
232impl Default for GeometryReg
233{
234    fn default() -> Self 
235    {
236        Self
237        {
238            schema: HashMap::new(),
239            init: js_sys::Function::default().into(),
240        }
241    }
242}
243impl From<&GeometryReg> for JsValue
244{
245    fn from(cmr: &GeometryReg) -> Self 
246    {
247        let js_value: JsValue = serde_wasm_bindgen::to_value(cmr).expect("Failed to convert GeometryReg into JsObject");
248        define_property(js_value.unchecked_ref(), "init", (cmr.init).unchecked_ref());
249        js_value
250    }
251}
252impl GeometryReg
253{
254    /// Register a custom geometry in aframe. Warning: Aframe must be initialized before this is called.
255    pub unsafe fn register(self, name: &str)
256    {
257        registerGeometry(name, (&self).into());
258    }
259}
260
261/// A property for a GeometryReg
262#[derive(Serialize, Clone)]
263pub struct GeometryProperty
264{
265    default: AframeVal,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    min: Option<AframeVal>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    max: Option<AframeVal>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    #[serde(rename = "type")] 
272    component_type: Option<&'static str>
273}
274
275impl GeometryProperty
276{
277    pub fn new(default: AframeVal, min: Option<AframeVal>, max: Option<AframeVal>, component_type: Option<&'static str>) -> Self
278    {
279        GeometryProperty{ default, component_type, min, max }
280    }
281}