janusweb 1.5.56 → 1.7.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center"><a href="https://web.janusvr.com" target="_blank"><img width="480" alt="JanusWeb" src="https://imgur.com/ejvyphR.jpg"></a></p>
2
2
 
3
- <p align="center"><b>A web framework for building social virtual reality experiences.</b></p>
3
+ <p align="center"><b>A web framework/browser for building rich virtual reality experiences.</b></p>
4
4
 
5
5
  <div align="center">
6
6
  <a href="https://janusvr.com">Site</a>
@@ -11,7 +11,9 @@
11
11
  &mdash;
12
12
  <a href="https://web.janusvr.com">Demo</a>
13
13
  &mdash;
14
- <a href="https://github.com/madjin/janus-guide">Docs</a>
14
+ <a href="https://coderofsalvation.github.io/janus-guide/">Docs</a>
15
+ &mdash;
16
+ <a href="https://discord.gg/7eyK2wE">Discord</a>
15
17
  </div>
16
18
 
17
19
 
@@ -39,20 +41,24 @@
39
41
 
40
42
  ## Features
41
43
 
44
+ - [Open Immersive Web-layer](https://madjin.github.io/janus-guide/#/home/about) for browsing XR experiences across the web
42
45
  - Build immersive 3D environments for desktop, mobile, and VR devices using HTML and JS
43
46
  - Rendering functionality provided by Three.js / WebGL
47
+ - Separately host WebXR experiences via ([JML](https://madjin.github.io/janus-guide/#/examples/markup) in) any textfile.
44
48
  - Oculus Rift, Vive, GearVR, Daydream, and Cardboard support via WebVR API
45
- - Realtime collaboration across all devices via built-in networking
49
+ - [Realtime collaboration](https://madjin.github.io/janus-guide/#/home/usecases) across all devices via built-in networking
46
50
  - Import Collada, OBJ, glTF, and other popular 3d file formats
47
51
  - 3D positional audio
48
52
  - Gamepad support via the HTML5 Gamepad API
49
53
  - Supports hand tracking peripherals like Leap Motion, Oculus Touch, and Vive controllers
50
54
  - Support for 2d, sbs3d/ou3d, and 360 degree video textures using HTML5 Video
51
55
  - Scriptable client enables many customized uses
56
+ - And [much more](https://madjin.github.io/janus-guide/#/home/toc)
57
+
52
58
 
53
59
  ## Using
54
60
 
55
- There are several different ways to use JanusWeb, depending on how much control you want to
61
+ There are [several different ways](https://madjin.github.io/janus-guide/#/home/usecases) to use JanusWeb, depending on how much control you want to
56
62
  have over the whole system.
57
63
 
58
64
  ### Use our viewer
@@ -126,7 +132,9 @@ JanusWeb supports several arguments at initialization time to control how it beh
126
132
  <table>
127
133
  <tr><th> Name </th><th> Description </th><th> Default </th></tr>
128
134
 
129
- <tr><td> autoload </td><td> Load URL by default or wait for script </td><td> true </td></tr>
135
+ <tr><td> autostart </td><td> call `elation.janusweb.init({..})` automatically or manually </td><td> true </td></tr>
136
+
137
+ <tr><td> autoload </td><td> Load URL by default or wait for script </td><td> true </td></tr>
130
138
 
131
139
  <tr><td> crosshair </td><td> Show player crosshair </td><td> true </td></tr>
132
140
 
@@ -142,10 +150,6 @@ JanusWeb supports several arguments at initialization time to control how it beh
142
150
 
143
151
  <tr><td> server </td><td> Presence server to connect to </td><td> wss://presence.janusvr.com:5567/</td></tr>
144
152
 
145
- <tr><td> shownavigation </td><td> Control visibility of navigation bar </td><td> true </td></tr>
146
-
147
- <tr><td> showchat </td><td> Control visibility of chat </td><td> true </td></tr>
148
-
149
153
  <tr><td> stats </td><td> Enable render performance stats </td><td> false </td></tr>
150
154
 
151
155
  <tr><td> url </td><td> Default page to load </td><td> (homepage) </td></tr>
@@ -160,12 +164,18 @@ JanusWeb supports several arguments at initialization time to control how it beh
160
164
  for Opus via WebAudio) </td><td> false </td></tr>
161
165
  </table>
162
166
 
167
+ > NOTE: see [client.js](https://github.com/jbaicoianu/janusweb/blob/master/scripts/client.js#L65) for the most up to date arguments
168
+
163
169
  ## Scripting
164
170
  After initializing the client, `elation.janusweb.init()` returns a Promise which provides a reference to the client.
165
171
  You can programatically control this client to do all sorts of things. For instance, we can make the client load a
166
172
  URL, wait for the world and all of its assets to load, and then take a screenshot of the world after a specified delay:
167
173
 
168
174
  ```javascript
175
+ <janus-viewer autostart="false"> <!-- disable autostart, we will -->
176
+ </janus-viewer> <!-- call elation.janusweb.init() -->
177
+
178
+ <script>
169
179
  var pageinfo = elation.utils.parseURL(document.location.href),
170
180
  urlargs = pageinfo.args || {},
171
181
  hashargs = pageinfo.hash || {};
@@ -173,11 +183,11 @@ var pageinfo = elation.utils.parseURL(document.location.href),
173
183
  var url = elation.utils.any(hashargs.url, urlargs.url, 'http://www.janusvr.com/index.html'),
174
184
  delay = elation.utils.any(hashargs.delay, urlargs.delay, 1000);
175
185
 
176
- elation.janusweb.init({
186
+ elation.janusweb.init({
177
187
  url: url,
178
188
  resolution: '1920x1080',
179
- showchat: false,
180
- shownavigation: false
189
+ uiconfig: "./media/assets/webui/default.json" // tweak ui here
190
+ // more options at https://github.com/jbaicoianu/janusweb/blob/master/scripts/client.js#L93
181
191
  }).then(function(client) {
182
192
  elation.events.add(client.janusweb.currentroom, 'room_load_complete', function() {
183
193
  setTimeout(function() {
@@ -188,7 +198,9 @@ elation.janusweb.init({
188
198
  });
189
199
  }, delay);
190
200
  });
201
+ elation.janusweb.init = function(){} // init only once
191
202
  });
203
+ </script>
192
204
 
193
205
  ```
194
206
 
@@ -209,6 +221,30 @@ If you need to load a specific version of JanusWeb, all previous versions are st
209
221
 
210
222
  JanusWeb versions follow the [Semantic Versioning 2.0.0](http://semver.org/) spec, which follows the format ```<major>.<minor>.<patch>``` - for example, at the time of writing (March 2017) the current stable release is **1.0.15**. So if you want to view this version, you could go to https://web.janusvr.com/1.0.15/ and if you wanted to pull this specific version into your page, you could do so with ```<script src="https://web.janusvr.com/1.0.15/janusweb.js"></script>```. We also support aliases for the most current version - for instance, https://web.janusvr.com/1.0/ will always refer to the most recent 1.0 release, https://web.janusvr.com/0.9/ the final 0.9 release, etc.
211
223
 
224
+ ## Ecosystem
225
+
226
+ Platforms using JanusXR + JanusWeb:
227
+
228
+ | | URL | source / docker |
229
+ |----|-----|------------|
230
+ | <img src="https://imgur.com/JMYi81Z.png"/><br><br> | [vesta.janusxr.org](https://vesta.janusxr.org) | |
231
+ | <img src="https://codeberg.org/coderofsalvation/xrforge/media/branch/master/xrforge.jpg"/><br><br> | [xrforge.isvery.ninja](https://xrforge.isvery.ninja) | [codeberg.org](https://codeberg.org/coderofsalvation/xrforge)
232
+
233
+ Visualisations of hyperlinked Janus clusters across the web:
234
+ * [augmentedperception.com](https://augmentedperception.com)
235
+ * [panopticon](https://panopticon.spyduck.net/)
236
+
237
+ Extra Tools / Components:
238
+ * [custom components](https://github.com/jbaicoianu/janus-custom-components)
239
+ * [janusxr-cli](https://github.com/coderofsalvation/janusxr-cli) swiss army CLI knife for room health/preservation #cli
240
+ * [corsanywhere](https://github.com/Rob--W/cors-anywhere) for hasslefree hopping clusters #stack
241
+ * [janus-gateway](https://janus.conf.meetecho.com/) webrtc/voip-layer [docs](https://janus.conf.meetecho.com/) #stack
242
+ * [janus-server](https://github.com/janusvr/janus-server) multiuser presence layer #stack
243
+
244
+ Reference:
245
+ * [quick scripting reference](https://github.com/jbaicoianu/janusweb/wiki/Scripting-Support-2.0)
246
+ * [janus guide](https://coderofsalvation.github.io/janus-guide/)
247
+
212
248
  ## Contributing
213
249
  JanusWeb is open source, and we welcome any contributions! Please do report bugs using GitHub Issues,
214
250
  and all pull requests will be considered. We could especially use help with documentation!
@@ -320,7 +320,20 @@ console.log('set translation snap', ev.data, ev);
320
320
  }
321
321
  handleExportClick(ev) {
322
322
  let exporter = new THREE.GLTFExporter();
323
+ let userDatas = new Map();
324
+ // Store and nullify userData objects for each object before serializing
325
+ room._target.objects['3d'].traverse(n => {
326
+ userDatas.set(n, n.userData);
327
+ n.userData = {};
328
+ });
323
329
  exporter.parse(room._target.objects['3d'], (data) => {
330
+ // Restore userData objects
331
+ room._target.objects['3d'].traverse(n => {
332
+ if (userDatas.has(n)) {
333
+ n.userData = userDatas.get(n);
334
+ }
335
+ });
336
+
324
337
  let filedata = new Blob([data], {type: 'model/gltf-binary'});
325
338
 
326
339
  var url = window.URL.createObjectURL(filedata);
@@ -0,0 +1,3 @@
1
+ [
2
+ { "assettype": "sound", "name": "button-teleport", "src": "./teleport.mp3" }
3
+ ]
@@ -1,8 +1,15 @@
1
+ /*
2
+ * for room-specific teleport sound use:
3
+ *
4
+ * <room teleport_sound_id="mysound" ...>
5
+ *
6
+ */
7
+
1
8
  janus.registerElement('locomotion_teleporter', {
2
9
  active: false,
3
10
  longpresstime: 500,
4
11
  deadzone: 5,
5
- xrplayer: null,
12
+ xrplayer: null, // *TODO* use janus.engine.client.xrplayer instead (and allow desktop too)
6
13
  linesegments: 100,
7
14
 
8
15
  create() {
@@ -102,7 +109,13 @@ janus.registerElement('locomotion_teleporter', {
102
109
  });
103
110
  player.head.add(this.shroud._target);
104
111
  this.particles.particle_vel = V(-.4, 0, -.4); // FIXME - particle velocity isn't being set on spawn
105
- this.sound = room.createObject('Sound', { id: 'teleport2' }, this);
112
+
113
+ let locomotion = janus.ui.apps.default.apps.locomotion;
114
+ let asseturl = locomotion.resolveFullURL('./locomotion-assets.json');
115
+ fetch(asseturl).then(res => res.json()).then(assetlist => {
116
+ this.assetpack = elation.engine.assets.loadJSON(assetlist, locomotion.resolveFullURL('./'));
117
+ this.sound = room.createObject('Sound', { id: room.teleport_sound_id || 'button-teleport' }, this);
118
+ })
106
119
 
107
120
  this.disableCursor();
108
121
  window.addEventListener('mousemove', this.handleMouseMove);
@@ -133,13 +146,15 @@ janus.registerElement('locomotion_teleporter', {
133
146
  });
134
147
  this.activateControlContext('teleporter');
135
148
 
136
- //room.addEventListener('mousedown', (ev) => this.handleMouseDown(ev));
137
- //room.addEventListener('mouseup', (ev) => this.handleMouseUp(ev));
149
+ // emulated mouse-events (via webxr handtracking e.g.)
150
+ room.addEventListener('mousemove', (ev) => this.handleMouseMove(ev));
151
+ room.addEventListener('mousedown', (ev) => this.handleMouseDown(ev));
152
+ room.addEventListener('mouseup', (ev) => this.handleMouseUp(ev));
138
153
  elation.events.add(janus._target, 'room_change', (ev) => this.handleRoomChange(ev));
139
154
  },
140
155
  update() {
141
156
  if (this.active) {
142
- let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
157
+ let hand = this.xrplayer ? this.xrplayer.trackedobjects['hand_' + this.teleporthand] : null
143
158
  let startpoint,
144
159
  segments = this.linesegments,
145
160
  duration = .25,
@@ -152,11 +167,13 @@ janus.registerElement('locomotion_teleporter', {
152
167
  if (hand) {
153
168
  startpoint = hand.pointer.localToWorld(V(0, 0, 0));
154
169
  v0 = hand.pointer.localToWorld(V(0, 0, -speed)).sub(startpoint);
170
+ hand.teleportPos = hand.teleportPos || v0
155
171
  } else {
156
172
  startpoint = player.head.localToWorld(V(0,0,0));
157
173
  v0 = player.head.localToWorld(V(0,0,-speed)).sub(startpoint);
158
174
  return; // Head-locked teleporter is awkward, do we really still support this?
159
175
  }
176
+
160
177
  let i = 0,
161
178
  p0 = startpoint.clone(),
162
179
  dir = v0.clone().normalize();
@@ -181,6 +198,8 @@ janus.registerElement('locomotion_teleporter', {
181
198
  this.pos = player.worldToLocal(hit.point);
182
199
  this.localToWorld(this.particles.emitter_pos.set(0,0,0));
183
200
  this.updateTeleportAngle();
201
+ // track movement to cancel longpresstimer
202
+ if( hand ) hand.teleportPos = hand.teleportPos || this.pos.clone()
184
203
  }
185
204
  laserpoints[i * 2].copy(p0);
186
205
  laserpoints[i * 2 + 1].copy(p1);
@@ -231,7 +250,6 @@ janus.registerElement('locomotion_teleporter', {
231
250
  if (len > .8) {
232
251
  this.updateTeleportAngle();
233
252
  } else if (len < .01) {
234
- this.showShroud();
235
253
  this.teleport();
236
254
  this.teleportactive = false;
237
255
  this.teleporthand = false;
@@ -262,16 +280,23 @@ janus.registerElement('locomotion_teleporter', {
262
280
 
263
281
  }
264
282
  },
265
- teleport() {
283
+ teleport(mouse) {
266
284
  //if (!room.teleport) return;
267
285
  let pos = player.localToWorld(this.pos.clone());
268
286
  pos.y += player.fatness; // FIXME - This accounts for wonky player sphere collider, when we switch to a capsule we won't need it
269
287
  player.pos = pos;
270
288
  player.vel = V(0,0,.01); // "wake up" physics engine
271
- player.angular.set(0,0,0);
272
- let playerangle = Math.atan2(this.pos.x, this.pos.z);
273
- console.log('Teleport player', pos, playerangle);
274
- this.xrplayer.orientation._target.setFromEuler(new THREE.Euler(0, this.teleportangle - playerangle, 0));
289
+ console.log('Teleport player', pos);
290
+ this.showShroud();
291
+ if( this.sound ){
292
+ this.sound.pos = pos;
293
+ this.sound.play();
294
+ }
295
+ if( this.xrplayer && this.teleportangle && !mouse){
296
+ player.angular.set(0,0,0);
297
+ let playerangle = Math.atan2(this.pos.x, this.pos.z);
298
+ this.xrplayer.orientation._target.setFromEuler(new THREE.Euler(0, this.teleportangle - playerangle, 0));
299
+ }
275
300
  },
276
301
  updateTeleportAngle() {
277
302
  let controls = this.activecontrols;
@@ -282,27 +307,28 @@ janus.registerElement('locomotion_teleporter', {
282
307
  handleMouseDown(ev) {
283
308
  if (!room.teleport) return;
284
309
  if (ev.button == 0 && (player.enabled || janus.hmd)) {
310
+ if( ev.inputSourceObject && this.xrplayer ){ // translate webxr emulated mousedown event
311
+ this.teleporthand = ev.inputSourceObject.device.handedness
312
+ let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
313
+ if( hand ) hand.teleportPos = false
314
+ }
285
315
  if (!this.longpresstimer) {
286
- //this.longpresstimer = setTimeout(() => { console.log('timer fired'); this.enableCursor(); }, this.longpresstime);
316
+ this.longpresstimer = setTimeout(() => { console.log('timer fired'); this.enableCursor(); }, this.longpresstime);
287
317
  this.mousediff = [0,0];
288
- }
289
- //this.active = true;
318
+ }else this.cancelLongPress()
290
319
  }
291
320
  },
292
321
  handleMouseMove(ev) {
293
322
  if (!room.teleport) return;
294
- if (this.longpresstimer) {
295
- this.mousediff[0] += ev.movementX;
296
- this.mousediff[1] += ev.movementY
297
- var distance = Math.sqrt(this.mousediff[0] * this.mousediff[0] + this.mousediff[1] * this.mousediff[1]);
298
- if (distance > this.deadzone) {
299
- clearTimeout(this.longpresstimer);
300
- this.longpresstimer = false;
301
- }
302
- }
323
+ //if (this.longpresstimer) {
324
+ // this.mousediff[0] += ev.movementX;
325
+ // this.mousediff[1] += ev.movementY
326
+ // var distance = Math.sqrt(this.mousediff[0] * this.mousediff[0] + this.mousediff[1] * this.mousediff[1]);
327
+ // if (distance > this.deadzone) this.cancelLongPress()
328
+ //}
303
329
  },
304
330
  handleTouchMove(ev) {
305
- if (!room.teleport) return;
331
+ if (!room.teleport || !this.active) return;
306
332
  if (this.longpresstimer) {
307
333
  var touch = ev.changedTouches[0];
308
334
 
@@ -315,9 +341,7 @@ janus.registerElement('locomotion_teleporter', {
315
341
  }
316
342
  this.lasttouch = [touch.clientX, touch.clientY];
317
343
  var distance = Math.sqrt(this.mousediff[0] * this.mousediff[0] + this.mousediff[1] * this.mousediff[1]);
318
- if (distance > this.deadzone) {
319
- clearTimeout(this.longpresstimer);
320
- }
344
+ if (distance > this.deadzone) this.cancelLongPress()
321
345
  if (this.active) {
322
346
  ev.stopPropagation();
323
347
  ev.preventDefault();
@@ -325,42 +349,50 @@ janus.registerElement('locomotion_teleporter', {
325
349
  }
326
350
  },
327
351
  handleMouseUp(ev) {
328
- //if (!room.teleport) return;
329
- if (this.longpresstimer) {
330
- clearTimeout(this.longpresstimer);
331
- this.longpresstimer = false;
332
- }
333
- if (this.active) {
334
- /*
335
- player.pos = this.pos;
336
- this.sound.pos = this.pos;
337
- this.sound.play();
338
- */
339
- this.showShroud();
340
- this.teleport();
341
- }
342
- this.disableCursor();
352
+ if (!room.teleport) return;
353
+ if (this.teleportactive) {
354
+ this.teleport(true);
355
+ this.active = false
356
+ this.teleportactive = false;
357
+ this.teleporthand = false;
358
+ this.disableCursor();
359
+ }else if( this.longpresstimer ) this.disableCursor();
343
360
  },
344
361
  enableCursor() {
345
362
  if (!room.teleport) return;
363
+ if( this.xrplayer ){
364
+ let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
365
+ if( hand ) hand.pointer.laser.visible = false // prevent (portal)clicks
366
+ }
367
+ this.active = true;
368
+ this.teleportactive = true;
346
369
  this.visible = true;
347
370
  this.particles.resetParticles();
348
371
  this.particles.visible = true;
349
372
  if (this.laser.room !== room._target) room.appendChild(this.laser);
350
373
  if (this.particles.room !== room._target) room.appendChild(this.particles);
351
374
  this.laser.visible = true;
352
- this.active = true;
353
375
  this.particles.start();
354
376
  },
355
377
  disableCursor() {
378
+ if( this.xrplayer ){
379
+ let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
380
+ if( hand ) hand.pointer.laser.visible = true // restore original laser
381
+ }
382
+ this.teleportactive = false;
356
383
  this.visible = false;
357
384
  this.particles.visible = false;
358
385
  this.laser.visible = false;
359
386
  this.active = false;
360
387
  this.particles.stop();
388
+ this.cancelLongPress()
361
389
  },
362
390
  showShroud() {
363
391
  this.shroud.visible = true;
364
392
  this.shroud.opacity = 1;
393
+ },
394
+ cancelLongPress(){
395
+ clearTimeout(this.longpresstimer);
396
+ this.longpresstimer = false
365
397
  }
366
398
  });
@@ -408,6 +408,11 @@ elation.elements.define('janus.ui.urlbar', class extends elation.elements.ui.pan
408
408
  }
409
409
  }
410
410
  handleBlur(ev) {
411
+ // enter-key on Quest devices (onscreen keyboards) is not emitted (blur-event instead)
412
+ // https://developers.meta.com/horizon/documentation/web/webxr-keyboard/
413
+ if (navigator.userAgent.indexOf('OculusBrowser') != -1) {
414
+ this.handleAccept({altKey:true}) // get straight to it
415
+ }
411
416
  setTimeout(() => {
412
417
  this.suggestions.hide();
413
418
  }, 100);
@@ -3,5 +3,6 @@
3
3
  { "assettype": "image", "name": "xrmenu-button-back", "src": "./images/previous-button.png", "hasalpha": true },
4
4
  { "assettype": "image", "name": "xrmenu-button-reset", "src": "./images/address-location.png", "hasalpha": true },
5
5
  { "assettype": "image", "name": "xrmenu-button-reload", "src": "./images/redo-arrow.png", "hasalpha": true },
6
- { "assettype": "image", "name": "xrmenu-button-sound", "src": "./images/audio-settings.png", "hasalpha": true }
6
+ { "assettype": "image", "name": "xrmenu-button-sound", "src": "./images/audio-settings.png", "hasalpha": true },
7
+ { "assettype": "image", "name": "xrmenu-button-teleport", "src": "./images/teleport.png", "hasalpha": true }
7
8
  ]
@@ -13,6 +13,7 @@ janus.registerElement('xrmenu', {
13
13
  renderorder: 9,
14
14
  });
15
15
  */
16
+ console.log("creating xrmenu")
16
17
 
17
18
  this.worldpos = V();
18
19
  this.worlddir = V();
@@ -82,25 +83,22 @@ janus.registerElement('xrmenu', {
82
83
  image_id: 'xrmenu-button-sound',
83
84
  //onactivate: (ev) => this.engine.client.stopXR(),
84
85
  }),
86
+ teleport: this.createObject('xrmenu-button', {
87
+ pos: V(.68, 0, 0),
88
+ label: 'Teleport',
89
+ image_id: 'xrmenu-button-teleport',
90
+ toggle: true,
91
+ enabled: room.teleport,
92
+ //onactivate: (ev) => this.engine.client.stopXR(),
93
+ }),
85
94
  };
86
95
  this.buttons.home.addEventListener('activate', ev => janus.navigateHome());
87
96
  this.buttons.back.addEventListener('activate', ev => janus.navigateBack());
88
97
  this.buttons.reset.addEventListener('activate', ev => player.reset_position());
89
98
  this.buttons.reload.addEventListener('activate', ev => location.reload());
90
99
  this.buttons.exitvr.addEventListener('activate', ev => this.engine.client.stopXR());
91
- this.buttons.sound.addEventListener('activate', ev => {
92
- if (!this.panels['sound']) {
93
- this.panels['sound'] = this.buttons.sound.createObject('xrmenu-popup', {
94
- content: 'janus-voip-picker'
95
- });
96
- } else {
97
- if (this.panels['sound'].parent === this.buttons.sound) {
98
- this.buttons.sound.removeChild(this.panels['sound']);
99
- } else {
100
- this.buttons.sound.appendChild(this.panels['sound']);
101
- }
102
- }
103
- });
100
+ this.buttons.sound.addEventListener('activate', ev => this.toggleSound());
101
+ this.buttons.teleport.addEventListener('activate', ev => this.toggleTeleport());
104
102
  this.reflow();
105
103
  });
106
104
  },
@@ -116,6 +114,23 @@ janus.registerElement('xrmenu', {
116
114
  button.pos.x = (-width / 2) + (this.buttonwidth * this.buttonmargin * (i + .5)) ;
117
115
  }
118
116
  },
117
+ toggleSound(){
118
+ if (!this.panels['sound']) {
119
+ this.panels['sound'] = this.buttons.sound.createObject('xrmenu-popup', {
120
+ content: 'janus-voip-picker'
121
+ });
122
+ } else {
123
+ if (this.panels['sound'].parent === this.buttons.sound) {
124
+ this.buttons.sound.removeChild(this.panels['sound']);
125
+ } else {
126
+ this.buttons.sound.appendChild(this.panels['sound']);
127
+ }
128
+ }
129
+ },
130
+ toggleTeleport(){
131
+ room.teleport = !room.teleport
132
+ this.buttons.teleport.state(room.teleport)
133
+ },
119
134
  /*
120
135
  update() {
121
136
  let worldpos = this.localToWorld(this.worldpos.set(0,0,0));
@@ -135,16 +150,20 @@ janus.registerElement('xrmenu', {
135
150
  janus.registerElement('xrmenu-button', {
136
151
  onactivate: null,
137
152
  image_id: '',
153
+ toggle: false,
154
+ enabled: true,
138
155
 
139
156
  create() {
140
157
  this.button = this.createObject('object', {
141
158
  id: 'plane',
142
159
  scale: V(.1, .1, .02),
143
160
  col: '#fff',
144
- pos: V(0, 0, .01),
161
+ pos: V(0, 0, this.enabled ? 0.01 : 0 ),
145
162
  collision_id: 'cube',
146
163
  image_id: this.image_id,
147
164
  renderorder: 10,
165
+ emissive: this.enabled ? (this.toggle ? '#FFF' : '#595')
166
+ : '#555',
148
167
  });
149
168
  this.button.addEventListener('mouseover', ev => this.handleMouseOver(ev));
150
169
  this.button.addEventListener('mouseout', ev => this.handleMouseOut(ev));
@@ -153,9 +172,11 @@ janus.registerElement('xrmenu-button', {
153
172
  this.button.addEventListener('click', ev => this.handleClick(ev));
154
173
  },
155
174
  handleMouseOver(ev) {
175
+ if( this.toggle ) return
156
176
  this.button.emissive = '#595';
157
177
  },
158
178
  handleMouseOut(ev) {
179
+ if( this.toggle ) return
159
180
  this.button.emissive = '#555';
160
181
  },
161
182
  handleMouseDown(ev) {
@@ -164,7 +185,10 @@ janus.registerElement('xrmenu-button', {
164
185
  handleMouseUp(ev) {
165
186
  this.button.pos.z = .01;
166
187
  },
167
-
188
+ state(enabled){
189
+ this.button.emissive = (this.enabled = enabled) ? '#FFF' : '#555'
190
+ this.button.pos.z = enabled ? .01 : 0
191
+ },
168
192
  handleClick(ev) {
169
193
  this.dispatchEvent({type: 'activate'});
170
194
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "janusweb",
3
3
  "description": "Web client for JanusVR worlds",
4
- "version": "1.5.56",
4
+ "version": "1.7.0",
5
5
  "main": "scripts/janusweb.js",
6
6
  "author": "James Baicoianu",
7
7
  "license": "MIT",
@@ -350,6 +350,7 @@ elation.require(['engine.things.generic', 'utils.template', 'janusweb.parts'], f
350
350
  room: ['property', 'room'],
351
351
  parts: ['property', 'jsparts'],
352
352
  js_id: ['property', 'js_id'],
353
+ jsid: ['property', 'js_id'],
353
354
  pos: ['property', 'position'],
354
355
  rotation: ['property', 'rotation'],
355
356
  scale: ['property', 'scale'],
@@ -377,6 +378,7 @@ elation.require(['engine.things.generic', 'utils.template', 'janusweb.parts'], f
377
378
  gazetime: ['property', 'gazetime'],
378
379
  layers: ['property', 'layers'],
379
380
  renderorder: ['property', 'renderorder'],
381
+ lighting: [ 'property', 'lighting' ],
380
382
 
381
383
  pickable: [ 'property', 'pickable'],
382
384
  collidable: [ 'property', 'collidable'],
@@ -674,15 +676,15 @@ elation.require(['engine.things.generic', 'utils.template', 'janusweb.parts'], f
674
676
  let object = args.object;
675
677
  if (args.mesh_verts) {
676
678
  let geo = new THREE.BufferGeometry();
677
- geo.addAttribute( 'position', new THREE.Float32BufferAttribute( args.mesh_verts, 3 ) );
679
+ geo.setAttribute( 'position', new THREE.Float32BufferAttribute( args.mesh_verts, 3 ) );
678
680
  if (args.mesh_faces) {
679
681
  geo.setIndex(args.mesh_faces);
680
682
  }
681
683
  if (args.mesh_normals) {
682
- geo.addAttribute( 'normal', new THREE.Float32BufferAttribute( args.mesh_normals, 3 ) );
684
+ geo.setAttribute( 'normal', new THREE.Float32BufferAttribute( args.mesh_normals, 3 ) );
683
685
  }
684
686
  if (args.mesh_uvs) {
685
- geo.addAttribute( 'uv', new THREE.Float32BufferAttribute( args.mesh_uvs, 2 ) );
687
+ geo.setAttribute( 'uv', new THREE.Float32BufferAttribute( args.mesh_uvs, 2 ) );
686
688
  }
687
689
  object = new THREE.Mesh(geo, new THREE.MeshPhongMaterial());
688
690
  }
@@ -1328,30 +1330,19 @@ console.log('its null', k, this[k], prop);
1328
1330
  }
1329
1331
  //if (this[k] !== prop.default && this[k] !== null && this[k] !== undefined) {
1330
1332
  }
1331
- console.log('clone', props);
1333
+
1334
+ if (!parent) parent = this.parent || room;
1335
+
1336
+ let clone = parent.createObject(this.tag, props);
1337
+
1332
1338
  if (cloneChildren) {
1333
1339
  let children = this.children;
1334
1340
  props.children = [];
1335
1341
  for (let i = 0; i < children.length; i++) {
1336
- props.children.push(children[i].clone(true, this));
1342
+ props.children.push(children[i].clone(true, clone));
1337
1343
  }
1338
1344
  }
1339
- //let parent = this.parent || room;
1340
- if (!parent) parent = this.parent || room;
1341
-
1342
- return parent.createObject(this.tag, props);
1343
- /*
1344
- {
1345
- pos: this.pos.clone(),
1346
- rotation: this.rotation.clone(),
1347
- scale: this.scale.clone(),
1348
- color: this.color.clone(),
1349
- lighting: this.lighting,
1350
- id: this.id,
1351
- js_id: this.js_id + '_copy',
1352
- collision_id: this.collision_id,
1353
- });
1354
- */
1345
+ return clone;
1355
1346
  }
1356
1347
  this.addControlContext = function(name, defs) {
1357
1348
  let legacydefs = {};
@@ -1375,7 +1366,7 @@ console.log('clone', props);
1375
1366
  }
1376
1367
  }];
1377
1368
  }
1378
- return this.engine.systems.controls.addContext(name, legacydefs);
1369
+ return this.engine.systems.controls.addContext(name, legacydefs, this);
1379
1370
  }
1380
1371
  this.activateControlContext = function(name) {
1381
1372
  this.engine.systems.controls.activateContext(name, this);
@@ -313,7 +313,7 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
313
313
  if (!animasset.loaded) {
314
314
  let animationsLoaded = false;
315
315
  elation.events.add(animasset, 'asset_load_complete', ev => {
316
- if (!animationsLoaded) {
316
+ if (!animationsLoaded && animasset.animations) {
317
317
  animationsLoaded = true;
318
318
  if (this.body.animationmixer) {
319
319
  this.cloneAnimations(animasset);
@@ -323,7 +323,7 @@ elation.require(['janusweb.janusbase', 'engine.things.leapmotion'], function() {
323
323
  });
324
324
  animasset.load();
325
325
  } else {
326
- if (this.body.animationmixer) {
326
+ if (this.body.animationmixer && animasset.animations) {
327
327
  this.cloneAnimations(animasset);
328
328
  }
329
329
  //this.body.setAnimation('idle');
@@ -3,7 +3,7 @@ elation.require(['janusweb.janusbase'], function() {
3
3
  this.postinit = function() {
4
4
  elation.engine.things.janusparagraph.extendclass.postinit.call(this);
5
5
  this.defineProperties({
6
- text: {type: 'string', default: '', set: this.updateTexture},
6
+ text: {type: 'string', default: '', set: this.toInlineHTML },
7
7
  font_size: {type: 'integer', default: 16, set: this.updateTexture},
8
8
  text_col: {type: 'color', default: 0x000000, set: this.updateTexture},
9
9
  back_col: {type: 'color', default: 0xffffff, set: this.updateTexture},
@@ -11,6 +11,7 @@ elation.require(['janusweb.janusbase'], function() {
11
11
  cull_face: { type: 'string', default: 'back', set: this.updateMaterial },
12
12
  css: {type: 'string', set: this.updateTexture },
13
13
  depth_write: { type: 'boolean', default: true },
14
+ transparent: {type: 'boolean', set: this.updateTexture },
14
15
  depth_test: { type: 'boolean', default: true },
15
16
  collision_id: { type: 'string', default: 'cube' },
16
17
  collision_scale: { type: 'vector3', default: V(.5, .5, .02) },
@@ -75,7 +76,7 @@ elation.require(['janusweb.janusbase'], function() {
75
76
  var basestyle = 'font-family: sans-serif;' +
76
77
  'font-size: ' + this.font_size + 'px;' +
77
78
  'color: ' + text_col + ';' +
78
- 'background: ' + back_col + ';' +
79
+ 'background: ' + (this.transparent ? 'transparent' : back_col) + ';' +
79
80
  'max-width: 1014px;' +
80
81
  'padding: 5px;';
81
82
 
@@ -85,7 +86,7 @@ elation.require(['janusweb.janusbase'], function() {
85
86
  // styled divs instead.
86
87
 
87
88
  var sanitarydiv = document.createElement('div');
88
- sanitarydiv.innerHTML = this.text;
89
+ sanitarydiv.innerHTML = this.html;
89
90
  var content = sanitarydiv.innerHTML.replace(/<br\s*\/?>/g, '<div class="br"></div>');
90
91
  content = content.replace(/<hr\s*\/?>/g, '<div class="hr"></div>');
91
92
  content = content.replace(/<img(.*?)>/g, "<img$1 />");
@@ -150,5 +151,40 @@ elation.require(['janusweb.janusbase'], function() {
150
151
  }
151
152
  return this._proxyobject;
152
153
  }
154
+
155
+ this.toInlineHTML = async function(){
156
+ this.html = this.text
157
+ try{
158
+ // find all img tags and capture src attributes
159
+ const imgRegex = /<img[^>]*src=["'](?!data:)([^"']+)["']/gi;
160
+ const matches = [];
161
+ let match;
162
+ while ((match = imgRegex.exec(this.html)) !== null) { // Collect all src values
163
+ matches.push(match[1]);
164
+ }
165
+ // fill array with base64 image-strings
166
+ const dataURLs = await Promise.all( matches.map(src => this.toDataURL(src)) );
167
+ // Replace each src in the html with the corresponding data URL
168
+ let updatedHtml = this.html;
169
+ matches.forEach((src, i) => {
170
+ updatedHtml = updatedHtml.replace( `src="${src}"`, `src="${dataURLs[i]}"`)
171
+ });
172
+ this.html = updatedHtml
173
+ }catch(e){ console.error(e) } // continue when inlining failed
174
+ this.updateTexture()
175
+ };
176
+
177
+ this.toDataURL = function(url){
178
+ const finalUrl = `${elation.engine.assets.corsproxy||''}${url}`
179
+ return fetch(finalUrl)
180
+ .then(response => response.blob())
181
+ .then(blob => new Promise((resolve, reject) => {
182
+ const reader = new FileReader()
183
+ reader.onloadend = () => resolve(reader.result)
184
+ reader.onerror = reject
185
+ reader.readAsDataURL(blob)
186
+ }))
187
+ }
188
+
153
189
  }, elation.engine.things.janusbase);
154
190
  });
@@ -56,7 +56,7 @@ elation.require(['engine.things.player', 'janusweb.external.JanusVOIP', 'ui.butt
56
56
  elation.events.add(this.engine.client.view.container, 'touchcancel', elation.bind(this, this.handleTouchEnd));
57
57
 
58
58
  this.controlstate2 = this.engine.systems.controls.addContext('janusplayer', {
59
- 'toggle_view': ['keyboard_nomod_v,keyboard_shift_v', elation.bind(this, this.toggleCamera)],
59
+ 'toggle_view': ['keyboard_nomod_v,keyboard_shift_v', ev => { if (ev.value == 1) this.toggleCamera() }],
60
60
  'zoom_out': ['mouse_wheel_down', ev => this.zoomView(-1, ev)],
61
61
  'zoom_in': ['mouse_wheel_up', ev => this.zoomView(1, ev)],
62
62
  //'browse_back': ['gamepad_any_button_4', elation.bind(this, this.browseBack)],
@@ -945,9 +945,9 @@ document.body.dispatchEvent(click);
945
945
  */
946
946
  this.pickable = false;
947
947
  this.setCollider('capsule', {
948
- radius: this.collision_radius,
949
- length: 1,
950
- offset: V(0, this.collision_radius, 0)
948
+ radius: this.fatness,
949
+ length: this.height - (this.fatness * 2),
950
+ offset: V(0, this.fatness, 0)
951
951
  });
952
952
  } else {
953
953
  this.removeCollider();
@@ -1230,8 +1230,11 @@ document.body.dispatchEvent(click);
1230
1230
  this.defaultanimation = 'portal';
1231
1231
  setTimeout(() => this.defaultanimation = 'idle', 3000);
1232
1232
  }
1233
- this.toggleCamera = function(ev) {
1234
- if (ev.value == 1) {
1233
+ this.toggleCamera = function(arg) {
1234
+ if (typeof arg == 'string') {
1235
+ this.setCameraView(arg);
1236
+ } else if (!arg || arg.value) {
1237
+ // Legacy handling - if arg is a control input event, check to make sure this is a button press, not a release
1235
1238
  if (this.cameraview == 'firstperson') {
1236
1239
  this.setCameraView('thirdperson');
1237
1240
  } else {
@@ -412,7 +412,7 @@ elation.require(['engine.things.generic', 'janusweb.external.webxr-input-profile
412
412
  this.buttonwaspressed = false;
413
413
  if (!this.menu.parent || this.menu.parent === this) {
414
414
  player.appendChild(this.menu);
415
- player.worldToLocal(this.pointer.localToWorld(this.menu.pos.set(0,0,-.25)));
415
+ player.worldToLocal(this.pointer.localToWorld(this.menu.pos.set(0.1,-0.07,-.2)));
416
416
  this.menu.billboard = 'y';
417
417
  } else {
418
418
  //this.appendChild(this.menu);
@@ -656,8 +656,9 @@ elation.require(['engine.things.generic', 'janusweb.external.webxr-input-profile
656
656
  }
657
657
  },
658
658
  handleRaycastEnter(ev) {
659
- let proxyobj = ev.data.object,
660
- obj = proxyobj._target;
659
+ let proxyobj = ev.data.object
660
+ if( !proxyobj || !proxyobj?._target ) return
661
+ obj = proxyobj._target;
661
662
  let oldactiveobject = this.activeobject;
662
663
  if (this.activeobject) {
663
664
  this.engine.client.view.proxyEvent({
@@ -723,6 +724,7 @@ elation.require(['engine.things.generic', 'janusweb.external.webxr-input-profile
723
724
  this.engine.client.view.proxyEvent(evdata, evdata.element);
724
725
  },
725
726
  handleRaycastMove(ev) {
727
+ if( ! ev?.data?.intersection?.face?.normal ) return // skip those!
726
728
  this.updateLaserEndpoint(ev.data.intersection.point, ev.data.intersection.face.normal);
727
729
  if (this.activeobject) {
728
730
  let obj = this.activeobject.objects['3d'];
@@ -779,7 +781,7 @@ elation.require(['engine.things.generic', 'janusweb.external.webxr-input-profile
779
781
  }
780
782
  },
781
783
  handleSelect(ev) {
782
- if (this.activeobject) {
784
+ if (this.activeobject && this.laser.visible ) {
783
785
  let obj = this.activeobject.objects['3d'];
784
786
  console.log('CLICK IT', this.activeobject);
785
787
  this.engine.client.view.proxyEvent({
package/scripts/room.js CHANGED
@@ -3,10 +3,10 @@ elation.require([
3
3
  'engine.things.generic', 'engine.things.label', 'engine.things.skybox',
4
4
  'janusweb.object', 'janusweb.portal', 'janusweb.image', 'janusweb.video', 'janusweb.text', 'janusweb.janusparagraph',
5
5
  'janusweb.sound', 'janusweb.januslight', 'janusweb.janusparticle', 'janusweb.janusghost',
6
- 'janusweb.translators.bookmarks', 'janusweb.translators.reddit', 'janusweb.translators.error', 'janusweb.translators.blank', 'janusweb.translators.default', 'janusweb.translators.dat', 'janusweb.translators.janusvfs'
6
+ 'janusweb.translators.bookmarks', 'janusweb.translators.reddit', 'janusweb.translators.error', 'janusweb.translators.blank', 'janusweb.translators.default', 'janusweb.translators.dat', 'janusweb.translators.janusvfs', 'janusweb.translators.xrfragments'
7
7
  ], function() {
8
8
  let roomTranslators = false;
9
- function initRoomTranslators() {
9
+ function initRoomTranslators(room) {
10
10
  roomTranslators = {
11
11
  '^janus-vfs:': elation.janusweb.translators.janusvfs({janus: janus}),
12
12
  '^about:blank$': elation.janusweb.translators.blank({janus: janus}),
@@ -14,8 +14,9 @@ elation.require([
14
14
  '^dat:': elation.janusweb.translators.dat({janus: janus}),
15
15
  '^https?:\/\/(www\.)?reddit.com': elation.janusweb.translators.reddit({janus: janus}),
16
16
  '^error$': elation.janusweb.translators.error({janus: janus}),
17
+ '.*\.(gltf|glb|dae)$': elation.janusweb.translators.xrfragments({janus: janus}),
17
18
  '^default$': elation.janusweb.translators.default({janus: janus})
18
- };
19
+ }
19
20
  }
20
21
  elation.component.add('engine.things.janusroom', function() {
21
22
  this.postinit = function() {
@@ -100,7 +101,7 @@ elation.require([
100
101
  });
101
102
 
102
103
  if (!roomTranslators) {
103
- initRoomTranslators();
104
+ initRoomTranslators(this);
104
105
  }
105
106
 
106
107
  this.spawnpoint = new THREE.Object3D();
@@ -301,7 +302,8 @@ elation.require([
301
302
  }
302
303
  }
303
304
  } else if (this.urlhash) {
304
- let obj = this.getObjectById(this.urlhash);
305
+ // XR Fragments deeplink spec (Level1: URL) https://xrfragment.org/#teleport%20camera
306
+ let obj = this.getObjectById(this.urlhash) || this.getObjectByDeepName(this.urlhash)
305
307
  if (obj) {
306
308
  obj.localToWorld(spawnpoint.position.set(0,0,0));
307
309
  spawnpoint.orientation.setFromRotationMatrix(obj.objects['3d'].matrixWorld.lookAt(spawnpoint.position, obj.localToWorld(V(0,0,-1)), obj.localToWorld(V(0,1,0).sub(spawnpoint.position))));
@@ -567,12 +569,12 @@ elation.require([
567
569
  }
568
570
  var baseurl = baseurloverride;
569
571
  if (!baseurl) {
570
- if (url && !this.baseurl) {
572
+ if (url && url.match("://") && !this.baseurl) {
571
573
  baseurl = url.split('/');
572
574
  if (baseurl.length > 3) baseurl.pop();
573
575
  baseurl = baseurl.join('/') + '/';
574
- this.baseurl = baseurl;
575
576
  }
577
+ this.baseurl = baseurl || "";
576
578
  } else {
577
579
  this.baseurl = baseurl;
578
580
  }
@@ -590,7 +592,7 @@ elation.require([
590
592
  var fullurl = url;
591
593
  if (fullurl[0] == '/' && fullurl[1] != '/') fullurl = this.baseurl + fullurl;
592
594
  if (!fullurl.match(/^https?:/) && !fullurl.match(/^\/\//)) {
593
- fullurl = self.location.origin + fullurl;
595
+ fullurl = self.location.origin + (fullurl[0] == '/' ? '' :'/') + fullurl;
594
596
  } else if (!fullurl.match(/^https?:\/\/(localhost|127\.0\.0\.1)/) && fullurl.indexOf(document.location.origin) != 0) {
595
597
  fullurl = proxyurl + fullurl;
596
598
  }
@@ -706,11 +708,22 @@ elation.require([
706
708
  }
707
709
  }));
708
710
  }
709
- this.loadFromSource = function(sourcecode, baseurl) {
711
+ this.loadFromSource = async function(sourcecode, baseurl) {
710
712
  if (baseurl) {
711
713
  this.baseurl = baseurl;
712
714
  }
713
- var source = this.parseSource(sourcecode);
715
+
716
+ var source
717
+ // scan sourcecode for non-JML microformats within translators
718
+ for( m in roomTranslators ){
719
+ let rt = roomTranslators[m]
720
+ if( rt.parseSource && rt.parseSource.regex ){
721
+ source = await rt.parseSource(sourcecode,this)
722
+ if( source ) break;
723
+ }
724
+ }
725
+ if( !source ) source = this.parseSource(sourcecode)
726
+
714
727
  if (source && source.source) {
715
728
  this.roomsrc = source.source;
716
729
  var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
@@ -1955,8 +1968,19 @@ elation.require([
1955
1968
  */
1956
1969
 
1957
1970
  }
1958
- this.getObjectById = function(js_id) {
1959
- return this.jsobjects[js_id];
1971
+ this.getObjectByDeepName = function(name) {
1972
+ if( !this.janus.currentroom ) return
1973
+ let obj = this.janus.currentroom.objects['3d'].getObjectByName(name)
1974
+ if( obj ){ // return polyglot THREE/janusweb object for convenience
1975
+ return new Proxy(obj,{
1976
+ set(me,k,v){ obj[k] = v; return true; },
1977
+ get(me,k){
1978
+ if( k == 'objects' ) return { '3d': me }
1979
+ return obj[k];
1980
+ }
1981
+ })
1982
+ }
1983
+ return obj
1960
1984
  }
1961
1985
  this.getObjectsByClassName = function(classname) {
1962
1986
  var objects = [];
package/scripts/sound.js CHANGED
@@ -65,7 +65,7 @@ elation.require(['janusweb.janusbase'], function() {
65
65
  }
66
66
  //this.audio.panner.maxDistance = this.properties.distance;
67
67
  if (this.dist) {
68
- this.audio.setRefDistance(this.dist);
68
+ this.audio.setRefDistance(Math.max(0, this.dist));
69
69
  } else {
70
70
  //this.audio.panner.distanceModel = 'linear';
71
71
  }
package/scripts/text.js CHANGED
@@ -11,7 +11,7 @@ elation.require(['engine.things.label'], function() {
11
11
  'align': { type: 'string', default: 'center', refreshGeometry: true },
12
12
  'verticalalign': { type: 'string', default: 'bottom', refreshGeometry: true },
13
13
  'zalign': { type: 'string', default: 'back', refreshGeometry: true },
14
- 'emissive': { type: 'color', default: 0x000000 },
14
+ 'emissive': { type: 'color', default: 0x000000, set: this.updateMaterial },
15
15
  'opacity': { type: 'float', default: 1.0 },
16
16
  'depth_write': { type: 'bool', default: true },
17
17
  'depth_test': { type: 'bool', default: true },
@@ -120,6 +120,7 @@ elation.require(['engine.things.label'], function() {
120
120
  this.material.metalness = this.metalness;
121
121
  this.material.envMap = this.getEnvmap();
122
122
  }
123
+ this.material.emissive = this.emissive;
123
124
  this.material.needsUpdate = true;
124
125
  }
125
126
  }
@@ -0,0 +1,93 @@
1
+ elation.require([], function() {
2
+ elation.component.add('janusweb.translators.xrfragments', function() {
3
+ this.init = function() {
4
+ this.description = "this implements the https://xrfragment.org standard for immersive 3d file browsing"
5
+ }
6
+ this.exec = function(args) {
7
+ return new Promise(elation.bind(this, function(resolve, reject) {
8
+
9
+ var room = this.room = args.room;
10
+ this.setupEvents()
11
+
12
+ var datapath = elation.config.get('janusweb.datapath', '/media/janusweb');
13
+
14
+ var roomdata = {
15
+ assets: {
16
+ assetlist: [
17
+ {assettype: 'model', name: 'scene', src: args.url},
18
+ ]
19
+ },
20
+ room: {
21
+ pos: [0,0,0],
22
+ xdir: "1 0 0",
23
+ zdir: "0 0 1",
24
+ },
25
+ object: [
26
+ {id: 'scene', js_id: 0, pos: "0 0 0", xdir: "-1 0 0", zdir: "0 0 -1", lighting: "false"}
27
+ ],
28
+ link: []
29
+ };
30
+ resolve(roomdata);
31
+ }));
32
+ }
33
+
34
+ this.spawnUserAtFragment = function(source) {
35
+ // XR Fragments deeplink spec: explicit or default spawn
36
+ // https://xrfragment.org/#teleport%20camera
37
+ if( ! this.room.urlhash ) this.room.urlhash = 'spawn'
38
+ this.room.setPlayerPosition.apply(this.room)
39
+ console.log("[xrfragment] camera teleport")
40
+ }
41
+
42
+ this.setupEvents = function(){
43
+ elation.events.add(room._room, 'room_load_complete', this.spawnUserAtFragment.bind(this) )
44
+ }
45
+
46
+ // translate XR Fragments microformat into JML
47
+ this.parseSource = async function(sourcecode, room){
48
+ this.room = room
49
+ const isJML = /<fireboxroom>[\s\S]*?<\/fireboxroom>/si;
50
+ if( sourcecode.match(isJML) ) return // JML takes precedence over microformats
51
+
52
+ // extract href/title value
53
+ let el = document.createElement("div")
54
+ el.innerHTML = sourcecode
55
+ const link = el.querySelector("link[as=spatial-entrypoint]")
56
+ if( !link ) return
57
+ const title = el.querySelector("title")
58
+ const href = link.getAttribute("href")
59
+ if( !href ) return
60
+ console.log("[xrfragment] detected XRF microformat")
61
+ const hrefNoHash = href.replace(/#.*/,'')
62
+
63
+ // if microformat has xr fragment in URI, use it if room-url has no xr fragment
64
+ if( href.match(/#/) && !room.urlhash ){
65
+ room.urlhash = href.replace(/.*#/,'')
66
+ }
67
+ // check if link exists
68
+ const hrefFull = hrefNoHash.match('://') ? hrefNoHash : String(room.baseurl+hrefNoHash)
69
+ const exist = await fetch( hrefFull ,{method:'HEAD'})
70
+ if( !exist.ok ) return console.warn(`[xrfragment] ${link.outerHTML} resolves to invalid url ${hrefFull}`)
71
+
72
+ // setup events
73
+ this.setupEvents()
74
+ // return JML
75
+ return room.parseSource(`
76
+ <title>${ title ? title.innerText.replace(/\n.*/g,'') : room.baseurl.split("/").pop() }</title>
77
+ <FireBoxRoom>
78
+ <Assets>
79
+ <assetobject id="scene" src="${hrefNoHash}"/>
80
+ </Assets>
81
+ <Room>
82
+ <object pos="0 0 0" collision_id="scene" id="scene" />
83
+ </Room>
84
+ </FireBoxRoom>
85
+ `)
86
+ }
87
+ // microformat heuristic (https://xrfragment.org/#XRF%20microformat)
88
+ // example: <link rel="alternate" as="spatial-entrypoint" src="https://foo.org/bar.glb">
89
+ this.parseSource.regex = /<link\s+[^>]*rel=['"]spatial-entrypoint['"][^>]*\/?>/si;
90
+
91
+ });
92
+ });
93
+
@@ -2,3 +2,8 @@
2
2
 
3
3
  [ -e elation ] && rm -r elation
4
4
  [ -e node_modules ] && rm -r node_modules
5
+ [ -d build ] && rm -r build
6
+ read -p "[?] bleeding-edge dependencies? (y/n) " ok
7
+ test "$ok" = y && rm package-lock.json
8
+ npm install
9
+ utils/build.sh