diff --git a/index.html b/index.html index 6d45781..fd0b788 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ - + @@ -41,29 +41,10 @@ function listBoundGestures(){ console.log('pinchprimary pinchsecondary wristattachsecondary="target: #box" pressable start-on-press') } -function reBindGestureTest(){ - document.querySelector("#leftHand").setAttribute("pinchsecondary", "") -} - -function unBindGestureTest(){ - document.querySelector("[pinchsecondary]").removeAttribute("pinchsecondary") - setTimeout( _ => reBindGestureTest(), 2000) -} - -function unBindGestureTestPrimary(){ - document.querySelector("[pinchprimary]").removeAttribute("pinchprimary") - setTimeout( _ => document.querySelector("#rightHand").setAttribute("pinchprimary", "") , 2000) -} - -function switchPrimarySecondaryTest(){ - console.log('hands switched') - let formerPrimary = document.querySelector("[pinchprimary]") - let formerSecondary = document.querySelector("[pinchsecondary]") - document.querySelector("[pinchprimary]").removeAttribute("pinchprimary") - document.querySelector("[pinchsecondary]").removeAttribute("pinchsecondary") - formerSecondary.setAttribute("pinchprimary", "") - formerPrimary.setAttribute("pinchsecondary", "") -} +// showcase a selector/attribute model. Maybe others do work this way. +// might be possible to have a ondrop on top of a selector as a way to manage gestures + // e.g ondrop="" on #rightHand or #leftHand could add/remove the attribute, e.g pinchprimary/pinchsecondary + // it could also remove it for any other entity if it's an exclusive attribute (can only be used once) @@ -88,9 +69,7 @@ function switchPrimarySecondaryTest(){ - - - + diff --git a/jxr-core.js b/jxr-core.js new file mode 100644 index 0000000..fa70128 --- /dev/null +++ b/jxr-core.js @@ -0,0 +1,860 @@ +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") + } + // on remove should also remove from targets, e.g targets = targets.filter( e => e != target) +}) + +function getClosestTargetElements( pos, threshold=0.05 ){ // if done frequently on large amount of targets, e.g hover on keyboard keys, consider proper structure e.g octree instead + // 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") ) } }) + // limited to local position + .map( t => { + let posTarget = new THREE.Vector3() + t.object3D.getWorldPosition( posTarget ) + let d = pos.distanceTo( posTarget ) + return { el: t, dist : d } + }) + // needs reparenting to scene via attach() otherwise lead to strange behavior + .filter( t => t.dist < threshold && t.dist > 0 ) + .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', { + events: { + 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 + }, + pinchmoved: function (event) { + if (selectionPinchMode){ + bbox.min.copy( event.detail.position ) + setFeedbackHUD( "selectionPinchMode updated min") + if (!bbox.max.equal(zeroVector3)) + selectionBox.update(); + } + }, + pinchstarted: function (event) { + if (!selectionPinchMode) bbox.min.copy( zeroVector3 ) + if (selectionPinchMode) setFeedbackHUD( "selectionPinchMode started") + }, + }, +}); + +// grouping and distance between last two pinches should be rewritten, simplified and more reliable +AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right one, should be switchable + events: { + pinchended: function (event) { + let closests = getClosestTargetElements( event.detail.position ) + let dist = 100 + if ( document.querySelector("#box") ) + 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") + selectedElement.emit('released', {element:selectedElement, timestamp:Date.now(), primary:true}) + } + // unselect current target if any + selectedElement = null; + if ( groupingMode ) addToGroup( event.detail.position ) + selectionPinchMode = false + 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() + }, + pinchmoved: function (event) { + if (selectionPinchMode){ + bbox.max.copy( event.detail.position ) + if (!bbox.min.equal(zeroVector3)) + selectionBox.update(); + } + if (selectedElement && !groupingMode) { + selectedElement.setAttribute("position", event.detail.position) + this.el.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", {element:selectedElement, timestamp:Date.now(), primary:true}) + // might be costly... + }, + pinchstarted: function (event) { + primaryPinchStarted = true + if (!selectionPinchMode) bbox.max.copy( zeroVector3 ) + + selectedElement = getClosestTargetElement( event.detail.position ) + if (selectedElement) { + selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:true}) + selectedElement.emit("picked", {element:selectedElement, timestamp:Date.now(), primary:true}) + } + } + } + // should remove event listeners +}) + +// avoiding setOnDropFromAttribute() as it is not idiosyncratic and creates timing issues +AFRAME.registerComponent('onreleased', { // changed from ondrop to be coherent with event name + schema: {default: ""}, // type: "string" forced to avoid object type guess parsing +// could support multi +// could check if target component is already present on this.el, if not, add it as it's required + events: { + released: function (e) { + let code = this.el.getAttribute('onreleased') + // 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) + // note that event details are avaible within that code as e.detail which might not be very clear + } catch (error) { + console.error(`Evaluation failed with ${error}`); + } + } + } +}) + +AFRAME.registerComponent('onpicked', { + schema: {default: ""}, // type: "string" forced to avoid object type guess parsing +// could support multi +// could check if target component is already present on this.el, if not, add it as it's required + events: { + picked: function (e) { + let code = this.el.getAttribute('onpicked') + // 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) + // note that event details are avaible within that code as e.detail which might not be very clear + } catch (error) { + console.error(`Evaluation failed with ${error}`); + } + } + } +}) + +function onNextPrimaryPinch(callback){ + // could add an optional filter, e.g only on specific ID or class + // e.g function onNextPrimaryPinch(callback, filteringSelector){} + let lastPrimary = selectedElements.filter( e => e.primary ).length + let checkForNewPinches = setInterval( _ => { + if (selectedElements.filter( e => e.primary ).length > lastPrimary){ + let latest = selectedElements[selectedElements.length-1].element + if (latest) callback(latest) + clearInterval(checkForNewPinches) + } + }, 50) // relatively cheap check, filtering on small array +} + +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(){ + // should work properly now + 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", "") + if ( document.querySelector("#box") ) + 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-postitnote.js b/jxr-postitnote.js new file mode 100644 index 0000000..cb7ef42 --- /dev/null +++ b/jxr-postitnote.js @@ -0,0 +1,29 @@ +function addNewNoteAsPostItNote( 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" ){ + let note = addNewNote( text, position, scale, id, classes, visible, rotation) + //note.setAttribute("troika-text","clipRect","100,100,100,100") + const colorSchemes = { + yellow: {light:'yellow', dark:'orange'}, + blue: {light:'cyan', dark:'blue'}, + pink: {light:'pink', dark:'red'}, + } + let selectedColor = 'yellow' + if (text.match(prefix) ) selectedColor = "blue" + note.setAttribute("troika-text","maxWidth","1") + note.setAttribute("troika-text","outlineWidth","0") + note.setAttribute("troika-text","color","black") + note.setAttribute("troika-text","anchor","left") + note.setAttribute("troika-text","baseline","top") + let backgroundEl = document.createElement("a-plane") // could curve ever so slightly + backgroundEl.setAttribute("color", colorSchemes[selectedColor].light ) + backgroundEl.setAttribute("material", "side", "double") + backgroundEl.setAttribute("position", "0.45 -0.45 -0.001") + note.appendChild(backgroundEl) + let cornerEl = document.createElement("a-triangle") + cornerEl.setAttribute("color", colorSchemes[selectedColor].dark ) + cornerEl.setAttribute("position", ".8 -.8 0") + cornerEl.setAttribute("rotation", "0 0 45") + cornerEl.setAttribute("scale", ".3 .145 1") + //backgroundEl.setAttribute("vertex-c", "0 0 -0.001") + note.appendChild(cornerEl) + return note +} diff --git a/jxr.js b/jxr.js deleted file mode 100644 index 1f78307..0000000 --- a/jxr.js +++ /dev/null @@ -1,3772 +0,0 @@ -/* -// just text -import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js"; -import define from "https://api.observablehq.com/@utopiah/from-pim-to-2d-to-3d-to-xr-explorations@2010.js?v=3"; -import define2 from "https://api.observablehq.com/d/f219f0c440c6d5a2.js?v=3"; -new Runtime().module(define, name => { - if (name === "numberOfPages") return new Inspector(document.querySelector("#observablehq-numberOfPages-835aa7e9")); - document.querySelector(".a-enter-vr").style.position = "fixed" -}); - -// HTML with interactable input -new Runtime().module(define2, name => { - if (name === "viewof offsetExample") return new Inspector(document.querySelector("#observablehq-viewof-offsetExample-ab4c1560")); - if (name === "result_as_html") return new Inspector(document.querySelector("#observablehq-result_as_html-ab4c1560")); - return ["result_no_name","result"].includes(name); -}); -// setTimeout( _ => document.querySelector("#gui3d").setAttribute("html", "html:#observablehq-key;cursor:#cursor;" ) , 2000) - // -*/ - -/* - -motion to data - - integer, e.g distance from beginning to end - - curve, sampling N points between beginning and end - -being able to use that in jxr commands, with example related to positioning entities - see https://git.benetou.fr/utopiah/text-code-xr-engine/issues/52#issuecomment-229 - -warning that selectedElement will get overwritten once executing a command by pinching - consequently in addition to have a history of executed commands - there should be a history of selected elements - and maybe their changed position states -*/ - -// motivated by https://git.benetou.fr/utopiah/text-code-xr-engine/issues/63 -var reservedKeywords = ["selectedElement", "lastPointSketch ", "commandhistory", "groupSelection", "targets", "observe", "sa", "qs"] -// see generated file reserved-keywords for more yet not sufficient, see instead parseJXR() -// should also include some documentation - -const jxrrootURL = 'https://fabien.benetou.fr/pub/home/future_of_text_demo/' -const prefix = /^jxr / -const codeFontColor = "lightgrey" -const fontColor= "white" -const wikiAsImages = "https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph.json" -const maxItems = 10 -const now = Math.round( +new Date()/1000 ) //ms in JS, seconds in UNIX epoch -const baseCachedURL = "https://vatelier.benetou.fr/MyDemo/newtooling/textures/fabien.benetou.fr_" -const baseLiveURL = "https://vatelier.benetou.fr/MyDemo/newtooling/web/renders/fabien.benetou.fr_" -const queryFormatBaseURL = "https://fabien.benetou.fr/" -const imageExtension = ".png" -const renderSuffix = "?action=serverrender" -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 visible = true -var setupMode = false -var setupBBox = {} -var wristShortcut = "jxr switchToWireframe()" -var selectionPinchMode = false -var groupingMode = false -var sketchEl -var lastPointSketch -var hudTextEl // should instead rely on the #typinghud selector in most cases -const startingText = "[]" -var drawingMode = false -var added = [] -const maxItemsFromSources = 20 -let alphabet = ['abcdefghijklmnopqrstuvwxyz', '0123456789', '<>']; -var commandhistory = [] -const savedProperties = [ "src", "position", "rotation", "scale", "value", ] // add newer properties e.g visibility and generator as class -var groupSelection = [] -var cabin //storage for load/save. Should use a better name as this is a reminder of a past version rather than something semantically useful. -const url = "https://fabien.benetou.fr/PIMVRdata/CabinData?action=" -var primarySide = 0 -const sides = ["right", "left"] -var generators = "line-link-entities link screenstack dynamic-view selectionboxonpinches keyboard " - + "commands-from-external-json glossary timeline issues web-url background-via-url observableui hidableenvironmentfot fot" -// could be an array proper completed on each relevant component registration -var heightAdjustableClasses = ["commands-from-external-json"] -var pinches = [] // position, timestamp, primary vs secondary -var dl2p = null // from distanceLastTwoPinches -var selectedElements = []; - -let editors = [] -var editorBaseWidth = 50 - -// could add a dedicated MakeyMakey mode with a fixed camera, e.g bird eye view, and an action based on some physical input that others, thanks to NAF, could see or even use. - // ?inputmode=makeymakey - -AFRAME.registerComponent('enable-components-via-url', { - init: function () { - var src = AFRAME.utils.getUrlParameter('enable-components-via-url') - if (src && src != "") { - src.split(",").map( c => { - this.el.setAttribute(c, "") - }) - } - } -}) - -AFRAME.registerComponent('disable-components-via-url', { - init: function () { - var src = AFRAME.utils.getUrlParameter('disable-components-via-url') - if (src && src != "") { - src.split(",").map( c => { - Array.from( document.querySelectorAll("["+c+"]") ).map( e => { e.removeAttribute(c) }) - }) - } - } -}) - -// e.g background https://fabien.benetou.fr/pub/home/metaverse.png might have to allow options like scale to allow for modifying both size and ratio -AFRAME.registerComponent('background-via-url', { // non interactive mode - init: function () { - let generatorName = this.attrName - var src = AFRAME.utils.getUrlParameter('background') - if (src && src != "") { - this.el.setAttribute( "visible", "true") - this.el.setAttribute( "src", src ) - this.el.className += generatorName - Array.from( document.querySelectorAll(".mural-instructions") ).map( i => { - i.setAttribute("visible", "true") - i.className += generatorName - }) - } - } -}) - -AFRAME.registerComponent('web-url', { -// e.g -// motivated by https://glitch.com/edit/#!/aframe-lil-gui?path=observablewidget.html - init: function () { - const url = "https://fabien.benetou.fr/Fabien/Principle?action=webvr" - var target = url - var src = AFRAME.utils.getUrlParameter('url') - // could also be a component parameter - var el = this.el - let generatorName = this.attrName - if (src && src != "") target = src - fetch(target).then( res => res.text() ).then( r => { - pageEl = document.createElement("div") - pageEl.id = "page" - pageEl.innerHTML = r - pageEl.style = "visibility:hidden;" - document.body.appendChild(pageEl) - el.setAttribute("html", "html:#page;cursor:#cursor;" ) - el.className += generatorName - //backdrop - const geometry = new THREE.PlaneGeometry( el.object3D.children[0].geometry.parameters.width*1.1, - el.object3D.children[0].geometry.parameters.height*1.1 ); - const material = new THREE.MeshBasicMaterial( {color: 0xffffff, side: THREE.DoubleSide} ); - const plane = new THREE.Mesh( geometry, material ); - plane.position.z = -.1 - el.object3D.add( plane ); - }) - } -}) - -function sendGlbFromEl(el){ - const gltfExporter = new THREE.GLTFExporter(); - const mesh = el.object3D - - const options = { - trs: true, - onlyVisible: true, - truncateDrawRange: false, - binary: true, - maxTextureSize: Infinity - }; - - gltfExporter.parse( - mesh, - function (result) { - if (immersClient) immersClient.sendModel("testing", new Blob([result]), "public") - console.log("sent blob") - // worked as https://immers.benetou.fr/s/639cb4171757b8382c120da1 of type model - // with glb as URL https://immers.benetou.fr/media/edf5641922e6371abb3118f56cd20b9b - }, - function (error) { - console.log('An error happened during parsing', error); - }, - options - ); -} - -var immersClient -// See dedicated issue https://git.benetou.fr/utopiah/text-code-xr-engine/issues/47 -if (false) // disabled for offline tests -document.querySelector("#immersbundle").addEventListener('load',(event) => { - immersClient = document.querySelector("immers-hud").immersClient - document.querySelector('immers-hud').immersClient.sendModel = async function sendModel (name, glb, privacy, icon, to = []) { - return this.activities.model(name, glb, icon, to, privacy) - } // shim until API update - document.querySelector("immers-hud").setAttribute("access-role", "modFull") - document.querySelector("immers-hud").immersClient.addEventListener("immers-client-connected", _ => { - //immersClient.addEventListener("immers-client-new-message", e => addNewNote(e.detail.message.messageHTML) ) - immersClient.addEventListener("immers-client-new-message", async e => { - if (e.detail.message.type == "chat"){ - let msg = ( await immersClient.activities.getObject( e.detail.message.id )) - if (msg.object.context.location ) - addNewNote( e.detail.message.messageHTML, msg.object.context.location.position , - "0.1 0.1 0.1", null, "immerschat", "true", msg.object.context.location.rotation ) - else - addNewNote( e.detail.message.messageHTML ) - // could hook on pinchended - // immersClient.place.location = { position: "0 1.5 -2", rotation: "0 190 0" }; - // immersClient.sendChatMessage(textvalue, "public"); - } - if (e.detail.message.type == "media" && e.detail.message.mediaType == "image"){ - console.log("src", e.detail.message.url) - let el = document.createElement("a-box") - el.setAttribute("position", -Math.random()+" "+Math.random()*3 + " -1") - el.setAttribute("width", ".1") - el.setAttribute("height", ".15") - el.setAttribute("depth", ".01") - el.setAttribute("src", e.detail.message.url.href) - AFRAME.scenes[0].appendChild(el) - } - if (e.detail.message.type == "other"){ - let msg = ( await immersClient.activities.getObject( e.detail.message.id )) - console.log("maybe model, see object.type.model==model", msg ) - } - }) - immersClient.friendsList().then( r => { - if (r.length>0) addNewNote( "Friends:", "-1 1.65 -0.5") - r.map( (u,i) => { - let friendData = u.profile.displayName - if (u.locationName) friendData += " at " + u.locationName - if (u.locationURL) friendData += " (" + u.locationURL + " )" - // addNewNote( friendData, "-1 " + (1.6-i/20) + " -0.5") // should make this interpretable to join there - // hidden for workshop - } ) - } ) - }) -}); - -function ims(msg){ - if (!immersClient) { setFeedbackHUD("not connected via Immers"); return; } - immersClient.sendChatMessage(msg, "public") -} // shorthand for jxr command, still requires parenthesis and quotes though, could be better to have a dedicated visual shorthand, e.g >> -// can send code too e.g immersClient.sendChatMessage("jxr loadPageRange(3,4)", "public") - -/* not sure what's the right way... but timeout works, others don't. - - immers-client-friends-update or immers-client-new-message to keep track of conversations between recurring meeting? Say you join a room, spend a working session with colleagues then leave. Could these be used to in this context to send reminders to those who subscribed to that event? -*/ - -var polys -async function getPolyList(keyword){ - //return await fetch('/search?keyword='+keyword).then( res => res.json() ).then( res => return res ) - var response = await fetch('/search?keyword='+keyword); - var polys = await response.json() - return polys -} - -// for testing purposes, disable when not local with asset caching server -//getPolyList("pizza").then( p => polys = p.results ) - -function cachePoly(res){ - var n = 0; - res.map( i => { fetch(i.Thumbnail.replace("https://static.poly.pizza/","http://localhost:7777/getpoly?id=").replace(".webp","")) } ) ; - // see await Promise.all() -} -// should properly wait. Only once all queries are done then try to load. - -function loadPolyThumbnails(res){ - var n = 0; - res.map( i => { - var el = document.createElement("a-image"); - el.setAttribute("src", i.Thumbnail.replace("https://static.poly.pizza/","http://localhost:7777polydata/")); - el.setAttribute("position", "0 1 "+n--/10); - // could instead attach e.g 9 items to the wrist using wristattachsecondary on a palette - el.setAttribute("scale", ".1 .1 .1") - el.setAttribute("loadpolyfomthumbnail", "") // that could then be used to execute on pinch based on src property - AFRAME.scenes[0].appendChild(el); - } ) -} - -function loadFirstPolyModel(res){ - var n = 0; - var i = res[n] - var el = document.createElement("a-gltf-model"); - el.setAttribute("src", i.Thumbnail.replace("https://static.poly.pizza/","http://localhost:7777polydata/").replace("webp","glb")); - el.setAttribute("position", "0 1 "+n--); - AFRAME.scenes[0].appendChild(el); - return el -} - -function loadPolyModels(res){ -// to load all models, rarely a good idea - var n = 0; - res.map( i => { - var el = document.createElement("a-gltf-model"); - el.setAttribute("src", i.Thumbnail.replace("https://static.poly.pizza/","http://localhost:7777polydata/").replace("webp","glb")); - el.setAttribute("position", "0 1 "+n--); - AFRAME.scenes[0].appendChild(el); - // optionally rescale e.g rescaleModelFromPoly(el) // probably has to wait for it to be properly loaded, cf modelHasLoaded event - // e.g rescaleModelFromPoly ( loadFirstPolyModel(polys) ) won't work - } ) -} - -function rescaleModelFromPoly(modelEl){ -// rescale to fit in 1m3 box - var bbox = new THREE.Box3().setFromObject( modelEl.object3D ); - var rescale = 1 / ( (( bbox.max.x - bbox.min.x) + (bbox.max.y - bbox.min.y) + (bbox.max.z - bbox.min.z) ) /3 ); - modelEl.setAttribute("scale", rescale+ " " + rescale + " " + rescale) - // could also leave untouched if within boundaries, e.g > 0.1 and < 1 -} - -// SYNC WITH HMD EDITS before trying this -/* - -source as URL e.g https://fabien.benetou.fr/Fabien/Principle?action=source (locally Fabien.Principle.pmwiki) - usual parsing (e.g stop words) - dedicated PmWiki cleanup e.g no URL - clean up or rather highlight (e.g color not being black) with presence from https://github.com/wordset/wordset-dictionary - could also have a short dictionnary of stop words based on popularity - e.g https://en.wikipedia.org/wiki/Most_common_words_in_English#100_most_common_words - but seems so short it might not help much, could try long and popular instead -WPM * distance travelled as metric, not just 1 - -can generate layout for keyboard as (Ctrl shortcut) copiable items to append to AR clipbard - https://twitter.com/utopiah/status/1533690234424139779 - could also use an optional type in addition to target - such items would be copied without pressing Ctrl - -see remoteSave for saving to be able to use the result outside the HMD -*/ - -/* - -use imagesFromURLs (used in screenstack) on https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph.json - and line-link-entities="target:#instructionA; source:#instructionC" between pages - -*/ - -// see https://remote-keyboard.glitch.me on how to provide a remote keyboard (no BT) for hud keydown/keyup events - // consider alt server e.g 9000-peers-peerjsserver-bxw59h3fm87.ws-eu47.gitpod.io as peerjs isn't always reliable - // to do the same offline could add to express too, cf https://github.com/peers/peerjs-server#combining-with-existing-express-app -//new Peer("2022xrkbd").on('connection', conn => conn.on('data', data => processRemoteInputData(data) )) -const altServer = "9000-peers-peerjsserver-bxw59h3fm87.ws-eu47.gitpod.io" -//new Peer("2022xrkbd", {host: altServer}).on('connection', conn => conn.on('data', data => processRemoteInputData(data) )) -// disabled for now -function processRemoteInputData( data ){ - // .status : keydown keyup pointermove - // on keydown or keyup, result un .key - // on pointermove, result un .x and .y - - // could try to throw back as an event... - parseKeys( data.status, data.key ) - if (data.status == "pointermove") parsePointer( data.x, data.y ) -} - -// for testing purposes, disable when not local with asset caching server -// SSE on a specific route to know if this file was updated, if so reload (would force leave VR) cf Inventing on Principle -/* -const source = new EventSource("streaming"); -source.onmessage = message => { - console.log(message.data) // showing the updates without manually forcing a reload - if ((JSON.parse(message.data)).status == "reload" ) location.reload(true); - } ; -*/ -// monitored server-side, index.js with fs - -/* extrusion and more generally compactness of 3D object description : - - Constructive Solid Geometry (CSG) https://openjscad.nodebb.com/topic/235/threejs-integration/11 as example of integration of JSCAD (modelling with code basically) with threejs, naively looks like an interesting intersection, ideally with spacial editing after (i.e pinching a vertex to update the resulting geometry) -*/ - -// consider PinePhone keyboard as something more usable that BT rollable - // PeerJS/WS(S) to share key.events - // would also work with iPad keyboard ... or another other device with keyboard and browser. - -// refactoring : consider pluggable execution models and targets e.g eval(), containers, Observable, etc - // right now all mashed up together so creates both complexity and security risk - -// consider also STT and translation experiment -// Codex by OpenAI (cf EP account) https://beta.openai.com/account/api-keys stored on ~/.openai-codex-test-xr used via backend -// with token e.g JWT could also consider ~/.bashrc ~/.bin or ~/Prototypes as commands - // esp. those allowing to integrate with specific hardware - -// load as page loads -// -// interactive -// - -function newNoteFromObservableCell( cell ){ - var targetEl = document.querySelector("#"+cell) - var potentialRes = document.querySelector("#observablehq-numberOfPages-835aa7e9>span") - if (potentialRes && potentialRes.children[1]){ - addNewNote( potentialRes.children[1].innerText ) - return - } - - let observer = new MutationObserver(mutationRecords => { - addNewNote( mutationRecords[0].addedNodes[0].children[1].innerText ) - }); - - observer.observe(targetEl, { - childList: true, // observe direct children - subtree: true, // and lower descendants too - characterDataOldValue: true // pass old data to callback - }) -} - -AFRAME.registerComponent('observablecell', { // non interactive mode - schema: { - targetid: {type: 'string'} - }, - init: function () { - var el = this.el - var targetEl = document.querySelector("#"+this.data.targetid) - let observer = new MutationObserver(mutationRecords => { - addNewNote( mutationRecords[0].addedNodes[0].children[1].innerText ) - }); - - observer.observe(targetEl, { - childList: true, // observe direct children - subtree: true, // and lower descendants too - characterDataOldValue: true // pass old data to callback - }); -}}) - -// might mess thing up on Quest somehow... like typing does not seem to work anymore since. - -/* -const registerServiceWorker = async () => { - if ('serviceWorker' in navigator) { - try { - const registration = await navigator.serviceWorker.register( - 'sw-test/sw.js', - { - scope: '', - } - ); - if (registration.installing) { - console.log('Service worker installing'); - } else if (registration.waiting) { - console.log('Service worker installed'); - } else if (registration.active) { - console.log('Service worker active'); - } - } catch (error) { - console.error(`Registration failed with ${error}`); - } - } -}; -*/ - -// 2 modes : interact/display (see the .hidableenvironment class) - // interact : small scale, 3D model of impact visible, keyboard visible, instruction visible - // display : changeable scale, everything but content not visible - -// could try new movements dedicated to modifying text, in particular dedicated to coding - // e.g replacing a word by an expression el sa color red => dropping qs elname on el => qs elname sa color red - -// for snapping display the current position (like now) - // and future position as transparency when within a certain radius to target + offset - // see getClosestTargetElement( pos ) when an object is already selected an moving - // only snap there if within certain distance - -// replace console with an in VR equivalent, at least during the try/catch of eval() to get some feedback - -function displaySnappablePositions( position ){ - // to show while moving an object - // has to be close enough (below threshold) -} - -function coloredBlocksFromScreens(colors, el){ - // those are NOT updated at the moment - colors.map( (u,i) => { - var e = document.createElement("a-box") - e.setAttribute("color", u.split(" ")[1]) - e.setAttribute("position",`2 1.8 -${i}`) - e.setAttribute("width","0.2") - e.setAttribute("depth","0.2") - el.appendChild(e) - }) -} - -function imagesFromURLs(urls, el, classes=null){ - urls.map( (u,i) => { - var e = document.createElement("a-image") - if (u.indexOf("http")>-1) - e.setAttribute("src", u) - else - e.setAttribute("src", `screens/${u}`) - //e.setAttribute("position",`0 1.8 -${i}`) - e.setAttribute("position",`0 1.1 -${i/50}`) // flight mode - e.setAttribute("rotation",`-30 0 0`) // flight mode - e.setAttribute("scale", ".05 .05 .05") // have to scale down here otherwise move interactions aren't good - // could instead rely on https://github.com/visjs/vis-timeline - // as previously used in https://mobile.twitter.com/utopiah/status/1110576751577567233 - e.setAttribute("width","2") - if (classes) e.className += classes - - el.appendChild(e) - targets.push(e) - }) -} - -function URLs(urls, el){ - urls.map( (u,i) => { - var e = document.createElement("a-text") - e.setAttribute("value", u.split(" ")[1]) - e.setAttribute("position",`-1 1.25 -${i}`) - // incorrect as screens (and their average color) are done per minute but URL done per change of tab - //does not help, should be a text property instead - // e.setAttribute("width","10") - e.setAttribute("text", "wrapCount","200") - // el.appendChild(e) // disabled in flight mode - }) -} - -function stringWithPositionFromCoordinates(pos){ - var el = getClosestTargetElement( pos, 0.5 ) - // loosen up the threshold as we normally pick from the top left - - // assumes a lot : - // NO rotation of the text, at all! - // single line of text - // scale only of 1 depth and uniform scaling - // left aligned - // probably only positive values - var selectedGlyph = {} - selectedGlyph.index = -1 // if we get an empty string - selectedGlyph.element = el - if (!el) return selectedGlyph - var glyph = el.object3D.children[0].geometry.visibleGlyphs - const matches = glyph.map( (t,i) => { - return { - el: el, - dist : Math.abs( pos.x - ( - el.object3D.position.x + t.position[0]/(150/el.object3D.scale.x) ) ), - index : i - } - }) - .filter( t => t.dist < 0.5 ) - .sort( (a,b) => a.dist - b.dist ) - // https://twitter.com/utopiah/status/1532766336941686784 - if (matches.length > 0) { - selectedGlyph.index = matches[0].index - } - return selectedGlyph -} - -function plot(equation,variablename="x",scale=5,step=1){ - // could delete the past one document.querySelector("#plot") - // but nice to compare curves... should rather avoid adding grids instead. - var plot = document.querySelector("#plot") - if (!plot){ - plot = document.createElement("a-entity") - targets.push(plot) // adding only once - plot.setAttribute("position", "0 1.5 -.5") // convenient position to test on desktop - plot.setAttribute("scale", ".01 .01 .01") - var idx = 0 - for (var i=-scale;i<=scale;i+=step){ - xl = `start: ${-scale} ${i} 0; end : ${scale} ${i} 0; opacity: 1;` - // weirdest "trick"... something using only `` on setAttribute produces empty string - // but indirecting once by setting a variable make the following one work?! - plot.setAttribute("line__"+ ++idx, xl) - plot.setAttribute("line__"+ ++idx, `start: ${i} ${-scale} 0; end : ${i} ${scale} 0; opacity: 1;`) - } - xaxis = `start: ${-scale} 0 0; end : ${scale} 0 0; opacity: 1; color:white;` - plot.setAttribute("line__axis_x", xaxis) - plot.setAttribute("line__axis_y", `start: 0 ${-scale} 0; end : 0 ${scale} 0; opacity: 1; color:white;`) - plot.setAttribute("line__axis_z", `start: 0 0 ${-scale}; end : 0 0 ${scale}; opacity: 1; color:white;`) - plot.id = "plot" - AFRAME.scenes[0].appendChild( plot ) - } - var previousPoint = null - var curveId = +Date.now() - idx = 0 - for (var i=-scale;i<=scale;i+=step/10){ - var pos = i + " " + eval( "x="+i +";"+ equation) + " .1" - if (previousPoint) { - plot.setAttribute("line__user"+curveId+"section"+ ++idx, 'start: ' + previousPoint+ '; end : ' + pos + '; color:red;') - } - previousPoint = pos - } - // variablename seems unused -} - -AFRAME.registerComponent('target', { - init: function () { - targets.push( this.el ) - this.el.classList.add("collidable") - } -}) - -AFRAME.registerComponent('line-link-entities', { - schema: { - source: {type: 'selector'}, - target: {type: 'selector'}, - steps: {type: 'number', default: 1}, - }, - init: function () { - let generatorName = this.attrName - setTimeout( _ => { // stupid... but works. - if (!this.data.source || !this.data.target) return - var sourcePos = this.data.source.object3D.position - var targetPos = this.data.target.object3D.position - if (!sourcePos || !targetPos) return // might not be needed anymore - // adding a gltf inside an element prevents the parent from having coordinates (fast enough?) - var step = 0 - var points = cut ([sourcePos, targetPos], 0, ++step) - points = cut (points, 0, ++step) - points = cut (points, points.length-2, step) - var el = this.el - el.className += generatorName - points.map( (p,i,arr) => { - if (arr[i+1]) - el.setAttribute("line__"+i, "start:" + AFRAME.utils.coordinates.stringify( arr[i] ) + ";end: " + AFRAME.utils.coordinates.stringify( arr[i+1] ) ) - }) - }, 100 ) // could check instead if both elements have loaded - - function cut(points, pos, step){ - var a = points[pos] - var b = points[pos+1] - var midPos = new THREE.Vector3() - midPos.copy(a).add(b).divideScalar(2) - midPos.z -= a.distanceTo(b)/(step*10) // smoothed out but axis aligned - return [...points.slice(0,pos+1), midPos, ...points.slice(pos+1)] - } - } -}); - -function tryCachedImageOtherwiseRenderLive(pages){ - let urls = [] - pages.map( i => { - let cached = baseCachedURL + i.group + "_" + i.name + imageExtension - urls.push( cached ) - fetch( cached).then( res => res.status ).then( r => { if (r==404) - // console.log("try to get new one", r, cached) - replaceCachedImageByLive(i.group, i.name) - } ) - }) - return urls -} - -function replaceCachedImageByLive(group, name){ - const live = baseLiveURL+group+"_"+name+imageExtension - fetch( live ).then( res => res.status ).then( r => { - if (r==200) - // check if in the "new" cache before doing a live query first - document.querySelector("[src='"+baseCachedURL+group+"_"+name+imageExtension+"']") - .setAttribute("src", live) - else - fetch( queryFormatBaseURL+group+"/"+name+renderSuffix ).then( res => res.json() ) - .then( document.querySelector("[src='"+baseCachedURL+group+"_"+name+imageExtension+"']") - .setAttribute("src", live) - ) - } ) -} - -AFRAME.registerComponent('screenstack', { -// this could be potentially be replaced with web-url - init: function () { - //load() - //if (cabin && cabin.length > 0) return // test doesn't seem to work well on new page / 1st load - // see CEREMA project, seems to handle caching better - var el = this.el - let generatorName = this.attrName - fetch(wikiAsImages).then(response => response.json()).then(data => - imagesFromURLs( - tryCachedImageOtherwiseRenderLive( - Object.entries(data.Nodes).map(( [k, v] ) => { return {group:v.Group, name:v.Label} } ).slice(0,maxItems) - ) - , el, generatorName ) - ) - // example time sorting - /* fetch('/screens').then(response => response.json()).then(data => console.log( - data.files.filter( i => i.indexOf("_000") < 0 ).map( i => Number(i.replace(".png", "")) ).filter( i => i > now-60 ) - ) ) - */ - - // works only locally for privacy reasons. - //fetch('colors.txt').then(response => response.text()).then(data => coloredBlocksFromScreens(data.split("\n").splice(-maxItems), el)) - // timings should match as colors are generated from the screens - - //fetch('currentURL.txt').then(response => response.text()).then(data => URLs(data.split("\n").splice(-maxItems), el )) - // could slice the array based on dates and e.g limit on current day or last 24hrs - } -}); - -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 -} - -/* -alternatively could looks for the intersecting bounding boxes of all targets -then from those pick the closest one (again based on center) - -Both work well... but without any depth/thickness the chance of intersection are null... -extruding the plane to a volume on a known axis, e.g z here (no rotation) is trivial but limiting. - -Could rely on the bounding sphere instead but not ideal when text is beside other pieces of text - -Note that in practice we plan to bring geometry with the text, a la Scratch, to showcase grammar and potential to combine. -Consequently those problems would probably go away by intersecting with that geometry instead. - -That still means an efficient solution, e.g convex hull or BVH -*/ - -// e.g addBackgroundBoxToTextElements( targets.filter( e => e.localName == "a-text" ) ) -// note that background is "just" for the user in the sense that an invisible bounding box is enough for interactions -function addBackgroundBoxToTextElements( textElements ){ - textElements.map( el => { - - addBoundingBoxToTextElement( el ) - - var bbox = new THREE.Box3().setFromObject( el.object3D.children[0] ) - // the text element itself has no volume whereas its first children is a mesh - var scale = el.getAttribute("scale").x // assume being uniform - var box = document.createElement("a-box") - el.appendChild( box ) - box.setAttribute("width", (bbox.max.x - bbox.min.x) * 1/scale) - box.setAttribute("height", (bbox.max.y - bbox.min.y) * 1/scale) - box.setAttribute("depth", scale) - box.setAttribute("position", (bbox.max.x - bbox.min.x) * (1/scale) / 2 + " 0 " + -scale/2+0.0001 ) - - setTimeout( _ => { // AFrame induced delay - // should check instead how to make sure an object is indeed created. - // maybe via el.getObject3D() - var compoundbbox = new THREE.Box3().setFromObject( box.object3D ) - compoundbbox.expandByObject( el.object3D ) - var helperbox = new THREE.BoxHelper( box.object3D, 0x00ff00); - helperbox.update(); - AFRAME.scenes[0].object3D.add(helperbox); - /* - var helpercompound = new THREE.BoundingBoxHelper( compoundbbox, 0x0000ff); - helpercompound.update(); - AFRAME.scenes[0].object3D.add(helpercompound); - */ - }, 100) - }) -} - -function addBoundingBoxToTextElement( el ){ - var meshEl = el.object3D.children.filter( e => (e.type == "Mesh") )[0] - var helper = new THREE.BoxHelper(meshEl, 0xff0000); - // otherwise doesn't work with icon... - helper.update(); - AFRAME.scenes[0].object3D.add(helper); - el.setAttribute("box-uuid", helper.uuid ) - groupHelpers.push( helper ) -} - -function removeBoundingBoxToTextElement( el ){ - var uuid = el.getAttribute("box-uuid") - el.removeAttribute("box-uuid") - //AFRAME.scenes[0].object3D.traverse( e => { if (e.uuid == uuid) e.removeFromParent() }) - //AFRAME.scenes[0].object3D.traverse( e => { if (e.uuid == uuid) AFRAME.scenes[0].object3D.remove(e) }) - AFRAME.scenes[0].object3D.traverse( e => { console.log(e.uuid == uuid) }) - AFRAME.scenes[0].object3D.traverse( e => { if (e.uuid == uuid) console.log("found", e)}) - // somehow removing did work before ... -} - -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 ) -} - -function addToGroup( position ){ - var el = getClosestTargetElement( position ) - if (!el) return - groupSelection.push( el ) - addBoundingBoxToTextElement( el ) -} - -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) -} - -AFRAME.registerComponent('waistattach',{ - schema: { - target: {type: 'selectorAll'}, - }, - init: function () { - var el = this.el - this.worldPosition=new THREE.Vector3(); - }, - tick: function () { - var worldPosition=this.worldPosition; - worldPosition.copy(this.el.object3D.position);this.el.object3D.parent.updateMatrixWorld();this.el.object3D.parent.localToWorld(worldPosition) - Array.from( this.data.target ).map( t => { - t.object3D.position.x = worldPosition.x - t.object3D.position.z = worldPosition.z - }) - }, -}); - -AFRAME.registerComponent('attach',{ - schema: { - target: {type: 'selector'}, - }, - init: function () { - var el = this.el - this.worldPosition=new THREE.Vector3(); - }, - tick: function () { - var worldPosition=this.worldPosition; - worldPosition.copy(this.el.position); - this.el.parent.updateMatrixWorld(); - this.el.parent.localToWorld(worldPosition) - rotation = this.el.rotation.x*180/3.14 + " " + this.el.rotation.y*180/3.14 + " " + this.el.rotation.z*180/3.14 - this.data.target.setAttribute("rotation", rotation) - this.data.target.setAttribute("position", - AFRAME.utils.coordinates.stringify( worldPosition ) ) - }, - remove: function() { - } -}); - -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. - } -}); - -AFRAME.registerComponent('pinchsecondary', { - events: { - 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 - if (setupMode) setupBBox["B"] = event.detail.position - if ( setupBBox["A"] && setupBBox["B"] ) { - setupMode = false - setFeedbackHUD( JSON.stringify(setupBBox)) - } - /* - selectionPinchMode = false - setHUD( AFRAME.utils.coordinates.stringify( bbox.min ), - AFRAME.utils.coordinates.stringify( bbox.max ) ) - bbox.min.copy( zeroVector3 ) - bbox.man.copy( zeroVector3 ) - */ - }, - pinchmoved: function (event) { - if (selectionPinchMode){ - bbox.min.copy( event.detail.position ) - setFeedbackHUD( "selectionPinchMode updated min") - if (!bbox.max.equal(zeroVector3)) - selectionBox.update(); - } - }, - pinchstarted: function (event) { - if (!selectionPinchMode) bbox.min.copy( zeroVector3 ) - if (selectionPinchMode) setFeedbackHUD( "selectionPinchMode started") - } - } -}); - -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 - - events: { - 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") - if (content && immersClient && immersClient.connected){ - immersClient.place.location = { - position: AFRAME.utils.coordinates.stringify(event.detail.position), - rotation: AFRAME.utils.coordinates.stringify( selectedElement.getAttribute("rotation") ) - }; - immersClient.sendChatMessage(content, "public"); - } - selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:true}) - selectedElement.emit('released') - } - // unselect current target if any - selectedElement = null; - save() - if (setupMode) setupBBox["A"] = event.detail.position - // somehow keeps on setting up... shouldn't once done. - if ( setupBBox["A"] && setupBBox["B"] ) { - setupMode = false - setFeedbackHUD( JSON.stringify(setupBBox)) - } - if ( drawingMode ) draw( event.detail.position ) - 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() - }, - 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) - this.el.object3D.traverse( e => { // should be hand independant - //document.querySelector("#rightHand").object3D.traverse( e => { // TODO this should be hand independant - 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") - }, - 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 - } - } -}); - -// testing on desktop -function switchToWireframe(){ - visible = !visible - /* - targets.map( e => { - scale = 50// should be a variable instead - e.setAttribute("scale", visible ? ".05 .05 .05" : ".1 .1 .1" ) - var pos = AFRAME.utils.coordinates.parse( e.getAttribute("position") ) - //visible ? pos.z *= scale : pos.z /= scale // might be the opposite but anyway give the principle - e.setAttribute("position", AFRAME.utils.coordinates.stringify(pos)) - // should actually be just for src, not for text notes... even though could be interesting - }) - */ - var model = document.querySelector("#environment").object3D - model.traverse( o => { if (o.material) { - o.material.wireframe = visible; - o.material.opacity = visible ? 0.05 : 1; - o.material.transparent = visible; - } }) -} - -// add (JXR) shortcuts as PIM function from e.g https://observablehq.com/@utopiah/from-pim-to-2d-to-3d-to-xr-explorations - // allowing to search within PIM then show manipulable pages as preview. - -// note that can be tested in VR also as jxr switchToWireframe() - // could make for nice in VR testing setup as eved notes - -function enterSetupMode(){ - // rely on 2 pinches to create a bounding box of safe interaction - // https://threejs.org/docs/#api/en/math/Box3.containsBox - setupMode = true -} - -AFRAME.registerComponent('start-on-press', { - // should become a property of the component instead to be more flexible. - init: function(){ - var el = this.el - this.el.addEventListener('pressedended', function (event) { - if (!primaryPinchStarted && wristShortcut.match(prefix)) interpretJXR(wristShortcut) - // other action could possibly based on position relative to zones instead, i.e a list of bbox/functions pairs - }) - } -}) -//---- other components : ------ -// could become like https://twitter.com/utopiah/status/1264131327269502976 - // can include a mini typing game to warm up finger placement - -function distanceLastTwoPinches(){ - let dist = null - if (pinches.length>1){ - dist = pinches[pinches.length-1].position.distanceTo( pinches[pinches.length-2].position ) - } - return dist -} - -function startSelectionVolume(){ - selectionPinchMode = true - // see setupBBox in pinchprimary and pinchsecondary - // then addBoundingBoxToTextElement() -} -// note that the bbox with vertical position model is still interesting - // (if within bounding box, try to execute code) - // because it allows grouping and sequentially rather executing line by line - // see https://threejs.org/docs/#api/en/math/Box3.containsBox - -// save pose of targets and src locally and if available on PIM -/* -savingJSON = targets.map( e => { - rot : e.getAttribute("rotation"), - pos : e.getAttribute("position"), - scale : e.getAttribute("scale"), - src : e.getAttribute("src"), - value : e.getAttribute("value"), -}) -*/ -// load alt set of items e.g from https://observablehq.com/@utopiah/from-pim-to-2d-to-3d-to-xr-explorations - // or https://fabien.benetou.fr/pub/home/pimxr-experimentation/sources.json - -// position should be configurable as rotation is handled by the OS - -function parsePointer( x,y ){ - console.log(x,y) - if (!sketchEl) { - sketchEl = document.createElement("a-entity") - // sketchEl.setAttribute("position", "0 1.4 -0.3") otherwise lines don't align - // could counter that offset but might be problematic later on with translations/rotations - targets.push( sketchEl ) - AFRAME.scenes[0].appendChild(sketchEl) - } - var el = document.createElement("a-sphere") - var pos = x/1000 + " " + y/1000 + " 0" - // should offset and flip properly - el.setAttribute("position", pos) - el.setAttribute("radius", 0.01) - el.setAttribute("color", "green") - sketchEl.appendChild( el ) - if (lastPointSketch){ - var oldpos = AFRAME.utils.coordinates.stringify( lastPointSketch.getAttribute("position") ) - sketchEl.setAttribute("line__"+ Date.now(), `start: ${oldpos}; end : ${pos};`) - } - lastPointSketch = el - -} - -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 ) - } - save() - } -} - -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 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_" + Date.now() // 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 -} - -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 - }) -} - -var pastPoints = [] -function draw( position ){ - let drawingMoment = +Date.now() // might not be fast enough to get a UUID - let uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2); - // from https://stackoverflow.com/a/44078785/1442164 - let pos = AFRAME.utils.coordinates.stringify( position ) - // add sphere per point - let el = document.createElement("a-sphere") - let drawing - el.setAttribute("position", pos) - el.setAttribute("radius", 0.001) - el.setAttribute("color", "lightblue") - el.setAttribute("dateadded", drawingMoment ) - // if previous point exist, draw line between both - if (pastPoints.length) { - let previousPoint = pastPoints[pastPoints.length-1] // should check time, e.g max 1s - drawing = previousPoint.element.parentElement - let oldpos = AFRAME.utils.coordinates.stringify( previousPoint.position ) - drawing.setAttribute("line__"+ uniqueId, `start: ${oldpos}; end : ${pos};`) - } else { - drawing = document.createElement("a-entity") - drawing.className = "drawing" - AFRAME.scenes[0].appendChild( drawing ) - } - drawing.appendChild( el ) - // if sufficiently close to another sphere (the first) close the loop - if (pastPoints.length>1) { - let lastPoint = pastPoints[pastPoints.length-1] - let oldpos = AFRAME.utils.coordinates.stringify( lastPoint.position ) - let lastPosV3 = new THREE.Vector3().copy( lastPoint.position ) - if (lastPosV3.distanceTo( position ) < 0.1) // threshold - drawing.setAttribute("line__"+ drawingMoment + "_closeloop", `start: ${oldpos}; end : ${pos};`) - // then enter extrude mode (assume they are on 1 plane) - // should also prevent from adding points to the current drawing - // before investing too much effort in this, should consider how it would actually improve usage - // especially as we can add AFrame primitives with the keyboard - // intersection of kbd and hand tracked 6DoF being the primary usage - } - - pastPoints.push({position:pos, element:el, timestamp: drawingMoment}) - /* test values, must wait .1 second between otherwise there is no known position - (most likely AFrame to threejs delay) - draw( new THREE.Vector3(-0.04, 1.7, -1) ); - draw( new THREE.Vector3(0.04, 1.7, -1) ); - draw( new THREE.Vector3(0, 1.72, -1) ); - */ - - -} - -// the goal is to associate objects as shape with volume to code snippet -function addGltfFromURLAsTarget( url, scale=1, position="0 1.7 -0.3" ){ - var el = document.createElement("a-entity") - AFRAME.scenes[0].appendChild(el) - el.setAttribute("gltf-model", url) - el.setAttribute("position", position) - el.setAttribute("scale", scale + " " + scale + " " + scale) - targets.push(el) - - return el - // consider https://sketchfab.com/developers/download-api/downloading-models/javascript -} - -function showhistory(){ - setFeedbackHUD("history :\n") - commandhistory.map( i => appendToHUD(i.uninterpreted+"\n") ) -} - -function saveHistoryAsCompoundSnippet(){ - addNewNote( commandhistory.map( e => e.uninterpreted ).join("\n") ) -} - -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 ) -} - -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 -} - -AFRAME.registerComponent('toolbox', { // ununsed - init: function(){ - var el = this.el - var e = document.createElement("a-sphere") - e.setAttribute("scale", "0.1 0.1 0.1") - e.setAttribute("color", "lightblue") - e.setAttribute("pressable") - e.id = "toolboxsphere" - el.appendChild( e ) - var e = document.createElement("a-cylinder") - e.setAttribute("scale", "0.1 0.1 0.1") - e.setAttribute("color", "darkred") - e.setAttribute("pressable") - e.id = "toolboxcylinder" - el.appendChild( e ) - var e = document.createElement("a-box") - e.setAttribute("scale", "0.1 0.1 0.1") - e.setAttribute("color", "pink") - e.setAttribute("pressable") - e.id = "toolbox" - el.appendChild( e ) - }, - tick: function(){ - var toolbox = document.querySelector("#toolbox") - var cam = document.querySelector("[camera]") - toolbox.object3D.position.x = cam.getAttribute("position").x-0.5 - toolbox.object3D.position.z = cam.getAttribute("position").z+0.2 - //toolbox.object3D.rotation.y = cam.getAttribute("rotation").y - } -}) - -// 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 { - 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) - } - }) - } -}) - -AFRAME.registerComponent('capturegesture', { - init:function(){this.handEls=document.querySelectorAll('[hand-tracking-controls]');}, - tick:function(){ - document.querySelector("#rightHand").object3D.traverse( e => { if (e.name == "b_r_wrist") console.log("rw", e.rotation) }) - document.querySelector("#leftHand" ).object3D.traverse( e => { if (e.name == "b_l_wrist") console.log("rl", e.rotation) }) - // should look up thumb-metacarpal and index-finger-metacarpal if not sufficient - // might trickle down iif wrist rotation itself is already good - // https://immersive-web.github.io/webxr-hand-input/ - } -}); - -AFRAME.registerComponent('timeline', { - init:function(){ - let generatorName = this.attrName - fetch("../content/fot_timeline.json").then(res => res.json() ).then(res => { - res.fot_timeline.slice(0,maxItemsFromSources).map( (c,i) => addNewNote( c.year+"_"+c.event, "1 "+i/10+" -1", ".1 .1 .1", null, generatorName) ) - }) - }, -}); - -AFRAME.registerComponent('glossary', { - init:function(){ - let generatorName = this.attrName - fetch("content/glossary.json").then(res => res.json() ).then(res => { - Object.values(res.entries).slice(0,maxItemsFromSources).map( (c,i) => addNewNote( c.phrase + c.entry.slice(0,50)+"..." , "-1 "+i/10+" -1", ".1 .1 .1", null, generatorName) ) - }) - }, -}); - -AFRAME.registerComponent('fot', { - init:function(){ - this.tick = AFRAME.utils.throttleTick(this.tick, 500, this); - }, - tick: function(){ - let generatorName = this.attrName - fetch("https://fabien.benetou.fr/PIMVRdata/FoT?action=source#" + Date.now()).then(res => res.text() ).then(res => { - res.split("\n").slice(0,maxItemsFromSources).map( (n,i) => { - found = added.find((str) => str === n) - if (typeof found === 'undefined'){ - added.push(n) - addNewNote( n, "-1 "+(1+i/10)+" -2.5", ".1 .1 .1", null, generatorName ) - } - }) - }) - } -}); - -AFRAME.registerComponent('issues', { - init:function(){ - let generatorName = this.attrName - // fetch("https://api.github.com/repos/Utopiah/relax-plus-think-space/issues").then(res => res.json() ).then(res => { - fetch("https://git.benetou.fr/api/v1/repos/utopiah/text-code-xr-engine/issues").then(res => res.json() ).then(res => { - res.slice(0,maxItemsFromSources).map( (n,i) => addNewNote( n.title, "0 "+(1+i/10)+" -1.8", ".1 .1 .1", null, generatorName ) ) - }) - }, -}); - -AFRAME.registerComponent('dynamic-view', { - init:function(){ - let generatorName = this.attrName - fetch("content/DynamicView.json").then(res => res.json() ).then(res => { - res.nodes.slice(0,maxItemsFromSources).map( n => addNewNote( n.title, "" + res.layout.nodePositions[n.identifier].x/100 + " " + res.layout.nodePositions[n.identifier].y/100 + " -1", ".1 .1 .1", null, generatorName ) ) - }) - }, -}); - -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") -} - -AFRAME.registerComponent('adjust-height-in-vr', { - init: function(){ - AFRAME.scenes[0].addEventListener("enter-vr", _ => { - setTimeout( _ => { // getting the value right away returns 0, so short delay - userHeight = document.querySelector("#player").object3D.position.y - // assume the user does not change, some might prefer to use standing up first then sit down. - // otherwise explicit controls - heightAdjustableClasses.map( c => { - max = Math.max.apply(null, Array.from( document.querySelectorAll("."+c) ).map( e => e.object3D.position.y) ) - min = Math.min.apply(null, Array.from( document.querySelectorAll("."+c) ).map( e => e.object3D.position.y) ) - pushDownClass(c, userHeight - (max-min)/2 ) - setFeedbackHUD( "adjusted height by:" + ( userHeight - (max-min)/2 ) ) - } ) - }, 100 ) - }) - } -}) - -AFRAME.registerComponent('commands-from-external-json', { -/* -// following discussion with Yanick - -// for cabin.html to faciliate growth and flexibility - -fetch('./commands.json') // then load like A-Frame elements -// e.g command //{name,defaultpose,autorun,description} -// could even be stored as wiki page - -fetch('./templates.json') -// e.g [commands with optional pose] - -// watch can be a template too - - - - - - -*/ - init:function(){ - var el = this.el - let generatorName = this.attrName - var links = [ // could be in the commands file instead - "target:#instructionA; source:#instructionB", - "target:#instructionA; source:#instructionC", - "target:#instructionA; source:#instructionD", - ] - links = [] - //fetch("commands.json").then(res => res.json() ).then(res => { - var commandsURL = "https://fabien.benetou.fr/PIMVRdata/CabinCommands?action=source" - commandsURL = "https://fabien.benetou.fr/PIMVRdata/EngineSequentialTutorialCommands?action=source" // new default - var src = AFRAME.utils.getUrlParameter('commands-url') - if (src && src != "") commandsURL = src - fetch(commandsURL).then(res => res.json() ).then(res => { - // to consider for remoteLoad/remoteSave instead, to distinguish from url though. - // also potential security concern so might insure that only a specific user, with mandatory password access, added commands. - var visible = true - if (c.visible) visible = c.visible - res.map( c => addNewNote( c.value, c.position, c.scale, c.id, generatorName, c.visible) ) - // missing name/title, autorun (true/false), description, 3D icon/visual, visiblity (useful for sequential tutorial) - links.map( l => { var linkEl = document.createElement("a-entity"); - linkEl.setAttribute("line-link-entities", l) - el.appendChild(linkEl) - } ) - var hideRest = AFRAME.utils.getUrlParameter('commands-hide-rest') - if (hideRest && hideRest != "") setTimeout( _ => toggleVisibilityAllButClass('commands-from-external-json'), 5000) // waiting for everything to have loaded... - }) - }, -}); - -function save(){ - var data = targets.map( e => { return { - localname: e.localName, - src: e.getAttribute("src"), - position: e.getAttribute("position"), - rotation: e.getAttribute("rotation"), - scale: e.getAttribute("scale"), - value: e.getAttribute("value"), - } } ) - cabin = data - localStorage.setItem('cabin', JSON.stringify( data) ) - return data - // could be called on page exit, unsure if reliable in VR - // alternatively could be call after each content is moved or created -} - -function load(){ - if (localStorage.getItem('cabin')) - cabin = JSON.parse(localStorage.getItem('cabin')) - cabin.map( e => { - var newel = document.createElement(e.localname) - savedProperties.map( p => { - if (e[p] ) newel.setAttribute(p, e[p]) - }) - AFRAME.scenes[0].appendChild( newel ) - }) -} - -function remoteLoad(){ - fetch(url+'source') - .then( response => { return response.json() } ) - .then( data => { console.log("remote data:", data) }) - // does actually load back. Should consider what load() does instead. - - // for the reMarkable write back in source, OCR/HWR could be done on the WebXR device instead - // alternatively "just" sending the .jpg thumbnail would be a good enough start - // note that highlights are also JSON files - // both might not be ideal directly in the original JSON but could be attachement as URLs -} - -function remoteSave(){ - fetch(url+'edit', { - method: 'POST', - headers: {'Content-Type':'application/x-www-form-urlencoded'}, - body: "post=1&author=PIMVR&authpw=edit_password&text="+JSON.stringify( cabin ) - }).then(res => res).then(res => console.log("saved remotely", res)) -} - -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 cloneAndDistribute(){ - el = document.querySelector("a-box[src]") // page - // trying instead to rely on previously selected matching element and dl2p - - // lack visual feedback to show what is indeed lastly selected or the distance found - //el = selectedElements[selectedElements.length-2] // not current command - times = Math.floor(dl2p*10) // also assume it's been done properly - if (times < 2) times = 7 - - offset = .5 - for (var i = 0; i < times ; i++) { // equivalent of Blender array modifier - let newEl = el.cloneNode() - AFRAME.scenes[0].appendChild(newEl) // takes time... - setTimeout( setZ, 100, {el: newEl, z: -1-i*offset} ) - newEl.addEventListener('hasLoaded', function (event) { - //this.object3D.position.z = i*offset - console.log("loaded") // doesnt seem to happen - }) - } - - function setZ(params){ - params.el.object3D.position.z = params.z - } -} - -function loadPageRange(start=1, end=-1, startPosition={x:0, y:1.3, z:-.7}, stepVector={x:.2, y:0, z:0}){ - const baseURL = "https://fabien.benetou.fr/pub/home/future_of_text_demo/content/book_as_png/gfg_d-" - const extension = ".png" - - // assumes portrait A4-ish - var rootEl = AFRAME.scenes[0] - if (end<0) end = start - - let step = 0 - for (let i=start; i<=end; i++){ - step++ - let el = document.createElement("a-box") - el.setAttribute("target", true) - //el.setAttribute("position", ""+ step/5+ " 1.3 -.7") // could be based on selectedElements last position instead - let pos = "" + startPosition.x+stepVector.x*step + " " + startPosition.y+stepVector.y*step + " " + startPosition.z+stepVector.z*step - el.setAttribute("position", pos) - // layout system could be parametric, e.g over x or y or z or another system - el.setAttribute("width", ".1") - el.setAttribute("height", ".15") - el.setAttribute("depth", ".01") - pageNumber = i - if (pageNumber<10) pageNumber = "0"+pageNumber - if (pageNumber<100) pageNumber = "0"+pageNumber - el.setAttribute("src", baseURL+pageNumber+extension) - el.setAttribute("pagenumber", pageNumber) - el.id = pageNumber + "_" + Date.now() - rootEl.appendChild(el) - let posInterface = "" + startPosition.x+stepVector.x*step + " " + startPosition.y+1+stepVector.y*step + " " + startPosition.z+stepVector.z*step - let UI = addNewNote("jxr nextPage('"+el.id+"')", posInterface, "0.1 0.1 0.1", el.id+"_interface") - //el.setAttribute("attach","target:#"+el.id+"_interface") - } -} - -function writeWebDAV(){ - const webdavurl = "https://webdav.benetou.fr"; - const client = window.WebDAV.createClient(webdavurl) - async function w(path = "/file.txt"){ return await client.putFileContents(path, "SpaSca test"); } - w("/fot.txt") // need new permissions -} - -function getPagesFromWebDAV(){ - const webdavurl = "https://webdav.benetou.fr"; - const client = window.WebDAV.createClient(webdavurl) - async function getDirectory(path = "/"){ return await client.getDirectoryContents(path); } - getDirectory("book_as_png").then( d => d.sort( (a,b) => (a.filename>b.filename)).slice(0,10).map( (c,i) => addPageFromURL(webdavurl+c.filename))) -} - -function addPageFromURL(url){ - if (url.indexOf(".png")<0) return - let el = document.createElement("a-box") - el.setAttribute("position", -Math.random()+" "+Math.random()*3 + " -1") - el.setAttribute("width", ".1") - el.setAttribute("height", ".15") - el.setAttribute("depth", ".01") - el.setAttribute("src", url) - AFRAME.scenes[0].appendChild(el) - return el -} - -function getModelsFromWebDAV(){ - const webdavurl = "https://webdav.benetou.fr"; - const client = window.WebDAV.createClient(webdavurl) - async function getDirectory(path = "/"){ return await client.getDirectoryContents(path); } - getDirectory("models").then( d => d.sort( (a,b) => (a.filename>b.filename)).slice(0,10).map( (c,i) => addModelFromURL(webdavurl+c.filename))) -} - -function addModelFromURL(url){ - return addNewNote("jxr lg "+url+ " 0.001", -Math.random()+" "+Math.random()*3 + " -1") - // should try boxing it instead in 1m3 -} - -// same principle to go from nextPage() to openingLinkedPages() from wiki URL - // consider screenstack, could add a note to mode further - -function loadWikiAsGraph(){ - fetch(wikiAsImages).then(response => response.json()).then(data => { - Object.entries(data.Nodes).slice(0,maxItems).map( v => { - let pageName = v[0] - let targest = v[1].Targets - let el = addPageFromURL(baseLiveURL+pageName.replace(".","_")+imageExtension) - el.id = pageName - el.classname = "wikipage" - // should rely on tryCachedImageOtherwiseRenderLive(pages) instead - setTimeout( _ => { - let pos = el.getAttribute("position") - let UI = addNewNote("jxr openFromNode('"+el.id+"')", pos, "0.1 0.1 0.1", el.id+"_interface") - console.log("should add: addNewNote('jxr openNewNode("+pageName+")')") - }, 100 ) // wait for the entity to be actually added - // to be coupled with loadCodeFromPage() - // see also the idea that each wiki page wouldn't just be descriptive but also have code - // related pages - // https://fabien.benetou.fr/Fabien/Principle - // https://fabien.benetou.fr/CognitiveEnvironments/CognitiveEnvironments - // https://fabien.benetou.fr/Cookbook/Cognition - }) - }) -} - -function nextPage(id){ -console.log("nextpage()") - // assuming only direct parent for now - const baseURL = "https://fabien.benetou.fr/pub/home/future_of_text_demo/content/book_as_png/gfg_d-" - const extension = ".png" - let pageNumber = Number( id.split("_")[0] ) -console.log(pageNumber+1) - loadPageRange(pageNumber+1) -} - -function loadCodeFromPage(url="https://fabien.benetou.fr/Analysis/BeyondTheCaseAgainstBooks?action=source"){ - // alternatively could load from a page number - fetch(url) - .then( r => r.text() ) - .then(data => { - let code = data.split("\n").filter( l => (l.slice(0,2) == "[@") )[0].slice(2).slice(0,-2); - // example as PmWiki parsing - eval(code) - } ) -} - -function loadFromMastodon(statusesURL="https://mastodon.pirateparty.be/api/v1/accounts/56066/statuses"){ - fetch(statusesURL).then( r => r.json() ).then( t => t.filter( i => i.in_reply_to_id == null ).map( (i,n) => { - let div = document.createElement("div") - div.innerHTML = i.content - addNewNote(div.innerText, "1 "+ (1.2+(n+1)/20) +" -0.4") - } ) ) -} - -const tile_extension = ".glb" - -// could become a dedicated asset sets, e.g asset-metadata.json in that directory -const available_asset_kits = [ - { - tile_URL : "../content/asset_kits/KenneyHexTiles/", - tiles : ["building_cabin", "building_castle", "building_dock", "building_farm", "building_house", "building_market", "building_mill", "building_mine", "building_sheep", "building_smelter", "building_tower", "building_village", "building_wall", "building_water", "dirt", "dirt_lumber", "grass", "grass_forest", "grass_hill", "path_corner", "path_cornerSharp", "path_crossing", "path_end", "path_intersectionA", "path_intersectionB", "path_intersectionC", "path_intersectionD", "path_intersectionE", "path_intersectionF", "path_intersectionG", "path_intersectionH", "path_start", "path_straight", "river_corner", "river_cornerSharp", "river_crossing", "river_end", "river_intersectionA", "river_intersectionB", "river_intersectionC", "river_intersectionD", "river_intersectionE", "river_intersectionF", "river_intersectionG", "river_intersectionH", "river_start", "river_straight", "sand", "sand_rocks", "stone", "stone_hill", "stone_mountain", "stone_rocks", "unit_boat", "unit_house", "unit_houseLarge", "unit_mill", "unit_tower", "unit_tree", "unit_wallTower", "water", "water_island", "water_rocks",], - tiles_types_full : [ "building_", "river_", "sand", "stone", "water"], - tiles_types_parts : [ "unit_", "path_" ], - hex_type : true, - },{ - tile_URL : "../content/asset_kits/KenneyRetroMedieval/", - tiles_types_full : ["floor", "column", "tower", "wall"], - tiles_types_parts : ["battlement"], - tiles : [ "battlement", "battlement_cornerInner", "battlement_cornerOuter", "battlement_half", "column", "columnPaint", "columnPaint_damaged", "column_damaged", "detail_barrel", "detail_crate", "detail_crateSmall", "fence", "floor", "floor_flat", "floor_stairs", "floor_stairsCornerInner", "floor_stairsCornerOuter", "floor_steps", "floor_stepsCornerInner", "floor_stepsCornerOuter", "overhang", "overhang_fence", "overhang_round", "roof", "roof_corner", "roof_edge", "structure", "structure_poles", "structure_wall", "tower", "towerPaint", "towerPaint_base", "tower_base", "tower_edge", "tower_top", "wall", "wallFortified", "wallFortifiedPaint", "wallFortifiedPaint_gate", "wallFortifiedPaint_half", "wallFortified_gate", "wallFortified_gateHalf", "wallFortified_half", "wallPaint", "wallPaint_detail", "wallPaint_flat", "wallPaint_gate", "wallPaint_half", "wall_detail", "wall_flat", "wall_flatGate", "wall_gate", "wall_gateHalf", "wall_half", "wall_low" ], - hex_type : false, - } -] - -var selected_asset_kit = 1 - -// consider also a set of assets, e.g this one but also another kit from the same artist -// consider the other direction, i.e how a single glTF could become a set of tiles - -function displayAllTiles(){ - const scale = 1/10 - let last_type = null - available_asset_kits[selected_asset_kit].tiles.map( (t) => { - let x = -1*scale - if (!last_type) n = 0 - const tiles_types = [ ...available_asset_kits[selected_asset_kit].tiles_types_full, ...available_asset_kits[selected_asset_kit].tiles_types_parts] - tiles_types.map( (tile_type,ttn) => { - if (t.indexOf(tile_type) > -1) { - x = ttn/10 - if (tile_type != last_type) n = 0 - last_type = tile_type - } - } ) - let el = addGltfFromURLAsTarget( available_asset_kits[selected_asset_kit].tile_URL+t+tile_extension, - .09, - ""+x+" 0.7 -"+n*scale ) - // fine tuning should also be per asset set - // el.class = ... - n++ - } ) - // could consider a new spawner type so that picking a tile clones it first - // could do same behavior as on release or on picked, namely register listener then act on event -} -// try generating at scale, e.g 2, a landscape to explore based on type - // with scale adjusting as jxr line to be the Wondering pills/drinks/mushroom to change scale - // cf similar commands to move a class, consequently could add class after addGltfFromURLAsTarget - -function randomTileFull(){ - const tiles_full = available_asset_kits[selected_asset_kit].tiles.filter( t => { let present = false; available_asset_kits[selected_asset_kit].tiles_types_full.map(m => { if (t.indexOf(m)>-1) present = true; }); return present} ) - return tiles_full[Math.floor(Math.random()*tiles_full.length)] -} - -var tiles_snapping_grid = [] - -function getClosestTilesSnappingPosition( t, threshold=0.05 ){ - let point = null - let found = tiles_snapping_grid.map( i => { return { pos:i, dist: i.distanceTo(t) } } ) - .filter( t => t.dist < threshold ) - .sort( (a,b) => a.dist > b.dist) - if (found && found[0]) point = found[0].pos - return point -} - -var tile_snapping_enabled = true - -AFRAME.registerComponent('snap-on-pinchended', { - init: function(){ - let el = this.el - let clone - this.el.addEventListener('picked', function (event) { - if (tile_snapping_enabled) { - clone = el.object3D.clone() // worked with AFrame version but didnt get complex geometry - AFRAME.scenes[0].object3D.add( clone ) - clone.traverse( c => { - if (c.type == "Mesh") { - c.material = c.material.clone() - c.material.opacity = .5 - c.material.transparent = true - } - } ) - } - }) - this.el.addEventListener('moved', function (event) { - if (tile_snapping_enabled) { - var pos = AFRAME.utils.coordinates.parse( el.getAttribute("position") ) - pos.x = pos.x.toFixed(1) - pos.y = pos.y.toFixed(1) - pos.z = pos.z.toFixed(1) - clone.position.set(pos.x, pos.y, pos.z) - } - }) - this.el.addEventListener('released', function (event) { - if (tile_snapping_enabled) { // might generalize the name as now used for compound primitives too - el.setAttribute("rotation", "0 0 0") - // could limit to an axis or two, e.g here y axis probably should be kept or at least adjust to next 1/6th rotation - // could snap to invisible grid too, e.g every 1 or 1/10th unit - var pos = AFRAME.utils.coordinates.parse( el.getAttribute("position") ) - pos.x = pos.x.toFixed(1) // i.e .1m so 1/10th of a meter here, 10cm - pos.y = pos.y.toFixed(1) - pos.z = pos.z.toFixed(1) - // could check first if that "spot" is "free", e.g not other targets on that position - // but then if not, what? move to another of the closest 6th closest points? (2 vertical, 2 horizontal, 2 depth) or even 8th with diagonales? - // if not? now what? move until there is a free spot? - el.setAttribute("animation__snap"+Date.now(), "property: position; to: "+AFRAME.utils.coordinates.stringify(pos)+"; dur: 200;"); - //el.setAttribute("position", AFRAME.utils.coordinates.stringify(pos)) - //if (clone) clone.remove() - if (clone) AFRAME.scenes[0].object3D.remove( clone ) - - if (el.className == "compound_object"){ - let thresholdDistance = 0.2 // based on object size - targets.filter( i => ( - (i.className == el.className) - && el.getAttribute("position").distanceTo(i.getAttribute("position")) == 0.2) - && el.getAttribute("position").y == i.getAttribute("position").y - && el.getAttribute("position").z == i.getAttribute("position").z - ).map( _ => document.querySelector("#snapping-sound").components.sound.playSound() ) - } // very restrictive, also doesn't repulse away - - // if works, generalize and add to https://git.benetou.fr/utopiah/text-code-xr-engine/issues/66 - // should come back from emit('released') - // could rely on getClosestTilesSnappingPosition() - // if it works, might check if position is not already used by a tile - } - }) - } -}) - -function generateRandomPlace(max_i=10, max_j=10, scale=1/10, y=1.4){ - // lifesize, y : -2, scale 1 - // dollhouse, y : 1.4, scale 1/10 - for (let i=0;i { - scl = e.getAttribute("scale"); e.setAttribute("scale", scl.x*scale+ " " + scl.y*scale + " " + scl.z*scale) - pos = e.getAttribute("position"); e.setAttribute("position", pos.x*scale+ " " + (pos.y+yoffset) + " " + pos.z*scale) - let pos3 = new THREE.Vector3( pos.x*scale, pos.y+yoffset, pos.z*scale ) - tiles_snapping_grid.push( pos3 ) - } ) -} - -function addScreenshot(){ - screenshotcanvas = document.querySelector('a-scene').components.screenshot.getCanvas('perspective') - var sel = document.createElement("a-image") // could use a flat box instead, or use it as a frame - AFRAME.scenes[0].appendChild(sel) - sel.setAttribute("src", screenshotcanvas.toDataURL() ) - sel.setAttribute("height", .1) - sel.setAttribute("width", .2) - sel.setAttribute("position", "0 1.4 -0.1") - targets.push(sel) - return sel -} - -function newPrimitiveWithOutline( name="box", position="0 0 0", scale=".1 .1 .1" ){ - let el = document.createElement("a-"+name) - let el_outline = document.createElement("a-"+name) - el.appendChild(el_outline) - el.setAttribute("scale", scale) - el.setAttribute("position", position) - el_outline.setAttribute("scale", "1.01 1.01 1.01") - el_outline.setAttribute("color", "gray") - el_outline.setAttribute("wireframe", "true") - el_outline.className = "outline_object" - return el -} - -function addCompoundPrimitiveExample(position="0 1.4 -0.2"){ - let el = generateCompoundPrimitiveExample(position) - AFRAME.scenes[0].appendChild(el) - targets.push(el) - el.setAttribute('snap-on-pinchended', true) // could set the parameter here, e.g sound if close to same type - return el -} - -function addBlockCodeExample(text="hi", pos="0 1.4 -0.2", color="black", outlineColor="white"){ - let el = addNewNote( text ) - el.setAttribute("color", color) - el.setAttribute("outline-color", outlineColor) - el.setAttribute("position", pos) - let compountPrim = generateCompoundPrimitiveExample() - compountPrim.setAttribute("position", "0.1 0 -0.051") - el.appendChild(compountPrim) - // el.setAttribute('snap-on-pinchended', true) - return el -} - -function generateCompoundPrimitiveExample(position="0 1.4 -0.2"){ - var el = document.createElement("a-entity") - el.setAttribute("position", position) - el.id = "compound_object_" + Date.now() - el.className = "compound_object" - let parts = [] - parts.push( newPrimitiveWithOutline("box", "0 0 0", ".2 .1 .1") ) - parts.push( newPrimitiveWithOutline("box", ".125 0 0", ".05 .05 .05") ) - parts.push( newPrimitiveWithOutline("box", "-.125 0.0375 0", ".05 .025 .1") ) - parts.push( newPrimitiveWithOutline("box", "-.125 -0.0375 0", ".05 .025 .1") ) - parts.push( newPrimitiveWithOutline("box", "-.125 0 0.0375", ".05 .05 .025") ) - parts.push( newPrimitiveWithOutline("box", "-.125 0 -0.0375", ".05 .05 .025") ) - parts.map( p => el.appendChild(p) ) - return el -} - -function generateCompoundPrimitiveStart(position="0 1.4 -0.2"){ - var el = document.createElement("a-entity") - el.setAttribute("position", position) - el.id = "compound_object_" + Date.now() - el.className = "compound_object" - let parts = [] - parts.push( newPrimitiveWithOutline("box", "0 0 0", ".2 .1 .1") ) - parts.push( newPrimitiveWithOutline("box", ".125 0 0", ".05 .05 .05") ) - parts.map( p => el.appendChild(p) ) - return el -} - -function generateCompoundPrimitiveEnd(position="0 1.4 -0.2"){ - var el = document.createElement("a-entity") - el.setAttribute("position", position) - el.id = "compound_object_" + Date.now() - el.className = "compound_object" - let parts = [] - parts.push( newPrimitiveWithOutline("box", "0 0 0", ".2 .1 .1") ) - parts.push( newPrimitiveWithOutline("box", "-.125 0.0375 0", ".05 .025 .1") ) - parts.push( newPrimitiveWithOutline("box", "-.125 -0.0375 0", ".05 .025 .1") ) - parts.push( newPrimitiveWithOutline("box", "-.125 0 0.0375", ".05 .05 .025") ) - parts.push( newPrimitiveWithOutline("box", "-.125 0 -0.0375", ".05 .05 .025") ) - parts.map( p => el.appendChild(p) ) - return el -} - -function addPrimitive( name, position="0 1.4 -0.2" ){ - let el = newPrimitiveWithOutline( name ) - el.setAttribute("position", position) - AFRAME.scenes[0].appendChild(el) - el.id = "template_object_" + name - el.className = "template_object" - targets.push(el) - el.setAttribute('clone-on-primarypinchstarted', true) - return el -} - -AFRAME.registerComponent('clone-on-primarypinchstarted', { - init: function () { - let el = this.el - this.el.addEventListener('picked', function (event) { - selectedElement = instanciateFromPrimitive( selectedElement ) - }) - } -}) - -function instanciateFromPrimitive(element){ - var clone = element.cloneNode(true) - clone.removeAttribute('clone-on-primarypinchstarted') - clone.setAttribute( "scale", element.getAttribute("scale") ) // somehow lost? - clone.id += "_clone" + Date.now() - clone.className = "cloned" - targets.push(clone) - AFRAME.scenes[0].appendChild(clone) - return clone -} - -function addAllPrimitives(){ - const other_primitives = ["camera", "cursor", "sky", "light", "sound", "videosphere"] - const other_primitives_with_param_needed = ["text", "gltf-model", "obj-model", "troika-text"] - Object.getOwnPropertyNames(AFRAME.primitives.primitives) - // thanks to https://github.com/Utopiah/aframe-inVR-blocks-based-editor/blob/master/aframe-invr-inspect.js - .map( i => i.replace("a-","")) - .filter( i => other_primitives.indexOf(i) < 0 ) - .filter( i => other_primitives_with_param_needed.indexOf(i) < 0 ) // temporarilty disabled - .map( (i,j) => addPrimitive( i, ""+ j/7 + " 1.4 -0.5" ) ) -} - -const eventHighlighterReady = new Event("highlighterready"); -var highlighter -shiki.setCDN(jxrrootURL+'engine/'+'../content/shiki/') // see https://github.com/shikijs/shiki#specify-a-custom-root-directory -shiki.getHighlighter({ theme: 'monokai', langs: ['javascript' ] }).then(h => { highlighter = h; document.body.dispatchEvent(eventHighlighterReady); } ) - // see https://github.com/shikijs/shiki/blob/main/docs/languages.md - -function highlight(code = `console.log("Here is your code."); var x = 5;`, language='javascript'){ - if (!highlighter) return null - // check set colorRange if the result is {} which is the case when shiki highlighter isn't ready or available - const tokens = highlighter.codeToThemedTokens(code, language) - let pos=0 - let colorRange={} - tokens.map( line => { - line.map( (t,i) => { - colorRange[pos] = t.color/*.replace("#","0x")*/ - pos+=t.content.length - }) - pos++ - }) - return colorRange -} - -function startExperience(){ - //fetch("https://fabien.benetou.fr/Tools/Docker?action=source").then(r=>r.text()).then( page => { addCodeEditor( page, "" ) }) - //if (AFRAME.utils.device.checkHeadsetConnected()) AFRAME.scenes[0].enterVR(); - //document.querySelector("#snapping-sound").components.sound.playSound(); - document.querySelector("#mainbutton").style.display = "none" -} - -// track created editors then apply actions to the currently selected one -// problems happen when relying on querySelector/getElementById rather than a specific editor - // do these based on codeEditor.element rather than document -// still probably problematic for interactions - // consider for now only the currentEditor - -function nextLineCodeEditor(codeEditor, lines=1){ // can be negative to scroll up - if (codeEditor.line+lines < 0) return - codeEditor.line+=lines - let content=codeEditor.page.split("\n").slice(codeEditor.line,codeEditor.line+codeEditor.lengthWindowRange).join("\n"); - codeEditor.currentlyDisplayedText=content - codeEditor.element.setAttribute("troika-text", {value: content}) - if (codeEditor.language) codeEditor.element.setAttribute("troika-text", {colorRanges: highlight(content, language='javascript')}) - let gutterEl = codeEditor.element.querySelector(".leftgutter") - if (gutterEl){ - let lineNumbers = "\n" - for (let i=codeEditor.line+1;i<=codeEditor.line+codeEditor.lengthWindowRange;i++){ - for (let pad=0;pad nextLineCodeEditor(codeEditor), 100) -} - -function highlightAllOccurences(codeEditor, keyword="function"){ - let indices = [] - let lastfound = codeEditor.currentlyDisplayedText.indexOf(keyword,0) - while (lastfound>-1) { - indices.push(lastfound) - lastfound = codeEditor.currentlyDisplayedText.indexOf(keyword,lastfound+keyword.length) - } - indices.map( pos => { - let offset = (codeEditor.currentlyDisplayedText.slice(0,pos).match(/[\n\t ]/g)||[]).length - pos-=offset - highlightString(pos, keyword.length) - }) -} - -function hightlightNextKeyword(codeEditor, keyword="function"){ - let pos = codeEditor.currentlyDisplayedText.indexOf(keyword) - // invisible characters... some still left - let offset = (codeEditor.currentlyDisplayedText.slice(0,pos).match(/[\n\t ]/g)||[]).length - pos-=offset - highlightString(pos, keyword.length) -} - -function highlightString(codeEditor, pos, length){ - for (let c=pos;c { - el.object3D.children[0].addEventListener("synccomplete", e => { - // this can be used for resizing but without add the element - if (codeEditor.element.querySelector(".leftgutter")) return // already added, should unregister - - b = el.object3D.children[0]._textRenderInfo.blockBounds - w = b[2]-b[0] - h = b[3]-b[1] - - g = new THREE.BoxGeometry( w, h, .01 ); - m = new THREE.MeshBasicMaterial( {color: 0, opacity: 0.9, transparent: true} ); - c = new THREE.Mesh( g, m ); - el.object3D.add( c ); - c.name = "backdrop" - c.position.z=-.01 - c.position.x= w/2 - }) - }) -} - -function addGuttersToTroikaElement( codeEditor ){ - let el = codeEditor.element - el.addEventListener("object3dset", e => { - el.object3D.children[0].addEventListener("synccomplete", e => { - if (codeEditor.element.querySelector(".leftgutter")) return - // already added, should unregister, can be removed to allow dynamic resizing BUT should skip adding element - - b = el.object3D.children[0]._textRenderInfo.blockBounds - w = b[2]-b[0] - h = b[3]-b[1] - - gutterWidth = .2 * String(getNumberOfLinesFromCodeEditor(codeEditor)).length - //should adjust width based on number of lines in total first - g = new THREE.BoxGeometry( gutterWidth, h, .01 ); - m = new THREE.MeshBasicMaterial( {color: 0x333333, opacity: 0.9, transparent: true} ); - c = new THREE.Mesh( g, m ); - el.object3D.add( c ); - c.position.z=-.01 - c.position.x= -gutterWidth/2 - //c.rotation.y= .2 // looks nice but have to consider text on top first, could apply rotation to text too - var leftGutter = document.createElement("a-troika-text") - leftGutter.setAttribute("anchor", "left" ) - leftGutter.setAttribute("outline-width", "5%" ) - leftGutter.setAttribute("outline-color", "black" ) - let lineNumbers = "\n" - for (let i=codeEditor.line+1;i<=codeEditor.line+codeEditor.lengthWindowRange;i++){ - for (let pad=0;pade.element.id.includes(id))?.[0] -} - -// add jxr command on top of the editor e.g "jxr focusCodeEditor()" which would replace keyboard input - // switching keyboardInputTarget to 'codeeditor' then to 'hud' when done - // should also support clipboard or even a more direct way to have impact - // could save remotely (e.g wiki) or locally in localStorage -function addCodeEditor(page="jxr console.log('hello world')", language="javascript", - position="-.5 1.6 -.7", name="codeditor", width=50, preventReflow=false ){ - let codeEditor = { - element: null, - line: 0, - page: null, - startWindowRange: 0, - lengthWindowRange: 20, - scrollInterval: null, - currentlyDisplayedText: "", - caret: null, - language: '' - } - // could also add empty but with column and row for sizing - - let forcedLines = '' - let pos = 0 - let content = page - let line = '' - let parts = page.split(' ') - let n = 0 - - if (!preventReflow && !language.length){ - while ( pos < page.length ){ - while ( line.length < width && parts[n]){ - line += parts[n++] + ' ' - } - let potentialine = content.slice(pos, pos+width) - forcedLines += line.trim() + '\n' - pos+=line.length - if (line.length==0) pos++ - line = '' - } - - codeEditor.page = forcedLines - } else { - codeEditor.page = page - } - codeEditor.line = codeEditor.startWindowRange - let numberOfLines = getNumberOfLinesFromCodeEditor(codeEditor) - if (numberOfLinesevent.detail.position.y) - nextLineCodeEditor(1) - else - nextLineCodeEditor(-1) - previousPosition = event.detail.position.clone() - } - p.addEventListener('pinchstarted', pinchPrimaryScrollbarStarted ); - function pinchPrimaryScrollbarStarted(event){ - let rightGutterEl = codeEditor.element.querySelector(".rightgutter") - previousPosition = event.detail.position.clone() - rightGutterEl.object3D.getWorldPosition( target ); - if (previousPosition.distanceTo(target)<0.1) scrollbarPicked = true - } - - editors.push( codeEditor ) - return codeEditor -} - -function addCodeMultipleEditors(parts, src, language, name='splitededitor'){ - let editorParts = [] - let pl = src.length/parts - for (let n=0; n { - content += e.page // could add separator, e.g new line - if (language != e.language) console.warn('different language in editors to merge') - language = e.language - pos = e.element.getAttribute("position").clone() - e.element.setAttribute("visible", false) - }) - return addCodeEditor( content, language, AFRAME.utils.coordinates.stringify( pos ), "codeditormerge", editorBaseWidth, true ) -} - -// should reconsider the behavior as the content could still be the same but what is displayed changed -function splitEditorHorizontally( codeEditor, preventReflow=true){ - let p1, p2 - if (codeEditor.language.length){ - let content = codeEditor.page.split('\n') - p1 = content.slice(content.length/2).join('\n') - p2 = content.slice(0,content.length/2).join('\n') - } else { - p1 = codeEditor.page.slice(codeEditor.page.length/2) - p2 = codeEditor.page.slice(0,codeEditor.page.length/2) - } - let pos1 = codeEditor.element.getAttribute("position").clone() - let pos2 = codeEditor.element.getAttribute("position").clone() - //pos1.x-=1 - //pos2.x+=1 - pos1.y-=.1 - pos2.y+=.1 - // could also be set based on controllers/hands positions at the end of a stretch/pull gesture - let ce1 = addCodeEditor( p1, codeEditor.language, AFRAME.utils.coordinates.stringify( pos1 ), "codeditorsplit", editorBaseWidth, preventReflow) - let ce2 = addCodeEditor( p2, codeEditor.language, AFRAME.utils.coordinates.stringify( pos2 ), "codeditorsplit", editorBaseWidth, preventReflow ) - //codeEditor.language, codeEditor.element.getAttribute("position"), "codeditorsplit" ) - // should be smaller yet somehow displays it all - //updateCodeEditorWithContent(codeEditor, p2 ) - return [ce1, ce2] -} - -// could change model opacity based on hand position, fading out when within a (very small here) safe space - -function removeOutlineFromEntity( el ){ - [...el.querySelectorAll(".outline_object")].map( i => i.remove() ) -} - -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 changeColorLastClass(){ - let classFound = getClassFromPick() // applies on primary only - if (classFound) - [...document.querySelectorAll("."+classFound)].map( el => el.setAttribute("color", "red") ) -} - -function changeColorLastId(){ - let id = getIdFromPick() // applies on primary only - if (id) document.querySelector("#"+id).setAttribute("color", "red") -} - -function changeColorNextPinch(){ - //let id = getIdFromPick() // applies on primary only - // does not work anymore ... but could listen to selectedElements changes via an observer but deprecated - // proxy could be nice but requires to modify push() calls first - let lastPrimary = selectedElements.filter( e => e.primary ).length - - let checkForNewPinches = setInterval( _ => { - if (selectedElements.filter( e => e.primary ).length > lastPrimary){ - let id = getIdFromPick() // applies on primary only - if (id) document.querySelector("#"+id).setAttribute("color", "red") - clearInterval(checkForNewPinches) - } - }, 50) // relatively cheap check, filtering on small array -} - -// see demo ~30min in during https://www.youtube.com/watch?v=X9bQ-6oWKc4 -// should link to the right code already written - // see also cloneAndDistribute() & observe() but there is another one... between pinches - // observe being a shortcut to bindVariableValueToNewNote(variableName) -let pointsFromMovement = [] -function nextMovementToPoints(debut=false){ - pointsFromMovement = [] // could also add them to a larger array with timestamps - let el = document.querySelector('[pinchprimary]') - el.addEventListener('pinchended', function addThenRemove(event) { - addPointToPointsFromMovement(event) - // could add a timeout so that if no pinchended happens after e.g 10sec one doesn't forget - el.removeEventListener('pinchended', addThenRemove) - el.removeEventListener('pinchstarted', addPointToPointsFromMovement) - el.removeEventListener('pinchmoved', addPointToPointsFromMovement) - }); - el.addEventListener('pinchmoved', addPointToPointsFromMovement ); - el.addEventListener('pinchstarted', addPointToPointsFromMovement ); - function addPointToPointsFromMovement( event){ - pointsFromMovement.push( event.detail.position.clone() ) - } - -/* -could be a promise also - -//see examples for debugging/dev -testPoints = await fetch("pointsFromMovementExample.json").then( r => r.json() ) - -//could also otherwise down sample -let first = new THREE.Vector3( ).copy( testPoints[0] ) -let last = new THREE.Vector3( ).copy( testPoints[testPoints.length-1]) -let distance = first.distanceTo( last ) -let direction = new THREE.Vector3().subVectors( last, first ).normalize() - -//example of adding on curve -testPoints.map( p => addNewNote("something "+Math.random(), p) ) - -//example of animating from start to end -nn = addNewNote("something", testPoints[0]); -nn.setAttribute("animation", {property: "position" , to: AFRAME.utils.coordinates.stringify(testPoints[testPoints.length-1]) } ) -*/ -} - -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 -} - -/* - -generalize selector to pick last Nth rather than very last - adapt getIdFromPick() with .slice() after filter then map on length-N instead of length-1 - -selector pickers : pickClass and pickId - display result in 3D HUD with rotating objects and selector value - ideally themselves also selectable/usable, e.g clone from HUD to bring back "out" - requires extra work as becoming a child will not work, own positionning - should fix that - could compare to world coordinates instead of "just" position attribute - add a clear selector function to avoid making the HUD unusable - -could also pick via volume, e.g wireframe box - start with https://threejs.org/docs/#api/en/math/Box3.containsPoint - can iterate with https://threejs.org/docs/#api/en/math/Box3.containsBox - consider also https://threejs.org/docs/#api/en/math/Box3.intersectsBox - -consider pick then apply, i.e changeColorLastId() but for next Id - should be cancealable - -*/ - -function addGrid(){ // not actually correct but does the job - plot = document.createElement("a-entity") - var idx = 0 - for (var x=-5;x<=5;x++) - for (var y=0;y<=3;y++) - for (var z=-5;z<=5;z++){ - xp=-x - yp=-y - zp=-z - plot.setAttribute("line__"+ ++idx, `start: ${x} ${y} ${z}; end : ${xp} ${y} ${z}; opacity: 1;`) - plot.setAttribute("line__"+ ++idx, `start: ${x} ${y} ${z}; end : ${x} ${yp} ${z}; opacity: 1;`) - plot.setAttribute("line__"+ ++idx, `start: ${x} ${y} ${z}; end : ${x} ${y} ${zp}; opacity: 1;`) - } - plot.id = "grid" - AFRAME.scenes[0].appendChild( plot ) -} - -// functions call by event listener should be within this scope -// thus hopefully not conflicting with other listeners -function emptyPinchToMove(){ - const startRadius = .00001 - const maxRadius = .1 - const maxDist = .05 - const resetPosition = "0 9999 0" - let previousPosition - let controlSphere = document.createElement("a-sphere") - let r = startRadius - controlSphere.setAttribute("radius", r) - controlSphere.setAttribute("color", "blue") - controlSphere.setAttribute("wireframe", "true") - controlSphere.setAttribute("position", resetPosition) - AFRAME.scenes[0].appendChild( controlSphere ) - //targets.push(controlSphere) // keeping it out for specific control for now but should consider generalization - let el = document.querySelector('[pinchprimary]') - el.addEventListener('pinchended', function end(event) { - // el.removeEventListener('pinchended', end) - // el.removeEventListener('pinchstarted', pinched) - // el.removeEventListener('pinchmoved', moved) - r = startRadius - controlSphere.setAttribute("radius", r) - controlSphere.setAttribute("position", resetPosition) - }) - el.addEventListener('pinchmoved', moved ); - function moved(event){ - if (selectedElement) return - controlSphere.setAttribute("position", AFRAME.utils.coordinates.stringify( event.detail.position) ) - if (r >= maxDist){ - // start be movable after .05 diam - if (previousPosition){ - diff = previousPosition.sub(event.detail.position).clone() - pushBackClass("hidableenvironment", diff.z*10) // consider something exponential instead while maintaining sign - pushLeftClass("hidableenvironment", diff.x*10) - // could rely on getArrayFromClass(classname) to then move on these 2 dimensions - } - } - if (r < maxRadius) { - r += 0.01 - controlSphere.setAttribute("radius", r) - } - previousPosition = event.detail.position.clone() - } - el.addEventListener('pinchstarted', pinched ); - function pinched(event){ - } - - let previousPositionSecondary - let elSecondary = document.querySelector('[pinchsecondary]') - elSecondary.addEventListener('pinchmoved', movedSecondary ); - function movedSecondary(event){ - if (selectedElement) return - if (previousPositionSecondary){ - angle = previousPositionSecondary.sub(event.detail.position).clone() - let axis = new THREE.Vector3( 0, 1, 0 ); - applyToClass("hidableenvironment", (e, val ) => { - //let rot = e.getAttribute("rotation") - //e.setAttribute("rotation", ""+rot.x+" "+(rot.y+val*90)+" "+rot.z) - // rotating from the center of the model, not the player position -let obj = e.object3D -obj.position.sub(val.point) -obj.position.applyAxisAngle( axis, val.angle ) // no offset but reset when pinching again -//obj.position.applyAxisAngle( axis, obj.rotation.y+val.angle ) // rotates with offset -obj.position.add(val.point) -obj.rotateOnAxis(axis, val.angle) -// cf https://stackoverflow.com/questions/42812861/three-js-pivot-point/42866733#42866733 -// does reset on each new pinch though - -// might be a threejs vs AFrame rotation setup? - }, {angle:angle.x, point:event.detail.position.clone()}) - } - previousPositionSecondary = event.detail.position.clone() - } -} - -AFRAME.registerComponent('refresh-text-content-from-wiki-page', { - schema: { - pagename: {type: 'string', default: 'FoT'}, - }, - init:function(){ - this.added = [] - let = forcedPagename = AFRAME.utils.getUrlParameter('roomname') - forcedPagename?this.pagename=forcedPagename:this.pagename=this.data.pagename - this.tick = AFRAME.utils.throttleTick(this.tick, 500, this); - }, - tick: function(){ - let generatorName = this.attrName - fetch("https://fabien.benetou.fr/PIMVRdata/"+ this.pagename +"?action=source#" + Date.now()).then(res => res.text() ).then(res => { - res.split("\n").slice(0,maxItemsFromSources).map( (n,i) => { - found = this.added.find((str) => str === n) - if (typeof found === 'undefined'){ - this.added.push(n) - addNewNote( n, "-1 "+(1+i/10)+" -2.5", ".1 .1 .1", null, generatorName ) - } - }) - }) - } -}); - -function sendPerspectiveToServer(){ - let pagename - let = forcedPagename = AFRAME.utils.getUrlParameter('roomname') - forcedPagename?pagename=forcedPagename:pagename="TestingPairCollaboration" - document.querySelector('a-scene').components.screenshot.getCanvas('perspective').toBlob( blob => { - let img = new File([blob], pagename+".jpg", { type: "image/jpeg"}); - - form = new FormData(); - form.append('authpw', 'upload_pass_for_api'); - form.append('n', 'PIMVRdata.TestFormUpoad'); - form.append('action', 'postupload'); - form.append('uploadfile', img); - - fetch('https://fabien.benetou.fr/PIMVRdata/TestFormUpoad', { - method: 'POST', - body: form - }); - }, "image/jpeg", 0.8); -} - -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 - } -} - -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 startMesher(){ -// consider preview triangle from primary moved - let meshPoints = [] - let meshTriangles = [] - let offset - let meshEl = document.createElement("a-entity") - meshEl.className += "meshed" - AFRAME.scenes[0].appendChild( meshEl ) - - let elSecondary = document.querySelector('[pinchsecondary]') - elSecondary.addEventListener('pinchended', endedSecondary ); - function endedSecondary(){ - targets.push(meshEl) - meshEl.setAttribute('dynamic-body', "shape:hull") - // using 'dynamic-unless-picked' crashes the browser - //makeAnchorsVisibleOnTargets() // too large here - applyToClass("meshvertex", (e, val ) => e.setAttribute("visible", val), "false") - el.removeEventListener('pinchended', end) - elSecondary.removeEventListener('pinchended', endedSecondary ); - } - - let el = document.querySelector('[pinchprimary]') - el.addEventListener('pinchended', end) - - function end(event) { - if (selectedElement) return - let pos = event.detail.position.clone() - if (meshPoints.length==0){ - meshOffset = event.detail.position.clone() - meshEl.getAttribute("position").add(meshOffset) - } - let currentPos = AFRAME.utils.coordinates.stringify( pos.sub(meshOffset) ) - let controlSphere = document.createElement("a-sphere") - controlSphere.className += "meshvertex" - controlSphere.setAttribute("radius", 0.01) - controlSphere.setAttribute("color", "green") - controlSphere.setAttribute("wireframe", "true") - controlSphere.setAttribute("segments-width", 8) - controlSphere.setAttribute("segments-height", 8) - controlSphere.setAttribute("position", currentPos) - meshEl.appendChild( controlSphere ) - meshPoints.push(controlSphere) - if (meshPoints.length==2){ - let previousPos = AFRAME.utils.coordinates.stringify( - meshPoints[meshPoints.length-2].getAttribute("position") ) - meshEl.setAttribute("line", `start: ${previousPos}; end : ${currentPos}; opacity: 1; color:white;`) - } - if (meshPoints.length>2){ - meshEl.removeAttribute("line") - let ranked = meshPoints - .slice(0,-1) - .map( t => { return { el: t, dist : pos.distanceTo(t.getAttribute("position") ) } }) - .sort( (a,b) => a.dist - b.dist) - let triangle = document.createElement("a-triangle") - triangle.setAttribute("vertex-a", currentPos) - triangle.setAttribute("vertex-b", - AFRAME.utils.coordinates.stringify( ranked[0].el.getAttribute("position") )) - triangle.setAttribute("vertex-c", - AFRAME.utils.coordinates.stringify( ranked[1].el.getAttribute("position") )) - triangle.setAttribute("material", "side:double") - meshEl.appendChild( triangle ) - meshTriangles.push( triangle ) - } - } -} - -AFRAME.registerComponent('dynamic-unless-picked', { - init: function(){ - let el = this.el - el.setAttribute('dynamic-body', "") - this.el.addEventListener('picked', function (event) { - el.removeAttribute('dynamic-body') - el.setAttribute('static-body', "") - }) - this.el.addEventListener('released', function (event) { - el.removeAttribute('static-body') - el.setAttribute('dynamic-body', "") - }) - } - // should unregister on remove -}) - -AFRAME.registerComponent('collider-check', { - dependencies: ['raycaster'], - - init: function () { - let worldPosition=new THREE.Vector3(); - let v3 = new THREE.Vector3 - let comeCloserInterval - this.el.addEventListener('raycaster-intersection', function (e) { - console.log('intersected') - comeCloserInterval = setInterval( _=> { - document.getElementById("leftHand").object3D.traverse( e => { if (e.name == "wrist") { - worldPosition.copy(e.position);e.parent.updateMatrixWorld();e.parent.localToWorld(worldPosition) - } }) - e.detail.intersections[0]?.object.el.object3D.position.lerp(worldPosition, 0.1) - }, 100) - }); - this.el.addEventListener('raycaster-intersection-cleared', function (e) { - console.log('cleared') - clearInterval( comeCloserInterval ) - }); - } -}); - -// should generalize to selector, like pushDownClass and related -function tiltUpId(id){ tiltId(id, 0.1) } -function tiltDownId(id){ tiltId(id, -0.1) } -function tiltId(id, value){ - document.getElementById(id).object3D.position.y+=value; - document.getElementById(id).object3D.rotation.x+=value; -} - -function onNextPinchSplitReader(){ - //let id = getIdFromPick() // applies on primary only - // does not work anymore ... but could listen to selectedElements changes via an observer but deprecated - // proxy could be nice but requires to modify push() calls first - let lastPrimary = selectedElements.filter( e => e.primary ).length - - let checkForNewPinches = setInterval( _ => { - if (selectedElements.filter( e => e.primary ).length > lastPrimary){ - let id = getIdFromPick() // applies on primary only - if (id) { - let srcEditor = getEditorFromId( id ) - let editorParts = splitEditorHorizontally( srcEditor ) - // could position based on hands positions - // should hide or even delete older one - srcEditor.element.setAttribute('visible', false) - // could display a line between current pinch and secondary hand - // or temporary transparency on the 2 new editors - // could attach on hand indexes - } - clearInterval(checkForNewPinches) - } - }, 50) // relatively cheap check, filtering on small array -} - -function addConnectorsToCodeEditor( codeEditor, input=true, output=true){ - let el = codeEditor.element - el.addEventListener("object3dset", e => { - el.object3D.children[0].addEventListener("synccomplete", e => { - b = el.object3D.children[0]._textRenderInfo.blockBounds - w = b[2]-b[0] - h = b[3]-b[1] - // this assumes an axis aligned entity, which is correct until now - if (input){ - el.setAttribute("line__input", `start: 0 0 0; end : -1 1 0; opacity: 1;`) - el.setAttribute("line__input__end", `start: -1 1 0; end : -2 1 0; opacity: 1;`) - // order matters for getConnectorsFromEditor, the very tip MUST be the end point - } - if (output){ - el.setAttribute("line__output", `start: ${w} ${-h} 0; end : ${w+1} ${-h-1} 0; opacity: 1;`) - el.setAttribute("line__output__end", `start: ${w+1} ${-h-1} 0; end : ${w+2} ${-h-1} 0; opacity: 1;`) - // order matters for getConnectorsFromEditor, the very tip MUST be the end point - } - }) - }) - return el -} - -function connectionsBetweenEditors( a, b ){ - const connectionThreshold = 1 // to adjust after tries in VR, should probably be much shorter - let ca = getConnectorsFromEditor( a ) - let cb = getConnectorsFromEditor( b ) - let links = [] - if ( ca.input && cb.output && ca.input.distanceTo(cb.output) < connectionThreshold ){ - links.push({source:a, target:b}) - } - if ( cb.input && ca.output && cb.input.distanceTo(ca.output) < connectionThreshold ){ - links.push({source:b, target:a}) - } - return links -} - -function getConnectorsFromEditor( codeEditor ){ - const pos = new THREE.Vector3() - const scale = new THREE.Vector3() - const quaternion = new THREE.Quaternion() - codeEditor.element.object3D.getWorldPosition(pos) - codeEditor.element.object3D.getWorldScale(scale) - codeEditor.element.object3D.getWorldQuaternion(quaternion) - let connectors = {source: codeEditor} - let res = ['input', 'output'].map( ctype => { // we might get different types of inputs or outputs later - let i = codeEditor.element.getObject3D('line__'+ctype+'__end') - if (i){ - let tip = new THREE.Vector3( ...i.geometry.attributes.position.array.slice(3)) // end point - tip.applyQuaternion( quaternion ) - tip.multiply(scale) - tip.add(pos) - connectors[ctype] = tip - //visualDebugSphere(tip) - } - }) - return connectors -} - -function generateGraphFromEditors( editors ){ - let inputs = editors.map( e => getConnectorsFromEditor(e) ).filter( c => c.input ) - let outputs = editors.map( e => getConnectorsFromEditor(e) ).filter( c => c.output ) - let connections = [] - // check distances between all inputs with outputs which are not from the same source - outputs.map( o => { - inputs.map( i => { - if (i.source != o.source && i.input.distanceTo( o.output ) < .2 ) - connections.push( { source: o.source, target: i.source }) - }) - }) - let graph = {} - editors.map( (e) => { - graph[e.element.id] = {} - let g = graph[e.element.id] - g.editor = e - g.predecessors = [] - g.successors = [] - connections.map( c => { - if (c.target == e) g.predecessors.push(c.source) - if (c.source == e) g.successors.push(c.target) - }) - }) - return graph -} - -function visualDebugSphere( pos ){ - let controlSphere = document.createElement("a-sphere") - controlSphere.setAttribute("radius", 0.01) - controlSphere.setAttribute("color", "blue") - controlSphere.setAttribute("wireframe", "true") - controlSphere.setAttribute("segments-width", 8) - controlSphere.setAttribute("segments-height", 8) - controlSphere.classList.add('visualdebug') - controlSphere.setAttribute("position", AFRAME.utils.coordinates.stringify( pos ) ) - AFRAME.scenes[0].appendChild( controlSphere ) - return controlSphere -} - -function numberOfPredecessors( g ){ return Object.keys( g ).map( k => g[k].predecessors.length ) } - -function numberOfSuccessors( g ){ return Object.keys( g ).map( k => g[k].successors.length ) } - -function traverseFunctionGraph( g ){ - let callStack = [] - Object.keys( g ).map( k => { - if (!g[k].predecessors.length) callStack.push( g[k] ) - } ) - if (callStack.length < 1){ - console.warn( 'no entry point found in editor graph' ) - return [] - } - if (callStack.length > 1){ - console.warn( 'multiple entry point founds in editor graph' ) - return [] - } - let pos = 0 - while ( callStack[pos].successors?.[0] ) { - callStack.push( g[ callStack[pos].successors[0].element.id ] ) - pos++ - } - return callStack -} - -var points2D = [] -function startDraw2D(){ -// consider draw( pos ) too - let p = document.querySelector('[pinchprimary]') - let target = new THREE.Vector3(); // create once an reuse it - p.addEventListener('pinchended', pinchPrimaryDraw2DEnded ); - let indexTipTracking - let controlSphere - function pinchPrimaryDraw2DEnded(event){ - let pos = event.detail.position.clone() - let controlSphere = points2D[points2D.length-1] - // check if close enough to starting point, if yes then remove listeners - /* - if ( pos.distanceTo( controlSphere.getAttribute("position") ) < .1 ){ - console.log('removed listeners') - p.removeEventListener('pinchended', pinchPrimaryDraw2DEnded) - p.removeEventListener('pinchmoved', pinchPrimaryDraw2DMoved) - p.removeEventListener('pinchstarted', pinchPrimaryDraw2DStarted) - } - */ - //clearInterval( indexTipTracking ) - - } - p.addEventListener('pinchmoved', pinchPrimaryDraw2DMoved ); - function pinchPrimaryDraw2DMoved(event){ - // update line ending point position - let pos = event.detail.position.clone() - //let controlSphere = points2D[points2D.length-1] - } - p.addEventListener('pinchstarted', pinchPrimaryDraw2DStarted ); - function pinchPrimaryDraw2DStarted(event){ - // creates an offset between last pinch and last index tip position - // could merge them by updating the previous line end to the current pinch position - // could add a new line between both - if (points2D.length && points2D.length > 0){ - let lastPoint = points2D[points2D.length-1] - let previouspos = - lastPoint.getAttribute("line__0").end.clone().add( lastPoint.getAttribute('position') ) - } - controlSphere = document.createElement("a-sphere") - points2D.push( controlSphere ) - let pos = event.detail.position.clone() - controlSphere.className += "draw2d" - controlSphere.setAttribute("radius", 0.005) - controlSphere.setAttribute("color", "purple") - controlSphere.setAttribute("wireframe", "true") - controlSphere.setAttribute("segments-width", 8) - controlSphere.setAttribute("segments-height", 8) - controlSphere.setAttribute("position", pos) - AFRAME.scenes[0].appendChild( controlSphere ) - controlSphere.setAttribute("line__0", `start: 0 0 0; end : 0 0 0; opacity: 1; color:purple;`) - clearInterval( indexTipTracking ) - indexTipTracking = setInterval( _ => { - target = p.components['hand-tracking-controls'].indexTipPosition - // sometimes getting strange values, might check against null/0 - let line = controlSphere.getAttribute("line__0") - let cspos = controlSphere.getAttribute("position") - if (line){ - let previousPos = AFRAME.utils.coordinates.stringify( target.sub(cspos) ) - //pos.z = line.start.z // stick to a single plane, here axis aligned - controlSphere.setAttribute("line__0", "end", previousPos) - } - }, 20) - } -} - -function colorGradient(fadeFraction, rgbColor1, rgbColor2, rgbColor3) { -// https://gist.github.com/gskema/2f56dc2e087894ffc756c11e6de1b5ed - var color1 = rgbColor1; - var color2 = rgbColor2; - var fade = fadeFraction; - - // Do we have 3 colors for the gradient? Need to adjust the params. - if (rgbColor3) { - fade = fade * 2; - - // Find which interval to use and adjust the fade percentage - if (fade >= 1) { - fade -= 1; - color1 = rgbColor2; - color2 = rgbColor3; - } - } - - var diffRed = color2.red - color1.red; - var diffGreen = color2.green - color1.green; - var diffBlue = color2.blue - color1.blue; - - var gradient = { - red: parseInt(Math.floor(color1.red + (diffRed * fade)), 10), - green: parseInt(Math.floor(color1.green + (diffGreen * fade)), 10), - blue: parseInt(Math.floor(color1.blue + (diffBlue * fade)), 10), - }; - - return 'rgb(' + Math.max(0,gradient.red) + ',' + Math.max(0,gradient.green) + ',' + Math.max(0,gradient.blue) + ')'; -} - -function presetColorGradient(fadeFraction){ - let highColor = { red: 217, green: 83, blue: 79 }; - let mediumColor = { red: 240, green: 173, blue: 78 }; - let lowColor = { red: 92, green: 184, blue: 91 }; - - return colorGradient(fadeFraction, lowColor, mediumColor, highColor); -} - -function tensionVisualized(){ - let p = document.querySelector('[pinchprimary]') - let ptarget = new THREE.Vector3(); // create once an reuse it - let s = document.querySelector('[pinchsecondary]') - let starget = new THREE.Vector3(); // create once an reuse it - let entity = document.createElement("a-entity") - AFRAME.scenes[0].appendChild( entity ) - entity.setAttribute("line__0", `start: 0 0 0; end : 0 0 0; opacity: 1; color:purple;`) - let indexesTipTracking = setInterval( _ => { - ptarget = p.components['hand-tracking-controls'].indexTipPosition - starget = s.components['hand-tracking-controls'].indexTipPosition - // sometimes getting strange values, might check against null/0 - let line = entity.getAttribute("line__0") - if (line){ - let start = AFRAME.utils.coordinates.stringify( ptarget ) - entity.setAttribute("line__0", "start", start) - let end = AFRAME.utils.coordinates.stringify( starget ) - entity.setAttribute("line__0", "end", end) - entity.setAttribute("line__0", "color", presetColorGradient( ptarget.distanceTo(starget)) ) - } - }, 20) -} - -function ontouch(){ - let p = document.querySelector('[pinchprimary]') - let ptarget = new THREE.Vector3(); // create once an reuse it - let indexesTipTracking = setInterval( _ => { - ptarget = p.components['hand-tracking-controls'].indexTipPosition - getClosestTargetElement( ptarget )?.setAttribute('wireframe', true) - }, 20) -} - -function thumbToIndexPull(){ - let p = document.querySelector('[pinchprimary]') - let tip = new THREE.Vector3(); // create once an reuse it - let proximal = new THREE.Vector3(); // create once an reuse it - let thumb = new THREE.Vector3(); // create once an reuse it - let touches = [] - const threshold_thumb2tip = 0.01 - const threshold_thumb2proximal = 0.05 - let indexesTipTracking = setInterval( _ => { - // 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 -} - -function thumbToIndexAngle(){ - let p = document.querySelector('[pinchprimary]') - let tip = new THREE.Vector3(); // create once an reuse it - let metacarpal = new THREE.Vector3(); // create once an reuse it - let thumb = new THREE.Vector3(); // create once an reuse it - let indexesTipTracking = setInterval( _ => { - p.object3D.traverse( e => { if (e.name == 'index-finger-tip' ) tip = e.position }) - p.object3D.traverse( e => { if (e.name == 'thumb-metacarpal' ) metacarpal = e.position }) - p.object3D.traverse( e => { if (e.name == 'thumb-tip' ) thumb = e.position }) - tip.sub(metacarpal) - thumb.sub(metacarpal) - let angle = thumb.angleTo(tip) - if (angle > 0.9 && angle < 1.2) { - console.log( 'r' ) - p.emit('thumb2indexpush') - // could insert (with max threshold) a targe entity between tip and thumb - // this entity could then ondrop add a new post it note or jxr element - } - // could also check angle against head to insure it's facing the user - }, 590) -} - -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 -} - -AFRAME.registerComponent('playground', { - //schema: { content : {type: 'string'} }, - init: function () { - // could come from content as a string for sufficiently small examples e.g 3x3 - let h = [ - [1,1,1,1,1,1,1,1,1,1], - [1,0,0,0,1,0,0,0,0,1], - [1,0,0,0,0,1,0,0,0,1], - [1,0,0,0,1,0,0,0,0,1], - [1,0,0,0,0,1,0,0,0,1], - [1,0,0,0,1,0,0,0,0,1], - [1,0,0,0,0,1,0,0,0,1], - [1,0,0,0,1,0,0,0,0,1], - [1,0,0,0,0,1,0,0,0,1], - [1,1,1,1,1,1,1,1,1,1], - ] - for (let z=0;z<10;z++) - for (let x=0;x<10;x++){ - let el = document.createElement("a-entity") - el.setAttribute('position', (x-5) + ' 0 ' + (z-5) ) - el.setAttribute('geometry', "primitive: cylinder; segmentsRadial: 8; segmentsHeight: 1; radius: 0.5; height: "+(h[x][z]+.1)+";" ) - el.setAttribute('material', 'color', 'lightblue') - this.el.appendChild(el) - } - } -}) - -// tested per scene for now -AFRAME.registerComponent('unpackable', { - //schema: { content : {type: 'string'} }, - init: function () { - // (this.el, this.data.content) - // see tensionVisualized() - // 0-20cm stretch vertically or horizontally using scale - // 20-50 go on as transparent/wireframe - // >50% hide then show building blocks - // themselves as unpackable - let pp = new THREE.Vector3(); // create once an reuse it - let ps = new THREE.Vector3(); // create once an reuse it - document.querySelector('[pinchprimary]').addEventListener('pinchmoved', event => { - pp = event.detail.position.clone() - if (!ps || !selectedElement) return - let d = pp.distanceTo(ps) - console.log( d ) - console.log( selectedElement.getAttribute('scale') ) - selectedElement.setAttribute('scale', d+' ' +d+' '+d ) - if (d>.2) selectedElement.setAttribute('transparent', true) - if (d>.2) selectedElement.setAttribute('wireframe', true) - }) - document.querySelector('[pinchsecondary]').addEventListener('pinchmoved', event => { - ps = event.detail.position.clone() - }) - document.querySelector('[pinchprimary]').addEventListener('pinchended', event => { pp = null }) - document.querySelector('[pinchprimary]').addEventListener('pinchended', event => { pp = null }) - }, - update: function () { - }, - remove: function () { - } -}) - -AFRAME.registerComponent('trail', { - init: function () { - let player = document.getElementById('player') // assuming single player, non networked - let previousPos - setInterval( _ => { - let pos = player.getAttribute('position').clone() - pos.y = 0 - // compare current pos with previous set ones and if below distance threshold, lose points - let hits = Array.from(document.querySelectorAll('.trail')) - .filter( e => e.getAttribute("visible") == true) - .map( t => { return { el: t, dist : pos.distanceTo(t.getAttribute("position") ) } }) - .filter( t => t.dist < 0.1 ) - .sort( (a,b) => a.dist > b.dist) - if (hits.length>0) { - setFeedbackHUD('touching') - hits[hits.length-1].el.setAttribute('color', 'red') - } - if (previousPos && previousPos.distanceTo(pos) < 0.1) return // threshold to avoid cluttering - let el = document.createElement('a-cylinder') - el.setAttribute('scale', '.2 .2 .2') - el.setAttribute('position', AFRAME.utils.coordinates.stringify( pos ) ) - el.setAttribute('color', 'green') - el.setAttribute('opacity', '0.03') - el.setAttribute('segments-radial', '6') - el.setAttribute('segments-height', '2') - el.className += 'trail' - this.el.appendChild(el) - previousPos = player.getAttribute('position').clone() - previousPos.y = 0 - }, 50) - }, - remove: function () { - // should remove interval, possible visual trail too - } -}) - -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() - }, - }, -}); - -AFRAME.registerComponent('control-points', { - init: function () { - let cPoints = this.el.getAttribute("path").split(",") - if (!this.el.id) this.el.id = 'control-point-target' + Date.now() - cPoints.map( (p,n) => { - let controlSphere = document.createElement("a-sphere") - controlSphere.setAttribute("radius", 0.05) - controlSphere.setAttribute("color", "red") - controlSphere.setAttribute("wireframe", "true") - controlSphere.setAttribute("segments-width", 8) - controlSphere.setAttribute("segments-height", 8) - controlSphere.setAttribute("target", '') - controlSphere.setAttribute("move-target-point", 'target:'+this.el.id+';number:'+n) - controlSphere.setAttribute("position", p) - controlSphere.setAttribute("value", '') - AFRAME.scenes[0].appendChild(controlSphere) - }) - }, -}); - -AFRAME.registerComponent('move-target-point', { - schema: { - target: {type: 'string'}, - number: {type: 'number'}, - }, - events: { - moved: function (evt) { - let targetEl = document.getElementById(this.data.target) - let newPath = targetEl.getAttribute("path").split(",") - newPath[this.data.number] = AFRAME.utils.coordinates.stringify( this.el.getAttribute('position') ) - targetEl.setAttribute( 'path', newPath.join(',') ) - }, - }, -}); - -AFRAME.registerComponent('levels-to-load', { - init: function () { - const generatorName = this.attrName - Array.from( this.el.children ).map( (l,n) => { - l.id = 'level_'+n - let pos = '1 '+ (1+n/5) +' -.5' - addNewNote( 'jxr loadLevel("'+l.id+'")', pos, ".1 .1 .1", null, generatorName) - unloadLevel(l, pos) - }) - }, -}); - -function unloadLevel(el, pos){ - const scale = .02 - el.setAttribute('position', pos) - el.setAttribute('scale', scale + ' ' +scale + ' ' +scale ) - console.log('unloading', el.id) -} - -function loadLevel(elementIdName){ - const el = document.getElementById(elementIdName) - Array.from( document.querySelector('[levels-to-load]').children ).map( (l) => { - let n = l.id.replace('level_','') - let pos = '1 '+ (1+n/5) +' -.5' - unloadLevel(l, pos) - }) - el.setAttribute('scale', '1 1 1') - el.setAttribute('position', '0 0 0') -} - -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); - */ -}); - -function setOnDropFromAttribute(){ - // add to file descriptor from offtopus - // could also be prototyped with a URL instead, doesn't need offtopus even cached version - // could also prototype by doing so via https://webdav.benetou.fr/fot-demo-day/mobydick-extract.txt - targets.map( el => { - if ( el.getAttribute('ondrop')?.length > 0 ) - el.addEventListener('released', e => { - let code = el.getAttribute('ondrop') - console.log('do', code) - try { - eval( code ) - } catch (error) { - console.error(`Evaluation failed with ${error}`); - } - }) - } ) - // if dropped close enough to an editor, load file content in editor - // could try a jxr command... -} - -// used for testing, now that jxr.js is outside of index.html, could consider putting this back in index.html instead to keep behavior one would expect from a library -// does indeed create problems, namely other pages relying on it do get this testing behavior -AFRAME.registerComponent('startfunctions', { - init: function () { - - setOnDropFromAttribute() -/* class clonableasset : Crystal.glb Fish.glb Mountains.glb Penguin.glb Pinetree.glb -consider also - backend needed for caching - getPolyList(keyword) cachePoly(res) loadPolyThumbnails(res) loadFirstPolyModel(res) loadPolyModels(res) - - -see https://git.benetou.fr/utopiah/text-code-xr-engine/issues/52 for more shorthands - -*/ - //const movePenguin = "jxr qs #penguin sa position 1 0 -2" - //const rotatePenguin = "jxr qs #penguin sa rotation 0 -20 0" - - //addBlockCodeExample(text="hi", pos="0 1.4 -0.2", color="black", outlineColor="white") - //addBlockCodeExample('add penguin', '0 1.5 -0.2') - //let elToAnnotate = addBlockCodeExample('move penguin forward', '0 1.6 -0.2') - //addAnnotation(elToAnnotate, 'fait avancer pengouin') - //console.log(elToAnnotate) - //addBlockCodeExample('add green cube', '0 1.4 -0.2') - - //addBlockCodeExample(movePenguin, '0 1.45 -0.2') - //addBlockCodeExample(rotatePenguin, '0 1.55 -0.2') -// should change color and enable the 2 new types - - //relies on addCompoundPrimitiveExample() which already uses snap-on-pinched-ended - // also relies on addNewNote() so means code might be executed on left pinch or move with right pinch indepdently from block, to verify - - //let el = generateCompoundPrimitiveStart(position="-0.2 1.5 -0.2") - //targets.push(el) - //el.setAttribute('snap-on-pinchended', true) - //el.setAttribute('scale', '.1 .1 .1') - //AFRAME.scenes[0].appendChild(el) - - //el = generateCompoundPrimitiveEnd(position="0.2 1.5 -0.2") - //targets.push(el) - //el.setAttribute('snap-on-pinchended', true) - //el.setAttribute('scale', '.1 .1 .1') - //AFRAME.scenes[0].appendChild(el) - - // consider instanciateFromPrimitive() also in order to clone a set of blocks - //AFRAME.scenes[0].setAttribute('trail', '') - } -})