minor bugfix
This commit is contained in:
parent
7778ff6be8
commit
6c1ecaa23d
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* v0.5.1 generated at Fri Mar 1 01:33:23 PM UTC 2024
|
||||
* v0.5.1 generated at Tue Mar 19 09:13:06 AM UTC 2024
|
||||
* https://xrfragment.org
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
@ -1217,6 +1217,9 @@ xrfragment_XRF.prototype = {
|
|||
,validate: function(value) {
|
||||
this.guessType(this,value);
|
||||
var ok = true;
|
||||
if(value.length == 0) {
|
||||
ok = false;
|
||||
}
|
||||
if(!this.is(xrfragment_XRF.T_FLOAT) && this.is(xrfragment_XRF.T_VECTOR2) && !(typeof(this.x) == "number" && typeof(this.y) == "number")) {
|
||||
ok = false;
|
||||
}
|
||||
|
@ -1386,19 +1389,6 @@ xrf.detectCameraRig = function(opts){
|
|||
}
|
||||
}
|
||||
|
||||
xrf.roundrobin = (frag, store) => {
|
||||
if( !frag.args || frag.args.length == 0 ) return 0
|
||||
if( !store.rr ) store.rr = {}
|
||||
let label = frag.fragment
|
||||
if( store.rr[label] ) return store.rr[label].next()
|
||||
store.rr[label] = frag.args
|
||||
store.rr[label].next = () => {
|
||||
store.rr[label].index = (store.rr[label].index + 1) % store.rr[label].length
|
||||
return store.rr[label].index
|
||||
}
|
||||
return store.rr[label].index = 0
|
||||
}
|
||||
|
||||
xrf.stats = () => {
|
||||
// bookmarklet from https://github.com/zlgenuine/threejs_stats
|
||||
(function(){
|
||||
|
@ -1459,13 +1449,13 @@ xrf.emit = function(eventName, data){
|
|||
return xrf.emit.promise(eventName,data)
|
||||
}
|
||||
|
||||
xrf.emit.normal = function(eventName, data) {
|
||||
xrf.emit.normal = function(eventName, opts) {
|
||||
if( !xrf._listeners ) xrf._listeners = []
|
||||
var callbacks = xrf._listeners[eventName]
|
||||
if (callbacks) {
|
||||
for (var i = 0; i < callbacks.length; i++) {
|
||||
for (var i = 0; i < callbacks.length && !opts.halt; i++) {
|
||||
try{
|
||||
callbacks[i](data);
|
||||
callbacks[i](opts);
|
||||
}catch(e){ console.error(e) }
|
||||
}
|
||||
}
|
||||
|
@ -1482,7 +1472,10 @@ xrf.emit.promise = function(e, opts){
|
|||
let succesful = opts.promises.reduce( (a,b) => a+b )
|
||||
if( succesful == opts.promises.length ) resolve(opts)
|
||||
})(opts.promises.length-1),
|
||||
reject: console.error
|
||||
reject: (reason) => {
|
||||
opts.halt = true
|
||||
console.warn(`'${e}' event rejected: ${reason}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
xrf.emit.normal(e, opts)
|
||||
|
@ -1986,7 +1979,11 @@ xrf.frag.href = function(v, opts){
|
|||
|
||||
let click = mesh.userData.XRF.href.exec = (e) => {
|
||||
|
||||
if( !mesh.material.visible ) return // ignore invisible nodes
|
||||
if( !mesh.material || !mesh.material.visible ) return // ignore invisible nodes
|
||||
|
||||
// update our values to the latest value (might be edited)
|
||||
xrf.Parser.parse( "href", mesh.userData.href, frag )
|
||||
const v = frag.href
|
||||
|
||||
// bubble up!
|
||||
mesh.traverseAncestors( (n) => n.userData && n.userData.href && n.dispatchEvent({type:e.type,data:{}}) )
|
||||
|
@ -2010,7 +2007,7 @@ xrf.frag.href = function(v, opts){
|
|||
}
|
||||
|
||||
let selected = mesh.userData.XRF.href.selected = (state) => () => {
|
||||
if( !mesh.material.visible && !mesh.isSRC ) return // ignore invisible nodes
|
||||
if( (!mesh.material && !mesh.material.visible) && !mesh.isSRC ) return // ignore invisible nodes
|
||||
if( mesh.selected == state ) return // nothing changed
|
||||
|
||||
xrf.interactive.objects.map( (o) => {
|
||||
|
@ -2107,7 +2104,9 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
let {scene,model} = opts;
|
||||
scene.traverse( (n) => {
|
||||
if( n.userData && n.userData['#'] ){
|
||||
xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments without affecting URL
|
||||
if( !n.parent && !document.location.hash ){
|
||||
xrf.navigator.to( n.userData['#'] )
|
||||
}else xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments without affecting URL
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -2624,7 +2623,7 @@ xrf.getCollisionMeshes = () => {
|
|||
})
|
||||
return meshes
|
||||
}
|
||||
// wrapper to survive in/outside modules
|
||||
// wrapper to collect interactive raycastable objects
|
||||
|
||||
xrf.interactiveGroup = function(THREE,renderer,camera){
|
||||
|
||||
|
@ -2652,6 +2651,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
const scope = this;
|
||||
scope.objects = []
|
||||
scope.raycastAll = false
|
||||
|
||||
|
||||
const raycaster = new Raycaster();
|
||||
const tempMatrix = new Matrix4();
|
||||
|
@ -2659,6 +2660,15 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
// Pointer Events
|
||||
const element = renderer.domElement;
|
||||
|
||||
const getAllMeshes = (scene) => {
|
||||
let objects = []
|
||||
xrf.scene.traverse( (n) => {
|
||||
if( !n.material || n.type != 'Mesh' ) return
|
||||
objects.push(n)
|
||||
})
|
||||
return objects
|
||||
}
|
||||
|
||||
function onPointerEvent( event ) {
|
||||
|
||||
//event.stopPropagation();
|
||||
|
@ -2670,7 +2680,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
raycaster.setFromCamera( _pointer, camera );
|
||||
|
||||
const intersects = raycaster.intersectObjects( scope.objects, false );
|
||||
let objects = scope.raycastAll ? getAllMeshes(xrf.scene) : scope.objects
|
||||
const intersects = raycaster.intersectObjects( objects, false )
|
||||
|
||||
if ( intersects.length > 0 ) {
|
||||
|
||||
|
@ -2680,7 +2691,7 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
const uv = intersection.uv;
|
||||
|
||||
_event.type = event.type;
|
||||
_event.data.set( uv.x, 1 - uv.y );
|
||||
if( uv ) _event.data.set( uv.x, 1 - uv.y );
|
||||
object.dispatchEvent( _event );
|
||||
|
||||
}else{
|
||||
|
@ -2719,19 +2730,20 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld );
|
||||
raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
|
||||
|
||||
const intersections = raycaster.intersectObjects( scope.objects, false );
|
||||
let objects = scope.raycastAll ? getAllMeshes(xrf.scene) : scope.objects
|
||||
const intersects = raycaster.intersectObjects( objects, false )
|
||||
|
||||
if ( intersections.length > 0 ) {
|
||||
if ( intersects.length > 0 ) {
|
||||
|
||||
console.log(object.name)
|
||||
|
||||
const intersection = intersections[ 0 ];
|
||||
const intersection = intersects[ 0 ];
|
||||
|
||||
object = intersection.object;
|
||||
const uv = intersection.uv;
|
||||
|
||||
_event.type = eventsMapper[ event.type ];
|
||||
_event.data.set( uv.x, 1 - uv.y );
|
||||
if( uv ) _event.data.set( uv.x, 1 - uv.y );
|
||||
|
||||
object.dispatchEvent( _event );
|
||||
|
||||
|
@ -2758,6 +2770,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
}
|
||||
|
||||
// we create our own add to avoid unnecessary unparenting of buffergeometries from
|
||||
// their 3D model (which breaks animations)
|
||||
add(obj, unparent){
|
||||
if( unparent ) Group.prototype.add.call( this, obj )
|
||||
this.objects.push(obj)
|
||||
|
@ -3847,7 +3861,7 @@ window.AFRAME.registerComponent('xrf', {
|
|||
}
|
||||
|
||||
// give headset users way to debug without a cumbersome usb-tapdance
|
||||
if( xrf.debug || document.location.hostname.match(/^(localhost|[1-9])/) && !aScene.getAttribute("vconsole") ){
|
||||
if( document.location.hostname.match(/^(localhost|[1-9])/) && !aScene.getAttribute("vconsole") ){
|
||||
aScene.setAttribute('vconsole','')
|
||||
}
|
||||
|
||||
|
@ -3886,14 +3900,13 @@ window.AFRAME.registerComponent('xrf', {
|
|||
let {mesh,clickHandler} = opts;
|
||||
let createEl = function(c){
|
||||
let el = document.createElement("a-entity")
|
||||
el.setAttribute("xrf-get",c.name ) // turn into AFRAME entity
|
||||
el.setAttribute("xrf-get",c.name ) // turn into AFRAME entity
|
||||
el.setAttribute("pressable", '' ) // detect click via hand-detection
|
||||
el.setAttribute("class","ray") // expose to raycaster
|
||||
el.setAttribute("pressable", '') // detect hand-controller click
|
||||
// respond to cursor via laser-controls (https://aframe.io/docs/1.4.0/components/laser-controls.html)
|
||||
el.addEventListener("click", clickHandler )
|
||||
el.addEventListener("mouseenter", mesh.userData.XRF.href.selected(true) )
|
||||
el.addEventListener("mouseleave", mesh.userData.XRF.href.selected(false) )
|
||||
el.addEventListener("pressedstarted", clickHandler )
|
||||
$('a-scene').appendChild(el)
|
||||
}
|
||||
createEl(mesh)
|
||||
|
@ -4201,6 +4214,80 @@ AFRAME.components['look-controls'].Component.prototype.updateOrientation = funct
|
|||
object3D.rotation.z = this.magicWindowDeltaEuler.z;
|
||||
object3D.matrixAutoUpdate = true
|
||||
}
|
||||
// this makes WebXR hand controls able to click things (by touching it)
|
||||
|
||||
AFRAME.registerComponent('pressable', {
|
||||
schema: {
|
||||
pressDistance: {
|
||||
default: 0.01
|
||||
}
|
||||
},
|
||||
init: function() {
|
||||
this.worldPosition = new THREE.Vector3();
|
||||
this.fingerWorldPosition = new THREE.Vector3();
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
this.handEls = document.querySelectorAll('[hand-tracking-controls]');
|
||||
this.pressed = false;
|
||||
this.distance = -1
|
||||
// we throttle by distance, to support scenes with loads of clickable objects (far away)
|
||||
this.tick = this.throttleByDistance( () => this.detectPress() )
|
||||
},
|
||||
throttleByDistance: function(f){
|
||||
return function(){
|
||||
if( this.distance < 0 ) return f() // first call
|
||||
if( !f.tid ){
|
||||
let x = this.distance
|
||||
let y = x*(x*0.05)*1000 // parabolic curve
|
||||
f.tid = setTimeout( function(){
|
||||
f.tid = null
|
||||
f()
|
||||
}, y )
|
||||
}
|
||||
}
|
||||
},
|
||||
detectPress: function(){
|
||||
if( !AFRAME.scenes[0].renderer.xr.isPresenting ) return
|
||||
|
||||
var handEls = this.handEls;
|
||||
var handEl;
|
||||
let minDistance = 5
|
||||
|
||||
// compensate for xrf-get AFRAME component (which references non-reparented buffergeometries from the 3D model)
|
||||
let object3D = this.el.object3D.child || this.el.object3D
|
||||
|
||||
for (var i = 0; i < handEls.length; i++) {
|
||||
handEl = handEls[i];
|
||||
let indexTipPosition = handEl.components['hand-tracking-controls'].indexTipPosition
|
||||
// Apply the relative position to the parent's world position
|
||||
handEl.object3D.updateMatrixWorld();
|
||||
handEl.object3D.getWorldPosition( this.fingerWorldPosition )
|
||||
this.fingerWorldPosition.add( indexTipPosition )
|
||||
|
||||
this.raycaster.far = this.data.pressDistance
|
||||
// Create a direction vector (doesnt matter because it is supershort for 'touch' purposes)
|
||||
const direction = new THREE.Vector3(1.0,0,0);
|
||||
this.raycaster.set(this.fingerWorldPosition, direction)
|
||||
intersects = this.raycaster.intersectObjects([object3D],true)
|
||||
|
||||
object3D.getWorldPosition(this.worldPosition)
|
||||
|
||||
distance = this.fingerWorldPosition.distanceTo(this.worldPosition)
|
||||
minDistance = distance < minDistance ? distance : minDistance
|
||||
|
||||
if (intersects.length ){
|
||||
if( !this.pressed ){
|
||||
this.el.emit('pressedstarted');
|
||||
this.el.emit('click');
|
||||
this.pressed = setTimeout( () => {
|
||||
this.el.emit('pressedended');
|
||||
this.pressed = null
|
||||
},300)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.distance = minDistance
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Touch-to-move-forward controls for mobile.
|
||||
*/
|
||||
|
@ -4404,8 +4491,9 @@ AFRAME.registerComponent('vconsole', {
|
|||
init: function () {
|
||||
//AFRAME.XRF.navigator.to("https://coderofsalvation.github.io/xrsh-media/assets/background.glb")
|
||||
let aScene = AFRAME.scenes[0]
|
||||
|
||||
return
|
||||
|
||||
// return
|
||||
document.head.innerHTML += `
|
||||
<style type="text/css">
|
||||
.vc-panel {
|
||||
|
@ -4550,10 +4638,14 @@ window.AFRAME.registerComponent('xrf-get', {
|
|||
mesh.scale.copy(world.scale)
|
||||
mesh.setRotationFromQuaternion(world.quat);
|
||||
}else{
|
||||
// lets create a dummy add function so that the mesh won't get reparented
|
||||
// lets create a dummy add function so that the mesh won't get reparented during setObject3D
|
||||
// as this would break animations
|
||||
this.el.object3D.add = (a) => a
|
||||
}
|
||||
this.el.setObject3D('mesh',mesh)
|
||||
|
||||
this.el.setObject3D('mesh',mesh) // (doing this.el.object3D = mesh causes AFRAME to crash when resetting scene)
|
||||
this.el.object3D.child = mesh // keep reference (because .children will be empty)
|
||||
|
||||
if( !this.el.id ) this.el.setAttribute("id",`xrf-${mesh.name}`)
|
||||
}else console.warn("xrf-get ignore: "+JSON.stringify(this.data))
|
||||
}, evt && evt.timeout ? evt.timeout: 500)
|
||||
|
@ -4577,6 +4669,34 @@ window.AFRAME.registerComponent('xrf-get', {
|
|||
|
||||
});
|
||||
|
||||
// poor man's way to move forward using hand gesture pinch
|
||||
|
||||
window.AFRAME.registerComponent('xrf-pinchmove', {
|
||||
schema:{
|
||||
rig: {type: "selector"}
|
||||
},
|
||||
init: function(){
|
||||
|
||||
this.el.addEventListener("pinchended", () => {
|
||||
// get the cameras world direction
|
||||
let direction = new THREE.Vector3()
|
||||
this.el.sceneEl.camera.getWorldDirection(direction);
|
||||
// multiply the direction by a "speed" factor
|
||||
direction.multiplyScalar(0.4)
|
||||
// get the current position
|
||||
var pos = player.getAttribute("position")
|
||||
// add the direction vector
|
||||
pos.x += direction.x
|
||||
pos.z += direction.z
|
||||
// set the new position
|
||||
this.data.rig.setAttribute("position", pos);
|
||||
// !!! NOTE - it would be more efficient to do the
|
||||
// position change on the players THREE.Object:
|
||||
// `player.object3D.position.add(direction)`
|
||||
// but it would break "getAttribute("position")
|
||||
})
|
||||
},
|
||||
})
|
||||
window.AFRAME.registerComponent('xrf-wear', {
|
||||
schema:{
|
||||
el: {type:"selector"},
|
||||
|
|
|
@ -1212,6 +1212,9 @@ xrfragment_XRF.prototype = {
|
|||
,validate: function(value) {
|
||||
this.guessType(this,value);
|
||||
var ok = true;
|
||||
if(value.length == 0) {
|
||||
ok = false;
|
||||
}
|
||||
if(!this.is(xrfragment_XRF.T_FLOAT) && this.is(xrfragment_XRF.T_VECTOR2) && !(typeof(this.x) == "number" && typeof(this.y) == "number")) {
|
||||
ok = false;
|
||||
}
|
||||
|
|
|
@ -3094,6 +3094,9 @@ end
|
|||
__xrfragment_XRF.prototype.validate = function(self,value)
|
||||
self:guessType(self, value);
|
||||
local ok = true;
|
||||
if (__lua_lib_luautf8_Utf8.len(value) == 0) then
|
||||
ok = false;
|
||||
end;
|
||||
if ((not self:is(__xrfragment_XRF.T_FLOAT) and self:is(__xrfragment_XRF.T_VECTOR2)) and not (__lua_Boot.__instanceof(self.x, Float) and __lua_Boot.__instanceof(self.y, Float))) then
|
||||
ok = false;
|
||||
end;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,263 @@
|
|||
// reactive component for displaying the menu
|
||||
|
||||
$editorPopup = (el) => new Proxy({
|
||||
|
||||
html: (opts) => `
|
||||
<div>
|
||||
<b>#${$editor.selected.name}</b>
|
||||
<table class="editorPopup">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b class="badge">href</a></td>
|
||||
<td>
|
||||
<input type="text" id="href" placeholder="https://foo.com" maxlength="255" list="objects"
|
||||
onkeydown="document.querySelector('#editActions').classList.add('show')"
|
||||
onkeyup="$editor.selected.edited = $editor.selected.userData.href = this.value"
|
||||
value="${$editor.selected.userData.href||''}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b class="badge">src</a></td>
|
||||
<td>
|
||||
<input type="text" id="src" placeholder="https://foo.com" maxlength="255" list="objects"
|
||||
onkeydown="document.querySelector('#editActions').classList.add('show')"
|
||||
onkeyup="$editor.selected.edited = $editor.selected.userData.src = this.value"
|
||||
value="${$editor.selected.userData.src||''}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b class="badge">tag</a></td>
|
||||
<td>
|
||||
<input type="text" id="tag" placeholder="foo bar" maxlength="255"
|
||||
onkeydown="document.querySelector('#editActions').classList.add('show')"
|
||||
onkeyup="$editor.selected.edited = $editor.selected.userData.tag = this.value"
|
||||
value="${$editor.selected.userData.tag||''}" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<datalist id="objects">
|
||||
<option>https://xrfragment.org/index.glb#pos=start</option>
|
||||
<option>
|
||||
${opts.objectNames.join('</option><option>')}
|
||||
</option>
|
||||
</datalist>
|
||||
<br>
|
||||
<div id="editActions">
|
||||
<button class="download" onclick="$editor.export()"><i class="gg-software-download"></i> download scene file</button>
|
||||
<br>
|
||||
NOTE: updates to src-values will require reloading the scene
|
||||
</div>
|
||||
</div>
|
||||
<style type="text/css">
|
||||
table.editorPopup input{
|
||||
min-width:200px;
|
||||
}
|
||||
table.editorPopup tr td:nth-child(1){
|
||||
text-align:left;
|
||||
}
|
||||
#editActions{
|
||||
visibility:hidden;
|
||||
}
|
||||
#editActions.show{
|
||||
visibility:visible;
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
|
||||
init(opts){
|
||||
el.innerHTML = this.html(opts)
|
||||
return (this.el = el)
|
||||
},
|
||||
|
||||
},{
|
||||
|
||||
get(me,k,v){ return me[k] },
|
||||
|
||||
set(me,k,v){
|
||||
me[k] = v
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
$editor = (el,opts) => new Proxy({
|
||||
|
||||
html: `
|
||||
<div style="position:absolute; width:100%; text-align:right; right:166px;">
|
||||
<button class="btn edit-btn">
|
||||
<i class="gg-pen"></i>
|
||||
</button>
|
||||
</div>
|
||||
<style type="text/css">
|
||||
.xrf button.edit-btn{
|
||||
height: 32px;
|
||||
width: 30px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
.edit-btn.enabled,
|
||||
.edit-btn.enabled:hover{
|
||||
background:black;
|
||||
}
|
||||
.edit-btn i.gg-pen{
|
||||
margin-top: -26px;
|
||||
margin-left: 4px;
|
||||
width: 10px;
|
||||
color: var(--xrf-white);
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
|
||||
selecting: false,
|
||||
editing: false,
|
||||
helper: null,
|
||||
selected: null,
|
||||
objectNames: [],
|
||||
|
||||
init(opts){
|
||||
el.innerHTML = this.html
|
||||
window.frontend.el.querySelector('#topbar').appendChild(el);
|
||||
el.querySelector('.edit-btn').addEventListener('click', () => {
|
||||
if( $editor.selecting || $editor.editing ) this.reset()
|
||||
else{
|
||||
$editor.selecting = true
|
||||
$editor.editing = false
|
||||
}
|
||||
})
|
||||
|
||||
xrf.addEventListener('export', (e) => this.updateOriginalScene(e) )
|
||||
xrf.addEventListener('href', (opts) => {
|
||||
if( $editor.selecting || $editor.editing ) return opts.promise().reject("$editor should block hrefs while editing") // never resolve (block hrefs from interfering)
|
||||
})
|
||||
return this
|
||||
},
|
||||
|
||||
reset(){
|
||||
if( this.helper) xrf.scene.remove(this.helper)
|
||||
$editor.selecting = false
|
||||
$editor.editing = false
|
||||
},
|
||||
|
||||
export(){
|
||||
window.frontend.download()
|
||||
this.reset()
|
||||
},
|
||||
|
||||
editNode(){
|
||||
if( !this.selecting ) return console.log("not editing")
|
||||
this.reset()
|
||||
$editor.editing = true
|
||||
this.collectObjects()
|
||||
//`<b>XR Fragment:</b> #${this.selected.name}<br><br>${this.getMetaData(this.selected)}`),{
|
||||
notify( $editorPopup( document.createElement('div') ).init(this) , {
|
||||
timeout:false,
|
||||
onclose: () => this.reset()
|
||||
})
|
||||
},
|
||||
|
||||
collectObjects(){
|
||||
this.objectNames = []
|
||||
const escape = (str) => {
|
||||
let d = document.createElement('div')
|
||||
d.innerText = str
|
||||
return d.innerHTML
|
||||
}
|
||||
xrf.scene.traverse( (n) => {
|
||||
if( n.userData && n.userData.href ){
|
||||
this.objectNames.push( escape(n.userData.href) )
|
||||
}
|
||||
})
|
||||
xrf.scene.traverse( (n) => {
|
||||
if( n.name ) this.objectNames.push( escape('#'+n.name) )
|
||||
})
|
||||
},
|
||||
|
||||
initEdit(scene){
|
||||
if( !this.listenersInstalled ){
|
||||
AFRAME.scenes[0].addEventListener('click', () => this.editNode() )
|
||||
this.listenersInstalled = true
|
||||
}
|
||||
scene.traverse( (n) => {
|
||||
let highlight = (n) => (e) => {
|
||||
if( !this.selecting || this.editing ) return // do nothing
|
||||
if( this.helper){
|
||||
if( this.helper.selected == n.uuid ) return // already selected
|
||||
xrf.scene.remove(this.helper)
|
||||
}
|
||||
|
||||
this.selected = n
|
||||
this.helper = new THREE.BoxHelper( n, 0xFF00FF )
|
||||
this.helper.material.linewidth = 4
|
||||
this.helper.material.color = xrf.focusLine.material.color
|
||||
this.helper.material.dashSize = xrf.focusLine.material.dashSize
|
||||
this.helper.material.gapSize = xrf.focusLine.material.gapSize
|
||||
this.helper.selected = n.uuid
|
||||
xrf.scene.add(this.helper)
|
||||
|
||||
let div = document.createElement('div')
|
||||
notify(`<b>#${n.name}</b><br>${this.getMetaData(this.selected)}`)
|
||||
|
||||
}
|
||||
if( n.material ) n.addEventListener('mousemove', n.highlightOnMouseMove = highlight(n) )
|
||||
})
|
||||
},
|
||||
|
||||
getMetaData(n){
|
||||
let html = `${n.userData.href ? `<b class="badge">href</b>${n.userData.href}<br>`:''}`
|
||||
html += `${n.userData.src ? `<b class="badge">src</b>${n.userData.src}<br>` :''}`
|
||||
html += `${n.userData.tag ? `<b class="badge">tag</b>${n.userData.tag}<br>` :''}`
|
||||
return html
|
||||
},
|
||||
|
||||
updateOriginalScene(e){
|
||||
const {scene,ext} = e
|
||||
scene.traverse( (n) => {
|
||||
if( !n.name ) return
|
||||
// overwrite node with modified userData from scene
|
||||
let o = xrf.scene.getObjectByName(n.name)
|
||||
if( o && o.edited ){
|
||||
for( let i in o.userData ) n.userData[i] = o.userData[i]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
|
||||
get(me,k,v){ return me[k] },
|
||||
|
||||
set(me,k,v){
|
||||
me[k] = v
|
||||
|
||||
switch( k ){
|
||||
|
||||
case "selecting":{
|
||||
lookctl = $('[look-controls]').components['look-controls']
|
||||
if( v ){
|
||||
lookctl.pause() // prevent click-conflict
|
||||
notify("click an object to reveal XR Fragment metadata")
|
||||
xrf.interactive.raycastAll = true
|
||||
me.initEdit(xrf.scene)
|
||||
lookctl.pause() // prevent click-conflict
|
||||
el.querySelector('.edit-btn').classList.add(['enabled'])
|
||||
}else{
|
||||
lookctl.pause() // prevent click-conflict
|
||||
xrf.scene.traverse( (n) => {
|
||||
if( n.highlightOnMouseMove ){
|
||||
n.removeEventListener( 'mousemove', n.highlightOnMouseMove )
|
||||
}
|
||||
})
|
||||
lookctl.play() // prevent click-conflict (resume)
|
||||
el.querySelector('.edit-btn').classList.remove(['enabled'])
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
// reactify component!
|
||||
document.addEventListener('frontend:ready', (e) => {
|
||||
window.$editor = $editor( document.createElement('div') ).init(e.detail)
|
||||
})
|
|
@ -0,0 +1,826 @@
|
|||
document.head.innerHTML += `
|
||||
<style type="text/css">
|
||||
:root {
|
||||
--xrf-primary: #6839dc;
|
||||
--xrf-primary-fg: #FFF;
|
||||
--xrf-light-primary: #ea23cf;
|
||||
--xrf-secondary: #872eff;
|
||||
--xrf-light-xrf-secondary: #ce7df2;
|
||||
--xrf-topbar-bg: #fffb;
|
||||
--xrf-box-shadow: #0005;
|
||||
--xrf-red: red;
|
||||
--xrf-dark-gray: #343334;
|
||||
--xrf-gray: #424280;
|
||||
--xrf-white: #fdfdfd;
|
||||
--xrf-light-gray: #efefef;
|
||||
--xrf-lighter-gray: #e4e2fb96;
|
||||
--xrf-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
|
||||
--xrf-font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
|
||||
--xrf-font-size-0: 12px;
|
||||
--xrf-font-size-1: 14px;
|
||||
--xrf-font-size-2: 17px;
|
||||
--xrf-font-size-3: 21px;
|
||||
}
|
||||
|
||||
/* CSS reset */
|
||||
html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type="button"],[type="reset"],[type="submit"],button{-webkit-appearance:button}[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
|
||||
|
||||
.xrf table tr td{
|
||||
vertical-align:top;
|
||||
}
|
||||
.xrf button,
|
||||
.xrf input[type="submit"],
|
||||
.xrf .btn {
|
||||
text-decoration:none;
|
||||
background: var(--xrf-primary);
|
||||
border: 0;
|
||||
border-radius: 25px;
|
||||
padding: 11px 15px;
|
||||
font-weight: bold;
|
||||
transition: 0.3s;
|
||||
height: 40px;
|
||||
font-size: var(--xrf-font-size-1);
|
||||
color: var(--xrf-primary-fg);
|
||||
line-height: var(--xrf-font-size-1);
|
||||
cursor:pointer;
|
||||
white-space:pre;
|
||||
min-width: 45px;
|
||||
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
.xrf button:hover,
|
||||
.xrf input[type="submit"]:hover,
|
||||
.xrf .btn:hover {
|
||||
background: var(--xrf-secondary);
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
.xrf, .xrf *{
|
||||
font-family: var(--xrf-font-sans-serif);
|
||||
font-size: var(--xrf-font-size-1);
|
||||
line-height:27px;
|
||||
}
|
||||
|
||||
textarea, select, input[type="text"] {
|
||||
background: transparent; /* linear-gradient( var(--xrf-lighter-gray), var(--xrf-gray) ) !important; */
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
color: var(--xrf-light-gray);
|
||||
}
|
||||
|
||||
input[type=text]{
|
||||
padding:7px 15px;
|
||||
}
|
||||
input{
|
||||
border-radius:7px;
|
||||
margin:5px 0px;
|
||||
}
|
||||
|
||||
.title {
|
||||
border-bottom: 2px solid var(--xrf-secondary);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#topbar{
|
||||
background: var(--xrf-topbar-bg);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
|
||||
opacity: 0.9;
|
||||
z-index:2000;
|
||||
display:none;
|
||||
}
|
||||
|
||||
#topbar .logo{
|
||||
width: 92px;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 93px;
|
||||
height: 30px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#topbar > input[type="submit"] {
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 2px;
|
||||
min-width:135px;
|
||||
}
|
||||
|
||||
#topbar > button#navback,
|
||||
#topbar > button#navforward {
|
||||
height: 32px;
|
||||
font-size: 24px;
|
||||
position: absolute;
|
||||
left: 9px;
|
||||
padding: 2px 13px;
|
||||
border-radius:6px;
|
||||
top: 8px;
|
||||
color: var(--xrf-light-gray);
|
||||
width: 36px;
|
||||
min-width: unset;
|
||||
}
|
||||
#topbar > button#navforward {
|
||||
left:49px;
|
||||
}
|
||||
|
||||
#topbar > #uri {
|
||||
height: 18px;
|
||||
font-size: var(--xrf-font-size-3);
|
||||
position: absolute;
|
||||
left: 200px;
|
||||
top: 9px;
|
||||
max-width: 550px;
|
||||
padding: 5px 0px 5px 5px;
|
||||
width: calc( 63% - 200px);
|
||||
background: #f0f0f0;
|
||||
border-color: #Ccc;
|
||||
border: 2px solid #CCC;
|
||||
border-radius: 7px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.footer > .menu .btn{
|
||||
display:inline-block;
|
||||
background: var(--xrf-primary);
|
||||
border-radius: 25px;
|
||||
border: 0;
|
||||
padding: 5px 19px;
|
||||
font-weight: 1000;
|
||||
font-family: sans-serif;
|
||||
font-size: var(--xrf-font-size-2);
|
||||
color:var(--xrf-primary-fg);
|
||||
height:33px;
|
||||
z-index:2000;
|
||||
cursor:pointer;
|
||||
min-width:145px;
|
||||
text-decoration:none;
|
||||
margin-top: 15px;
|
||||
line-height:36px;
|
||||
margin-right:10px;
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.xrf a.btn#more{
|
||||
z-index:3000;
|
||||
width: 19px;
|
||||
min-width: 19px;
|
||||
font-size:16px;
|
||||
text-align: center;
|
||||
background:white;
|
||||
color: var(--xrf-primary);
|
||||
}
|
||||
.xrf a.btn#more i.gg-menu{
|
||||
margin-top:15px;
|
||||
}
|
||||
.xrf a.btn#more i.gg-close,
|
||||
.xrf a.btn#more i.gg-menu{
|
||||
color:#888;
|
||||
}
|
||||
.xrf a.btn#meeting i.gg-user-add{
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.xrf a.btn#share i.gg-link{
|
||||
margin-right:24px;
|
||||
}
|
||||
|
||||
.xrf a.btn#accessibility i.gg-yinyang{
|
||||
margin-right:13px;
|
||||
}
|
||||
|
||||
html{
|
||||
max-width:unset;
|
||||
}
|
||||
|
||||
.render {
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
}
|
||||
|
||||
.lil-gui.autoPlace{
|
||||
right:0px !important;
|
||||
top:48px !important;
|
||||
height:33vh;
|
||||
}
|
||||
|
||||
#VRButton {
|
||||
margin-bottom:20vh;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
#uri{ display:none; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.lil-gui.root{
|
||||
top:auto !important;
|
||||
left:auto !important;
|
||||
}
|
||||
.js-snackbar__message{
|
||||
overflow-y:auto;
|
||||
max-height:600px;
|
||||
}
|
||||
.js-snackbar__message h1,h2,h3{
|
||||
font-size:22px;
|
||||
}
|
||||
.xrf table tr td {
|
||||
|
||||
}
|
||||
:root{
|
||||
--xrf-font-size-1: 13px;
|
||||
--xrf-font-size-2: 17px;
|
||||
--xrf-font-size-3: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.a-enter-vr-button, .a-enter-ar-button{
|
||||
height:41px;
|
||||
}
|
||||
|
||||
#qrcode{
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
height: 121px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input#share{
|
||||
font-size: var(--xrf-font-size-1);
|
||||
font-family: var(--xrf-font-monospace);
|
||||
border:2px solid #AAA;
|
||||
width:50vw;
|
||||
max-width:400px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
z-index:1000;
|
||||
display: flex;
|
||||
flex-direction: column-reverse; /* This reverses the stacking order of the flex container */
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 71px;
|
||||
right: 11px;
|
||||
bottom: 0;
|
||||
padding-bottom:140px;
|
||||
box-sizing:border-box;
|
||||
pointer-events:none;
|
||||
}
|
||||
.footer *{
|
||||
pointer-events:all;
|
||||
}
|
||||
.footer .menu{
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
.badge,
|
||||
#messages .msg.ui div.badge{
|
||||
box-sizing:border-box;
|
||||
display:inline-block;
|
||||
color: var(--xrf-white);
|
||||
font-weight: bold;
|
||||
background: var(--xrf-dark-gray);
|
||||
border-radius:16px;
|
||||
padding:0px 12px;
|
||||
font-size: var(--xrf-font-size-0);
|
||||
margin-right:10px;
|
||||
text-decoration:none !important;
|
||||
}
|
||||
#messages .msg.ui div.badge a{
|
||||
color:#FFF;
|
||||
}
|
||||
|
||||
.ruler{
|
||||
width:97%;
|
||||
margin:7px 0px;
|
||||
}
|
||||
|
||||
|
||||
a.badge {
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
.xrf select{
|
||||
border-inline: none;
|
||||
border-inline: none;
|
||||
border-block: none;
|
||||
border: 3px solid var(--xrf-primary);
|
||||
border-radius: 5px;
|
||||
background: none;
|
||||
border-radius:30px;
|
||||
}
|
||||
.xrf select,
|
||||
.xrf option{
|
||||
padding: 0px 16px;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.xrf input{
|
||||
border-radius:30px;
|
||||
padding: 7px 15px;
|
||||
border-block: none;
|
||||
border-inline: none;
|
||||
border: 1px solid #888;
|
||||
background: transparent;
|
||||
max-width:105px;
|
||||
}
|
||||
|
||||
.xrf table tr td {
|
||||
vertical-align:middle;
|
||||
text-align:right;
|
||||
}
|
||||
.xrf table tr td:nth-child(1){
|
||||
min-width:82px;
|
||||
height:40px;
|
||||
padding-right:15px;
|
||||
}
|
||||
|
||||
.xrf small{
|
||||
font-size: var(--xrf-font-size-0);
|
||||
}
|
||||
.disabled{
|
||||
opacity:0.5
|
||||
}
|
||||
|
||||
body.menu .js-snackbar__wrapper {
|
||||
top: 64px;
|
||||
}
|
||||
|
||||
.transcript{
|
||||
max-height:105px;
|
||||
max-width:405px;
|
||||
overflow-y:auto;
|
||||
border: 1px solid var(--xrf-gray);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.right { float:right }
|
||||
.left { float:left }
|
||||
|
||||
/*
|
||||
* tabs
|
||||
*/
|
||||
div.tab-frame > input{ display:none;}
|
||||
div.tab-frame > label{ display:block; float:left;padding:5px 10px; cursor:pointer; }
|
||||
div.tab-frame > input:checked + label{ cursor:default; border-bottom:1px solid #888; font-weight:bold; }
|
||||
div.tab-frame > div.tab{ display:none; padding:15px 10px 5px 10px;clear:left}
|
||||
|
||||
div.tab-frame > input:nth-of-type(1):checked ~ .tab:nth-of-type(1),
|
||||
div.tab-frame > input:nth-of-type(2):checked ~ .tab:nth-of-type(2),
|
||||
div.tab-frame > input:nth-of-type(3):checked ~ .tab:nth-of-type(3){ display:block;}
|
||||
|
||||
|
||||
/*
|
||||
* css icons from https://css.gg
|
||||
*/
|
||||
|
||||
.gg-close-o {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
transform: scale(var(--ggs,1));
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid;
|
||||
border-radius: 40px
|
||||
}
|
||||
.gg-close-o::after,
|
||||
.gg-close-o::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 5px;
|
||||
top: 8px;
|
||||
left: 3px
|
||||
}
|
||||
.gg-close-o::after {
|
||||
transform: rotate(-45deg)
|
||||
}
|
||||
|
||||
.gg-user-add {
|
||||
display: inline-block;
|
||||
transform: scale(var(--ggs,1));
|
||||
box-sizing: border-box;
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
background:
|
||||
linear-gradient(
|
||||
to left,
|
||||
currentColor 8px,
|
||||
transparent 0)
|
||||
no-repeat 14px 6px/6px 2px,
|
||||
linear-gradient(
|
||||
to left,
|
||||
currentColor 8px,
|
||||
transparent 0)
|
||||
no-repeat 16px 4px/2px 6px
|
||||
}
|
||||
.gg-user-add::after,.gg-user-add::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border: 2px solid
|
||||
}
|
||||
.gg-user-add::before {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 30px;
|
||||
top: 0;
|
||||
left: 2px
|
||||
}
|
||||
.gg-user-add::after {
|
||||
width: 12px;
|
||||
height: 9px;
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
top: 9px
|
||||
}
|
||||
|
||||
.gg-user {
|
||||
display: inline-block;
|
||||
transform: scale(var(--ggs,1));
|
||||
box-sizing: border-box;
|
||||
width: 12px;
|
||||
height: 18px
|
||||
}
|
||||
.gg-user::after,
|
||||
.gg-user::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border: 2px solid
|
||||
}
|
||||
.gg-user::before {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 30px;
|
||||
top: 0;
|
||||
left: 2px
|
||||
}
|
||||
.gg-user::after {
|
||||
width: 12px;
|
||||
height: 9px;
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
top: 9px
|
||||
}
|
||||
|
||||
.gg-menu {
|
||||
transform: scale(var(--ggs,1))
|
||||
}
|
||||
.gg-menu,
|
||||
.gg-menu::after,
|
||||
.gg-menu::before {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
border-radius: 3px;
|
||||
background: currentColor
|
||||
}
|
||||
.gg-menu::after,
|
||||
.gg-menu::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -6px
|
||||
}
|
||||
.gg-menu::after {
|
||||
top: 6px
|
||||
}
|
||||
|
||||
.gg-close {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
transform: scale(var(--ggs,1)) scale(var(--ggs,1)) translate(-2px,5px);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 40px
|
||||
}
|
||||
.gg-close::after,
|
||||
.gg-close::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 5px;
|
||||
top: 8px;
|
||||
left: 1px
|
||||
}
|
||||
.gg-close::after {
|
||||
transform: rotate(-45deg)
|
||||
}
|
||||
|
||||
.gg-link {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
-moz-transform: rotate(-45deg) scale(var(--ggs,1));
|
||||
transform: translate(4px,-5px) rotate(-45deg) scale(var(--ggs,1));
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
line-height:11px;
|
||||
border-radius: 4px
|
||||
}
|
||||
.gg-link::after,
|
||||
.gg-link::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
border: 2px solid;
|
||||
top: -4px
|
||||
}
|
||||
.gg-link::before {
|
||||
border-right: 0;
|
||||
border-top-left-radius: 40px;
|
||||
border-bottom-left-radius: 40px;
|
||||
left: -6px
|
||||
}
|
||||
.gg-link::after {
|
||||
border-left: 0;
|
||||
border-top-right-radius: 40px;
|
||||
border-bottom-right-radius: 40px;
|
||||
right: -6px
|
||||
}
|
||||
|
||||
.gg-info {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transform: scale(var(--ggs,1)) translate(-3px, 3px);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid;
|
||||
border-radius: 40px
|
||||
}
|
||||
.gg-info::after,
|
||||
.gg-info::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
width: 2px;
|
||||
background: currentColor;
|
||||
left: 7px
|
||||
}
|
||||
.gg-info::after {
|
||||
bottom: 2px;
|
||||
height: 8px
|
||||
}
|
||||
.gg-info::before {
|
||||
height: 2px;
|
||||
top: 2px
|
||||
}
|
||||
|
||||
.gg-yinyang {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transform: rotate(95deg) scale(var(--ggs,1)) translate(4px,4px);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid;
|
||||
border-radius: 22px
|
||||
}
|
||||
.gg-yinyang::after,
|
||||
.gg-yinyang::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 10px;
|
||||
top: 4px
|
||||
}
|
||||
.gg-yinyang::before {
|
||||
border: 2px solid;
|
||||
left: 0
|
||||
}
|
||||
.gg-yinyang::after {
|
||||
border: 2px solid transparent;
|
||||
right: 0;
|
||||
box-shadow:
|
||||
inset 0 0 0 4px,
|
||||
0 -3px 0 1px,
|
||||
-2px -4px 0 1px,
|
||||
-8px -5px 0 -1px,
|
||||
-11px -3px 0 -2px,
|
||||
-12px -1px 0 -3px,
|
||||
-6px -6px 0 -1px
|
||||
}
|
||||
|
||||
.gg-image {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transform: scale(var(--ggs,1)) translate(1px,2px);
|
||||
width: 20px;
|
||||
height: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 2px;
|
||||
border-radius: 2px
|
||||
}
|
||||
.gg-image::after,
|
||||
.gg-image::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border: 2px solid
|
||||
}
|
||||
.gg-image::after {
|
||||
transform: rotate(45deg);
|
||||
border-radius: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 9px;
|
||||
left: 6px
|
||||
}
|
||||
.gg-image::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
top: 2px;
|
||||
left: 2px
|
||||
}
|
||||
.gg-serverless {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transform: scale(var(--ggs,1)) translate(2px,1px);
|
||||
width: 15px;
|
||||
height: 13px;
|
||||
overflow: hidden
|
||||
}
|
||||
.gg-serverless::after,
|
||||
.gg-serverless::before {
|
||||
background: currentColor;
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 3px;
|
||||
box-shadow: 0 5px 0,0 10px 0;
|
||||
transform: skew(-20deg)
|
||||
}
|
||||
.gg-serverless::before {
|
||||
width: 8px;
|
||||
left: -2px
|
||||
}
|
||||
.gg-serverless::after {
|
||||
width: 12px;
|
||||
right: -5px
|
||||
}
|
||||
.gg-software-download {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transform: scale(var(--ggs,1)) translate(3px,3px);
|
||||
width: 16px;
|
||||
height: 6px;
|
||||
border: 2px solid;
|
||||
border-top: 0;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
line-height:15px;
|
||||
}
|
||||
.gg-software-download::after {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-left: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
transform: rotate(-45deg);
|
||||
left: 2px;
|
||||
bottom: 4px
|
||||
}
|
||||
.gg-software-download::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
width: 2px;
|
||||
height: 10px;
|
||||
background: currentColor;
|
||||
left: 5px;
|
||||
bottom: 5px
|
||||
}
|
||||
.gg-arrow-left-r {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid;
|
||||
transform: scale(var(--ggs,1));
|
||||
border-radius: 4px
|
||||
}
|
||||
.gg-arrow-left-r::after,
|
||||
.gg-arrow-left-r::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 4px
|
||||
}
|
||||
.gg-arrow-left-r::after {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-bottom: 2px solid;
|
||||
border-left: 2px solid;
|
||||
transform: rotate(45deg);
|
||||
bottom: 6px
|
||||
}
|
||||
.gg-arrow-left-r::before {
|
||||
width: 10px;
|
||||
height: 2px;
|
||||
bottom: 8px;
|
||||
background: currentColor
|
||||
}
|
||||
|
||||
.gg-pen {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
transform: rotate(-45deg) scale(var(--ggs,1));
|
||||
width: 14px;
|
||||
height: 4px;
|
||||
border-right: 2px solid transparent;
|
||||
box-shadow:
|
||||
0 0 0 2px,
|
||||
inset -2px 0 0;
|
||||
border-top-right-radius: 1px;
|
||||
border-bottom-right-radius: 1px;
|
||||
margin-right: -2px
|
||||
}
|
||||
.gg-pen::after,
|
||||
.gg-pen::before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute
|
||||
}
|
||||
.gg-pen::before {
|
||||
background: currentColor;
|
||||
border-left: 0;
|
||||
right: -6px;
|
||||
width: 3px;
|
||||
height: 4px;
|
||||
border-radius: 1px;
|
||||
top: 0
|
||||
}
|
||||
.gg-pen::after {
|
||||
width: 8px;
|
||||
height: 7px;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-right: 7px solid;
|
||||
left: -11px;
|
||||
top: -2px
|
||||
}
|
||||
</style>
|
||||
`
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,800 @@
|
|||
(function(){
|
||||
// this orchestrates multiplayer events from the scene graph
|
||||
|
||||
window.network = (opts) => new Proxy({
|
||||
|
||||
connected: false,
|
||||
pos: '',
|
||||
posName: '',
|
||||
meetinglink: "",
|
||||
peers: {},
|
||||
plugin: {},
|
||||
opts,
|
||||
|
||||
init(){
|
||||
document.addEventListener('network.disconnect', () => this.connected = false )
|
||||
document.addEventListener('network.connected', () => this.connected = true )
|
||||
setTimeout( () => window.frontend.emit('network.init'), 100 )
|
||||
return this
|
||||
},
|
||||
|
||||
connect(opts){
|
||||
window.frontend.emit(`network.${this.connected?'disconnect':'connect'}`,opts)
|
||||
},
|
||||
|
||||
add(peerid,data){
|
||||
data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
|
||||
this.peers[peerid] = data
|
||||
window.frontend.emit(`network.peer.add`,{peer})
|
||||
},
|
||||
|
||||
remove(peerid,data){
|
||||
delete this.peers[peerid]
|
||||
window.frontend.emit(`network.peer.remove`,{peer})
|
||||
},
|
||||
|
||||
send(opts){
|
||||
window.frontend.emit('network.send',opts)
|
||||
},
|
||||
|
||||
receive(opts){
|
||||
|
||||
},
|
||||
|
||||
getMeetingFromUrl(url){
|
||||
let hash = url.replace(/.*#/,'')
|
||||
let parts = hash.split("&")
|
||||
let meeting = ''
|
||||
parts.map( (p) => {
|
||||
if( p.split("=")[0] == 'meet' ) meeting = p.split("=")[1]
|
||||
})
|
||||
return meeting
|
||||
},
|
||||
|
||||
randomRoom(){
|
||||
var names = []
|
||||
let add = (s) => s.length < 6 && !s.match(/[0-9$]/) && !s.match(/_/) ? names.push(s) : false
|
||||
for ( var i in window ) add(i)
|
||||
for ( var i in Object.prototype ) add(i)
|
||||
for ( var i in Function.prototype ) add(i)
|
||||
for ( var i in Array.prototype ) add(i)
|
||||
for ( var i in String.prototype ) add(i)
|
||||
var a = names[Math.floor(Math.random() * names.length)];
|
||||
var b = names[Math.floor(Math.random() * names.length)];
|
||||
return String(`${a}-${b}-${String(Math.random()).substr(13)}`).toLowerCase()
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
// auto-trigger events on changes
|
||||
get(data,k,receiver){ return data[k] },
|
||||
set(data,k,v){
|
||||
let from = data[k]
|
||||
data[k] = v
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('frontend:ready', (e) => {
|
||||
window.network = network(e.detail).init()
|
||||
document.dispatchEvent( new CustomEvent("network:ready", e ) )
|
||||
})
|
||||
connectionsComponent = {
|
||||
|
||||
html: `
|
||||
<div id="connections">
|
||||
<i class="gg-close-o" id="close" onclick="$connections.visible = false"></i>
|
||||
<br>
|
||||
<div class="tab-frame">
|
||||
<input type="radio" name="tab" id="login" checked>
|
||||
<label for="login">login</label>
|
||||
|
||||
<input type="radio" name="tab" id="io">
|
||||
<label for="io">devices</label>
|
||||
|
||||
<input type="radio" name="tab" id="networks">
|
||||
<label for="networks">advanced</label>
|
||||
|
||||
<div class="tab">
|
||||
<div id="settings"></div>
|
||||
<table>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<div id="devices">
|
||||
<a class="badge ruler">Webcam and/or Audio</a>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Video</td>
|
||||
<td>
|
||||
<select id="videoInput"></select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mic</td>
|
||||
<td>
|
||||
<select id="audioInput"></select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="display:none"> <!-- not used (for now) -->
|
||||
<td>Audio</td>
|
||||
<td>
|
||||
<select id="audioOutput"></select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<div id="networking">
|
||||
Networking a la carte:<br>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Webcam</td>
|
||||
<td>
|
||||
<select id="webcam"></select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Chat</td>
|
||||
<td>
|
||||
<select id="chatnetwork"></select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>World sync</td>
|
||||
<td>
|
||||
<select id="scene"></select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
init: (el) => new Proxy({
|
||||
|
||||
visible: true,
|
||||
|
||||
webcam: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||
chatnetwork: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||
scene: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||
|
||||
selectedWebcam: '',
|
||||
selectedChatnetwork:'',
|
||||
selectedScene: '',
|
||||
|
||||
$webcam: $webcam = el.querySelector("#webcam"),
|
||||
$chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"),
|
||||
$scene: $scene = el.querySelector("#scene"),
|
||||
$settings: $settings = el.querySelector("#settings"),
|
||||
$devices: $devices = el.querySelector("#devices"),
|
||||
$connect: $connect = el.querySelector("#connect"),
|
||||
$networking: $networking = el.querySelector("#networking"),
|
||||
|
||||
$audioInput: el.querySelector('select#audioInput'),
|
||||
$audioOutput: el.querySelector('select#audioOutput'),
|
||||
$videoInput: el.querySelector('select#videoInput'),
|
||||
|
||||
install(opts){
|
||||
this.opts = opts;
|
||||
(['change']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
|
||||
this.reactToNetwork()
|
||||
$menu.buttons = ([
|
||||
`<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i> connect</a><br>`
|
||||
]).concat($menu.buttons)
|
||||
|
||||
if( document.location.href.match(/meet=/) ) this.show()
|
||||
|
||||
setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
|
||||
},
|
||||
|
||||
toggle(){
|
||||
$chat.visible = !$chat.visible
|
||||
},
|
||||
|
||||
change(id,e){
|
||||
if( id.match(/^(webcam|chatnetwork|scene)$/) ){
|
||||
this.renderSettings() // trigger this when 'change' event fires on children dom elements
|
||||
}
|
||||
},
|
||||
|
||||
show(opts){
|
||||
opts = opts || {}
|
||||
if( opts.hide ){
|
||||
if( el.parentElement ) el.parentElement.parentElement.style.display = 'none' // hide along with wrapper elements
|
||||
if( !opts.showChat ) $chat.visible = false
|
||||
}else{
|
||||
$chat.visible = true
|
||||
this.visible = true
|
||||
// hide networking settings if entering thru meetinglink
|
||||
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
|
||||
if( !network.connected ){
|
||||
document.querySelector('body > .xrf').appendChild(el)
|
||||
$chat.send({message:"", el, class:['ui']})
|
||||
if( !network.meetinglink ){ // set default
|
||||
$webcam.value = opts.webcam || 'Peer2Peer'
|
||||
$chatnetwork.value = opts.chatnetwork || 'Peer2Peer'
|
||||
$scene.value = opts.scene || 'Peer2Peer'
|
||||
}
|
||||
this.renderSettings()
|
||||
}else{
|
||||
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
update(){
|
||||
this.selectedWebcam = $webcam.value
|
||||
this.selectedChatnetwork = $chatnetwork.value
|
||||
this.selectedScene = $scene.value
|
||||
},
|
||||
|
||||
forSelectedPluginsDo(cb){
|
||||
// this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations
|
||||
let plugins = {}
|
||||
let select = (name) => (o) => o.profile.name == name ? plugins[ o.profile.name ] = o : ''
|
||||
this.webcam.find( select(this.selectedWebcam) )
|
||||
this.chatnetwork.find( select(this.selectedChatnetwork) )
|
||||
this.scene.find( select(this.selectedScene) )
|
||||
for( let i in plugins ){
|
||||
try{ cb(plugins[i]) }catch(e){ console.error(e) }
|
||||
}
|
||||
},
|
||||
|
||||
renderSettings(){
|
||||
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
|
||||
this.update()
|
||||
$settings.innerHTML = ''
|
||||
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config({...opts,plugin}) ) )
|
||||
this.renderInputs()
|
||||
},
|
||||
|
||||
renderInputs(){
|
||||
if( !this.selectedWebcam || this.selectedWebcam == 'No thanks' ){
|
||||
return this.$devices.style.display = 'none'
|
||||
}else this.$devices.style.display = ''
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: true
|
||||
})
|
||||
.then( () => {
|
||||
|
||||
const selectors = [this.$audioInput, this.$audioOutput, this.$videoInput];
|
||||
|
||||
const gotDevices = (deviceInfos) => {
|
||||
// Handles being called several times to update labels. Preserve values.
|
||||
const values = selectors.map(select => select.value);
|
||||
selectors.forEach(select => {
|
||||
while (select.firstChild) {
|
||||
select.removeChild(select.firstChild);
|
||||
}
|
||||
});
|
||||
for (let i = 0; i !== deviceInfos.length; ++i) {
|
||||
const deviceInfo = deviceInfos[i];
|
||||
const option = document.createElement('option');
|
||||
option.value = deviceInfo.deviceId;
|
||||
if (deviceInfo.kind === 'audioinput') {
|
||||
option.text = deviceInfo.label || `microphone ${this.$audioInput.length + 1}`;
|
||||
this.$audioInput.appendChild(option);
|
||||
} else if (deviceInfo.kind === 'audiooutput') {
|
||||
option.text = deviceInfo.label || `speaker ${this.$audioOutput.length + 1}`;
|
||||
this.$audioOutput.appendChild(option);
|
||||
} else if (deviceInfo.kind === 'videoinput') {
|
||||
option.text = deviceInfo.label || `camera this.${this.$videoInput.length + 1}`;
|
||||
this.$videoInput.appendChild(option);
|
||||
} else {
|
||||
console.log('Some other kind of source/device: ', deviceInfo);
|
||||
}
|
||||
}
|
||||
// hide if there's nothing to choose
|
||||
let totalDevices = this.$audioInput.options.length + this.$audioOutput.options.length + this.$videoInput.options.length
|
||||
this.$devices.style.display = totalDevices > 3 ? 'block' : 'none'
|
||||
|
||||
selectors.forEach((select, selectorIndex) => {
|
||||
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
|
||||
select.value = values[selectorIndex];
|
||||
}
|
||||
});
|
||||
}
|
||||
// after getUserMedia we can enumerate
|
||||
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.warn);
|
||||
})
|
||||
},
|
||||
|
||||
reactToNetwork(){ // *TODO* move to network?
|
||||
|
||||
document.addEventListener('network.connect', () => {
|
||||
this.show({hide:true, showChat: true})
|
||||
})
|
||||
document.addEventListener('network.disconnect', () => {
|
||||
this.connected = false
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
},{
|
||||
|
||||
get(data,k,v){ return data[k] },
|
||||
set(data,k,v){
|
||||
data[k] = v
|
||||
switch( k ){
|
||||
case "visible": el.style.display = v ? '' : 'none'; break;
|
||||
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
||||
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
||||
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
||||
case "selectedScene": $scene.value = v; data.renderSettings(); break;
|
||||
case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
|
||||
case "selectedWebcam": {
|
||||
$webcam.value = v;
|
||||
data.renderSettings();
|
||||
$devices.style.display = v ? 'block' : 'none'
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// reactify component!
|
||||
document.addEventListener('$menu:ready', (opts) => {
|
||||
opts = opts.detail
|
||||
document.head.innerHTML += connectionsComponent.css
|
||||
window.$connections = document.createElement('div')
|
||||
$connections.innerHTML = connectionsComponent.html
|
||||
$connections = connectionsComponent.init($connections)
|
||||
$connections.install(opts)
|
||||
})
|
||||
|
||||
// alpine component for displaying meetings
|
||||
|
||||
connectionsComponent.css = `
|
||||
<style type="text/css">
|
||||
button#connect{
|
||||
height: 43px;
|
||||
width:100%;
|
||||
margin: 0px;
|
||||
}
|
||||
#messages .msg #connections{
|
||||
position:relative;
|
||||
}
|
||||
.connecthide {
|
||||
transform:translateY(-1000px);
|
||||
}
|
||||
#close{
|
||||
display: block;
|
||||
position: relative;
|
||||
float: right;
|
||||
top: 16px;
|
||||
}
|
||||
#messages .msg.ui div.tab-frame > div.tab{ padding:25px 10px 5px 10px;}
|
||||
</style>`
|
||||
chatComponent = {
|
||||
|
||||
html: `
|
||||
<div id="chat">
|
||||
<div id="videos" style="pointer-events:none"></div>
|
||||
<div id="messages" aria-live="assertive" aria-relevant></div>
|
||||
<div id="chatfooter">
|
||||
<div id="chatbar">
|
||||
<input id="chatline" type="text" placeholder="chat here"></input>
|
||||
</div>
|
||||
<button id="showchat" class="btn">show chat</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
init: (el) => new Proxy({
|
||||
|
||||
scene: null,
|
||||
visible: true,
|
||||
messages: [],
|
||||
oneMessagePerUser: false,
|
||||
|
||||
username: '', // configured by 'network.connected' event
|
||||
|
||||
$videos: el.querySelector("#videos"),
|
||||
$messages: el.querySelector("#messages"),
|
||||
$chatline: el.querySelector("#chatline"),
|
||||
$chatbar: el.querySelector("#chatbar"),
|
||||
|
||||
install(opts){
|
||||
this.opts = opts
|
||||
this.scene = opts.scene
|
||||
this.$chatbar.style.display = 'none'
|
||||
el.className = "xrf"
|
||||
el.style.display = 'none' // start hidden
|
||||
document.body.appendChild( el )
|
||||
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
|
||||
this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] })
|
||||
},
|
||||
|
||||
initListeners(){
|
||||
let {$chatline} = this
|
||||
|
||||
$chatline.addEventListener('click', (e) => this.inform() )
|
||||
|
||||
$chatline.addEventListener('keydown', (e) => {
|
||||
if (e.key == 'Enter' ){
|
||||
if( $chatline.value[0] != '/' ){
|
||||
document.dispatchEvent( new CustomEvent("network.send", {detail: {message:$chatline.value}} ) )
|
||||
}
|
||||
this.send({message: $chatline.value })
|
||||
$chatline.value = ''
|
||||
if( window.innerHeight < 600 ) $chatline.blur()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('network.connect', (e) => {
|
||||
this.visible = true
|
||||
this.$chatbar.style.display = '' // show
|
||||
})
|
||||
|
||||
document.addEventListener('network.connected', (e) => {
|
||||
if( e.detail.username ) this.username = e.detail.username
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
inform(){
|
||||
if( !this.inform.informed && (this.inform.informed = true) ){
|
||||
window.notify("Connected via P2P. You can now type message which will be visible to others.")
|
||||
}
|
||||
},
|
||||
|
||||
toggle(){
|
||||
this.visible = !this.visible
|
||||
if( this.visible && window.meeting.status == 'offline' ) window.meeting.start(this.opts)
|
||||
},
|
||||
|
||||
hyphenate(str){
|
||||
return String(str).replace(/[^a-zA-Z0-9]/g,'-')
|
||||
},
|
||||
|
||||
// sending messages to the #messages div
|
||||
// every user can post maximum one msg at a time
|
||||
// it's more like a 'status' which is more friendly
|
||||
// for accessibility reasons
|
||||
// for a fullfledged chat/transcript see matrix clients
|
||||
send(opts){
|
||||
let {$messages} = this
|
||||
opts = { linebreak:true, message:"", class:[], ...opts }
|
||||
if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
|
||||
opts.pos = opts.pos || network.posName || network.pos
|
||||
let div = document.createElement('div')
|
||||
let msg = document.createElement('div')
|
||||
let br = document.createElement('br')
|
||||
let nick = document.createElement('div')
|
||||
msg.className = "msg"
|
||||
let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}`
|
||||
if( $messages.last == html ) return
|
||||
msg.innerHTML = html
|
||||
if( opts.el ) msg.appendChild(opts.el)
|
||||
opts.id = Math.random()
|
||||
if( opts.class ){
|
||||
msg.classList.add.apply(msg.classList, opts.class)
|
||||
br.classList.add.apply(br.classList, opts.class)
|
||||
div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
|
||||
}
|
||||
if( !msg.className.match(/(info|guide|ui)/) ){
|
||||
let frag = xrf.URI.parse(document.location.hash)
|
||||
opts.from = 'you'
|
||||
if( frag.pos ) opts.pos = frag.pos.string
|
||||
msg.classList.add('self')
|
||||
}
|
||||
if( opts.from ){
|
||||
nick.className = "user"
|
||||
nick.innerText = opts.from+' '
|
||||
div.appendChild(nick)
|
||||
if( opts.pos ){
|
||||
let a = document.createElement("a")
|
||||
a.href = a.innerText = `#pos=${opts.pos}`
|
||||
nick.appendChild(a)
|
||||
}
|
||||
}
|
||||
div.appendChild(msg)
|
||||
// force one message per user
|
||||
if( this.oneMessagePerUser && opts.from ){
|
||||
div.id = this.hyphenate(opts.from)
|
||||
let oldMsg = $messages.querySelector(`#${div.id}`)
|
||||
if( oldMsg ) oldMsg.remove()
|
||||
}
|
||||
// remove after timeout
|
||||
if( opts.timeout ) setTimeout( (div) => div.remove(), opts.timeout, div )
|
||||
// finally add the message on top
|
||||
$messages.appendChild(div)
|
||||
if( opts.linebreak ) div.appendChild(br)
|
||||
$messages.scrollTop = $messages.scrollHeight // scroll down
|
||||
$messages.last = msg.innerHTML
|
||||
},
|
||||
|
||||
getChatLog(){
|
||||
return ([...this.$messages.querySelectorAll('.envelope')])
|
||||
.filter( (d) => !d.className.match(/(info|ui)/) )
|
||||
.map( (d) => d.innerHTML )
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
},{
|
||||
|
||||
get(me,k,v){ return me[k] },
|
||||
set(me,k,v){
|
||||
me[k] = v
|
||||
switch( k ){
|
||||
case "visible": {
|
||||
el.style.display = me.visible ? 'block' : 'none'
|
||||
if( !el.inited && (el.inited = true) ) me.initListeners()
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// reactify component!
|
||||
document.addEventListener('$menu:ready', (opts) => {
|
||||
opts = opts.detail
|
||||
document.head.innerHTML += chatComponent.css
|
||||
window.$chat = document.createElement('div')
|
||||
$chat.innerHTML = chatComponent.html
|
||||
$chat = chatComponent.init($chat)
|
||||
$chat.install(opts)
|
||||
//$menu.buttons = ([`<a class="btn" aria-label="button" aria-description="toggle text" id="meeting" onclick="$chat.toggle()">📜 toggle text</a><br>`])
|
||||
// .concat($menu.buttons)
|
||||
})
|
||||
|
||||
// alpine component for displaying meetings
|
||||
|
||||
chatComponent.css = `
|
||||
<style type="text/css">
|
||||
#videos{
|
||||
display:grid-auto-columns;
|
||||
grid-column-gap:5px;
|
||||
margin-bottom:15px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 15px;
|
||||
z-index:1500;
|
||||
}
|
||||
#videos > video{
|
||||
border-radius:7px;
|
||||
display:inline-block;
|
||||
background:black;
|
||||
width:80px;
|
||||
height:60px;
|
||||
margin-right:5px;
|
||||
margin-bottom:5px;
|
||||
vertical-align:top;
|
||||
pointer-events:all;
|
||||
}
|
||||
#videos > video:hover{
|
||||
filter: brightness(1.8);
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
#chatbar,
|
||||
button#showchat{
|
||||
z-index: 1500;
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
height: 34px;
|
||||
left: 20px;
|
||||
width: 48%;
|
||||
background: white;
|
||||
padding: 0px 0px 0px 15px;
|
||||
border-radius: 30px;
|
||||
max-width: 500px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 0px 5px 5px #0002;
|
||||
}
|
||||
button#showchat{
|
||||
z-index:1550;
|
||||
color:white;
|
||||
border:0;
|
||||
display:none;
|
||||
height: 44px;
|
||||
background:#07F;
|
||||
font-weight:bold;
|
||||
}
|
||||
#chatbar input{
|
||||
border:none;
|
||||
width:90%;
|
||||
box-sizing:border-box;
|
||||
height: 24px;
|
||||
font-size: var(--xrf-font-size-2);
|
||||
max-width:unset;
|
||||
}
|
||||
#messages{
|
||||
/*
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 91%;
|
||||
max-width: 500px;
|
||||
*/
|
||||
width:100%;
|
||||
align-items: flex-start;
|
||||
position: absolute;
|
||||
transition:1s;
|
||||
top: 77px;
|
||||
left: 0;
|
||||
bottom: 49px;
|
||||
padding: 20px;
|
||||
overflow:hidden;
|
||||
overflow-y: scroll;
|
||||
pointer-events:none;
|
||||
transition:1s;
|
||||
z-index: 100;
|
||||
}
|
||||
body.menu #messages{
|
||||
top:50px;
|
||||
}
|
||||
#messages:hover {
|
||||
pointer-events:all;
|
||||
}
|
||||
#messages *{
|
||||
pointer-events:none;
|
||||
-webkit-user-select:none;
|
||||
-moz-user-select:-moz-none;
|
||||
-ms-user-select:none;
|
||||
user-select:none;
|
||||
}
|
||||
#messages .msg{
|
||||
transition:all 1s ease;
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
padding: 1px 17px;
|
||||
border-radius: 20px;
|
||||
color: #000c;
|
||||
margin-bottom: 10px;
|
||||
line-height:23px;
|
||||
line-height:33px;
|
||||
cursor:grabbing;
|
||||
border: 1px solid #0002;
|
||||
}
|
||||
#messages .msg *{
|
||||
pointer-events:all;
|
||||
-webkit-user-select:text;
|
||||
-moz-user-select:-moz-text;
|
||||
-ms-user-select:text;
|
||||
user-select:text;
|
||||
}
|
||||
|
||||
#messages .msg.self{
|
||||
border-radius: 20px;
|
||||
background:var(--xrf-box-shadow);
|
||||
}
|
||||
#messages .msg.self,
|
||||
#messages .msg.self div{
|
||||
color:#FFF;
|
||||
}
|
||||
#messages .msg.info{
|
||||
background: #473f7f;
|
||||
border-radius: 20px;
|
||||
color: #FFF;
|
||||
text-align: right;
|
||||
line-height: 19px;
|
||||
}
|
||||
#messages .msg.info,
|
||||
#messages .msg.info *{
|
||||
font-size: var(--xrf-font-size-0);
|
||||
}
|
||||
#messages .msg a {
|
||||
text-decoration:underline;
|
||||
color: var(--xrf-primary);
|
||||
font-weight:bold;
|
||||
transition:0.3s;
|
||||
}
|
||||
#messages .msg.info a,
|
||||
#messages a.ruler{
|
||||
color:#FFF;
|
||||
}
|
||||
#messages .msg a:hover{
|
||||
color:#000;
|
||||
}
|
||||
#messages .msg.ui,
|
||||
#messages .msg.ui div{
|
||||
background: white;
|
||||
border:none;
|
||||
color: #333;
|
||||
border-radius: 20px;
|
||||
margin:0;
|
||||
padding:0px 5px 5px 5px;
|
||||
}
|
||||
#messages.guide, .guide{
|
||||
display:unset;
|
||||
}
|
||||
#messages .guide, .guide{
|
||||
display:none;
|
||||
}
|
||||
br.guide{
|
||||
display:inline-block;
|
||||
}
|
||||
#messages .msg.info a:hover,
|
||||
#messages button:hover{
|
||||
filter: brightness(1.4);
|
||||
}
|
||||
#messages .msg.multiline {
|
||||
padding: 2px 14px;
|
||||
}
|
||||
#messages button {
|
||||
text-decoration:none;
|
||||
margin: 0px 15px 10px 0px;
|
||||
background: var(--xrf-primary);
|
||||
font-family: var(--xrf-font-sans-serif);
|
||||
color: #FFF;
|
||||
border-radius: 7px;
|
||||
padding: 11px 15px;
|
||||
border: 0;
|
||||
font-weight: bold;
|
||||
box-shadow: 0px 0px 5px 5px #0002;
|
||||
pointer-events:all;
|
||||
}
|
||||
#messages,#chatbar,#chatbar *, #messages *{
|
||||
}
|
||||
|
||||
|
||||
#messages button.emoticon,
|
||||
#messages .btn.emoticon {
|
||||
line-height:2px;
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
padding: 0px 0px;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
background: none;
|
||||
border: none;
|
||||
min-width: 31px;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
#messages button.emoticon:hover,
|
||||
#messages .btn.emoticon:hover {
|
||||
border: 1px solid #ccc !important;
|
||||
background:#EEE;
|
||||
}
|
||||
|
||||
.nomargin{
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.envelope,
|
||||
.envelope * {
|
||||
overflow:hidden;
|
||||
transition:1s;
|
||||
pointer-events:none;
|
||||
}
|
||||
.envelope a,
|
||||
.envelope button,
|
||||
.envelope input,
|
||||
.envelope textarea,
|
||||
.envelope msg,
|
||||
.envelope msg * {
|
||||
pointer-events:all;
|
||||
}
|
||||
|
||||
.user{
|
||||
margin-left:13px;
|
||||
font-weight: bold;
|
||||
color: var(--xrf-dark-gray);
|
||||
}
|
||||
.user, .user *{
|
||||
font-size: var(--xrf-font-size-0);
|
||||
}
|
||||
</style>`
|
||||
}).apply({})
|
|
@ -2398,6 +2398,8 @@ class xrfragment_XRF:
|
|||
def validate(self,value):
|
||||
self.guessType(self,value)
|
||||
ok = True
|
||||
if (len(value) == 0):
|
||||
ok = False
|
||||
if (((not self._hx_is(xrfragment_XRF.T_FLOAT)) and self._hx_is(xrfragment_XRF.T_VECTOR2)) and (not ((Std.isOfType(self.x,Float) and Std.isOfType(self.y,Float))))):
|
||||
ok = False
|
||||
if (((not ((self._hx_is(xrfragment_XRF.T_VECTOR2) or self._hx_is(xrfragment_XRF.T_STRING)))) and self._hx_is(xrfragment_XRF.T_VECTOR3)) and (not (((Std.isOfType(self.x,Float) and Std.isOfType(self.y,Float)) and Std.isOfType(self.z,Float))))):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* v0.5.1 generated at Fri Mar 1 01:33:23 PM UTC 2024
|
||||
* v0.5.1 generated at Tue Mar 19 09:13:06 AM UTC 2024
|
||||
* https://xrfragment.org
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
@ -1217,6 +1217,9 @@ xrfragment_XRF.prototype = {
|
|||
,validate: function(value) {
|
||||
this.guessType(this,value);
|
||||
var ok = true;
|
||||
if(value.length == 0) {
|
||||
ok = false;
|
||||
}
|
||||
if(!this.is(xrfragment_XRF.T_FLOAT) && this.is(xrfragment_XRF.T_VECTOR2) && !(typeof(this.x) == "number" && typeof(this.y) == "number")) {
|
||||
ok = false;
|
||||
}
|
||||
|
@ -1386,19 +1389,6 @@ xrf.detectCameraRig = function(opts){
|
|||
}
|
||||
}
|
||||
|
||||
xrf.roundrobin = (frag, store) => {
|
||||
if( !frag.args || frag.args.length == 0 ) return 0
|
||||
if( !store.rr ) store.rr = {}
|
||||
let label = frag.fragment
|
||||
if( store.rr[label] ) return store.rr[label].next()
|
||||
store.rr[label] = frag.args
|
||||
store.rr[label].next = () => {
|
||||
store.rr[label].index = (store.rr[label].index + 1) % store.rr[label].length
|
||||
return store.rr[label].index
|
||||
}
|
||||
return store.rr[label].index = 0
|
||||
}
|
||||
|
||||
xrf.stats = () => {
|
||||
// bookmarklet from https://github.com/zlgenuine/threejs_stats
|
||||
(function(){
|
||||
|
@ -1459,13 +1449,13 @@ xrf.emit = function(eventName, data){
|
|||
return xrf.emit.promise(eventName,data)
|
||||
}
|
||||
|
||||
xrf.emit.normal = function(eventName, data) {
|
||||
xrf.emit.normal = function(eventName, opts) {
|
||||
if( !xrf._listeners ) xrf._listeners = []
|
||||
var callbacks = xrf._listeners[eventName]
|
||||
if (callbacks) {
|
||||
for (var i = 0; i < callbacks.length; i++) {
|
||||
for (var i = 0; i < callbacks.length && !opts.halt; i++) {
|
||||
try{
|
||||
callbacks[i](data);
|
||||
callbacks[i](opts);
|
||||
}catch(e){ console.error(e) }
|
||||
}
|
||||
}
|
||||
|
@ -1482,7 +1472,10 @@ xrf.emit.promise = function(e, opts){
|
|||
let succesful = opts.promises.reduce( (a,b) => a+b )
|
||||
if( succesful == opts.promises.length ) resolve(opts)
|
||||
})(opts.promises.length-1),
|
||||
reject: console.error
|
||||
reject: (reason) => {
|
||||
opts.halt = true
|
||||
console.warn(`'${e}' event rejected: ${reason}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
xrf.emit.normal(e, opts)
|
||||
|
@ -1986,7 +1979,11 @@ xrf.frag.href = function(v, opts){
|
|||
|
||||
let click = mesh.userData.XRF.href.exec = (e) => {
|
||||
|
||||
if( !mesh.material.visible ) return // ignore invisible nodes
|
||||
if( !mesh.material || !mesh.material.visible ) return // ignore invisible nodes
|
||||
|
||||
// update our values to the latest value (might be edited)
|
||||
xrf.Parser.parse( "href", mesh.userData.href, frag )
|
||||
const v = frag.href
|
||||
|
||||
// bubble up!
|
||||
mesh.traverseAncestors( (n) => n.userData && n.userData.href && n.dispatchEvent({type:e.type,data:{}}) )
|
||||
|
@ -2010,7 +2007,7 @@ xrf.frag.href = function(v, opts){
|
|||
}
|
||||
|
||||
let selected = mesh.userData.XRF.href.selected = (state) => () => {
|
||||
if( !mesh.material.visible && !mesh.isSRC ) return // ignore invisible nodes
|
||||
if( (!mesh.material && !mesh.material.visible) && !mesh.isSRC ) return // ignore invisible nodes
|
||||
if( mesh.selected == state ) return // nothing changed
|
||||
|
||||
xrf.interactive.objects.map( (o) => {
|
||||
|
@ -2107,7 +2104,9 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
let {scene,model} = opts;
|
||||
scene.traverse( (n) => {
|
||||
if( n.userData && n.userData['#'] ){
|
||||
xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments without affecting URL
|
||||
if( !n.parent && !document.location.hash ){
|
||||
xrf.navigator.to( n.userData['#'] )
|
||||
}else xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments without affecting URL
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -2624,7 +2623,7 @@ xrf.getCollisionMeshes = () => {
|
|||
})
|
||||
return meshes
|
||||
}
|
||||
// wrapper to survive in/outside modules
|
||||
// wrapper to collect interactive raycastable objects
|
||||
|
||||
xrf.interactiveGroup = function(THREE,renderer,camera){
|
||||
|
||||
|
@ -2652,6 +2651,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
const scope = this;
|
||||
scope.objects = []
|
||||
scope.raycastAll = false
|
||||
|
||||
|
||||
const raycaster = new Raycaster();
|
||||
const tempMatrix = new Matrix4();
|
||||
|
@ -2659,6 +2660,15 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
// Pointer Events
|
||||
const element = renderer.domElement;
|
||||
|
||||
const getAllMeshes = (scene) => {
|
||||
let objects = []
|
||||
xrf.scene.traverse( (n) => {
|
||||
if( !n.material || n.type != 'Mesh' ) return
|
||||
objects.push(n)
|
||||
})
|
||||
return objects
|
||||
}
|
||||
|
||||
function onPointerEvent( event ) {
|
||||
|
||||
//event.stopPropagation();
|
||||
|
@ -2670,7 +2680,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
raycaster.setFromCamera( _pointer, camera );
|
||||
|
||||
const intersects = raycaster.intersectObjects( scope.objects, false );
|
||||
let objects = scope.raycastAll ? getAllMeshes(xrf.scene) : scope.objects
|
||||
const intersects = raycaster.intersectObjects( objects, false )
|
||||
|
||||
if ( intersects.length > 0 ) {
|
||||
|
||||
|
@ -2680,7 +2691,7 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
const uv = intersection.uv;
|
||||
|
||||
_event.type = event.type;
|
||||
_event.data.set( uv.x, 1 - uv.y );
|
||||
if( uv ) _event.data.set( uv.x, 1 - uv.y );
|
||||
object.dispatchEvent( _event );
|
||||
|
||||
}else{
|
||||
|
@ -2719,19 +2730,20 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld );
|
||||
raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
|
||||
|
||||
const intersections = raycaster.intersectObjects( scope.objects, false );
|
||||
let objects = scope.raycastAll ? getAllMeshes(xrf.scene) : scope.objects
|
||||
const intersects = raycaster.intersectObjects( objects, false )
|
||||
|
||||
if ( intersections.length > 0 ) {
|
||||
if ( intersects.length > 0 ) {
|
||||
|
||||
console.log(object.name)
|
||||
|
||||
const intersection = intersections[ 0 ];
|
||||
const intersection = intersects[ 0 ];
|
||||
|
||||
object = intersection.object;
|
||||
const uv = intersection.uv;
|
||||
|
||||
_event.type = eventsMapper[ event.type ];
|
||||
_event.data.set( uv.x, 1 - uv.y );
|
||||
if( uv ) _event.data.set( uv.x, 1 - uv.y );
|
||||
|
||||
object.dispatchEvent( _event );
|
||||
|
||||
|
@ -2758,6 +2770,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
}
|
||||
|
||||
// we create our own add to avoid unnecessary unparenting of buffergeometries from
|
||||
// their 3D model (which breaks animations)
|
||||
add(obj, unparent){
|
||||
if( unparent ) Group.prototype.add.call( this, obj )
|
||||
this.objects.push(obj)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* v0.5.1 generated at Fri Mar 1 01:33:23 PM UTC 2024
|
||||
* v0.5.1 generated at Tue Mar 19 09:13:06 AM UTC 2024
|
||||
* https://xrfragment.org
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
@ -1217,6 +1217,9 @@ xrfragment_XRF.prototype = {
|
|||
,validate: function(value) {
|
||||
this.guessType(this,value);
|
||||
var ok = true;
|
||||
if(value.length == 0) {
|
||||
ok = false;
|
||||
}
|
||||
if(!this.is(xrfragment_XRF.T_FLOAT) && this.is(xrfragment_XRF.T_VECTOR2) && !(typeof(this.x) == "number" && typeof(this.y) == "number")) {
|
||||
ok = false;
|
||||
}
|
||||
|
@ -1386,19 +1389,6 @@ xrf.detectCameraRig = function(opts){
|
|||
}
|
||||
}
|
||||
|
||||
xrf.roundrobin = (frag, store) => {
|
||||
if( !frag.args || frag.args.length == 0 ) return 0
|
||||
if( !store.rr ) store.rr = {}
|
||||
let label = frag.fragment
|
||||
if( store.rr[label] ) return store.rr[label].next()
|
||||
store.rr[label] = frag.args
|
||||
store.rr[label].next = () => {
|
||||
store.rr[label].index = (store.rr[label].index + 1) % store.rr[label].length
|
||||
return store.rr[label].index
|
||||
}
|
||||
return store.rr[label].index = 0
|
||||
}
|
||||
|
||||
xrf.stats = () => {
|
||||
// bookmarklet from https://github.com/zlgenuine/threejs_stats
|
||||
(function(){
|
||||
|
@ -1459,13 +1449,13 @@ xrf.emit = function(eventName, data){
|
|||
return xrf.emit.promise(eventName,data)
|
||||
}
|
||||
|
||||
xrf.emit.normal = function(eventName, data) {
|
||||
xrf.emit.normal = function(eventName, opts) {
|
||||
if( !xrf._listeners ) xrf._listeners = []
|
||||
var callbacks = xrf._listeners[eventName]
|
||||
if (callbacks) {
|
||||
for (var i = 0; i < callbacks.length; i++) {
|
||||
for (var i = 0; i < callbacks.length && !opts.halt; i++) {
|
||||
try{
|
||||
callbacks[i](data);
|
||||
callbacks[i](opts);
|
||||
}catch(e){ console.error(e) }
|
||||
}
|
||||
}
|
||||
|
@ -1482,7 +1472,10 @@ xrf.emit.promise = function(e, opts){
|
|||
let succesful = opts.promises.reduce( (a,b) => a+b )
|
||||
if( succesful == opts.promises.length ) resolve(opts)
|
||||
})(opts.promises.length-1),
|
||||
reject: console.error
|
||||
reject: (reason) => {
|
||||
opts.halt = true
|
||||
console.warn(`'${e}' event rejected: ${reason}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
xrf.emit.normal(e, opts)
|
||||
|
@ -1986,7 +1979,11 @@ xrf.frag.href = function(v, opts){
|
|||
|
||||
let click = mesh.userData.XRF.href.exec = (e) => {
|
||||
|
||||
if( !mesh.material.visible ) return // ignore invisible nodes
|
||||
if( !mesh.material || !mesh.material.visible ) return // ignore invisible nodes
|
||||
|
||||
// update our values to the latest value (might be edited)
|
||||
xrf.Parser.parse( "href", mesh.userData.href, frag )
|
||||
const v = frag.href
|
||||
|
||||
// bubble up!
|
||||
mesh.traverseAncestors( (n) => n.userData && n.userData.href && n.dispatchEvent({type:e.type,data:{}}) )
|
||||
|
@ -2010,7 +2007,7 @@ xrf.frag.href = function(v, opts){
|
|||
}
|
||||
|
||||
let selected = mesh.userData.XRF.href.selected = (state) => () => {
|
||||
if( !mesh.material.visible && !mesh.isSRC ) return // ignore invisible nodes
|
||||
if( (!mesh.material && !mesh.material.visible) && !mesh.isSRC ) return // ignore invisible nodes
|
||||
if( mesh.selected == state ) return // nothing changed
|
||||
|
||||
xrf.interactive.objects.map( (o) => {
|
||||
|
@ -2107,7 +2104,9 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
let {scene,model} = opts;
|
||||
scene.traverse( (n) => {
|
||||
if( n.userData && n.userData['#'] ){
|
||||
xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments without affecting URL
|
||||
if( !n.parent && !document.location.hash ){
|
||||
xrf.navigator.to( n.userData['#'] )
|
||||
}else xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments without affecting URL
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -2624,7 +2623,7 @@ xrf.getCollisionMeshes = () => {
|
|||
})
|
||||
return meshes
|
||||
}
|
||||
// wrapper to survive in/outside modules
|
||||
// wrapper to collect interactive raycastable objects
|
||||
|
||||
xrf.interactiveGroup = function(THREE,renderer,camera){
|
||||
|
||||
|
@ -2652,6 +2651,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
const scope = this;
|
||||
scope.objects = []
|
||||
scope.raycastAll = false
|
||||
|
||||
|
||||
const raycaster = new Raycaster();
|
||||
const tempMatrix = new Matrix4();
|
||||
|
@ -2659,6 +2660,15 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
// Pointer Events
|
||||
const element = renderer.domElement;
|
||||
|
||||
const getAllMeshes = (scene) => {
|
||||
let objects = []
|
||||
xrf.scene.traverse( (n) => {
|
||||
if( !n.material || n.type != 'Mesh' ) return
|
||||
objects.push(n)
|
||||
})
|
||||
return objects
|
||||
}
|
||||
|
||||
function onPointerEvent( event ) {
|
||||
|
||||
//event.stopPropagation();
|
||||
|
@ -2670,7 +2680,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
raycaster.setFromCamera( _pointer, camera );
|
||||
|
||||
const intersects = raycaster.intersectObjects( scope.objects, false );
|
||||
let objects = scope.raycastAll ? getAllMeshes(xrf.scene) : scope.objects
|
||||
const intersects = raycaster.intersectObjects( objects, false )
|
||||
|
||||
if ( intersects.length > 0 ) {
|
||||
|
||||
|
@ -2680,7 +2691,7 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
const uv = intersection.uv;
|
||||
|
||||
_event.type = event.type;
|
||||
_event.data.set( uv.x, 1 - uv.y );
|
||||
if( uv ) _event.data.set( uv.x, 1 - uv.y );
|
||||
object.dispatchEvent( _event );
|
||||
|
||||
}else{
|
||||
|
@ -2719,19 +2730,20 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld );
|
||||
raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
|
||||
|
||||
const intersections = raycaster.intersectObjects( scope.objects, false );
|
||||
let objects = scope.raycastAll ? getAllMeshes(xrf.scene) : scope.objects
|
||||
const intersects = raycaster.intersectObjects( objects, false )
|
||||
|
||||
if ( intersections.length > 0 ) {
|
||||
if ( intersects.length > 0 ) {
|
||||
|
||||
console.log(object.name)
|
||||
|
||||
const intersection = intersections[ 0 ];
|
||||
const intersection = intersects[ 0 ];
|
||||
|
||||
object = intersection.object;
|
||||
const uv = intersection.uv;
|
||||
|
||||
_event.type = eventsMapper[ event.type ];
|
||||
_event.data.set( uv.x, 1 - uv.y );
|
||||
if( uv ) _event.data.set( uv.x, 1 - uv.y );
|
||||
|
||||
object.dispatchEvent( _event );
|
||||
|
||||
|
@ -2758,6 +2770,8 @@ xrf.interactiveGroup = function(THREE,renderer,camera){
|
|||
|
||||
}
|
||||
|
||||
// we create our own add to avoid unnecessary unparenting of buffergeometries from
|
||||
// their 3D model (which breaks animations)
|
||||
add(obj, unparent){
|
||||
if( unparent ) Group.prototype.add.call( this, obj )
|
||||
this.objects.push(obj)
|
||||
|
|
Binary file not shown.
|
@ -4,7 +4,7 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
|||
let {scene,model} = opts;
|
||||
scene.traverse( (n) => {
|
||||
if( n.userData && n.userData['#'] ){
|
||||
if( !n.parent ){
|
||||
if( !n.parent && !document.location.hash ){
|
||||
xrf.navigator.to( n.userData['#'] )
|
||||
}else xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments without affecting URL
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@ xrf.frag.href = function(v, opts){
|
|||
if( !mesh.material || !mesh.material.visible ) return // ignore invisible nodes
|
||||
|
||||
// update our values to the latest value (might be edited)
|
||||
v = {string: mesh.userData.href }
|
||||
xrf.Parser.parse( "href", mesh.userData.href, frag )
|
||||
const v = frag.href
|
||||
|
||||
// bubble up!
|
||||
mesh.traverseAncestors( (n) => n.userData && n.userData.href && n.dispatchEvent({type:e.type,data:{}}) )
|
||||
|
|
Loading…
Reference in New Issue