1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//! Allows componenets to be registered in aframe. See the `component_def` macro for detailed docs.

use crate::sys::{registerComponent, registerGeometry};
use crate::utils::*;
use std::{borrow::Cow, collections::HashMap};
use serde::{Serialize};
use wasm_bindgen::{JsCast, prelude::*};

/// Top-level macro to define components. Usage resembles struct creation syntax.
/// The `js!` macro is available for writing inline javascript, and returns a
/// js_sys::Function object. This macro calls `into` on expressions passed into the 
/// fields expecting function, allowing the `js!` macro to be used as a catch-all.
/// Takes the optional fields described in the table below.
///
/// | field | syntax explanation | description |
/// |-------|--------------------|-------------|
/// | dependencies | strings separated by commas | names of components that must be initialized prior to this one |
/// | schema | A hashmap containing string keys and ComponentProperty values. Recommend the maplit crate | Describes component properties |
/// | multiple | boolean value | True to allow multiple components on a single entity |
/// | init | JsValue created from a js_sys::Function() | Called on initialization |
/// | update | JsValue created from a js_sys::Function(oldData) | Called whenever the component’s properties change |
/// | tick | JsValue created from a js_sys::Function(time, timeDelta) | Called on each tick or frame of the scene’s render loop |
/// | tock | JsValue created from a js_sys::Function(time, timeDelta, camera) | Identical to the tick method but invoked after the scene has rendered |
/// | remove | JsValue created from a js_sys::Function() | Called whenever the component is detached from the entity |
/// | pause | JsValue created from a js_sys::Function() | Called when the entity or scene pauses |
/// | play | JsValue created from a js_sys::Function() | Called when the entity or scene resumes |
/// | 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 |
///
/// All parameteres are optional, although the order must be exactly as shown. 
/// `dependencies` should be a comma-separated list of strings followed by a 
/// semicolon. `schema` should be a HashMap with string keys and `AframeProperty` 
/// values. `multiple` is a boolean value. The rest are strings containing 
/// javascript code. A `js!` macro is provided to allow inline javascript code 
/// to be included in the Rust code (See the docs for the `js!` macro for 
/// caveats and limitations). Here's an example:
/// ```ignore
/// // Example: 
/// let some_component = component_def!
/// (
///     dependencies: "dependency1", "dependency2", some_string,
///     schema: hashmap!
///     {
///         "position" => AframeProperty::float("number", None),
///         "text" => AframeProperty::string("string", Some(Cow::Borrowed("x"))),
///         "autoplay" => AframeProperty::boolean("boolean", Some(true))
///     },
///     multiple: true,
///     init: js!
///     (
///         this.radians = Math.PI * 2; 
///         this.initalRotation = this.el.object3D.rotation.clone();
///     ),
///     update: js!(oldData =>> this.rotation = this.el.object3D.rotation;),
///     tick: js!
///     (time, delta =>>
///         if (this.data.autoplay)
///         {
///             var amount = this.data.radiansPerMillisecond * delta * this.data.speedMult;
///             if (this.data.axis.includes('x'))
///                 this.rotation.x = (this.rotation.x + amount) % this.radians;
///             if (this.data.axis.includes('y'))
///                 this.rotation.y = (this.rotation.y + amount) % this.radians;
///             if (this.data.axis.includes('z'))
///                 this.rotation.z = (this.rotation.z + amount) % this.radians;
///         }
///     ),
///     remove: js!(this.rotation.copy(this.initialRotation);),
///     pause: js!(this.data.autoplay = false;),
///     play: js!(this.data.autoplay = true;),
/// );
/// unsafe
/// {
///     some_component.register("component_name");
/// }
/// ```
#[macro_export]
macro_rules! component_def
{
    (
        $(dependencies: $($deps:expr),*;)? 
        $(schema: $schema:expr,)?
        $(multiple: $mult:expr,)? 
        $(init: $init:expr,)?
        $(update: $update:expr,)?
        $(tick: $tick:expr,)?
        $(tock: $tock:expr,)?
        $(remove: $remove:expr,)?
        $(pause: $pause:expr,)?
        $(play: $play:expr,)?
        $(update_schema: $update_schema:expr,)?
    ) => 
    {
        $crate::component::ComponentReg
        {
            $(schema: $schema,)?
            $(dependencies: std::borrow::Cow::Borrowed(&[$(std::borrow::Cow::Borrowed($deps)),*]),)?
            $(multiple: $mult,)?
            $(init: $init.into(),)?
            $(update: $update.into(),)?
            $(tick: $tick.into(),)?
            $(tock: $tock.into(),)?
            $(remove: $remove.into(),)?
            $(pause: $pause.into(),)?
            $(play: $play.into(),)?
            $(update_schema: $update_schema.into(),)?
            ..$crate::component::ComponentReg::default()
        }
    }
}

/// Top-level macro to define custom geometries. Syntax resemles but is simpler
/// than the `component_def!` macro.
/// The `js!` macro is available for writing inline javascript, and returns a
/// js_sys::Function object. This macro calls `into` on expressions passed into the 
/// fields expecting function, allowing the `js!` macro to be used as a catch-all.
/// Takes the optional fields described in the table below.
///
/// | field | syntax explanation | description |
/// |-------|--------------------|-------------|
/// | schema | A hashmap containing string keys and GeometryProperty values. Recommend the maplit crate | Describes custom geometry properties |
/// | init | JsValue created from a js_sys::Function() | Called on initialization |
///
/// All parameteres are optional, although leaving either out may not result in 
/// a meaningful geometry definition.
/// ```ignore
/// // Example (this is an exact replica of the builtin `box` geometry): 
/// let newbox = geometry_def!
/// {
///     schema: hashmap!
///     {
///         "depth" => GeometryProperty::new(AframeVal::Float(1.0), Some(AframeVal::Float(0.0)), None, None),
///         "height" => GeometryProperty::new(AframeVal::Float(1.0), Some(AframeVal::Float(0.0)), None, None),
///         "width" => GeometryProperty::new(AframeVal::Float(1.0), Some(AframeVal::Float(0.0)), None, None),
///         "segmentsHeight" => GeometryProperty::new(AframeVal::Int(1), Some(AframeVal::Int(1)), Some(AframeVal::Int(20)), Some("int")),
///         "segmentsWidth" => GeometryProperty::new(AframeVal::Int(1), Some(AframeVal::Int(1)), Some(AframeVal::Int(20)), Some("int")),
///         "segmentsDepth" => GeometryProperty::new(AframeVal::Int(1), Some(AframeVal::Int(1)), Some(AframeVal::Int(20)), Some("int")),
///     },
///     init: js!(data =>> this.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth);)
/// };
/// unsafe
/// {
///     newbox.register("newbox");
/// }
/// ```
#[macro_export]
macro_rules! geometry_def
{
    (
        $(schema: $schema:expr,)?
        $(init: $init:expr)?
    ) => 
    {
        $crate::component::GeometryReg
        {
            $(schema: $schema,)?
            $(init: $init.into(),)?
            ..$crate::component::GeometryReg::default()
        }
    }
}

/// Component registration definition. All JsValues should be derived from [`js_sys::Function`]
#[derive(Serialize, Clone)]
pub struct ComponentReg
{
    pub schema: HashMap<&'static str, AframeProperty>,
    pub dependencies: Cow<'static, [Cow<'static, str>]>,
    pub multiple: bool,
    // TODO: events: HashMap<Cow<'static, str>, Function(event)>
    #[serde(skip)] pub init: JsValue,
    #[serde(skip)] pub update: JsValue,
    #[serde(skip)] pub tick: JsValue, 
    #[serde(skip)] pub tock: JsValue,
    #[serde(skip)] pub remove: JsValue,
    #[serde(skip)] pub pause: JsValue,
    #[serde(skip)] pub play: JsValue,
    #[serde(skip)] pub update_schema: JsValue
}
impl Default for ComponentReg
{
    fn default() -> Self 
    {
        let empty_fn: JsValue = js_sys::Function::default().into();
        Self
        {
            schema: HashMap::new(),
            dependencies: Cow::Borrowed(&[]),
            multiple: false,
            init: empty_fn.clone(),
            update: empty_fn.clone(),
            tick: empty_fn.clone(),
            tock: empty_fn.clone(),
            remove: empty_fn.clone(),
            pause: empty_fn.clone(),
            play: empty_fn.clone(),
            update_schema: empty_fn
        }
    }
}
impl From<&ComponentReg> for JsValue
{
    fn from(cmr: &ComponentReg) -> Self 
    {
        let js_value = serde_wasm_bindgen::to_value(cmr).expect("Failed to convert ComponentReg into JsObject");
        define_property(js_value.unchecked_ref(), "init", (cmr.init).unchecked_ref());
        define_property(js_value.unchecked_ref(), "update", (cmr.update).unchecked_ref());
        define_property(js_value.unchecked_ref(), "tick", (cmr.tick).unchecked_ref());
        define_property(js_value.unchecked_ref(), "tock", (cmr.tock).unchecked_ref());
        define_property(js_value.unchecked_ref(), "remove", (cmr.remove).unchecked_ref());
        define_property(js_value.unchecked_ref(), "pause", (cmr.pause).unchecked_ref());
        define_property(js_value.unchecked_ref(), "play", (cmr.play).unchecked_ref());
        define_property(js_value.unchecked_ref(), "update_schema", (cmr.update_schema).unchecked_ref());
        js_value
    }
}
impl ComponentReg
{
    /// Register a component in aframe. Warning: Aframe must be initialized before this is called.
    pub unsafe fn register(self, name: &str)
    {
        registerComponent(name, (&self).into());
    }
}

/// Geometry registration definition. The `init` JsValue should be derived from [`js_sys::Function`]
#[derive(Serialize, Clone)]
pub struct GeometryReg
{
    pub schema: HashMap<&'static str, GeometryProperty>,
    #[serde(skip)] pub init: JsValue,
}
impl Default for GeometryReg
{
    fn default() -> Self 
    {
        Self
        {
            schema: HashMap::new(),
            init: js_sys::Function::default().into(),
        }
    }
}
impl From<&GeometryReg> for JsValue
{
    fn from(cmr: &GeometryReg) -> Self 
    {
        let js_value: JsValue = serde_wasm_bindgen::to_value(cmr).expect("Failed to convert GeometryReg into JsObject");
        define_property(js_value.unchecked_ref(), "init", (cmr.init).unchecked_ref());
        js_value
    }
}
impl GeometryReg
{
    /// Register a custom geometry in aframe. Warning: Aframe must be initialized before this is called.
    pub unsafe fn register(self, name: &str)
    {
        registerGeometry(name, (&self).into());
    }
}

/// A property for a GeometryReg
#[derive(Serialize, Clone)]
pub struct GeometryProperty
{
    default: AframeVal,
    #[serde(skip_serializing_if = "Option::is_none")]
    min: Option<AframeVal>,
    #[serde(skip_serializing_if = "Option::is_none")]
    max: Option<AframeVal>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "type")] 
    component_type: Option<&'static str>
}

impl GeometryProperty
{
    pub fn new(default: AframeVal, min: Option<AframeVal>, max: Option<AframeVal>, component_type: Option<&'static str>) -> Self
    {
        GeometryProperty{ default, component_type, min, max }
    }
}