working menu with generalizing target mechanism

pinch-to-menu
Fabien Benetou 9 months ago
parent 9dbc57991d
commit 12364ed505
  1. 343
      index.html
  2. 26
      jxr-core.js

@ -4,214 +4,34 @@
<head> <head>
<!-- Suggestions? https://git.benetou.fr/utopiah/text-code-xr-engine/issues/ --> <!-- Suggestions? https://git.benetou.fr/utopiah/text-code-xr-engine/issues/ -->
<script src='dependencies/aframe.offline.min.js'></script> <script src='dependencies/aframe.offline.min.js'></script>
<script>
// testing spatial introspection, starting with console output
// see #spatial-introspection-test
// example of normal output
// which should print to the main console AND to the local one (might actually be the same output at first)
// for that could modifiy captureConsole of <a-console>
// itself passed to grabAllLogs() https://github.com/kylebakerio/a-console/blob/master/a-console.js#L680
// might require too much rework, could be interesting to have own equivalent instead
// define a new console, from https://stackoverflow.com/questions/7042611/override-console-log-for-production
/*
var console=(function(oldCons){
return {
debug: function(text){
oldCons.debug(text);
// Your code
},
log: function(text){
oldCons.log("modified!"+text, arguments.caller);
// this does though rewrite for ALL, so might not help much to find provenance, i.e which functions did call for that output
// could try to get a strack trace from here and only filter from what has gone through interpretJXR / onreleased / onpicked
// does NOT get picked by <a-console> though
// Your code
},
info: function (text) {
oldCons.info(text);
// Your code
},
warn: function (text) {
oldCons.warn(text);
// Your code
},
error: function (text) {
oldCons.error(text);
// Your code
}
};
}(window.console));
// for now could add mini console after each secondary pinch
//Then redefine the old console
window.console = console;
*/
</script>
<script src="dependencies/a-console.js"></script> <script src="dependencies/a-console.js"></script>
<script src='dependencies/aframe-troika-text.min.js'></script> <script src='dependencies/aframe-troika-text.min.js'></script>
<script src='dependencies/webdav.js'></script> <script src='dependencies/webdav.js'></script>
<script src='jxr-core.js?12'></script> <script src='jxr-core.js?1234'></script>
<script src='jxr-postitnote.js?13235'></script> <script src='jxr-postitnote.js?13235'></script>
</head> </head>
<body> <body>
<script> <script>
/*
SpaSca circular menu inspired by https://www.olegfrolov.design/spatialcomputing or the Meta OS menu
on empty pinch moving, if (moving duration above threshold 500ms)
then
show menu nearby position with JXR commands that hides on released,
on moving
highlight closest snippet
rewrite nearby getClosestTargetElements() to be only over a subset or targets, i.e the ones from active menu
on release
if (nearby one of the created JXR snippets), execute it
remove those JXR commands
<a-troika-text id="spatial-introspection-test" anchor=left value="console.log('executing from secondary pinch');"
var forceXaxis onreleased="console.log('run on released')"
// setInterval( _ => console.log(forceXaxis), 1000) onpicked="console.log('run on picked')"
target position=" -0.3 1.35 0" rotation="0 40 0" scale="0.1 0.1 0.1">
var translatingTargets = false </a-troika-text>
var clearRot */
function toggleTranslateTargets(){
translatingTargets = !translatingTargets
let scene = AFRAME.scenes[0].object3D
if (translatingTargets){
let anchor = new THREE.Object3D()
let latest = selectedElements[selectedElements.length-1].element
latest.object3D.add( anchor )
// also inherits rotation, could try cancel it as the opposite of latest rotation
// might be easier to copy the position only every few ms instead
anchor.position.sub( latest.object3D.position )
//targets.map( t => anchor.attach(t.object3D) )
// should attach all BUT the current moving entity!
Array.from(document.querySelectorAll('.mab')).map( t => anchor.attach(t.object3D) )
// they don't move... despite
} else {
clearInterval( clearRot )
Array.from(document.querySelectorAll('.mab')).map( t => scene.attach(t.object3D) )
//targets.map( t => scene.attach(t.object3D) )
// could delete anchor, cleaner
}
}
var attachToPlayer = false
function toggleAttachToSelf(){
attachToPlayer = !attachToPlayer
attachToPlayer ? parent=document.querySelector("#player") : parent=AFRAME.scenes[0]
targets.map( t => parent.object3D.attach(t.object3D) )
}
function checkIntersection(latest, nearby){
//let latest = selectedElements[selectedElements.length-1].element
//let nearby = getClosestTargetElements( latest.getAttribute('position') )
// https://threejs.org/docs/?q=box#api/en/math/Box3.containsBox
// https://threejs.org/docs/?q=box#api/en/math/Box3.expandByObject
let a = new THREE.Box3().expandByObject( latest.object3D ) // consider mesh.geometry.computeBoundingBox() first
let b = new THREE.Box3().expandByObject( nearby.object3D )
console.log(a,b, a.containsBox(b))
// testable as checkIntersection( document.querySelector("[color='yellow']"), document.querySelector("[color='purple']") )
// <a-box scale=".1 .1 .1" position=".5 .8 -.3" color="purple" ></a-box>
// <a-box scale=".2 .2 .2" position=".5 .8 -.3" color="yellow" ></a-box>
}
setTimeout( _ => {
let newPostIt = addNewNoteAsPostItNote("jxr console.log(222);", "0 1.2 -.5")
.setAttribute("onreleased", "grammarBasedSnap()")
let otherPostIt = addNewNoteAsPostItNote("jxr console.log(111);", "0 1.4 -.5")
.setAttribute("onreleased", "grammarBasedSnap()")
let postIt = addNewNoteAsPostItNote("hi this is a post-it note.", "0 1.6 -.5")
.setAttribute("onreleased", "runClosestJXR(); grammarBasedSnap()") // dunno how to share the event context back here...
// .setAttribute("onreleased", "snapNext()") // does NOT support multiple instances for now
// see https://aframe.io/docs/1.5.0/core/component.html#multiple
// maybe bind could help
//let cloneMe = addNewNote('jxr clone me from corner', '0 0 .1', '1 1 1', 'cmd')
// should rebind parent...
//setTimeout( _ => { _ => cloneMe.object3D.parent = postIt.object3D }, 1000 )
// should try object3D.attach() instead
//.addEventListener('loaded',
// entityIndexes( document.querySelector("[color='blue']").object3D.children[0] )
}, 1000 )
// e.g document.querySelector("[color='blue']").object3D.children[0]
function entityIndexes(mesh){ // needs a mesh with a geometry, not a group
// could also traverse
let gp = mesh.geometry.attributes.position;
let wPos = [];
for(let i = 0;i < gp.count; i++){
let p = new THREE.Vector3().fromBufferAttribute(gp, i); // set p from `position`
mesh.localToWorld(p); // p has wordl coords
wPos.push(p);
}
// many are duplicates, i.e a a cube will return 24 indexes (4 per 6 faces), not 8
//let l = [...new Set(wPos)].length; console.log( l )
[...new Set(wPos)].map( p => addNewNote("x", p))
console.log( [...new Set(wPos)].length )
// seems to add the duplicates again
// try to "de-dup" via .distanceTo() below a threshold instead
}
function snapToGrid(gridSize=1){ // default as 1 decimeter
let latest = selectedElements[selectedElements.length-1].element
latest.setAttribute("rotation", "0 0 0")
let pos = latest.getAttribute("position")
pos.multiplyScalar(gridSize*10).round().divideScalar(gridSize*10)
latest.setAttribute("position", pos )
}
// deeper question, making the rules themselves manipulable? JXR?
// So the result of the grammar becomes manipulable, but could you make the rules of the grammar itself visual? Even manipulable?
// could start by visualizing examples first e.g https://writer.com/wp-content/uploads/2024/03/grammar-1.webp
function snapMAB(){
// multibase arithmetic blocks aka MAB cf https://en.wikipedia.org/wiki/Base_ten_block
let latest = selectedElements[selectedElements.length-1].element
let nearby = getClosestTargetElements( latest.getAttribute('position') )
let linked = []
if (nearby.length>0){
latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("rotation") ) )
latest.setAttribute("position", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("position") ) )
latest.object3D.translateX( 1/10 )
linked.push( latest )
linked.push( nearby[0].el )
let overlap = Array.from( document.querySelectorAll(".mab") ).filter( e => e.object3D.position.distanceTo( latest.object3D.position ) < 0.01 && e!=latest )
while (overlap.length > 0 ){
latest.object3D.translateX( 1/10 )
linked.push( overlap[0] )
overlap = Array.from( document.querySelectorAll(".mab") ).filter( e => e.object3D.position.distanceTo( latest.object3D.position ) < 0.01 && e!=latest )
}
// do something special if it becomes 10, e.g become a single line, removing the "ridges"
if (linked.length > 3)
linked.map( e => Array.from( e.querySelectorAll("a-box") ).setAttribute("color", "orange") )
// also need to go backward too to see if it's the latest added
}
}
function snapRightOf(){
let latest = selectedElements[selectedElements.length-1].element
let nearby = getClosestTargetElements( latest.getAttribute('position') )
if (nearby.length>0){
latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("rotation") ) )
latest.setAttribute("position", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("position") ) )
latest.object3D.translateX( 1/10 )
// somehow... works only the 2nd time, not the 1st?!
}
}
function grammarBasedSnap(){
// verify if snappable, e.g of same type (or not)
// e.g check if both have .getAttribute('value').match(prefix) or not
let latest = selectedElements[selectedElements.length-1].element
let nearby = getClosestTargetElements( latest.getAttribute('position') )
if (nearby.length>0){
let closest = nearby[0].el
let latestTypeJXR = latest.getAttribute('value').match(prefix)
let closestTypeJXR = latest.getAttribute('value').match(prefix)
latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( closest.getAttribute("rotation") ) )
latest.setAttribute("position", AFRAME.utils.coordinates.stringify( closest.getAttribute("position") ) )
if ( latestTypeJXR && closestTypeJXR )
latest.object3D.translateX( 1/10 ) // same JXR type, snap close
else
latest.object3D.translateX( 2/10 ) // different types, snap away
// somehow... works only the 2nd time, not the 1st?!
}
}
function cloneTarget(target){ function cloneTarget(target){
let el = target.cloneNode(true) let el = target.cloneNode(true)
@ -230,28 +50,11 @@ function deleteTarget(target){
function runClosestJXR(){ function runClosestJXR(){
// ideally this would come from event details // ideally this would come from event details
let latest = selectedElements[selectedElements.length-1].element let latest = selectedElements[selectedElements.length-1].element
let nearby = getClosestTargetElements( latest.getAttribute('position') ) let el = getClosestTargetElement( latest.getAttribute('position') )
// if (nearby.length>0){ interpretJXR( nearby[0].el.getAttribute("value") ) } if (el) interpretJXR( el.getAttribute("value") )
nearby.map( n => interpretJXR( n.el.getAttribute("value") ) )
}
function notesFromArray(data, generatorName="", field="title", offset=1, step=1/10, depth=-.5 ){
data.slice(0,maxItemsFromSources).map( (n,i) => {
addNewNote( n[field], "0 "+(offset+i*step)+" "+depth, ".1 .1 .1", null, generatorName )
.setAttribute("onreleased","spreadItemsFromCollection('getcsljson', 1.5)")
})
}
function spreadItemsFromCollection( generatorName, offset=1, step=1/10, depth=-.5 ){
getArrayFromClass(generatorName).sort((a,b)=>a.getAttribute('position').y-b.getAttribute('position').y).map( (n,i) => {
n.setAttribute('position', "0 "+(offset+i*step)+" "+depth)
n.setAttribute('rotation', "0 0 0") // could also be based on the average of all items, the first item, last one, etc
// see also snap-on-pinchended component
})
let items = getArrayFromClass(generatorName).sort((b,a)=>a.getAttribute('position').y-b.getAttribute('position').y).map( n => n.getAttribute('value') )
shareLiveEvent('modified list', items)
} }
// should move to jxr-core.js
AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent with event name AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent with event name
init: function(){ init: function(){
AFRAME.scenes[0].addEventListener('enter-vr', e => { AFRAME.scenes[0].addEventListener('enter-vr', e => {
@ -259,10 +62,32 @@ AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent
document.querySelector("[cursor]").setAttribute("visible", "true") document.querySelector("[cursor]").setAttribute("visible", "true")
document.querySelector("[camera]").setAttribute("cursor", "") document.querySelector("[camera]").setAttribute("cursor", "")
}) })
this.menuTargets = []
this.menuEl = document.createElement("a-box")
this.menuEl.setAttribute("scale", ".01 .01 .01")
this.menuEl.setAttribute("opacity", 0)
this.menuEl.setAttribute("color", "red")
this.menuEl.setAttribute("wireframe", "true")
this.menuOpacity = 0
AFRAME.scenes[0].appendChild(this.menuEl)
let colors =["green", "blue", "orange", "yellow", "brown", "black"].map( (c,i) => {
let newEl = document.createElement("a-box")
newEl.setAttribute("position", "" + ((i+1)*3) + " 0 0")
newEl.setAttribute("opacity", 0)
newEl.setAttribute("wireframe", "true")
newEl.setAttribute("color", c )
this.menuEl.appendChild( newEl )
this.menuTargets.push( newEl )
})
this.menuTargets.push( this.menuEl )
}, },
// could support multi // could support multi
events: { events: {
emptypinch: function (e) { emptypinch: function (e) {
this.menuEl.setAttribute("position", e.detail.position)
// works with AFRAME.scenes[0].emit('emptypinch', {position:"0 0 0"}) // works with AFRAME.scenes[0].emit('emptypinch', {position:"0 0 0"})
let code = this.el.getAttribute('onemptypinch') let code = this.el.getAttribute('onemptypinch')
// if multi, should also look for onreleased__ not just onreleased // if multi, should also look for onreleased__ not just onreleased
@ -271,7 +96,43 @@ AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent
} catch (error) { } catch (error) {
console.error(`Evaluation failed with ${error}`); console.error(`Evaluation failed with ${error}`);
} }
} },
emptypinchmoved: function (e) {
let foundElement = getClosestElement( e.detail.position, threshold=0.01, this.menuTargets )
this.menuTargets.map( mt => mt.setAttribute("wireframe", "true") )
if ( foundElement ) foundElement.setAttribute("wireframe", "false")
if (this.menuOpacity < 1) {
this.menuOpacity += .01
this.menuTargets.map( mt => mt.setAttribute("opacity", this.menuOpacity) )
}
let code = this.el.getAttribute('onemptypinchmoved')
// if multi, should also look for onreleased__ not just onreleased
try {
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
} catch (error) {
console.error(`Evaluation failed with ${error}`);
}
},
emptypinchreleased: function (e) {
// could also check if above this.menuOpacity threshold, e.g not doing below .3 to avoid unexpected action
let foundElement = getClosestElement( e.detail.position, threshold=0.02, this.menuTargets )
// does not seem to execute anymore after teleporting, did work before though
if ( foundElement ){
document.getElementById("box").setAttribute("color", foundElement.getAttribute("color") )
}
this.menuOpacity = 0
this.menuEl.setAttribute("opacity", this.menuOpacity)
this.menuTargets.map( mt => mt.setAttribute("opacity", this.menuOpacity) )
let code = this.el.getAttribute('onemptypinchreleased')
// if multi, should also look for onreleased__ not just onreleased
try {
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
} catch (error) {
console.error(`Evaluation failed with ${error}`);
}
},
} }
}) })
@ -298,16 +159,6 @@ AFRAME.registerComponent('teleporter', {
} }
}); });
let page = "Wiki.VirtualRealityInterface";
// should do then only once graph loaded instead, should emit event
let pageFromParam = AFRAME.utils.getUrlParameter('page')
if (pageFromParam) page = pageFromParam
setTimeout( _ => {
Array.from( document.querySelectorAll("[value='"+page+"']") ).map( n =>
n.setAttribute("onreleased", "console.log('dropped, should toggle display children,"+n.id+"')"));
Array.from( document.querySelectorAll("[value='"+page+"']>a-sphere") ).map( n => n.setAttribute("color", "purple"))
}, 5000)
</script> </script>
<div style='position:fixed;z-index:1; top: 0%; left: 0%; border-bottom: 70px solid transparent; border-left: 70px solid #eee; '> <div style='position:fixed;z-index:1; top: 0%; left: 0%; border-bottom: 70px solid transparent; border-left: 70px solid #eee; '>
<a href="https://git.benetou.fr/utopiah/text-code-xr-engine/issues/"> <a href="https://git.benetou.fr/utopiah/text-code-xr-engine/issues/">
@ -315,7 +166,7 @@ setTimeout( _ => {
</a> </a>
</div> </div>
<a-scene startfunctions onemptypinch="onHoveredTeleport()"> <a-scene startfunctions onemptypinch="onHoveredTeleport()" >
<a-gltf-model hide-on-enter-ar="" id="environment" src="../content/CubeRoom.glb" rotation="0 -90 0" position="0 0 1" scale="" ></a-gltf-model> <a-gltf-model hide-on-enter-ar="" id="environment" src="../content/CubeRoom.glb" rotation="0 -90 0" position="0 0 1" scale="" ></a-gltf-model>
<!-- Cube Room by Anonymous [CC-BY] via Poly Pizza --> <!-- Cube Room by Anonymous [CC-BY] via Poly Pizza -->
@ -358,36 +209,6 @@ setTimeout( _ => {
<a-troika-text anchor=left value="jxr setTimeout( _ => toggleAttachToSelf(), 1000); toggleAttachToSelf()" target position=" -0.3 1.25 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text> <a-troika-text anchor=left value="jxr setTimeout( _ => toggleAttachToSelf(), 1000); toggleAttachToSelf()" target position=" -0.3 1.25 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-box scale=".07 .07 .07" class="mab" target position=".3 1.6 -.5" color="brown" onreleased="snapMAB()" >
<a-box scale="1.5 1.5 1" color="brown"></a-box>
<a-box scale="1 1.5 1.5" color="brown"></a-box>
<a-box scale="1.5 1 1.5" color="brown"></a-box>
</a-box>
<a-box scale=".07 .07 .07" class="mab" target position=".1 1.6 -.5" color="brown" onreleased="snapMAB()" >
<a-box scale="1.5 1.5 1" color="brown"></a-box>
<a-box scale="1 1.5 1.5" color="brown"></a-box>
<a-box scale="1.5 1 1.5" color="brown"></a-box>
</a-box>
<a-box scale=".07 .07 .07" class="mab" target position=".5 1.6 -.5" color="brown" onreleased="snapMAB()" >
<a-box scale="1.5 1.5 1" color="brown"></a-box>
<a-box scale="1 1.5 1.5" color="brown"></a-box>
<a-box scale="1.5 1 1.5" color="brown"></a-box>
</a-box>
<a-box scale=".07 .07 .07" class="mab" target position="-.5 1.6 -.5" color="brown" onreleased="snapMAB()" >
<a-box scale="1.5 1.5 1" color="brown"></a-box>
<a-box scale="1 1.5 1.5" color="brown"></a-box>
<a-box scale="1.5 1 1.5" color="brown"></a-box>
</a-box>
<a-box scale=".1 .1 .1" target position=".5 1.6 -.3" color="blue" onreleased="snapToGrid()"
annotation="content:could also show/hide grid with gridHelper on pinch started and hide on release"
></a-box>
<a-box scale=".1 .1 .1" target position=".5 1.8 -.3" color="blue" onreleased="snapToGrid()" ></a-box>
<a-box scale=".1 .1 .1" position=".5 .8 -.3" color="purple" ></a-box>
<a-box scale=".2 .2 .2" position=".5 .8 -.3" color="yellow" ></a-box>
<a-box teleporter height=".1" class="teleportable" material="color: cyan" position="3.5 0 -3.5" ></a-box> <a-box teleporter height=".1" class="teleportable" material="color: cyan" position="3.5 0 -3.5" ></a-box>
<a-box teleporter height=".1" class="teleportable" material="color: cyan" position="-4 0 4" ></a-box> <a-box teleporter height=".1" class="teleportable" material="color: cyan" position="-4 0 4" ></a-box>
<a-box teleporter height=".1" class="teleportable" material="color: cyan" position="3 3 4" > <a-box teleporter height=".1" class="teleportable" material="color: cyan" position="3 3 4" >

@ -35,10 +35,12 @@ AFRAME.registerComponent('target', {
} }
}) })
function getClosestTargetElements( pos, threshold=0.05 ){ // note that set is an array of elements from e.g getArrayFromClass(classname)
// it is NOT a selector!
function getClosestElements( pos, threshold=0.05, set ){
// assumes pos has now no offset // assumes pos has now no offset
// TODO Bbox intersects rather than position // TODO Bbox intersects rather than position
return targets.filter( e => e.getAttribute("visible") == true) return set.filter( e => e.getAttribute("visible") == true)
.map( t => { .map( t => {
let posTarget = new THREE.Vector3() let posTarget = new THREE.Vector3()
t.object3D.getWorldPosition( posTarget ) t.object3D.getWorldPosition( posTarget )
@ -49,18 +51,20 @@ function getClosestTargetElements( pos, threshold=0.05 ){
.sort( (a,b) => a.dist > b.dist) .sort( (a,b) => a.dist > b.dist)
} }
function getClosestTargetElement( pos, threshold=0.05 ){ // 10x lower threshold for flight mode function getClosestElement( pos, threshold=0.05, set ){ // 10x lower threshold for flight mode
var res = null var res = null
// assumes both hands have the same (single) parent, if any // assumes both hands have the same (single) parent, if any
let parentPos = document.getElementById('rig').getAttribute('position') let parentPos = document.getElementById('rig').getAttribute('position')
pos.add( parentPos ) pos.add( parentPos )
console.log( "from getClosestTargetElements, pos:", pos ) // relative pos, should thus remove rig position, even though it makes assumptions const matches = getClosestElements( pos, threshold, set)
const matches = getClosestTargetElements( pos, threshold)
if (matches.length > 0) res = matches[0].el if (matches.length > 0) res = matches[0].el
return res return res
} }
function getClosestTargetElement( pos, threshold=0.05 ){ // 10x lower threshold for flight mode
return getClosestElement( pos, threshold, targets)
}
// ==================================== HUD ====================================================== // ==================================== HUD ======================================================
var keyboardInputTarget = 'hud' var keyboardInputTarget = 'hud'
@ -169,7 +173,7 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
let pos = event.detail.position let pos = event.detail.position
let parentPos = document.getElementById('rig').getAttribute('position') let parentPos = document.getElementById('rig').getAttribute('position')
pos.add( parentPos ) pos.add( parentPos )
var closests = getClosestTargetElements( pos ) //var closests = getClosestTargetElements( pos )
//if (closests && closests.length > 0) // avoiding self reference //if (closests && closests.length > 0) // avoiding self reference
// setFeedbackHUD("close enough, could stack with "+ closests[1].el.getAttribute("value") ) // setFeedbackHUD("close enough, could stack with "+ closests[1].el.getAttribute("value") )
var dist = event.detail.position.distanceTo( document.querySelector("#box").object3D.position ) var dist = event.detail.position.distanceTo( document.querySelector("#box").object3D.position )
@ -180,6 +184,8 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
if (selectedElement){ if (selectedElement){
let content = selectedElement.getAttribute("value") let content = selectedElement.getAttribute("value")
selectedElement.emit('released', {element:selectedElement, timestamp:Date.now(), primary:true}) selectedElement.emit('released', {element:selectedElement, timestamp:Date.now(), primary:true})
} else {
AFRAME.scenes[0].emit('emptypinchreleased', {position:event.detail.position, timestamp:Date.now() })
} }
// unselect current target if any // unselect current target if any
selectedElement = null; selectedElement = null;
@ -219,7 +225,11 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
}) })
// rotation isn't ideal with the wrist as tend not have wrist flat as we pinch // rotation isn't ideal with the wrist as tend not have wrist flat as we pinch
} }
if (selectedElement) selectedElement.emit("moved") if (selectedElement) {
selectedElement.emit("moved")
} else {
AFRAME.scenes[0].emit('emptypinchmoved', {position:event.detail.position, timestamp:Date.now() })
}
}); });
this.el.addEventListener('pinchstarted', function (event) { this.el.addEventListener('pinchstarted', function (event) {
primaryPinchStarted = true primaryPinchStarted = true

Loading…
Cancel
Save