janusweb 1.5.56 → 1.7.1

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.
Files changed (42) hide show
  1. package/.github/workflows/deploy.yml +71 -0
  2. package/Dockerfile +14 -0
  3. package/README.md +258 -19
  4. package/janusxr.com/.args +2 -0
  5. package/janusxr.com/.init.lua +30 -0
  6. package/media/assets/webui/apps/comms/voip.js +27 -25
  7. package/media/assets/webui/apps/editor/editor.js +16 -0
  8. package/media/assets/webui/apps/locomotion/locomotion-assets.json +3 -0
  9. package/media/assets/webui/apps/locomotion/teleport.mp3 +0 -0
  10. package/media/assets/webui/apps/locomotion/teleporter.js +75 -43
  11. package/media/assets/webui/apps/navigation/navigation.js +6 -1
  12. package/media/assets/webui/apps/xrfragment/level0-sidecarfile.js +308 -0
  13. package/media/assets/webui/apps/xrfragment/level2-hyperlink.js +265 -0
  14. package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.export.js +93 -0
  15. package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.import.js +229 -0
  16. package/media/assets/webui/apps/xrfragment/patch/metaquest-fix-flickering.js +15 -0
  17. package/media/assets/webui/apps/xrfragment/xrfragment.json +14 -0
  18. package/media/assets/webui/apps/xrmenu/images/teleport.png +0 -0
  19. package/media/assets/webui/apps/xrmenu/xrmenu-assets.json +2 -1
  20. package/media/assets/webui/apps/xrmenu/xrmenu.js +39 -15
  21. package/media/assets/webui/default.json +1 -0
  22. package/media/images/portal.svg +130 -0
  23. package/media/index.html +48 -1
  24. package/package.json +1 -1
  25. package/scripts/client.js +24 -0
  26. package/scripts/config.js +12 -1
  27. package/scripts/janusbase.js +14 -22
  28. package/scripts/janusghost.js +2 -2
  29. package/scripts/janusparagraph.js +195 -12
  30. package/scripts/janusplayer.js +9 -6
  31. package/scripts/room.js +91 -44
  32. package/scripts/sound.js +1 -1
  33. package/scripts/text.js +2 -1
  34. package/scripts/translators/paragraph/html-xml-rss.js +47 -0
  35. package/scripts/translators/peertube.js +89 -0
  36. package/scripts/translators/xrfragments.js +93 -0
  37. package/tests/room/paragraph.xml +47 -0
  38. package/utils/build.sh +1 -0
  39. package/utils/clean-build.sh +5 -0
  40. package/utils/dev-link.sh +75 -0
  41. package/utils/release.binary.sh +36 -0
  42. package/utils/release.docker.sh +11 -0
@@ -0,0 +1,265 @@
1
+ /* XR URI Fragments spec (https://xrfragment.org)
2
+ *
3
+ * level2 explicit hyperlinks (embedded in 3D scenes/files) as per the XR URI Fragments spec
4
+ *
5
+ * When clicking an href-value, the user(camera) is teleported/imported to the referenced object.
6
+ * Usecases: spatial anchors, object imports, hyperlink bridge between JML<>3D files
7
+ * see more: https://xrfragment.org/#%F0%9F%93%9C%20level2%3A%20explicit%20hyperlinks
8
+ */
9
+
10
+
11
+ elation.require([], function() {
12
+
13
+ elation.extend('janusweb.hyperlink', class {
14
+ constructor(object) {
15
+ this.scene = elation.engine.instances.default.systems.world.scene['world-3d']
16
+ this._object = object
17
+ this.init()
18
+ }
19
+
20
+ init(){
21
+ this.cleanup()
22
+ this.scan( this.scene )
23
+ this.setupShroud()
24
+ }
25
+
26
+ setupShroud(){
27
+ // show shroud when teleporting
28
+ for( let i in player.head.children ){
29
+ let n = player.head.children[i]
30
+ if( n.js_id == 'xrf_shroud' ) this.shroud = n // found previrous one
31
+ }
32
+ if( this.shroud ) return // we're done!
33
+ this.shroud = room.createObject('object', {
34
+ id: 'sphere',
35
+ js_id: 'xrf_shroud',
36
+ scale: V(1),
37
+ lighting: false,
38
+ col: 'black',
39
+ cull_face: 'none',
40
+ depth_test: false,
41
+ depth_write: false,
42
+ shadow_cast: false,
43
+ shadow_receive: false,
44
+ renderorder: 1000,
45
+ visible: false,
46
+ });
47
+ player.head.add(this.shroud._target);
48
+
49
+ // patch setPlayerPosition() with shroud animations during local teleports
50
+ room.setPlayerPosition = (
51
+ (original) => function(room){
52
+ this.hyperlink.showShroud()
53
+ return original.apply(this,room)
54
+ }.bind(room)
55
+ )(room.setPlayerPosition)
56
+ }
57
+
58
+ scan(scene,cb){
59
+ scene.traverse( (object) => {
60
+ this.detectHUDLUT(object)
61
+ this.detectHref(object)
62
+ })
63
+ }
64
+
65
+ detectHref(object){
66
+ if( !object?.userData?.href || object.hasHref ) return
67
+
68
+ const jobj = this.toJanusObject(object)
69
+ jobj.addEventListener("click", () => this.execute(object.userData.href,{jobj,scene:this.scene}) )
70
+ object.hasHref = true
71
+ }
72
+
73
+ cleanup(){
74
+ const head = player.head.objects['3d']
75
+ head.children
76
+ .filter( (child) => child.xrf_cleanup ? child : false )
77
+ .map( (child) => child.xrf_cleanup() )
78
+ }
79
+
80
+ detectHUDLUT(object){
81
+ // XR Fragment HUD extensions: https://xrfragment.org/#teleport%20camera%20spawnpoint
82
+ if( object.type == 'PerspectiveCamera' && object.name == 'spawn' && object.children.length ){
83
+ const head = player.head.objects['3d']
84
+ // move children to player head
85
+ while( object.children.length ){
86
+ object.children[0].xrf_cleanup = function(me,object){
87
+ object.add( me ) // add back
88
+ }.bind(null, object.children[0], object )
89
+ head.add( object.children[0] )
90
+ }
91
+ }
92
+ }
93
+
94
+ execute = function(href,opts){
95
+ const {url,hash} = this.getUrlObject(href)
96
+ opts = opts ? {...opts, url, hash} : {url,hash}
97
+ console.log("hyperlink: "+href)
98
+ elation.events.fire({element: this, type: 'href', data: {href,opts}});
99
+ if( room.url != url.origin+url.pathname ) return this.executeExternal(href,opts)
100
+ hash.forEach( (v,k) => {
101
+ const {operator,param} = this.getOperators(k)
102
+ switch( param ){
103
+ case "t": // W3C URI Time fragment not implemented (yet)
104
+ case "loop": this._object.loop = operator != '-'
105
+ case "pos": // legacy fallthrough
106
+ default: // level2: internal teleports/spawn
107
+ // https://xrfragment.org/#%F0%9F%93%9C%20level2%3A%20explicit%20hyperlinks
108
+ room.referrer = room.urlhash
109
+ room.urlhash = v || k
110
+ if( this.scene.getObjectByName(v) ){
111
+ room.setPlayerPosition()
112
+ this.spawnBackLink()
113
+ }
114
+ // level2: animation triggers
115
+ // https://xrfragment.org/#%F0%9F%93%9C%20level2%3A%20explicit%20hyperlinks
116
+ for( let i in this._object.children ){
117
+ const model = this._object.children[i]
118
+ if( model.animations && model.animations.map ){
119
+ model.animations.map( (a) => {
120
+ if( !String(a.name).toLowerCase() == String(v).toLowerCase() ) return
121
+ console.log("xrfragment: activating animation '"+a.name+"'")
122
+ model.anim_id = a.name
123
+ })
124
+ }
125
+ }
126
+ return
127
+ break;
128
+ }
129
+ })
130
+
131
+ const fullUrl = href.match(/^#/) ? `${room.url}${url.hash}` : url.href
132
+ room.url = fullUrl
133
+ if( !url.protocol.match(/^xrf:/) ){
134
+ // level4: https://xrfragment.org/doc/RFC_XR_Fragments.html#xrf-uri-scheme
135
+ janus.updateClientURL(fullUrl)
136
+ }
137
+ elation.events.fire({element: room, type: 'room_change', data: room});
138
+ }
139
+
140
+ spawnBackLink(){
141
+ for( let i in room.objects ){
142
+ if( i.match(/^reciprocal-hashlink/) ) room.removeObject(i)
143
+ }
144
+ let link = room.createObject('link', {
145
+ rotation: '0 45 0',
146
+ url: room.url+'#'+room.referrer,
147
+ title: 'go back',
148
+ round: true,
149
+ shader_id: 'defaultportal',
150
+ js_id: 'reciprocal-hashlink',
151
+ });
152
+ setTimeout( () => {
153
+ link.position = player.localToWorld(V(1.5,0,1.5))
154
+ },10)
155
+ }
156
+
157
+ executeExternal(href, opts){
158
+ if( opts.portalActivateDelay ){
159
+ setTimeout( () => {
160
+ janus.setActiveRoom( href, room.url)
161
+ }, opts.portalActivateDelay)
162
+ }
163
+ player.spawnPortal(href)
164
+ elation.events.fire({element: this, type: 'href_portal', data: {href,opts}});
165
+ }
166
+
167
+ getUrlObject = function(href){
168
+ const urlExpanded = href[0] == '#'
169
+ ? room.url+href
170
+ : href.match(/:\//) ? href : room.baseurl+href
171
+ const url = new URL( urlExpanded )
172
+ const hash = new URLSearchParams( String(url.hash).substr(1) )
173
+ return {url,hash}
174
+ }
175
+
176
+ getOperators = function(k){
177
+ let operator = ''
178
+ // scan for operator
179
+ if( k[0].match(/[-+]/) ){
180
+ operator = k[0]
181
+ k = k.substr(1)
182
+ }
183
+ return {operator,param:k}
184
+ }
185
+
186
+ toJanusObject = function(object){
187
+ // we are not using object.parts[ ... ] because
188
+ // glTF animated collidable objects require special sync/setup:
189
+ // collider must be child of THREE object (to get animated), not janusobject
190
+ // NOTE: avoid jobj.add() since that reparents the object (andw breaks glTF anims)
191
+ let jo = this._object.createObject("object",{
192
+ js_id: object.name
193
+ })
194
+ object.parent.add( jo.objects['3d'] )
195
+ jo.objects['3d'] = object
196
+ jo.collidable = true
197
+ jo.removeCollider();
198
+ const collider = object.clone()
199
+ collider.position.set(0,0,0)
200
+ collider.rotation.set(0,0,0)
201
+ collider.scale.set(1,1,1)
202
+ jo.setCollider('mesh',{mesh: collider})
203
+ jo.colliders.parent = object
204
+ return jo
205
+ }
206
+
207
+ showShroud = function(){
208
+ this.shroud.visible = true;
209
+ this.shroud.opacity = 1;
210
+ }
211
+
212
+ update = function(){
213
+ if (this.shroud?.visible && !janus.loading) {
214
+ if (this.shroud.opacity > .001) {
215
+ this.shroud.opacity *= .9;
216
+ if (this.shroud.opacity <= .001) {
217
+ this.shroud.visible = false;
218
+ this.shroud.opacity = 0;
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ })
225
+ });
226
+
227
+ xrf_install_hyperlinks = function(){
228
+ if( !room.objects?.scene?.modelasset?.loaded ) {
229
+ return setTimeout( xrf_install_hyperlinks, 300 )
230
+ }
231
+ if( !room.hyperlink ){
232
+ room.hyperlink = new elation.janusweb.hyperlink(room)
233
+ janus.loading = false // force!
234
+ }
235
+ }
236
+
237
+ xrf_install_hyperlinks()
238
+
239
+ // update urlbar when user or browser activates href
240
+ elation.events.add(null, 'href', function(e){
241
+ const scene = elation.engine.instances.default.systems.world.scene['world-3d']
242
+ const urlbar = document.querySelector('janus-ui-urlbar ui-input')
243
+ const href = e?.data?.href
244
+ if( urlbar ){
245
+ urlbar.value = href[0] == '#' ? urlbar.value.replace(/#.*/,'') + href : href
246
+ }else console.warn("xrfragment: cannot find urlbar")
247
+ })
248
+
249
+ elation.events.add(null, 'room_load_complete', xrf_install_hyperlinks )
250
+ elation.events.add(null, 'room_disable', function(e){
251
+ if( room?.hyperlink ) room.hyperlink.cleanup()
252
+ })
253
+
254
+ elation.events.add(null, 'room_enable', function(e){
255
+ if( room?.hyperlink ) room.hyperlink.init()
256
+ else xrf_install_hyperlinks()
257
+ })
258
+
259
+ elation.events.add(null, 'janusweb_script_frame', function(){
260
+ if( room?.hyperlink ) room.hyperlink.update()
261
+ })
262
+
263
+ if( room.urlhash ){
264
+ elation.events.fire({element: this, type: 'href', data: {href: `#${room.urlhash}`}});
265
+ }
@@ -0,0 +1,93 @@
1
+ xrf_export = {
2
+
3
+ node(e){
4
+ const {userData,n} = e.data
5
+ for( let k in userData ){
6
+ if( k.match(/^-(three)-/) || k == 'href' ){ // keep hrefs + `-three-*` engine prefixes
7
+ n.userData[k] = userData[k] // but ditch `-janus-*` (we'll regenerate them)
8
+ }
9
+ if( k == 'thing' && String(userData[k].componentname).match('engine.things.janus') ){
10
+ const thing = userData[k]
11
+ switch( thing.componentname ){
12
+ case "engine.things.janusparagraph":
13
+ case "engine.things.janustext":
14
+ case "engine.things.janusportal":
15
+ case "engine.things.januswebsurface":
16
+ case "engine.things.janusroom":
17
+
18
+ if( thing.componentname != 'engine.things.janusroom' ){
19
+ n.name = String(`-janus-${thing.js_id}`).replace(/.*janus-/,'-janus-')
20
+ n.userData['-janus-tag'] = thing.componentname
21
+ .replace('janusportal','januslink')
22
+ .replace('engine.things.janus','')
23
+ }
24
+ if( thing.componentname.match(/engine\.things\.janus(portal|link)/) && n.children[0] ){
25
+ // convert portal to XRF href
26
+ n.children[0].userData['href'] = thing.url
27
+ }
28
+ // write XRF `-janus-*` engine prefixes
29
+ const attrs = xrf_export.getAttributes(thing)
30
+ for( let i in attrs ){
31
+ if( !i.match(/^-janus-(use_local_asset)/) ){
32
+ n.userData[`-janus-${i}`] = attrs[i]
33
+ }
34
+ }
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ },
40
+ scene(e){
41
+ // https://xrfragment.org/#%F0%9F%93%9Clevel7%3A%20engine%20prefixes
42
+ e.data.scene.userData['-janus-source'] = room.getRoomSource()
43
+ },
44
+ getAttributes(thing){
45
+ if( !thing ) return {}
46
+ let proxy = thing.getProxyObject(),
47
+ propdefs = thing._thingdef.properties,
48
+ proxydefs = proxy._proxydefs,
49
+ attrs = {};
50
+
51
+ // this code is almost a duplicate of elation's .summarizeXML() but with inversed (glTF) position
52
+ for (let k in proxydefs) {
53
+ let proxydef = proxydefs[k],
54
+ propdef = elation.utils.arrayget(propdefs, proxydef[1]);
55
+ if ( k != 'room' && k != 'tagName' && k != 'classList' && proxydef[0] == 'property' && propdef) {
56
+ let val = elation.utils.arrayget(thing.properties, proxydef[1]);
57
+ let defaultval = propdef.default;
58
+ if (val instanceof THREE.Vector2) {
59
+ if (defaultval instanceof THREE.Vector2) defaultval = defaultval.toArray();
60
+ if (!('default' in propdef) || ('default' in propdef && !(val.x == defaultval[0] && val.y == defaultval[1]))) {
61
+ attrs[k] = val.toArray().map(n => Math.round(n * 10000) / 10000).join(' ');
62
+ }
63
+ } else if (val instanceof THREE.Vector3) {
64
+ if (defaultval instanceof THREE.Vector3) defaultval = defaultval.toArray();
65
+ if (!('default' in propdef) || ('default' in propdef && !(val.x == defaultval[0] && val.y == defaultval[1] && val.z == defaultval[2]))) {
66
+ attrs[k] = val.toArray().map(n => Math.round(n * -10000) / 10000).join(' ');
67
+ }
68
+ } else if (val instanceof THREE.Color) {
69
+ if (defaultval instanceof THREE.Color) defaultval = defaultval.toArray();
70
+ if (!('default' in propdef) || defaultval === null || ('default' in propdef && !(val.r == defaultval[0] && val.g == defaultval[1] && val.b == defaultval[2]))) {
71
+ attrs[k] = val.toArray().map(n => Math.round(n * 10000) / 10000).join(' ');
72
+ }
73
+ } else if (val instanceof THREE.Euler) {
74
+ if (defaultval instanceof THREE.Euler) defaultval = defaultval.toArray();
75
+ if (!('default' in propdef) || ('default' in propdef && !(val.x == defaultval[0] && val.y == defaultval[1] && val.z == defaultval[2]))) {
76
+ attrs[k] = val.toArray().slice(0, 3).map(n => Math.round(n * 10000) / 10000).join(' ');
77
+ }
78
+ } else if (val instanceof THREE.Quaternion) {
79
+ if (defaultval instanceof THREE.Quaternion) defaultval = defaultval.toArray();
80
+ if (!('default' in propdef) || ('default' in propdef && !(val.x == defaultval[0] && val.y == defaultval[1] && val.z == defaultval[2] && val.w == defaultval[3]))) {
81
+ attrs[k] = val.toArray().map(n => Math.round(n * 10000) / 10000).join(' ');
82
+ }
83
+ } else if (val !== propdef.default && val !== null && val !== '') {
84
+ attrs[k] = val;
85
+ }
86
+ }
87
+ }
88
+ return attrs;
89
+ }
90
+ }
91
+
92
+ elation.events.add(null, 'webui_editor_export_node', xrf_export.node )
93
+ elation.events.add(null, 'webui_editor_export', xrf_export.scene )
@@ -0,0 +1,229 @@
1
+ // https://xrfragment.org/#%F0%9F%93%9Clevel7%3A%20engine%20prefixes
2
+ // There are cases where the 3D scene file might want to hint the
3
+ // specific features to the viewer-engine (JANUSWEB, THREE.js, AFRAME, Godot e.g.).
4
+
5
+ xrf_engines = function(){
6
+
7
+ const {toJanusObject,applyPrefixes,applyCleanup} = xrf_engines
8
+ let cleanup = []
9
+
10
+ const map = (obj,key,realKey) => {
11
+ let match = true
12
+
13
+ // compatibility workaround: some 3D editors omit false booleans in export :/
14
+ // therefore we support boolean strings
15
+ if( obj.userData[key] == 'false' ) obj.userData[key] = false
16
+ if( obj.userData[key] == 'true' ) obj.userData[key] = true
17
+
18
+ // increment/decrement
19
+ let scroller = key.match(/[+]$/)
20
+ if( scroller && !scene.scrollers ){
21
+ if( typeof obj.userData[key] != 'number' ) obj.userData[key] = parseFloat(obj.userData[key])
22
+ scene.scrollers = []
23
+ elation.events.add(null, 'janusweb_script_frame', function(e){
24
+ scene.scrollers.map( (f) => f(e.data) ) // e.data == delta
25
+ })
26
+ }
27
+
28
+ // special cases
29
+ switch( key ){
30
+ case "-three-material.blending": if( obj.material ){
31
+ const modes = {
32
+ 'THREE.NoBlending': THREE.NoBlending,
33
+ 'THREE.NormalBlending': THREE.NormalBlending,
34
+ 'THREE.AdditiveBlending': THREE.AdditiveBlending,
35
+ 'THREE.SubtractiveBlending': THREE.SubtractiveBlending,
36
+ 'THREE.MultiplyBlending': THREE.MultiplyBlending
37
+ }
38
+ setTimeout( () => { // not sure why this only works in setTimeout
39
+ if( modes[ obj.userData[key] ] ) obj.material.blending = modes[ obj.userData[key] ]
40
+ },10)
41
+ }
42
+ break;
43
+
44
+ case "-three-material.sides": if( obj.material ){
45
+ const modes = {
46
+ 'THREE.FrontSide': THREE.NoBlending,
47
+ 'THREE.BackSide': THREE.NormalBlending,
48
+ 'THREE.DoubleSide': THREE.DoubleSide
49
+ }
50
+ setTimeout( () => { // not sure why this only works in setTimeout
51
+ if( modes[ obj.userData[key] ] ) obj.material.sides = modes[ obj.userData[key] ]
52
+ },10)
53
+ }
54
+ break;
55
+
56
+ case "-janus-use_local_asset": room.use_local_asset = obj.userData[key]
57
+ room.localasset = room.createObject('object', {
58
+ id: room.use_local_asset,
59
+ collision_id: room.use_local_asset + '_collision',
60
+ collision_scale: V(1,1,1),
61
+ collision_pos: V(0,0,0),
62
+ col: room.col,
63
+ //fwd: room.fwd,
64
+ xdir: room.xdir,
65
+ ydir: room.ydir,
66
+ zdir: room.zdir,
67
+ shadows: true
68
+ });
69
+ break;
70
+
71
+ // DECLARATIVE entities
72
+ case "-janus-tag": let opts = {}// rotation: '0 -180 0' }
73
+ for( let i in obj.userData ){
74
+ if( !i.match(/^-janus-/) ) continue
75
+ opts[ i.replace(/-janus-/,'') ] = obj.userData[i]
76
+ }
77
+
78
+ // create asset
79
+ if( obj.userData['-janus-tag'].match(/^asset/) ){
80
+ if( opts.src && !opts.src.match(/(^\.|:\/)/) ) opts.src = room.baseurl + opts.src
81
+ room.loadNewAsset( opts['tag'].replace(/^asset/,''), opts )
82
+ }else{
83
+ opts.js_id = opts.name = String(`-janus-${obj.name}_${obj.userData['-janus-tag']}`).replace(/.*janus-/,'-janus-')
84
+ // create room object
85
+ const jo = room.createObject( opts.tag, opts )
86
+ jo.objects['3d'].name = opts.js_id
87
+ jo.visible = false
88
+
89
+ // replace janusobject with nested THREE obj
90
+ obj.parent.add( jo.objects['3d'] )
91
+
92
+ // fix unclickable objects (since obj might be nested in the scene-tree)
93
+ if( jo.colliders ){
94
+ jo.removeCollider()
95
+ jo.colliders.parent.children.push( jo.objects['3d'] )
96
+ }
97
+
98
+ // we need setTimeout otherwise quaternion is not updated
99
+ // https://github.com/jbaicoianu/janusweb/issues/306
100
+ setTimeout( () => {
101
+ jo.orientation.copy( obj.quaternion)
102
+ jo.position.copy( obj.position )
103
+ //jo.scale.copy( obj.scale )
104
+ jo.visible = true
105
+ },200)
106
+ // mark previously generated geo/materials by janusweb export for deletion
107
+ obj.traverse ( (o) => cleanup.push(o) )
108
+ cleanup.push(obj)
109
+ }
110
+ break;
111
+ // OBJECTS
112
+ case "-janus-collision_id": const collision_id = obj.userData[key]
113
+ if( collision_id != obj.name ){
114
+ console.warn(`xrfragment: ${obj.name}.collision_id can be '${obj.name}' only (for now)..skipping '${collision_id}'`)
115
+ }else{
116
+ const jo = toJanusObject(obj,{noreparent:true})
117
+ jo.collidable = true
118
+ jo.collision_id = collision_id
119
+ jo.removeCollider();
120
+ const collider = obj.clone()
121
+ collider.position.set(0,0,0)
122
+ collider.rotation.set(0,0,0)
123
+ collider.scale.set(1,1,1)
124
+ jo.collision_trigger = true
125
+ jo.setCollider('mesh',{mesh: collider})
126
+ jo.colliders.parent = obj
127
+ //jo.objects.dynamics.mass = 1
128
+ //jo.objects.dynamics.addForce('static', new THREE.Vector3(0, room.gravity, 0));
129
+ //elation.events.add(jo.objects.dynamics, 'physics_collide', elation.bind(jo, jo.handleCollision));
130
+ console.log(`xrfragment: setting collision_id = ${collision_id}`)
131
+ }
132
+ break;
133
+
134
+ default: match = false
135
+
136
+ // JANUS property fallthrough
137
+ if( key.match(/^-janus-/) && !key.match("-janus-tag") ){
138
+ // *TODO* more heuristics to determine scene
139
+ if( obj.name == 'Scene' || obj?.parent?.name == '' || obj.userData['-janus-source']){
140
+ room[realKey] = obj.userData[key];
141
+ }else{
142
+ toJanusObject(obj)[realKey] = obj.userData[key]
143
+ }
144
+ match = true
145
+ }
146
+
147
+ // THREE fallthrough
148
+ if( key.match(/^-three-/) ){
149
+
150
+ if( realKey.match('material.') && obj.material ){ // clone shared materials
151
+ obj.material = obj.material.clone()
152
+ if( realKey.match('material.map') ) obj.material.map = obj.material.map.clone()
153
+ }
154
+
155
+ const setKeyVal = (o, path, val ) => new Function('o', 'v', `o.${path} = v `)(o, val);
156
+ if( scroller ){
157
+ const setKeyValIncr = (o, path, val, delta) => new Function('o', 'speed', `o.${path} += ${delta} * speed`)(o, val);
158
+ try{
159
+ const myscroller = function(path,val,delta){
160
+ setKeyValIncr( this, path, val, delta)
161
+ }.bind(obj, realKey.replace(/[+]$/,''), obj.userData[key])
162
+ myscroller(0.000001) // see if it triggers error
163
+ scene.scrollers.push(myscroller) // otherwise add frame-function
164
+ }catch(e){
165
+ console.error(`could not apply engine prefix: ${obj.name} => ${key} (tip: dont use them on <paragraph> objects)`)
166
+ }
167
+ }else{
168
+ try{
169
+ setKeyVal( obj, realKey, obj.userData[key] )
170
+ }catch(e){ console.error(e) }
171
+ }
172
+ match = true
173
+ }
174
+ break;
175
+ }
176
+
177
+ if( match ){
178
+ //console.log(`xrfragment: engine prefix '${key}:${realKey}' = '${obj.userData[key]}'`)
179
+ }
180
+ }
181
+
182
+ room.gravity = 0 // new default unless specified otherwise
183
+ let scene = elation.engine.instances.default.systems.world.scene['world-3d']
184
+ const isAsset = (obj) => String(obj.userData['-janus-tag']).match(/^asset/)
185
+ const isJanusTag = (obj) => String(obj.userData['-janus-tag']) && !isAsset(obj)
186
+ applyPrefixes(scene,map, (o) => isAsset(o) ) // janus requires first initialzing of assets
187
+ applyPrefixes(scene,map, (o) => isJanusTag(o) ) // then regular janus tags
188
+ applyPrefixes(scene,map, (o) => !isJanusTag(o) && !isAsset(o) ) // then set properties on the rest
189
+ applyCleanup(cleanup)
190
+ }
191
+
192
+
193
+ xrf_engines.toJanusObject = function(obj,opts){
194
+ if( room.objects[ obj.name ] ) return room.objects[ obj.name ]
195
+ opts = opts || {}
196
+ opts.tag = opts.tag || 'object'
197
+ const create = () => room.createObject( opts.tag,{ js_id: obj.name, ...opts })
198
+ let jo = create()
199
+ jo.objects['3d'] = obj
200
+ return jo
201
+ }
202
+
203
+ xrf_engines.applyPrefixes = function(scene,map,criteria){
204
+ criteria = criteria ? criteria : (obj) => true
205
+ scene.traverse( (obj) => {
206
+ if( criteria(obj) ){
207
+ for( let field in obj.userData ){
208
+ if( obj.userData[field] ){
209
+ try{
210
+ map(obj,field, field.replace(/^-(janus|three)-/,'') )
211
+ }catch(e){ console.error(e) }
212
+ }
213
+ }
214
+ }
215
+ })
216
+ }
217
+
218
+ xrf_engines.applyCleanup = function(cleanup){
219
+ const clean = (o) => {
220
+ if( o.geometry ) o.geometry.dispose()
221
+ if( o.material ) o.material.dispose()
222
+ o.removeFromParent()
223
+ }
224
+ cleanup.map(clean)
225
+ }
226
+
227
+
228
+ elation.events.add(null, 'room_load_complete', xrf_engines ) // future scenes
229
+ if( room.loaded ) xrf_engines() // current scene
@@ -0,0 +1,15 @@
1
+ // Meta Quest 2 results in rapid flickering when loading a new room
2
+ // therefore we show the shroud while loading
3
+ setActiveRoom = room.janus.setActiveRoom.bind(room.janus)
4
+ room.janus.setActiveRoom = function(url,referer,skipURL){
5
+ if( url.replace(/#.*/,'') != room.url ){
6
+ room.fadeAudioOut()
7
+ setTimeout( function(){
8
+ setActiveRoom.apply(room.janus, [url, referer, skipURL])
9
+ }, 500)
10
+ }else{
11
+ room.urlhash = url.match('#') ? url.replace(/.*#/,'') : 'spawn'
12
+ room.setPlayerPosition()
13
+ // ignore same-room setActiveRoom-calls (it restarts audio when clicking internal hyperlinks)
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "templates": {
3
+ },
4
+ "scripts": [
5
+ "./level2-hyperlink.js",
6
+ "./level0-sidecarfile.js",
7
+ "./level7-engine-prefixes.import.js",
8
+ "./level7-engine-prefixes.export.js",
9
+ "./patch/metaquest-fix-flickering.js"
10
+ ],
11
+ "css": []
12
+ }
13
+
14
+
@@ -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
  ]