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}