@mcolabs/threebox-plugin 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,482 @@
1
+ /**
2
+ * @author peterqliu / https://github.com/peterqliu
3
+ * @author jscastro / https://github.com/jscastro76
4
+ */
5
+ import * as THREE from 'three';
6
+ import utils from '../utils/utils.js';
7
+
8
+ function AnimationManager(map) {
9
+
10
+ this.map = map
11
+ this.enrolledObjects = [];
12
+ this.previousFrameTime;
13
+
14
+ };
15
+
16
+ AnimationManager.prototype = {
17
+
18
+ unenroll: function (obj) {
19
+ this.enrolledObjects.splice(this.enrolledObjects.indexOf(obj), 1);
20
+ },
21
+
22
+ enroll: function (obj) {
23
+
24
+ //[jscastro] add the object default animations
25
+ obj.clock = new THREE.Clock();
26
+ obj.hasDefaultAnimation = false;
27
+ obj.defaultAction;
28
+ obj.actions = [];
29
+ obj.mixer;
30
+
31
+ //[jscastro] if the object includes animations
32
+ if (obj.animations && obj.animations.length > 0) {
33
+
34
+ obj.hasDefaultAnimation = true;
35
+
36
+ //check first if a defaultAnimation is defined by options
37
+ let daIndex = (obj.userData.defaultAnimation ? obj.userData.defaultAnimation : 0);
38
+ obj.mixer = new THREE.AnimationMixer(obj);
39
+
40
+ setAction(daIndex);
41
+ }
42
+
43
+ //[jscastro] set the action to play
44
+ function setAction(animationIndex) {
45
+ for (let i = 0; i < obj.animations.length; i++) {
46
+
47
+ if (animationIndex > obj.animations.length)
48
+ console.log("The animation index " + animationIndex + " doesn't exist for this object");
49
+ let animation = obj.animations[i];
50
+ let action = obj.mixer.clipAction(animation);
51
+ obj.actions.push(action);
52
+
53
+ //select the default animation and set the weight to 1
54
+ if (animationIndex === i) {
55
+ obj.defaultAction = action;
56
+ action.setEffectiveWeight(1);
57
+ }
58
+ else {
59
+ action.setEffectiveWeight(0);
60
+ }
61
+ action.play();
62
+
63
+ }
64
+ }
65
+
66
+ let _isPlaying = false;
67
+ //[jscastro] added property for isPlaying state
68
+ Object.defineProperty(obj, 'isPlaying', {
69
+ get() { return _isPlaying; },
70
+ set(value) {
71
+ if (_isPlaying != value) {
72
+ _isPlaying = value;
73
+ // Dispatch new event IsPlayingChanged
74
+ obj.dispatchEvent({ type: 'IsPlayingChanged', detail: obj});
75
+ }
76
+ }
77
+ })
78
+
79
+ /* Extend the provided object with animation-specific properties and track in the animation manager */
80
+ this.enrolledObjects.push(obj);
81
+
82
+ // Give this object its own internal animation queue
83
+ obj.animationQueue = [];
84
+
85
+ obj.set = function (options) {
86
+
87
+ //if duration is set, animate to the new state
88
+ if (options.duration > 0) {
89
+
90
+ let newParams = {
91
+ start: Date.now(),
92
+ expiration: Date.now() + options.duration,
93
+ endState: {}
94
+ }
95
+
96
+ utils.extend(options, newParams);
97
+
98
+ let translating = options.coords;
99
+ let rotating = options.rotation;
100
+ let scaling = options.scale || options.scaleX || options.scaleY || options.scaleZ;
101
+
102
+ if (rotating) {
103
+
104
+ let r = obj.rotation;
105
+ options.startRotation = [r.x, r.y, r.z];
106
+
107
+
108
+ options.endState.rotation = utils.types.rotation(options.rotation, options.startRotation);
109
+ options.rotationPerMs = options.endState.rotation
110
+ .map(function (angle, index) {
111
+ return (angle - options.startRotation[index]) / options.duration;
112
+ })
113
+ }
114
+
115
+ if (scaling) {
116
+ let s = obj.scale;
117
+ options.startScale = [s.x, s.y, s.z];
118
+ options.endState.scale = utils.types.scale(options.scale, options.startScale);
119
+
120
+ options.scalePerMs = options.endState.scale
121
+ .map(function (scale, index) {
122
+ return (scale - options.startScale[index]) / options.duration;
123
+ })
124
+ }
125
+
126
+ if (translating) options.pathCurve = new THREE.CatmullRomCurve3(utils.lnglatsToWorld([obj.coordinates, options.coords]));
127
+
128
+ let entry = {
129
+ type: 'set',
130
+ parameters: options
131
+ }
132
+
133
+ this.animationQueue
134
+ .push(entry);
135
+
136
+ tb.map.repaint = true;
137
+ }
138
+
139
+ //if no duration set, stop object's existing animations and go to that state immediately
140
+ else {
141
+ this.stop();
142
+ options.rotation = utils.radify(options.rotation);
143
+ this._setObject(options);
144
+ }
145
+
146
+ return this
147
+
148
+ };
149
+
150
+ //[jscastro] animation method, is set by update method
151
+ obj.animationMethod = null;
152
+
153
+ //[jscastro] stop animation and the queue
154
+ obj.stop = function (index) {
155
+ if (obj.mixer) {
156
+ obj.isPlaying = false;
157
+ cancelAnimationFrame(obj.animationMethod);
158
+ }
159
+ //TODO: if this is removed, it produces an error in
160
+ this.animationQueue = [];
161
+ return this;
162
+ }
163
+
164
+ obj.followPath = function (options, cb) {
165
+
166
+ let entry = {
167
+ type: 'followPath',
168
+ parameters: utils._validate(options, defaults.followPath)
169
+ };
170
+
171
+ utils.extend(
172
+ entry.parameters,
173
+ {
174
+ pathCurve: new THREE.CatmullRomCurve3(
175
+ utils.lnglatsToWorld(options.path)
176
+ ),
177
+ start: Date.now(),
178
+ expiration: Date.now() + entry.parameters.duration,
179
+ cb: cb
180
+ }
181
+ );
182
+
183
+ this.animationQueue
184
+ .push(entry);
185
+
186
+ tb.map.repaint = true;
187
+
188
+ return this;
189
+ };
190
+
191
+ obj._setObject = function (options) {
192
+
193
+ //default scale always
194
+ obj.setScale();
195
+
196
+ let p = options.position; // lnglat
197
+ let r = options.rotation; // radians
198
+ let s = options.scale; // custom scale
199
+ let w = options.worldCoordinates; //Vector3
200
+ let q = options.quaternion; // [axis, angle in rads]
201
+ let t = options.translate; // [jscastro] lnglat + height for 3D objects
202
+ let wt = options.worldTranslate; // [jscastro] Vector3 translation
203
+
204
+ if (p) {
205
+ this.coordinates = p;
206
+ let c = utils.projectToWorld(p);
207
+ this.position.copy(c)
208
+ }
209
+
210
+ if (t) {
211
+ this.coordinates = [this.coordinates[0] + t[0], this.coordinates[1] + t[1], this.coordinates[2] + t[2]];
212
+ let c = utils.projectToWorld(t);
213
+ this.position.copy(c)
214
+ //this.translateX(c.x);
215
+ //this.translateY(c.y);
216
+ //this.translateZ(c.z);
217
+ options.position = this.coordinates;
218
+ }
219
+
220
+ if (wt) {
221
+ this.translateX(wt.x);
222
+ this.translateY(wt.y);
223
+ this.translateZ(wt.z);
224
+ let p = utils.unprojectFromWorld(this.position);
225
+ this.coordinates = options.position = p;
226
+ }
227
+
228
+ if (r) {
229
+ this.rotation.set(r[0], r[1], r[2]);
230
+ options.rotation = new THREE.Vector3(r[0], r[1], r[2]);
231
+ }
232
+
233
+ if (s) {
234
+ this.scale.set(s[0], s[1], s[2]);
235
+ options.scale = this.scale;
236
+ }
237
+
238
+ if (q) {
239
+ this.quaternion.setFromAxisAngle(q[0], q[1]);
240
+ options.rotation = q[0].multiplyScalar(q[1]);
241
+ }
242
+
243
+ if (w) {
244
+ this.position.copy(w);
245
+ let p = utils.unprojectFromWorld(w);
246
+ this.coordinates = options.position = p;
247
+ }
248
+
249
+ //Each time the object is positioned, project the floor and correct shadow plane
250
+ this.setBoundingBoxShadowFloor();
251
+ this.setReceiveShadowFloor();
252
+
253
+ this.updateMatrixWorld();
254
+ tb.map.repaint = true;
255
+
256
+ //const threeTarget = new THREE.EventDispatcher();
257
+ //threeTarget.dispatchEvent({ type: 'event', detail: { object: this, action: { position: options.position, rotation: options.rotation, scale: options.scale } } });
258
+ // fire the ObjectChanged event to notify UI object change
259
+ let e = { type: 'ObjectChanged', detail: { object: this, action: { position: options.position, rotation: options.rotation, scale: options.scale } } };
260
+ this.dispatchEvent(e);
261
+
262
+ };
263
+
264
+ //[jscastro] play default animation
265
+ obj.playDefault = function (options) {
266
+ if (obj.mixer && obj.hasDefaultAnimation) {
267
+
268
+ let newParams = {
269
+ start: Date.now(),
270
+ expiration: Date.now() + options.duration,
271
+ endState: {}
272
+ }
273
+
274
+ utils.extend(options, newParams);
275
+
276
+ obj.mixer.timeScale = options.speed || 1;
277
+
278
+ let entry = {
279
+ type: 'playDefault',
280
+ parameters: options
281
+ };
282
+
283
+ this.animationQueue
284
+ .push(entry);
285
+
286
+ tb.map.repaint = true
287
+ return this;
288
+ }
289
+ }
290
+
291
+ //[jscastro] play an animation, requires options.animation as an index, if not it will play the default one
292
+ obj.playAnimation = function (options) {
293
+ if (obj.mixer) {
294
+
295
+ if (options.animation) {
296
+ setAction(options.animation)
297
+ }
298
+ obj.playDefault(options);
299
+
300
+ }
301
+ }
302
+
303
+ //[jscastro] pause all actions animation
304
+ obj.pauseAllActions = function () {
305
+ if (obj.mixer) {
306
+ obj.actions.forEach(function (action) {
307
+ action.paused = true;
308
+ });
309
+ }
310
+ }
311
+
312
+ //[jscastro] unpause all actions
313
+ obj.unPauseAllActions = function () {
314
+ if (obj.mixer) {
315
+ obj.actions.forEach(function (action) {
316
+ action.paused = false;
317
+ });
318
+ }
319
+
320
+ }
321
+
322
+ //[jscastro] stop all actions
323
+ obj.deactivateAllActions = function () {
324
+ if (obj.mixer) {
325
+ obj.actions.forEach(function (action) {
326
+ action.stop();
327
+ });
328
+ }
329
+ }
330
+
331
+ //[jscastro] play all actions
332
+ obj.activateAllActions = function () {
333
+ if (obj.mixer) {
334
+ obj.actions.forEach(function (action) {
335
+ action.play();
336
+ });
337
+ }
338
+ }
339
+
340
+ //[jscastro] move the model action one tick just to avoid issues with initial position
341
+ obj.idle = function () {
342
+ if (obj.mixer) {
343
+ // Update the animation mixer and render this frame
344
+ obj.mixer.update(0.01);
345
+ }
346
+ tb.map.repaint = true;
347
+ return this;
348
+ }
349
+
350
+ },
351
+
352
+ update: function (now) {
353
+
354
+ if (this.previousFrameTime === undefined) this.previousFrameTime = now;
355
+
356
+ let dimensions = ['X', 'Y', 'Z'];
357
+
358
+ //[jscastro] when function expires this produces an error
359
+ if (!this.enrolledObjects) return false;
360
+
361
+ //iterate through objects in queue. count in reverse so we can cull objects without frame shifting
362
+ for (let a = this.enrolledObjects.length - 1; a >= 0; a--) {
363
+
364
+ let object = this.enrolledObjects[a];
365
+
366
+ if (!object.animationQueue || object.animationQueue.length === 0) continue;
367
+
368
+ //[jscastro] now multiple animations on a single object is possible
369
+ for (let i = object.animationQueue.length - 1; i >= 0; i--) {
370
+
371
+ //focus on first item in queue
372
+ let item = object.animationQueue[i];
373
+ if (!item) continue;
374
+ let options = item.parameters;
375
+
376
+ // if an animation is past its expiration date, cull it
377
+ if (!options.expiration) {
378
+ // console.log('culled')
379
+
380
+ object.animationQueue.splice(i, 1);
381
+
382
+ // set the start time of the next animation
383
+ if (object.animationQueue[i]) object.animationQueue[i].parameters.start = now;
384
+
385
+ return
386
+ }
387
+
388
+ //if finished, jump to end state and flag animation entry for removal next time around. Execute callback if there is one
389
+ let expiring = now >= options.expiration;
390
+
391
+ if (expiring) {
392
+ options.expiration = false;
393
+ if (item.type === 'playDefault') {
394
+ object.stop();
395
+ } else {
396
+ if (options.endState) object._setObject(options.endState);
397
+ if (typeof (options.cb) != 'undefined') options.cb();
398
+ }
399
+ }
400
+
401
+ else {
402
+
403
+ let timeProgress = (now - options.start) / options.duration;
404
+
405
+ if (item.type === 'set') {
406
+
407
+ let objectState = {};
408
+
409
+ if (options.pathCurve) objectState.worldCoordinates = options.pathCurve.getPoint(timeProgress);
410
+
411
+ if (options.rotationPerMs) {
412
+ objectState.rotation = options.startRotation.map(function (rad, index) {
413
+ return rad + options.rotationPerMs[index] * timeProgress * options.duration
414
+ })
415
+ }
416
+
417
+ if (options.scalePerMs) {
418
+ objectState.scale = options.startScale.map(function (scale, index) {
419
+ return scale + options.scalePerMs[index] * timeProgress * options.duration
420
+ })
421
+ }
422
+
423
+ object._setObject(objectState);
424
+ }
425
+
426
+ if (item.type === 'followPath') {
427
+
428
+ let position = options.pathCurve.getPointAt(timeProgress);
429
+ let objectState = { worldCoordinates: position };
430
+
431
+ // if we need to track heading
432
+ if (options.trackHeading) {
433
+
434
+ let tangent = options.pathCurve
435
+ .getTangentAt(timeProgress)
436
+ .normalize();
437
+
438
+ let axis = new THREE.Vector3(0, 0, 0);
439
+ let up = new THREE.Vector3(0, 1, 0);
440
+
441
+ axis
442
+ .crossVectors(up, tangent)
443
+ .normalize();
444
+
445
+ let radians = Math.acos(up.dot(tangent));
446
+
447
+ objectState.quaternion = [axis, radians];
448
+
449
+ }
450
+
451
+ object._setObject(objectState);
452
+
453
+ }
454
+
455
+ //[jscastro] play default animation
456
+ if (item.type === 'playDefault') {
457
+ object.activateAllActions();
458
+ object.isPlaying = true;
459
+ object.animationMethod = requestAnimationFrame(this.update);
460
+ object.mixer.update(object.clock.getDelta());
461
+ tb.map.repaint = true;
462
+ }
463
+
464
+ }
465
+ }
466
+
467
+ }
468
+
469
+ this.previousFrameTime = now;
470
+ }
471
+
472
+ }
473
+
474
+ const defaults = {
475
+ followPath: {
476
+ path: null,
477
+ duration: 1000,
478
+ trackHeading: true
479
+ }
480
+ }
481
+
482
+ export default AnimationManager;