From a3042794db5c82aee72ae9412a510899ea987470 Mon Sep 17 00:00:00 2001 From: Fabien Benetou Date: Mon, 8 Apr 2024 12:42:36 +0200 Subject: [PATCH] basis --- index.html | 206 +------------ jxr-core.js | 841 ++++++++++++++++++++++++++++++++++++++++++++++++++ jxr-extras.js | 3 + 3 files changed, 852 insertions(+), 198 deletions(-) create mode 100644 jxr-core.js create mode 100644 jxr-extras.js diff --git a/index.html b/index.html index 80049d7..94cbe17 100644 --- a/index.html +++ b/index.html @@ -3,95 +3,16 @@ SpaSca : Spatial Scaffolding - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + -
- - - - - - - - - - - - - - - + + + + hud camera look-controls wasd-controls position="0 1.6 0"> - - - - - - + - - - - - - - - diff --git a/jxr-core.js b/jxr-core.js new file mode 100644 index 0000000..941b716 --- /dev/null +++ b/jxr-core.js @@ -0,0 +1,841 @@ +const prefix = /^jxr / +const codeFontColor = "lightgrey" +const fontColor= "white" +var selectedElement = null; +var targets = [] +const zeroVector3 = new THREE.Vector3() +var bbox = new THREE.Box3() +bbox.min.copy( zeroVector3 ) +bbox.max.copy( zeroVector3 ) +var selectionBox = new THREE.BoxHelper( bbox.object3D, 0x0000ff); +var groupHelpers = [] +var primaryPinchStarted = false +var wristShortcut = "jxr switchToWireframe()" +var selectionPinchMode = false +var groupingMode = false +var hudTextEl // should instead rely on the #typinghud selector in most cases +const startingText = "[]" +var added = [] +const maxItemsFromSources = 20 +let alphabet = ['abcdefghijklmnopqrstuvwxyz', '0123456789', '<>']; +var commandhistory = [] +var groupSelection = [] +var primarySide = 0 +const sides = ["right", "left"] +var pinches = [] // position, timestamp, primary vs secondary +var dl2p = null // from distanceLastTwoPinches +var selectedElements = []; + +// ==================================== picking ====================================================== + +AFRAME.registerComponent('target', { + init: function () { + targets.push( this.el ) + this.el.classList.add("collidable") + } +}) + +function getClosestTargetElements( pos, threshold=0.05 ){ + // TODO Bbox intersects rather than position + return targets.filter( e => e.getAttribute("visible") == true).map( t => { return { el: t, dist : pos.distanceTo(t.getAttribute("position") ) } }) + .filter( t => t.dist < threshold ) + .sort( (a,b) => a.dist > b.dist) +} + +function getClosestTargetElement( pos, threshold=0.05 ){ // 10x lower threshold for flight mode + var res = null + const matches = getClosestTargetElements( pos, threshold) + if (matches.length > 0) res = matches[0].el + return res +} + +// ==================================== HUD ====================================================== + +var keyboardInputTarget = 'hud' + +AFRAME.registerComponent('hud', { + init: function(){ + var feedbackHUDel= document.createElement("a-troika-text") + feedbackHUDel.id = "feedbackhud" + feedbackHUDel.setAttribute("value", "") + feedbackHUDel.setAttribute("position", "-0.05 0.01 -0.2") + feedbackHUDel.setAttribute("scale", "0.05 0.05 0.05") + this.el.appendChild( feedbackHUDel ) + var typingHUDel = document.createElement("a-troika-text") + typingHUDel.id = "typinghud" + typingHUDel.setAttribute("value", startingText) + typingHUDel.setAttribute("position", "-0.05 0 -0.2") + typingHUDel.setAttribute("scale", "0.05 0.05 0.05") + this.el.appendChild( typingHUDel ) + hudTextEl = typingHUDel // should rely on the id based selector now + document.addEventListener('keyup', function(event) { + if (keyboardInputTarget != 'hud') return + parseKeys('keyup', event.key) + }); + document.addEventListener('keydown', function(event) { + if (keyboardInputTarget != 'hud') return + parseKeys('keydown', event.key) + }); + } +}) + + +function appendToFeedbackHUD(txt){ + setFeedbackHUD( document.querySelector("#feedbackhud").getAttribute("value") + " " + txt ) +} + +function setFeedbackHUD(txt){ + document.querySelector("#feedbackhud").setAttribute("value",txt) + setTimeout( _ => document.querySelector("#feedbackhud").setAttribute("value","") , 2000) +} + +function appendToHUD(txt){ + const textHUD = document.querySelector("#typinghud").getAttribute("value") + if ( textHUD == startingText) + setHUD( txt ) + else + setHUD( textHUD + txt ) +} + +function setHUD(txt){ + document.querySelector("#typinghud").setAttribute("value",txt) +} + +function showhistory(){ + setFeedbackHUD("history :\n") + commandhistory.map( i => appendToHUD(i.uninterpreted+"\n") ) +} + +function saveHistoryAsCompoundSnippet(){ + addNewNote( commandhistory.map( e => e.uninterpreted ).join("\n") ) +} + +// ==================================== pinch primary and secondary ====================================================== + +AFRAME.registerComponent('pinchsecondary', { + init: function () { + this.el.addEventListener('pinchended', function (event) { + selectedElement = getClosestTargetElement( event.detail.position ) + selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:false}) + // if close enough to a target among a list of potential targets, unselect previous target then select new + if (selectedElement) interpretJXR( selectedElement.getAttribute("value") ) + selectedElement = null + }); + this.el.addEventListener('pinchmoved', function (event) { + if (selectionPinchMode){ + bbox.min.copy( event.detail.position ) + setFeedbackHUD( "selectionPinchMode updated min") + if (!bbox.max.equal(zeroVector3)) + selectionBox.update(); + } + }); + this.el.addEventListener('pinchstarted', function (event) { + if (!selectionPinchMode) bbox.min.copy( zeroVector3 ) + if (selectionPinchMode) setFeedbackHUD( "selectionPinchMode started") + }); + }, + remove: function() { + // should remove event listeners here. Requires naming them. + } +}); + +AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right one, should be switchable + +// consider instead https://github.com/AdaRoseCannon/handy-work/blob/main/README-AFRAME.md for specific poses +// or https://aframe.io/aframe/examples/showcase/hand-tracking/pinchable.js + + init: function () { + var el = this.el + this.el.addEventListener('pinchended', function (event) { + // if positioned close enough to a target zone, trigger action + // see own trigger-box component. Could use dedicated threejs helpers instead. + // https://github.com/Utopiah/aframe-triggerbox-component/blob/master/aframe-triggerbox-component.js#L66 + // could make trigger zones visible as debug mode + var closests = getClosestTargetElements( event.detail.position ) + //if (closests && closests.length > 0) // avoiding self reference + // setFeedbackHUD("close enough, could stack with "+ closests[1].el.getAttribute("value") ) + var dist = event.detail.position.distanceTo( document.querySelector("#box").object3D.position ) + if (dist < .1){ + setFeedbackHUD("close enough, replaced shortcut with "+ selectedElement.getAttribute("value") ) + wristShortcut = selectedElement.getAttribute("value") + } + if (selectedElement){ + let content = selectedElement.getAttribute("value") + selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:true}) + selectedElement.emit('released') + } + // unselect current target if any + selectedElement = null; + if ( groupingMode ) addToGroup( event.detail.position ) + selectionPinchMode = false + /* + setHUD( AFRAME.utils.coordinates.stringify( bbox.min ), + AFRAME.utils.coordinates.stringify( bbox.max ) ) + bbox.min.copy( zeroVector3 ) + bbox.man.copy( zeroVector3 ) + */ + setTimeout( _ => primaryPinchStarted = false, 200) // delay otherwise still activate on release + + var newPinchPos = new THREE.Vector3() + newPinchPos.copy(event.detail.position ) + pinches.push({position:newPinchPos, timestamp:Date.now(), primary:true}) + dl2p = distanceLastTwoPinches() + + }); + this.el.addEventListener('pinchmoved', function (event) { + // move current target if any + if (selectionPinchMode){ + bbox.max.copy( event.detail.position ) + if (!bbox.min.equal(zeroVector3)) + selectionBox.update(); + } + if (selectedElement && !groupingMode) { + selectedElement.setAttribute("position", event.detail.position) + document.querySelector("#rightHand").object3D.traverse( e => { + if (e.name == "ring-finger-tip"){ + selectedElement.object3D.rotation.copy( e.rotation ) + } + }) + // rotation isn't ideal with the wrist as tend not have wrist flat as we pinch + } + if (selectedElement) selectedElement.emit("moved") + }); + this.el.addEventListener('pinchstarted', function (event) { + primaryPinchStarted = true + if (!selectionPinchMode) bbox.max.copy( zeroVector3 ) + + //var clone = getClosestTargetElement( event.detail.position ).cloneNode() + // might want to limit cloning to unmoved element and otherwise move the cloned one + //AFRAME.scenes[0].appendChild( clone ) + //targets.push( clone ) + //selectedElement = clone + + selectedElement = getClosestTargetElement( event.detail.position ) + if (selectedElement) selectedElement.emit("picked") + // is it truly world position? See https://github.com/aframevr/aframe/issues/5182 + // setFeedbackHUD( AFRAME.utils.coordinates.stringify( event.detail.position ) ) + // if close enough to a target among a list of potential targets, unselect previous target then select new + }); + }, + remove: function() { + // should remove event listeners here. Requires naming them. + } +}); + +// avoiding setOnDropFromAttribute() as it is not idiosyncratic and creates timing issues +AFRAME.registerComponent('onreleased', { // changed from ondrop to be coherent with event name + events: { + released: function (e) { + let code = this.el.getAttribute('onreleased') + try { + eval( code ) + } catch (error) { + console.error(`Evaluation failed with ${error}`); + } + } + } +}) + +function distanceLastTwoPinches(){ + let dist = null + if (pinches.length>1){ + dist = pinches[pinches.length-1].position.distanceTo( pinches[pinches.length-2].position ) + } + return dist +} + +function groupSelectionToNewNote(){ + var text = "" + groupSelection.map( grpel => { + //removeBoundingBoxToTextElement( grpel ) + // somehow fails... + text += grpel.getAttribute("value") + "\n" + }) + groupHelpers.map( e => e.removeFromParent() ) + groupHelpers = [] + groupSelection = [] + addNewNote( text ) +} + +// ==================================== keyboard ====================================================== + +AFRAME.registerComponent('keyboard', { + init:function(){ + let generatorName = this.attrName + const horizontaloffset = .7 + const horizontalratio = 1/20 + alphabet.map( (line,ln) => { + for (var i = 0; i < line.length; i++) { + var pos = i * horizontalratio - horizontaloffset + addNewNote( line[i], pos+" "+(1.6-ln*.06)+" -.4", ".1 .1 .1", null, generatorName) + } + }) + } +}) + + +function parseKeys(status, key){ + var e = hudTextEl + if (status == "keyup"){ + if (key == "Control"){ + groupingMode = false + groupSelectionToNewNote() + } + } + if (status == "keydown"){ + var txt = e.getAttribute("value") + if (txt == "[]") + e.setAttribute("value", "") + if (key == "Backspace" && txt.length) + e.setAttribute("value", txt.slice(0,-1)) + if (key == "Control") + groupingMode = true + if (key == "Shift" && selectedElement) + e.setAttribute("value", selectedElement.getAttribute("value") ) + else if (key == "Enter") { + if ( selectedElement ){ + var clone = selectedElement.cloneNode() + clone.setAttribute("scale", "0.1 0.1 0.1") // somehow lost + AFRAME.scenes[0].appendChild( clone ) + targets.push( clone ) + selectedElement = clone + } else { + if (txt.match(prefix)) interpretJXR(txt) + // check if text starts with jxr, if so, also interpret it. + addNewNote(e.getAttribute("value")) + e.setAttribute("value", "") + } + } else { + // consider also event.ctrlKey and multicharacter ones, e.g shortcuts like F1, HOME, etc + if (key.length == 1) + e.setAttribute("value", e.getAttribute("value") + key ) + } + } +} + +// ==================================== note as text and possibly executable snippet ====================================================== + +function addNewNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=null, classes="notes", visible="true", rotation="0 0 0" ){ + var newnote = document.createElement("a-troika-text") + newnote.setAttribute("anchor", "left" ) + newnote.setAttribute("outline-width", "5%" ) + newnote.setAttribute("outline-color", "black" ) + newnote.setAttribute("visible", visible ) + + if (id) + newnote.id = id + else + newnote.id = "note_" + crypto.randomUUID() // not particularly descriptive but content might change later on + if (classes) + newnote.className += classes + newnote.setAttribute("side", "double" ) + var userFontColor = AFRAME.utils.getUrlParameter('fontcolor') + if (userFontColor && userFontColor != "") + newnote.setAttribute("color", userFontColor ) + else + newnote.setAttribute("color", fontColor ) + if (text.match(prefix)) + newnote.setAttribute("color", codeFontColor ) + newnote.setAttribute("value", text ) + //newnote.setAttribute("font", "sw-test/Roboto-msdf.json") + newnote.setAttribute("position", position) + newnote.setAttribute("rotation", rotation) + newnote.setAttribute("scale", scale) + AFRAME.scenes[0].appendChild( newnote ) + targets.push(newnote) + return newnote +} + +AFRAME.registerComponent('annotation', { +// consider also multiple annotation but being mindful that it might clutter significantly + schema: { + content : {type: 'string'} + }, + init: function () { + addAnnotation(this.el, this.data.content) + }, + update: function () { + this.el.querySelector('.annotation').setAttribute('value', this.data.content ) + // assuming single annotation + }, + remove: function () { + this.el.querySelector('.annotation').removeFromParent() + //Array.from( this.el.querySelectorAll('.annotation') ).map( a => a.removeFromParent() ) + } +}) + +function addAnnotation(el, content){ + // could also appear only when in close proximity or while pinching + let annotation = document.createElement( 'a-troika-text' ) + annotation.classList.add( 'annotation' ) + annotation.setAttribute('value', content) + annotation.setAttribute('position', '0 .1 -.1') + annotation.setAttribute('rotation', '-90 0 0') + annotation.setAttribute("anchor", "left" ) + annotation.setAttribute("outline-width", "5%" ) + annotation.setAttribute("outline-color", "black" ) + el.appendChild(annotation) + return el +} + + +function interpretAny( code ){ + + if (!code.match(/^dxr /)) return + var newcode = code + newcode = newcode.replace("dxr ", "") + //newcode = newcode.replace(/bash ([^\s]+)/ ,`debian '$1'`) // syntax delegated server side + fetch("/command?command="+newcode).then( d => d.json() ).then( d => { + console.log( d.res ) + appendToHUD( d.res ) // consider shortcut like in jxr to modify the scene directly + // res might return that said language isn't support + // commandlistlanguages could return a list of supported languages + }) +} + +function parseJXR( code ){ +// should make reserved keywords explicit. + var newcode = code + newcode = newcode.replace("jxr ", "") + newcode = newcode.replace(/(\d)s (.*)/ ,`setTimeout( _ => { $2 }, $1*1000)`) + + // qs X => document.querySelector("X") + newcode = newcode.replace(/qs ([^\s]+)/ ,`document.querySelector('$1')`) + + // sa X Y => .setAttribute("X", "Y") + newcode = newcode.replace(/ sa ([^\s]+) (.*)/,`.setAttribute('$1','$2')`) + // problematic for position as they include spaces + + newcode = newcode.replace(/obsv ([^\s]+)/ ,`newNoteFromObservableCell('$1')`) + + // TODO + // + newcode = newcode.replace(/observe ([^\s]+)/,`bindVariableValueToNewNote('$1')`) + // could proxy instead... but for now, the quick and dirty way : + + // e.g qs a-sphere sa color red => + // document.querySelector("a-sphere").setAttribute("color", "red") + + newcode = newcode.replace(/lg ([^\s]+) ([^\s]+)/ ,`addGltfFromURLAsTarget('$1',$2)`) + // order matters, here we only process the 2 params if they are there, otherwise 1 + newcode = newcode.replace(/lg ([^\s]+)/ ,`addGltfFromURLAsTarget('$1')`) + return newcode +} + +function interpretJXR( code ){ + if (!code) return + if (code.length == 1) { // special case of being a single character, thus keyboard + if (code == ">") { // Enter equivalent + content = hudTextEl.getAttribute("value") + if (Number.isFinite(Number(content))) { + loadPageRange(Number(content)); + } else { + addNewNote( content ) + } + setHUD("") + } else if (code == "<") { // Backspace equivalent + setHUD( hudTextEl.getAttribute("value").slice(0,-1)) + } else { + appendToHUD( code ) + } + } + if (!code.match(prefix)) return + var uninterpreted = code + var parseCode = "" + code.split("\n").map( lineOfCode => parseCode += parseJXR( lineOfCode ) + ";" ) + // could ignore meta code e.g showhistory / saveHistoryAsCompoundSnippet + commandhistory.push( {date: +Date.now(), uninterpreted: uninterpreted, interpreted: parseCode} ) + + console.log( parseCode ) + try { + eval( parseCode ) + } catch (error) { + console.error(`Evaluation failed with ${error}`); + } + + // unused keyboard shortcuts (e.g BrowserSearch) could be used too + // opt re-run it by moving the corresponding text in target volume +} + +function bindVariableValueToNewNote(variableName){ + // from observe jxr keyword + const idName = "bindVariableValueToNewNote"+variableName + addNewNote( variableName + ":" + eval(variableName), `-0.15 1.4 -0.1`, "0.1 0.1 0.1", idName, "observers", "true" ) + // could add to the HUD instead and have a list of these + return setInterval( _ => { + const value = variableName+";"+eval(variableName) + // not ideal for DOM elements, could have shortcuts for at least a-text with properties, e.g value or position + document.getElementById(idName).setAttribute("value", value) + }, 100 ) +} + +AFRAME.registerComponent('gltf-jxr', { + events: { + "model-loaded": function (evt) { + this.el.object3D.traverse( n => { if (n.userData.jxr) { + console.log(n.userData) + // need to make gltf become a child of a note to be executable on pinch + // try reparenting first... otherwise var clone = this.el.cloneNode(true) + // might not be great, cf https://github.com/aframevr/aframe/issues/2425 + let pos = this.el.object3D.position.clone() + let rot = this.el.object3D.rotation.clone() + this.el.remove() + + let note = addNewNote( n.userData.jxr, pos, "0.1 0.1 0.1", null, "gltf-jxr-source") + let clone = this.el.cloneNode(true) + clone.setAttribute('position', '0 0 0') + clone.setAttribute('scale', '10 10 10') // assuming not scaled until now, surely wrong + // need rescaling to current scale by 1/0.1, clone.setAttribute( + clone.removeAttribute('gltf-jxr') + note.appendChild(clone) + } + }) + }, + }, + + /* example of backend code to annotate the glTF + import { NodeIO } from '@gltf-transform/core'; + import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; + const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); + const document = await io.read('PopsicleChocolate.glb'); + const node = document.getRoot() // doesn't seem to work.listNodes().find((node) => node.getName() === 'RootNode'); + node.setExtras({jxr: "jxr addNewNote('hi')"}); + await io.write('output.glb', document); + */ +}); + + +// ==================================== interactions beyond pinch ====================================================== + +AFRAME.registerComponent('wristattachsecondary',{ + schema: { + target: {type: 'selector'}, + }, + init: function () { + var el = this.el + this.worldPosition=new THREE.Vector3(); + }, + tick: function () { + // could check if it exists first, or isn't 0 0 0... might re-attach fine, to test + // somehow very far away... need to convert to local coordinate probably + // localToWorld? + (primarySide == 0) ? secondarySide = 1 : secondarySide = 0 + var worldPosition=this.worldPosition; + this.el.object3D.traverse( e => { if (e.name == "wrist") { + worldPosition.copy(e.position);e.parent.updateMatrixWorld();e.parent.localToWorld(worldPosition) + rotation = e.rotation.x*180/3.14 + " " + e.rotation.y*180/3.14 + " " + e.rotation.z*180/3.14 + this.data.target.setAttribute("rotation", rotation) + this.data.target.setAttribute("position", + AFRAME.utils.coordinates.stringify( worldPosition ) ) + // doesnt work anymore... + //this.data.target.setAttribute("rotation", AFRAME.utils.coordinates.stringify( e.getAttribute("rotation") ) + } + }) + }, + remove: function() { + // should remove event listeners here. Requires naming them. + } +}); + +function doublePinchToScale(){ + let initialPositionSecondary + let initialScale + let elSecondary = document.querySelector('[pinchsecondary]') + elSecondary.addEventListener('pinchmoved', movedSecondary ); + function movedSecondary(event){ + if (!selectedElement) return + let scale = initialScale * initialPositionSecondary.distanceTo(event.detail.position) * 50 + selectedElement.setAttribute("scale", ""+scale+" "+scale+" "+scale+" ") + } + elSecondary.addEventListener('pinchstarted', startedSecondary ); + function startedSecondary(event){ + initialPositionSecondary = event.detail.position.clone() + if (!selectedElement) return + initialScale = AFRAME.utils.coordinates.parse( selectedElement.getAttribute("scale") ).x + } +} + +// from https://aframe.io/aframe/examples/showcase/hand-tracking/pressable.js +AFRAME.registerComponent('pressable', { + schema:{pressDistance:{default:0.06}}, + init:function(){this.worldPosition=new THREE.Vector3();this.handEls=document.querySelectorAll('[hand-tracking-controls]');this.pressed=false;}, + tick:function(){var handEls=this.handEls;var handEl;var distance;for(var i=0;i { + // cpnsider getObjectByName() instead + p.object3D.traverse( e => { if (e.name == 'index-finger-tip' ) tip = e.position }) + //index-finger-phalanx-distal + //index-finger-phalanx-intermediate + p.object3D.traverse( e => { if (e.name == 'index-finger-phalanx-proximal' ) proximal = e.position }) + p.object3D.traverse( e => { if (e.name == 'thumb-tip' ) thumb = e.position }) + let touch = {} + touch.date = Date.now() + touch.thumb2tip = thumb.distanceTo(tip) + if (!touch.thumb2tip) return + touch.thumb2proximal = thumb.distanceTo(proximal) + //console.log( touch.thumb2tip, touch.thumb2proximal ) + // usually <1cm <4cm (!) + //if ((touch.thumb2tip && touch.thumb2tip < threshold_thumb2tip) + //|| (touch.thumb2proximal && touch.thumb2proximal < threshold_thumb2proximal)) + if (touch.thumb2tip < threshold_thumb2tip + || touch.thumb2proximal < threshold_thumb2proximal){ + if (touches.length){ + let previous = touches[touches.length-1] + if (touch.date - previous.date < 300){ + if (touch.thumb2tip < threshold_thumb2tip && + previous.thumb2proximal < threshold_thumb2proximal){ + console.log('^') + p.emit('thumb2indexpull') + } + if (touch.thumb2proximal < threshold_thumb2proximal && + previous.thumb2tip < threshold_thumb2tip){ + console.log('v') + p.emit('thumb2indexpush') + } + } + } + touches.push(touch) + } + }, 50) + // TODO + // Bind thumb2indexpush/thumb2indexpull to zoom in/out "world" i.e all assets that aren't "special" e.g self, lights, UI +} + +let changeovercheck +AFRAME.registerComponent('changeover', { + schema: { color : {type: 'string'} }, + init: function () { + // (this.el, this.data.content) + if (changeovercheck) return + let player = document.getElementById('player') // assuming single player, non networked + console.log('adding timer') + changeovercheck = setInterval( _ => { + let pos = player.getAttribute('position').clone() + pos.y = 0.1 // hard coded but should be from component element + let hits = Array.from(document.querySelectorAll('[changeover]')) + .filter( e => e.getAttribute("visible") == true) + .map( t => { return { el: t, dist : pos.distanceTo(t.getAttribute("position") ) } }) + .filter( t => t.dist < 0.02 ) + .sort( (a,b) => a.dist > b.dist) + //console.log(hits.length) + if (hits.length>0) { + setFeedbackHUD('touching cone') + console.log('touching cone') + hits[hits.length-1].el.setAttribute('color', 'red') + } + }, 50) + } +}) + +// to add only on selectable elements, thus already with a target component attached +AFRAME.registerComponent('pull', { + events: { + picked: function (evt) { + this.startePos = this.el.getAttribute('position').clone() + this.starteRot = this.el.getAttribute('rotation')//.clone() not necessary as converted first + this.decimtersEl = document.createElement('a-troika-text') + AFRAME.scenes[0].appendChild(this.decimtersEl) + }, + moved: function (evt) { + let pos = AFRAME.utils.coordinates.stringify( this.startePos ) + let oldpos = AFRAME.utils.coordinates.stringify( this.el.getAttribute('position') ) + AFRAME.scenes[0].setAttribute("line__pull", `start: ${oldpos}; end : ${pos};`) + let d = this.startePos.distanceTo( this.el.getAttribute('position') ) + // could show a preview state before release, e.g + let decimeters = Math.round(d*10) + console.log('pulling '+decimeters+' pages') + // update visible value instead, ideally under line but still facing user + let textPos = new THREE.Vector3() + textPos.lerpVectors(this.startePos, this.el.getAttribute('position'), .7) + this.decimtersEl.setAttribute('position', textPos ) + this.decimtersEl.setAttribute('rotation', this.el.getAttribute('rotation') ) + this.decimtersEl.setAttribute('value', decimeters ) + }, + released: function (evt) { + let d = this.startePos.distanceTo( this.el.getAttribute('position') ) + console.log('This entity was released '+ d + 'm away from picked pos') + this.el.setAttribute('position', AFRAME.utils.coordinates.stringify( this.startePos )) + this.el.setAttribute('rotation', AFRAME.utils.coordinates.stringify( this.starteRot )) + AFRAME.scenes[0].removeAttribute("line__pull") + this.decimtersEl.remove() + }, + }, +}); +// ==================================== utils on entities and classes ====================================================== + +function toggleVisibilityEntitiesFromClass(classname){ + let entities = Array.from( document.querySelectorAll("."+classname) ) + if (entities.length == 0) return + let state = entities[0].getAttribute("visible") // assume they are all the same + if (state) + entities.map( e => e.setAttribute("visible", "false")) + else + entities.map( e => e.setAttribute("visible", "true")) +} + +function pushLeftClass(classname, value=.1){ + Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.x -= value) +} + +function pushRightClass(classname, value=.1){ + Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.x += value) +} + +function pushUpClass(classname, value=.1){ + Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.y += value) +} + +function pushDownClass(classname, value=.1){ +// can be used for accessibiliy, either directly or sampling e.g 10s after entering VR to lower based on the estimated user height + Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.y -= value) +} + +function pushBackClass(classname, value=.1){ + Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.z -= value) +} + +function pushFrontClass(classname, value=.1){ + Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.z += value) +} + +function toggleVisibilityAllGenerators(){ + generators.split(" ").map( g => toggleVisibilityEntitiesFromClass(g) ) + // not hidableassets though +} + +function toggleVisibilityAll(){ + toggleVisibilityAllGenerators() + toggleVisibilityEntitiesFromClass("hidableassets") +} + +function toggleVisibilityAllButClass(classname){ + generators.split(" ").filter( e => e != classname).map( g => toggleVisibilityEntitiesFromClass(g) ) + toggleVisibilityEntitiesFromClass("hidableassets") +} + +function switchSide(){ + // mostly works... but event listeners are not properly removed. Quickly creates a mess, low performance and unpredictable. + document.querySelector("#"+sides[primarySide]+"Hand").removeAttribute("pinchprimary") + document.querySelector("#"+sides[secondarySide]+"Hand").removeAttribute("pinchsecondary") + document.querySelector("#"+sides[secondarySide]+"Hand").removeAttribute("wristattachsecondary") + document.querySelector("#"+sides[secondarySide]+"Hand").setAttribute("pinchprimary", "") + document.querySelector("#"+sides[primarySide]+"Hand").setAttribute("pinchsecondary", "") + document.querySelector("#"+sides[primarySide]+"Hand").setAttribute("wristattachsecondary", "target: #box") + if (primarySide == 0) { + secondarySide = 0 + primarySide = 1 + } else { + primarySide = 0 + secondarySide = 1 + } +} + +function getIdFromPick(){ + let id = null + let pp = selectedElements.filter( e => e.primary ) + if (pp && pp[pp.length-1] && pp[pp.length-1].element ){ + if (!pp[pp.length-1].element.id) pp[pp.length-1].element.id= "missingid_"+Date.now() + id = pp[pp.length-1].element.id + setFeedbackHUD(id) + } + return id +} + +function getClassFromPick(){ // should be classes, for now assuming one + let classFound = null + let pp = selectedElements.filter( e => e.primary ) + if (pp && pp[pp.length-1] && pp[pp.length-1].element ){ + //if (!pp[pp.length-1].element.className) pp[pp.length-1].element.className= "missingclass" + // arguable + classFound = pp[pp.length-1].element.className + setFeedbackHUD(classFound) + } + return classFound +} + +function getArrayFromClass(classname){ + return Array.from( document.querySelectorAll("."+classname) ) +} + +function applyToClass(classname, callback, value){ +// example applyToClass("template_object", (e, val ) => e.setAttribute("scale", val), ".1 .1 .2") + getArrayFromClass(classname).map( e => callback(e, value)) +// could instead become a jxr shortcut, namely apply a set attribute to a class of entities +} + +function addDropZone(position="0 1.4 -0.6", callback=setFeedbackHUD, radius=0.11){ +// consider how this behavior could be similar to the wrist watch shortcut +// namely binding it to a jxr function + let el = document.createElement("a-sphere") + el.setAttribute("wireframe", true) + el.setAttribute("radius", radius) + el.setAttribute("position", position) + el.id = "dropzone_"+Date.now() + AFRAME.scenes[0].appendChild( el ) + let sphere = new THREE.Sphere( AFRAME.utils.coordinates.parse( position ), radius ) + // could become movable but would then need to move the matching sphere too + // could be a child of that entity + let pincher = document.querySelector('[pinchprimary]') + pincher.addEventListener('pinchended', function (event) { + if (selectedElements.length){ + let lastDrop = selectedElements[selectedElements.length-1] + if ((Date.now() - lastDrop.timestamp) < 1000){ + if (sphere.containsPoint( lastDrop.element.getAttribute("position"))){ + // should be a threejs sphere proper, not a mesh + console.log("called back" ) + callback( lastDrop.selectedElement ) + } + } + } + }) + // never unregister + return el +} + +// ==================================== facilitating debugging ====================================================== + +function makeAnchorsVisibleOnTargets(){ + targets.map( t => { + let controlSphere = document.createElement("a-sphere") + controlSphere.setAttribute("radius", 0.05) + controlSphere.setAttribute("color", "blue") + controlSphere.setAttribute("wireframe", "true") + controlSphere.setAttribute("segments-width", 8) + controlSphere.setAttribute("segments-height", 8) + t.appendChild( controlSphere ) + }) // could provide a proxy to be able to monitor efficiently +} + +function switchToWireframe(){ + let model = document.querySelector("#environment")?.object3D + if (model) model.traverse( o => { if (o.material) { + let visible = !o.material.wireframe + o.material.wireframe = visible; + o.material.opacity = visible ? 0.05 : 1; + o.material.transparent = visible; + } }) +} diff --git a/jxr-extras.js b/jxr-extras.js new file mode 100644 index 0000000..a1e82df --- /dev/null +++ b/jxr-extras.js @@ -0,0 +1,3 @@ +console.log('jxr extras to gradually add, by default could refer to branches instead then add as components proper') +console.log('arguably utils and additional interactions beyond pinche could be extras rather than core') +console.log('extra could also be empty...')