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', '')
- }
-})