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.
- package/.github/workflows/deploy.yml +71 -0
- package/Dockerfile +14 -0
- package/README.md +258 -19
- package/janusxr.com/.args +2 -0
- package/janusxr.com/.init.lua +30 -0
- package/media/assets/webui/apps/comms/voip.js +27 -25
- package/media/assets/webui/apps/editor/editor.js +16 -0
- package/media/assets/webui/apps/locomotion/locomotion-assets.json +3 -0
- package/media/assets/webui/apps/locomotion/teleport.mp3 +0 -0
- package/media/assets/webui/apps/locomotion/teleporter.js +75 -43
- package/media/assets/webui/apps/navigation/navigation.js +6 -1
- package/media/assets/webui/apps/xrfragment/level0-sidecarfile.js +308 -0
- package/media/assets/webui/apps/xrfragment/level2-hyperlink.js +265 -0
- package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.export.js +93 -0
- package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.import.js +229 -0
- package/media/assets/webui/apps/xrfragment/patch/metaquest-fix-flickering.js +15 -0
- package/media/assets/webui/apps/xrfragment/xrfragment.json +14 -0
- package/media/assets/webui/apps/xrmenu/images/teleport.png +0 -0
- package/media/assets/webui/apps/xrmenu/xrmenu-assets.json +2 -1
- package/media/assets/webui/apps/xrmenu/xrmenu.js +39 -15
- package/media/assets/webui/default.json +1 -0
- package/media/images/portal.svg +130 -0
- package/media/index.html +48 -1
- package/package.json +1 -1
- package/scripts/client.js +24 -0
- package/scripts/config.js +12 -1
- package/scripts/janusbase.js +14 -22
- package/scripts/janusghost.js +2 -2
- package/scripts/janusparagraph.js +195 -12
- package/scripts/janusplayer.js +9 -6
- package/scripts/room.js +91 -44
- package/scripts/sound.js +1 -1
- package/scripts/text.js +2 -1
- package/scripts/translators/paragraph/html-xml-rss.js +47 -0
- package/scripts/translators/peertube.js +89 -0
- package/scripts/translators/xrfragments.js +93 -0
- package/tests/room/paragraph.xml +47 -0
- package/utils/build.sh +1 -0
- package/utils/clean-build.sh +5 -0
- package/utils/dev-link.sh +75 -0
- package/utils/release.binary.sh +36 -0
- 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
|
+
}
|
|
Binary file
|
|
@@ -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
|
]
|