You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

6034 lines
293 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<script>
const urlParams = new URLSearchParams(window.location.search);
// done all the way here in order to be able to test for emulation value, itself potentially used by IWER which must run before AFrame
</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>JXR filesystem and mimetype based explorations</title>
<script src="https://unpkg.com/iwer@2.0.1/build/iwer.min.js"></script>
<script>
window.usernamesForQuarterlyReport = [] // used from Q3 onward to pre-populate demo_q1.json for e.g. https://companion.benetou.fr/demos_example.html?filename=demo_q1.json
const emulatexr = urlParams.get('emulatexr');
const speedup_emulatexr_test = urlParams.get('speedup_emulatexr_test');
const username = urlParams.get('username');
unPinchHand = (side="right", dur=1000) => AFRAME.ANIME( { duration: dur, targets: xrdevice.hands.side, easing: 'linear', update: function(anim) { xrdevice.hands[side].updatePinchValue(1-anim.progress/100) } })
pinchHand = (side="right", dur=1000) => AFRAME.ANIME( { duration: dur, targets: xrdevice.hands.side, easing: 'linear', update: function(anim) { xrdevice.hands[side].updatePinchValue(anim.progress/100) } })
if (emulatexr && emulatexr=="true"){
// import IWER and inject runtime before aframe script is run
// so that aframe can pick up "native" webxr implementation (IWER)
const xrDevice = new IWER.XRDevice(IWER.metaQuest3);
xrDevice.installRuntime();
window.xrdevice = xrDevice;
let timestep = 1000
if (speedup_emulatexr_test) timestep /= 2 // values higher than 2 do not seem to work
// ---------------------------------------------------------------------------------------------------------------------
if (username && username == "q2_lense") {
const rightHand = xrDevice.hands['right'];
setTimeout( _ => {
AFRAME.scenes[0].enterVR()
t = 2
let pos = document.querySelector( "#lense_handle" ).getAttribute("position") // assume no rotation offset
setTimeout( _ => {
rightHand.position.set(pos.x, pos.y, pos.z )
pinchHand("right", timestep)
}, ++t*timestep )
/*
setTimeout( _ => {
AFRAME.ANIME( { targets: xrdevice.hands.right.position, x:pos.x-0.5, y:pos.y, z:pos.z, easing: 'linear', duration: timestep })
}, ++t*timestep )
*/
setTimeout( _ => {
rightHand.quaternion.set(1, 1, 0, 1) // was reliable for a dozen tries... now isn't?!
// xrDevice.recenter(); didn't help
AFRAME.ANIME( { targets: xrdevice.hands.right.position, x:pos.x, y:pos.y-2, z:pos.z, easing: 'linear', duration: timestep })
}, ++t*timestep )
}, 500 ) // not very clean...
}
// end of test scenario ------------------------------------------------------------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------
if (username && username == "q2_step_refcards_filtering") {
const rightHand = xrDevice.hands['right'];
setTimeout( _ => {
AFRAME.scenes[0].enterVR()
t = 2
setTimeout( _ => {
let targetSelector = "[value='.reference-entry-card']"
let pos = document.querySelector( targetSelector ).getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x, pos.y, pos.z )
pinchHand("right", timestep)
}, ++t*timestep )
timestemp = 200
setTimeout( _ => {
let pos = document.querySelector( "[value='filterBibtex']" ).parentEl.getAttribute("position")
rightHand.position.set(pos.x, pos.y, pos.z-.5 )
AFRAME.ANIME( { targets: xrdevice.hands.right.position, x:pos.x, y:pos.y, z:pos.z+.5, easing: 'linear', duration: timestep })
}, ++t*timestep )
++t*timestep // letting animation through first
setTimeout( _ => unPinchHand("right", timestep) , ++t*timestep )
setTimeout( _ => {
// need to grab newly created note with bibtex ref
let pos = targets.at(-5).getAttribute("position") // assume no rotation offset
// not the last target due to newly created contextual menu
rightHand.position.set(pos.x, pos.y, pos.z )
pinchHand("right", timestep)
}, ++t*timestep )
++t*timestep // letting animation through first
setTimeout( _ => {
let pos = active_rings.querySelector("[color=blue]").getAttribute("position")
rightHand.position.set(pos.x, pos.y, pos.z-.5 )
AFRAME.ANIME( { targets: xrdevice.hands.right.position, x:pos.x, y:pos.y, z:pos.z+.41, easing: 'linear', duration: timestep})
}, ++t*timestep )
}, 500 ) // not very clean...
}
// end of test scenario ------------------------------------------------------------------------------------------------------------------------------------------
// fast testing here does NOT work
// test scenario for ?username=q2_step_volumetric_frames -----------------------------------------------------------------------------------------------------
if (username && username == "q2_step_volumetric_frames") {
const rightHand = xrDevice.hands['right'];
setTimeout( _ => {
AFRAME.scenes[0].enterVR()
t = 2
setTimeout( _ => {
const showLinks = !false // true
// add <a-tube> for visual debugging
let r = Array.from( volumetricframes.querySelectorAll(".panel") )
if (showLinks) r.map( (el,i) => {
if (r[i+1]){
let elLink = document.createElement("a-tube")
elLink.setAttribute("radius", 0.01)
let mid = new THREE.Vector3()
mid.copy ( el.getAttribute("position") )
mid.add ( r[i+1].getAttribute("position") )
mid.divideScalar(2)
mid.y = 0.3
// could try to show a directed arrow (as a cone) here, in direction or end
let start = el.getAttribute("position").clone()
start.y -= 1/2
let end = r[i+1].getAttribute("position").clone()
end.y -= 1/2
// could offset on x and z too but would have check rotation too first
// can't translate as this is a Vector3, not an Object3D
let path = [start, mid, end].map( p => AFRAME.utils.coordinates.stringify( p ) ).join(", ")
elLink.setAttribute("path", path )
AFRAME.scenes[0].appendChild(elLink)
}
})
let targetSelector = ".reference-entry"
let pos = document.querySelector( targetSelector ).getAttribute("position") // assume no rotation offset
pos = Array.from( document.querySelectorAll( targetSelector ))[1].getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x+.005, pos.y+.022, pos.z+.011 ) // does not grab
// somehow <a-box> is blocking the pinch
/* // works, consequently it's about this specific element being "hidden" away by another target (non visible a-box!?)
rings.setAttribute("visible", true)
let targetSelector = "[value=a-sky]"
let pos = document.querySelector( targetSelector ).getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x, pos.y, pos.z )
*/
pinchHand("right",timestep)
}, ++t*timestep )
// setTimeout( _ => { unPinchHand("right",timestep) }, ++t*timestep )
++t
setTimeout( _ => {
rightHand.position.set(0.3, 1.5, -.6);
}, ++t*timestep )
setTimeout( _ => unPinchHand("right",timestep) , ++t*timestep )
setTimeout( _ => {
let targetSelector = ".reference-entry"
let pos = document.querySelector( targetSelector ).getAttribute("position") // assume no rotation offset
pos = Array.from( document.querySelectorAll( targetSelector ))[1].getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x+.005, pos.y+.022, pos.z+.011 ) // does not grab
pinchHand("right",timestep)
xrDevice.position.z += 0.8
}, ++t*timestep )
setTimeout( _ => {
rightHand.position.set(0.9, 1.5, -.3);
}, ++t*timestep )
setTimeout( _ => unPinchHand("right",timestep) , ++t*timestep )
n = 2
setTimeout( _ => {
let targetSelector = ".reference-entry"
let pos = document.querySelector( targetSelector ).getAttribute("position") // assume no rotation offset
pos = Array.from( document.querySelectorAll( targetSelector ))[n].getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x+.005, pos.y+.022, pos.z+.011 ) // does not grab
pinchHand("right",timestep)
}, ++t*timestep )
setTimeout( _ => {
rightHand.position.set(0.9, 1.5, -.3);
}, ++t*timestep )
setTimeout( _ => unPinchHand("right",timestep) , ++t*timestep )
setTimeout( _ => {
let potentialTestingTargets = Array.from( document.querySelectorAll( ".reference-entry" )).filter( el => el.data["bibtex-data"]["source-pdf"].length )
potentialTestingTargets.slice(5).map( ptt => {
setTimeout( _ => {
pos = ptt.getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x+.005, pos.y+.022, pos.z+.011 ) // does not grab
pinchHand("right",timestep)
}, ++t*timestep )
setTimeout( _ => {
rightHand.position.set(0.9, 1.5, -.3);
}, ++t*timestep )
setTimeout( _ => unPinchHand("right",timestep) , ++t*timestep )
})
potentialTestingTargets.slice(0,5).map( ptt => {
setTimeout( _ => {
pos = ptt.getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x+.005, pos.y+.022, pos.z+.011 ) // does not grab
pinchHand("right",timestep)
}, ++t*timestep )
setTimeout( _ => {
rightHand.position.set(0.1, 1.5, -.7);
}, ++t*timestep )
setTimeout( _ => unPinchHand("right",timestep) , ++t*timestep )
})
potentialTestingTargets.slice(5,10).map( ptt => {
setTimeout( _ => {
pos = ptt.getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x+.005, pos.y+.022, pos.z+.011 ) // does not grab
pinchHand("right",timestep)
}, ++t*timestep )
setTimeout( _ => {
rightHand.position.set(-0.5, 1.5, -.5);
}, ++t*timestep )
setTimeout( _ => unPinchHand("right",timestep) , ++t*timestep )
})
}, 2000)
}, 500 ) // not very clean...
}
// end of test scenario ------------------------------------------------------------------------------------------------------------------------------------------
// test scenario for ?username=q2_step_layout_animationtests -----------------------------------------------------------------------------------------------------
// ( enabled by &emulatexr=true )
if (username && username == "q2_step_layout_animationtests") {
const rightHand = xrDevice.hands['right'];
setTimeout( _ => AFRAME.scenes[0].enterVR(), 500 ) // not very clean...
t = 1
setTimeout( _ => {
let targetSelector = "[value=a-sky]"
let pos = document.querySelector( targetSelector ).getAttribute("position") // assume no rotation offset
rightHand.position.set(pos.x, pos.y, pos.z )
pinchHand()
}, ++t*1000 )
setTimeout( _ => {
// toggleAnchors() // useful to debug
rightHand.position.set(0.3, 1.5, -.6);
// prints getClosestTargetElements on released rather than on pinched
}, ++t*1000 )
setTimeout( _ => unPinchHand() , ++t*1000 )
setTimeout( _ => xrDevice.hands['right'].position.set(0.1, 1.8, -.6), ++t*1000 )
setTimeout( _ => AFRAME.ANIME( { targets: xrdevice.hands.right.position, x:1, y:1.5, z:-1, easing: 'linear', loop: true, direction: 'alternate' }), ++t*1000 )
}
// end of test scenario ------------------------------------------------------------------------------------------------------------------------------------------
let theta = 0
xrDevice.primaryInputMode = 'hand';
// example of manipulating XRDevice
document.addEventListener("keydown", function (event) {
switch (event.key) {
case "a": xrDevice.position.x -= 0.1; break;
case "f": xrDevice.position.x += 0.1; break;
case "s": xrDevice.position.y += 0.1; break;
case "d": xrDevice.position.y -= 0.1; break;
case "x": xrDevice.position.z += 0.1; break;
case "c": xrDevice.position.z -= 0.1; break;
case "e": theta-=.1; xrDevice.quaternion.set(t, 0, 0, 1); break;
case "r": theta+=.1; xrDevice.quaternion.set(t, 0, 0, 1); break;
}
});
}
</script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
<script>const zip = new JSZip();</script>
<script src="https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/dependencies/webdav.js"></script>
<script type="module">
import { createHighlighter } from 'https://esm.sh/shiki@3.0.0'
createHighlighter({ themes: ['nord'], langs: ['javascript'], }).then( h => { window.highlighter = h })
</script>
<script>
let jxrshortcuts = [ /(\d)s (.*)/, /qs ([^\s]+)/, / sa ([^\s]+) (.*)/, /obsv ([^\s]+)/, /observe ([^\s]+)/, /lg ([^\s]+) ([^\s]+)/, /lg ([^\s]+)/]
// could add color per found group
function jxrhighlight(){
// could be run after each note is created instead
[...document.querySelectorAll("[value]")]
.filter( el => (el.getAttribute("value").match(/^jxr /)))
.map( el => { colorJxrEl(el) })
// see also generalization via https://shiki.matsu.io custom renderers
}
function colorJxrEl( el ){
let code = el.getAttribute("value")
let foundRanges = {0: 0x0099ff, 3: 0xffffff}
jxrshortcuts.filter( pattern => code.match(pattern) )
.map( fp => foundRanges[code.search(fp)] = 0xff0000 )
el.setAttribute("troika-text", {colorRanges: foundRanges})
// ------ tinkering
// el.setAttribute("value", el.getAttribute("value").slice(4) )
let syntaxHighlightFoundRanges = highlight( el.getAttribute("value") )
el.setAttribute("troika-text", {colorRanges: {...syntaxHighlightFoundRanges, ...foundRanges}})
}
function highlight(code = `console.log("Here is your code."); var x = 5;`, language='javascript'){
const tokens = highlighter.codeToTokens(code, {lang: language, theme: 'nord'}).tokens
let pos=0
let colorRange={}
let parsing = [].concat(...tokens).map((t,i) => {
colorRange[pos] = t.color/*.replace("#","0x")*/
pos+=t.content.length
})
return colorRange
}
</script>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/gh/aframevr/aframe@edca48b2e71a0838690c7541fab5ede279def7a1/dist/aframe-master.min.js"></script> -->
<!-- to test for MX pen but didn't work -->
<script src="https://unpkg.com/aframe-simple-sun-sky@^1.2.3/simple-sun-sky.js"></script>
<!-- <script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/kylebakerio/a-console@1.0.2/a-console.js"></script>
<script>
// from https://stackoverflow.com/questions/8000009/is-there-a-way-in-javascript-to-listen-console-events
const shareerrors = urlParams.get('shareerrors');
// see also https://stackoverflow.com/questions/13815640/a-proper-wrapper-for-console-log-with-correct-line-number
setTimeout( _ => {
// this method will proxy your custom method with the original one
function proxy(context, method, message) {
return function() {
method.apply(context, [message].concat(Array.prototype.slice.apply(arguments)))
if (shareerrors && shareerrors=="true" && message.includes("Error:") ) fetch('https://ntfy.benetou.fr/shareerrors', { method: 'POST', body: JSON.stringify(arguments)})
// for truly remote debugging (or when its not otherwise possible, e.g. on the go BUT with connectivity)
}
}
// let's do the actual proxying over originals
window.console.log = proxy(console, console.log, 'Log:')
window.console.error = proxy(console, console.error, 'Error:')
window.console.warn = proxy(console, console.warn, 'Warning:')
// re-written by a-console hence why delaying execution
// see https://github.com/kylebakerio/a-console/issues/2
// https://github.com/kylebakerio/a-console/blob/bd44d684a74d5dc0937436450e273010f1486482/a-console.js#L662
}, 2000)
// should be a component requiring a-console instead in order to insure proper timing
</script>
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.4/dist/aframe-extras.min.js"></script>
<script>
let sequentialFiltersInteractionOnPicked = []
let sequentialFiltersInteractionOnReleased = []
</script>
<script src="interactions/onreleased/color_change.js"></script>
<script src="https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/jxr-core_branch_teleport_alt_rot.js?version=cachebusing123"></script>
<!-- modified to include fixed onreleased/ondpicked, should truly be merged on master -->
<script src="https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/jxr-postitnote.js"></script>
<!-- use to define targets and left/right pinch interactions, respectively execute code and move targets -->
<script src="gesture-exploration.js"></script>
<script>
let sequentialFilters = []
</script>
<!-- order matters -->
<script src="filters/image_and_glb_gltf.js"></script>
<script src="filters/screenshot_ui.js"></script>
<script src="filters/svg.js"></script>
<script src="filters/modifications_via_url.js"></script>
<script src="filters/srt_to_json.js"></script>
<script src="filters/txt.js"></script>
<script src="filters/json_ref_manual.js"></script>
<script src="filters/visualmeta.json.js"></script>
<script src="filters/mapvisualmeta.json.js"></script>
<script src="filters/tapestry.json.js"></script>
<script src="filters/odt_unpacked.xml.js"></script>
<script src="filters/docx_unpacked.xml.js"></script>
<script src="filters/docx_packed.xml.js"></script>
<script src="filters/pmwiki.js"></script>
<script src="filters/pdf_unpacked.xml.js"></script>
<script src="filters/layout.json.js"></script>
<script src="filters/markdown.js"></script>
<script src="filters/csv.js"></script>
<script src="filters/mapvisualmeta.jsons.zip.js"></script>
<script src="filters/keymap.js"></script>
<script src="filters/rete.bitbybit.json.js"></script>
<script src="filters/q2layout.json.js"></script>
<script src="filters/peertubeapi.js"></script>
</head>
<body>
<div style="position:fixed;z-index:1; top: 0%; left: 0%; border-bottom: 70px solid transparent; border-left: 70px solid #eee;">
<a href="https://git.benetou.fr/utopiah/text-code-xr-engine/issues/">
<img style="position:fixed;left:10px;" title="code repository"
src="https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/gitea_logo.svg">
</a>
</div>
<!-- <input type="file" id="myFile" name="filename" onchange="updateImage(this)" style="z-index: 1000;position: absolute;"/> -->
<script>
if ( window.navigator.userAgent.includes("AppleWebKit") ){
console.log('Apple Web Kit... should prompt audio element play')
//latestAudioPlay() // fails and blocks page
// "this operation is not supported" also when trying to play audio after
}
const modifierName = "Shift"
const modifierColor = new THREE.Color( 'green' )
const modifierColorRevert = new THREE.Color( 'white' )
addEventListener("keydown", (event) => {
document.getElementById("typinghud").setAttribute("material","opacity", .5)
// get reset after a short while
if (event.key == modifierName && AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode") )
AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode").material.color = modifierColor
})
addEventListener("keyup", (event) => {
if (event.key == modifierName && AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode"))
AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode").material.color = modifierColorRevert
})
/* does not seem to work
window.addEventListener('popstate', function (event){
// executed when user enters new urlquery in browserbar
console.log('popstate, should updated urlParams then parametersViaURL(urlParams)')
// const urlParams = new URLSearchParams(window.location.search);
})
*/
// ---- file upload via WebDAV, notification via ntfy
let usernamePrefix = ""
const sourceFromNextDemo = urlParams.get('sourceFromNextDemo');
const allowNtfyFeedbackHUD = urlParams.get('allowNtfyFeedbackHUD');
if (username) {
usernamePrefix = username+"_"
setTimeout( _ => AFRAME.scenes[0].setAttribute("current-demo-metadata", ''), 1000 )
}
// https://github.com/binwiederhier/ntfy/blob/main/examples/web-example-eventsource/example-sse.html#L24
const webdavURL = "https://webdav.benetou.fr";
//const subdirWebDAV = "/fotsave/" // could use /fot_sloan_companion_public/ instead
const subdirWebDAV = "/fotsave/fot_sloan_companion_public/"
var webdavClient = window.WebDAV.createClient(webdavURL)
// setTimeout( _ => document.querySelector(".a-canvas").ondrop = ev => dropHandler(ev), 1000 )
function uploadHandler(ev){
let file = file_input.files[0]
console.log("File(s) inputed", ev, file);
const reader = new FileReader();
reader.onload = (evt) => {
fileContent = evt.target.result
async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, fileContent); }
written = w(subdirWebDAV+usernamePrefix+file.name)
if (written){
fetch('https://ntfy.benetou.fr/fileuploadtowebdav', { method: 'POST', body: 'added '+usernamePrefix+file.name })
}
};
reader.readAsArrayBuffer(file);
}
function dropHandler(ev) {
// consider doing so over the entire window instead
// or AFrame canvas as .a-canvas
// document.querySelector(".a-canvas").ondrop = ...
console.log("File(s) dropped");
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
[...ev.dataTransfer.items].forEach((item, i) => {
// TODO seems to only work for 1 file, not multiple files
// If dropped items aren't files, reject them
if (item.kind === "file") {
const file = item.getAsFile();
console.log(`… file[${i}].name = ${file.name}`);
if (file.name == "index.html") {
console.warn('can not rewrite over index.html')
return
}
const reader = new FileReader();
reader.onload = (evt) => {
fileContent = evt.target.result
async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, fileContent); }
written = w(subdirWebDAV+usernamePrefix+file.name)
if (written){
fetch('https://ntfy.benetou.fr/fileuploadtowebdav', { method: 'POST', body: 'added '+usernamePrefix+file.name })
}
};
reader.readAsArrayBuffer(file);
}
});
} else {
// Use DataTransfer interface to access the file(s)
[...ev.dataTransfer.files].forEach((file, i) => {
console.log(`… file[${i}].name = ${file.name}`);
});
}
}
function dragOverHandler(ev) {
console.log("File(s) in drop zone");
drop_zone.style.background = "white"
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
}
// ------------------------------------------------------------------------------------------------
let currentFilter = null
function applyNextFilter( filename ){
if ( currentFilter == null ) currentFilter = -1
currentFilter++
if ( sequentialFilters[currentFilter] ){
sequentialFilters[ currentFilter ]( filename )
} else {
console.log( "done filtering for", filename )
currentFilter = null
}
}
if (allowNtfyFeedbackHUD){
const eventSourceConverted = new EventSource( `https://ntfy.benetou.fr/feedbackhud/sse` )
eventSourceConverted.onmessage = (e) => {
console.log('converted', event)
setFeedbackHUD(JSON.parse(event.data).message)
// inView(targetSelector)
// could also parse and share if not relying on inView to help with each step
}
}
// use for content_filter_examples.js
const eventSourceFSWatch = new EventSource( `https://ntfy.benetou.fr/fswatch/sse` )
// eventSourceFSWatch.onmessage = (e) => { console.log('fswatch', event) }
// not particularly useful for now
// both events looks very similar so should be refactored and simplified
const eventSourceConverted = new EventSource( `https://ntfy.benetou.fr/convertedwebdav/sse` )
eventSourceConverted.onmessage = (e) => {
console.log('converted', event)
if (!event.data) return
let data = JSON.parse( event.data )
if (!data) return
let message = JSON.parse( data.message )
console.log('checking via /fileswithmetadata again', message)
fetch('/fileswithmetadata').then( r => r.json() ).then( r => {
r.map( f => filesWithMetadata[f.name] = f.metadata )
let matchingFiles = r.filter( f => f.name.startsWith(message.source) )
.filter( f => f.name.endsWith(message.extension) )
.sort( (a,b) => a.metadata.mtimeMs > b.metadata.mtimeMs )
showFile( matchingFiles[0].name) // could replace number by 0 for PDF
})
}
function toggleShowFile( filename ){
let idFromFilename = filename.replaceAll('.','') // has to remove from proper CSS ID
let el = document.getElementById( idFromFilename )
if (el) {
let vis = el.getAttribute("visible")
if (vis == true)
el.setAttribute("visible", false)
else
el.setAttribute("visible", true)
} else {
showFile( filename )
}
}
function showFile( filename, openingOptions = {}){
console.log('showFile', filename)
fetch( filename ).then( r => {
let idFromFilename = filename.replaceAll('.','') // has to remove from proper CSS ID
if (!filesWithMetadata[filename] ) filesWithMetadata[filename] = {}
filesWithMetadata[filename].contentType = r.headers.get('Content-Type')
filesWithMetadata[filename].idFromFilename = idFromFilename
filesWithMetadata[filename].openingOptions = openingOptions
console.log( 'ct metadata', filename, filesWithMetadata[filename].contentType )
applyNextFilter( filename )
// const showdefinitions = urlParams.get('showdefinitions');
// should be showFile() configuration parameter instead
// can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
// should emit an event when done, for now just console.log which isn't programmatic
})
}
const eventSource = new EventSource( `https://ntfy.benetou.fr/fileuploadtowebdav/sse` )
// TODO consider updating the URL itself
eventSource.onmessage = (e) => {
console.log(event)
if (!event.data) return
let data = JSON.parse( event.data )
if (!data) return
let filename = data.message.replace("added ","")
// should actual trigger when server side writting is done, not before otherwise the file is not yet available
// could also wait for conversion
if (!filename) return
if (usernamePrefix && !filename.startsWith(usernamePrefix)) return
setTimeout( _ => {
fetch( filename ).then( r => {
addIcon( filename, 0, document.getElementById("virtualdesktopplane") )
let idFromFilename = filename.replaceAll('.','') // has to remove from proper CSS ID
if (!r.ok){}
if (!filesWithMetadata[filename] ) filesWithMetadata[filename] = {}
filesWithMetadata[filename].contentType = r.headers.get('Content-Type')
filesWithMetadata[filename].idFromFilename = idFromFilename
console.log( 'ct metadata', filename, filesWithMetadata[filename].contentType )
applyNextFilter( filename )
})
}, 500) // random delay... fine for small files
}
let reader = new FileReader()
function updateImage(el){
console.log(el)
reader.readAsDataURL( document.getElementById("myFile").files[0] )
reader.addEventListener("load", event => {
let el = document.createElement("a-image")
AFRAME.scenes[0].appendChild(el)
el.setAttribute("position", "0 "+(Math.random()+1)+" -0.5" )
el.setAttribute("scale", ".1 .1 .1")
el.setAttribute("src", reader.result)
el.setAttribute("target", "")
el.id = "screenshot_"+Date.now()
})
}
</script>
<button id="mainbutton" style="display:none; z-index: 1; position: absolute; width:50%; margin: auto; text-align:center; top:45%; left:30%; height:30%;" onclick="startExperience()">Start the experience (hand tracking recommended)</button>
<script>
/* filtering on files based on all found metadata (200 on .visualmeta.json)
based on occurences of keywords (or concepts)
most listed (dynamic, e.g most used keywords, sorted, take top 10)
*/
function loadBook(){
fetch('book_chapters.json').then( r => r.json() ).then( r => {
test_filteringFromVisualMeta(r)
})
}
// could also rely on https://observablehq.com/@spencermountain/topics-named-entity-recognition rather than delegate keyword/concept generation via API
keywordsCount = {}
AFRAME.registerComponent('gridplace', {
init: function () {
// should hide on entering AR
const size = 1;
const divisions = 10;
const gridHelper = new THREE.GridHelper( size, divisions, 0xaaaaaa, 0xaaaaaa );
this.el.object3D.add( gridHelper );
let innerRing = [ {x:0,z:1}, {x:1,z:0}, {x:0,z:-1}, {x:-1,z:0} ].map( offset => {
const gridHelper = new THREE.GridHelper( size, divisions, 0xcccccc, 0xcccccc );
gridHelper.position.x += offset.x
gridHelper.position.z += offset.z
this.el.object3D.add( gridHelper );
})
let midRing = [ {x:-1,z:1}, {x:1,z:1}, {x:1,z:-1}, {x:-1,z:-1},
{x:0,z:2}, {x:2,z:0}, {x:0,z:-2}, {x:-2,z:0} ].map( offset => {
const gridHelper = new THREE.GridHelper( size, divisions, 0xdddddd, 0xdddddd );
gridHelper.position.x += offset.x
gridHelper.position.z += offset.z
this.el.object3D.add( gridHelper );
})
}
})
AFRAME.registerComponent('canonical-view', {
init: function () {
// try to load default.layout.json and if it exists, use it, as canonical view
let view = 'default.layout.json' // could be a component parameter
fetch(view).then( r => r.text() ).then( r => applyNextFilter(view) )
}
})
let targetLocations = []
// could have different resulting actions
// saveToCompanion() with emailing after
function makeTargetLocationsVisible(){
// visualize targetLocations, should only do it once though
targetLocations.map( tl => {
let el = addNewNote( tl.position, tl.position )
let elPlate = document.createElement("a-box")
elPlate.setAttribute("position", "0.5 -0.1 0.2" )
elPlate.setAttribute("width", "1")
elPlate.setAttribute("height", ".01")
elPlate.setAttribute("depth", "1")
//elPlate.setAttribute("wireframe", "true")
setTimeout( _ => el.appendChild( elPlate ), 100 )
})
}
function fileDropped(){
targetLocations.map( tl => {
let pos = new THREE.Vector3()
let el = selectedElements.at(-1).element
el.object3D.getWorldPosition( pos )
let dist = pos.distanceTo( AFRAME.utils.coordinates.parse( tl.position ) )
// does not seem accurate, maybe due to the moving element to have parent
// should get world coordinate instead
console.log( dist )
// simplistic, should instead be using e.g. https://threejs.org/docs/#api/en/math/Box3.containsPoint
// allowing for vertical and horizontal trays of different sizes
if (dist < tl.distance) {
el.setAttribute("color", tl.color)
// testing sending to remarkable pro
let filename = selectedElements.at(-1).element.filename
fetch('/send-remarkablepro/'+filename) // should be configurable too, callback
console.log(tl.description, filename)
}
})
}
var filesWithMetadata = {}
AFRAME.registerComponent('list-files-sorted', {
init: function () {
fetch('/fileswithmetadata').then( r => r.json() ).then( r => {
// icon mode
let rootEl = document.getElementById("virtualdesktopplane")
r.map( f => filesWithMetadata[f.name] = f.metadata )
r.sort( (a,b) => a.metadata.mtimeMs < b.metadata.mtimeMs )
//.filter( f => ( f.name.endsWith('.png') || f.name.endsWith('.jpg') || f.name.endsWith('.glb') || f.name.endsWith('.gltf') ))
// inclusive filter
.filter( f => ( !f.name.endsWith('.html') && !f.name.endsWith('.js') ) )
// exclusive filter
.filter( f => f.name.startsWith(usernamePrefix) )
.filter( f => f.metadata.mtimeMs > Date.now()-(60*60*1000) ) // added during the last hour
.map( (text,i) => {
let el = addIcon( text.name, i, rootEl)
})
})
}
})
AFRAME.registerComponent('list-files', {
init: function () {
fetch('/files').then( r => r.json() ).then( r => {
// 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" ){
// text only mode
// r.map( (text,i) => addNewNote( text, '-0.4 '+(1.1+i/10)+' -0.4') )
// icon mode
let rootEl = document.getElementById("virtualdesktopplane")
r.map( (text,i) => {
let el = addIcon( text, i, rootEl)
})
})
}
})
let lastExecuted = {}
function newContentWithRefractoryPeriod(content){
if ( Date.now() - lastExecuted['newContentWithRefractoryPeriod'] < 500 ){
console.warn('ignoring, executed during the last 500ms already')
return
}
lastExecuted['newContentWithRefractoryPeriod'] = Date.now()
// decorator equivalent
applyNextFilter(content)
}
function addIcon(text, i, rootEl){
let idFromFilename = text.replaceAll('.','') // has to remove from proper CSS ID
const icon_prefix = "icon_"
if ( document.getElementById(icon_prefix+idFromFilename) ) return // avoid duplicates (assume either single directory or fullpath)
if (text.match(/.*\.pdf-(\d+)\.jpg/)) return // could consider more
if (text.match(/swp$|swx$|tmp$|~$|#$|#$/) ) return // cf newContent filtering with .swp and more
let el = document.createElement("a-box")
let x = Math.round(i/10)/10-(4/2)/10
let z = (-i%10)/10+(2/2)/10
el.id = icon_prefix+idFromFilename
el.filename = text
el.setAttribute("position", x+ " " + (1+z) + " 0 ") // vertical layout, leading to broken snapping
// el.setAttribute("position", x+ " 0 "+z) // horizontal layout
el.setAttribute("target", "")
el.setAttribute("value", "jxr applyNextFilter('"+text+"')") // hidden text here, first time, not convinced it's a good idea, not respecting the prinple
//el.setAttribute("value", "jxr newContent('"+text+"')") // hidden text here, first time, not convinced it's a good idea, not respecting the prinple
// problematic as it can execute multiple times, leading to overlapping yet hidden viewers or content
// consider lastExecuted['viewerFullscreen'] equivalent yet without blocking when done programmatically, e.g. when used on load or layout
// on picked, could resize the bounding box height to .05, onreleased back to .005
el.setAttribute("onpicked", "selectedElements.at(-1).element.setAttribute('wireframe', 'false')")
el.setAttribute("onreleased", "let el = selectedElements.at(-1).element; el.setAttribute('wireframe', 'true'); el.setAttribute('rotation', '0 0 0'); el.object3D.position.y=0; fileDropped()")
// add animations on height and position for children, to become a box proper then flatten back
// box
// Array.from( document.getElementById('icon_withdefaultjxrstylesjson').children ).map( l => l.getAttribute("position").y -= .02); document.getElementById('icon_withdefaultjxrstylesjson').setAttribute("height", "0.05"); document.getElementById('icon_withdefaultjxrstylesjson').getAttribute("position").y += 0.02
// flatten back
// Array.from( document.getElementById('icon_withdefaultjxrstylesjson').children ).map( l => l.getAttribute("position").y += .02); document.getElementById('icon_withdefaultjxrstylesjson').setAttribute("height", "0.005"); document.getElementById('icon_withdefaultjxrstylesjson').getAttribute("position").y -= 0.02
el.setAttribute("wireframe", "true")
el.setAttribute("width", ".05")
el.setAttribute("height", ".005")
el.setAttribute("depth", ".05")
let elPlate = document.createElement("a-box")
elPlate.setAttribute("position", "0 0.02 0" )
elPlate.setAttribute("width", ".04")
elPlate.setAttribute("height", ".04")
elPlate.setAttribute("depth", ".001")
elPlate.setAttribute("rotation", "-30 0 0" )
if (text.includes('.layout.json')) {
//elPlate.setAttribute("color", "lightblue" )
let elVisibleKnownType = document.createElement("a-troika-text")
elVisibleKnownType.setAttribute("position", "-0.002 0.02 0.003" )
elVisibleKnownType.setAttribute("value", '{Layout}' )
elVisibleKnownType.setAttribute("color", "#000" )
elVisibleKnownType.setAttribute("scale", ".04 .04 .04" )
el.appendChild(elVisibleKnownType)
let el3Delement = document.createElement("a-dodecahedron")
el3Delement.setAttribute("wireframe", "true" )
el3Delement.setAttribute("position", "0 0.05 0" )
el3Delement.setAttribute("color", "#000" )
el3Delement.setAttribute("radius", ".01" )
el.appendChild(el3Delement)
}
// color coding based on typed
if (!text.includes('.')) elPlate.setAttribute("color", "lightblue" )
// heuristic, assuming directories do not have a . is their name
/* disabled for now
let thumbnailUrl = "/thumbnails/"+text+'.png'
fetch( thumbnailUrl ).then( r => { if (r.ok) elPlate.setAttribute("src", thumbnailUrl) })
// include a visual preview as thumbnail, if available
*/
el.appendChild(elPlate)
let elFilename = document.createElement("a-troika-text")
elFilename.setAttribute("position", "0 0 0.01" )
elFilename.setAttribute("value", text )
// alternatively could use text as annotation
// elFilename.setAttribute("annotation", "content", name)
// rotate -90deg on x
// then 'jxr newContent('+filename+')' as value to make it executable on left pinch to open (default function)
// could optionally add more metadata e.g. file size, number of pages in documents, etc
elFilename.setAttribute("color", "#000" )
elFilename.setAttribute("scale", ".04 .04 .04" )
// some metadata e.g. file size or number of pages in documents could change depth, thickness
el.appendChild(elFilename)
rootEl.appendChild(el)
return el
}
let checkNewContent
</script>
<!-- <canvas width="1000px" height="1000px" id="transparent" style="display:none;"></canvas>-->
<canvas width="100px" height="100px" id="transparent" style="display:none;"></canvas>
<!-- low res feels actually nicer -->
<style>
body {
font-family: 'Noto Sans Devanagari', 'Noto Sans Tamil', 'Noto Sans', 'Noto Sans Symbols 2', sans-serif;
}
</style>
<div id=flatuifeatures style="position:fixed;z-index:1; top: 0%; right: 0%; ">
<div style="border-style:solid" id="drop_zone" ondrop="dropHandler(event);" ondragleave='drop_zone.style.background = ""' ondragover="dragOverHandler(event);">
<center> <p>Drag File To Upload.</p> </center>
</div>
<input type="file" onchange="uploadHandler(event)" id="file_input" name="file">
<a class=flatuioptions id=imagedownload download='highlight.png' href=''>download highlight (image)</a>
<a class=flatuioptions id=jsondownload download='highlight.json' href='[]'>download highlight (JSON)</a>
<a class=flatuioptions onclick="setupRecorder()" href='#recorder'>setup recorder</a>
<a class=flatuioptions onclick="latestAudioPlay()" href='#playaudio'>play audio</a>
<a class=flatuioptions onclick="audio.play()" href='#playaudio'>safari play audio</a>
<br>
<a class=flatuioptions id=customizedlinkforsharing href='https://hmd.link/?https://companion.benetou.fr/index.html?set_IDenvironment_visible=false&showfile=Apartment.glb'>customization example</a>
<span class=flatuioptions>(that you can then open on HMD on the same WiFi via the hmd.link URL)</span>
<br>
<a class=flatuioptions target=_blank href='https://companion.benetou.fr/found_set_param.html'>customization documentation</a>
<a class=flatuioptions target=_blank href='https://companion.benetou.fr/demos_example.html?filename=demo_q1.json'>quarter 1 explorations</a>
<a class=flatuioptions target=_blank href='https://companion.benetou.fr/demos_example.html?filename=demo_q2.json'>quarter 2 explorations</a>
<!-- could add JS toggle to modifications live then update customizedlinkforsharing.href -->
</div>
<script>
document.querySelector('#jsondownload').href=URL.createObjectURL( new Blob([JSON.stringify([])], { type:`text/json` }) )
let canvas = document.getElementById("transparent");
let ctx = canvas.getContext("2d");
AFRAME.registerComponent('raycaster-listen', {
// could also add the overlay transparent panel in front rather than manually adding it
init: function () {
// Use events to figure out what raycaster is listening so we don't have to
// hardcode the raycaster.
this.el.addEventListener('raycaster-intersected', evt => {
this.raycaster = evt.detail.el;
});
this.el.addEventListener('raycaster-intersected-cleared', evt => {
this.raycaster = null;
});
window.highlightToExport = []
},
tick: function () {
// return // stopped for demo
if (!this.raycaster) { return; } // Not intersecting.
let intersection = this.raycaster.components.raycaster.getIntersection(this.el);
if (!intersection) { return; }
//console.log(intersection.uv);
// window.highlightToExport.push( intersection.uv )
// this could also be a data structure to export after then be applied back on the source of the image (e.g. PDF)
ctx.fillStyle = "#0cc";
// should be based on currently / lastly picked highlighter
ctx.fillStyle = selectedElements.at(-1).element.querySelector("[raycaster]").getAttribute("raycaster").lineColor
const highres = false // for now actually better in low res
let x, y
if (!highres){
x = intersection.uv.x * 100 // should be also offsetsed by 100- on curved image (?!)
y = 100-intersection.uv.y * 100
ctx.fillRect(x,y,1,1)
} else {
x = intersection.uv.x * 1000 // should be also offsetsed by 100- on curved image (?!)
y = 1000-intersection.uv.y * 1000
ctx.fillRect(x,y,1,10)
}
/*
let x = intersection.uv.x * 100 // should be also offsetsed by 100- on curved image (?!)
let y = 100-intersection.uv.y * 100
*/
// will probably cause flickering... consider texture.needsUpdate instead or canvas.toDataURL(), might be faster
this.el.setAttribute("src", canvas.toDataURL() )
//document.querySelector('#imagedownload').href=document.getElementById('transparent').toDataURL()
// skipping for now to test for perf without saving
//document.querySelector('[download]').href=document.getElementById('transparent').toDataURL()
}
});
// from https://git.benetou.fr/utopiah/text-code-xr-engine/src/branch/gesture-manager/index.html#L55
AFRAME.registerComponent('live-selector-line', {
multiple: true,
schema: {
// unfortunately problematic it here due to ID being based on filenames and thus including '.' in their name
// escaping .replaceAll('.','\.') while setting id also does not work
start: {type: 'selector'},
end: {type: 'selector'},
},
init: function(){
if (!this.data.start || !this.data.end){
console.warn('start or end selector on live-selector-line not found')
return
}
this.newLine = document.createElement("a-entity")
this.newLine.setAttribute("line", "start: 0, 0, 0; end: 0 0 0.01; color: red")
this.newLine.id = "start_"+this.data.start.id+"_end_"+this.data.end.id
AFRAME.scenes[0].appendChild( this.newLine )
this.lastStartPos=new THREE.Vector3()
this.lastEndPos=new THREE.Vector3()
},
tick: function(){
if (!this.data.start || !this.data.end){ return }
let startPos = this.data.start.getAttribute("position")
let endPos = this.data.end.getAttribute("position")
if (startPos != this.lastStartPos){
this.newLine.setAttribute("line", "start", AFRAME.utils.coordinates.stringify( startPos) )
this.lastStartPos = startPos.clone()
}
if (endPos != this.lastEndPos){
this.newLine.setAttribute("line", "end", AFRAME.utils.coordinates.stringify( endPos ) )
this.lastEndPos = endPos.clone()
}
},
})
function applyJXRStyle(userStyle){
userStyle.map( style => {
Array.from( document.querySelectorAll(style.selector) ).map( el => el.setAttribute(style.attribute, style.value))
})
}
function parametersViaURL(data){
for (const [key, value] of data) {
if (key.startsWith("set_")){
let [selector, componentName] = key.replaceAll('ID','#').split('_').slice(1)
Array.from( document.querySelectorAll(selector) ).map( el => el.setAttribute(componentName, value))
}
if (key == "showfile"){
let openingOptions = {}
const showdefinitions = urlParams.get('showdefinitions');
if (showdefinitions) openingOptions.showDefinitions = true
showFile(value, openingOptions)
// check for options here too... cf openingOptions for
// mereology (in PmWiki filter),
// VisualMeta dynamic map via JSON packed (zip)
// indexable (TODO unimplemented)
// seems to fail on Fortress.glb
// should be coupled with a filter cf sequentialFilters grew via e.g. <script src="filters/content_filter_examples.js"><...
// this would allow for re-usable yet optional modifications, so in practice nearly permanent
}
}
}
function cleanSlateUI(){
demoqueueq1.setAttribute("visible", false)
instructions.setAttribute("visible", false)
manuscript.setAttribute("visible", false)
basiccommands.setAttribute("visible", false)
middlecommands.setAttribute("visible", false)
topsidecommands.setAttribute("visible", false)
document.querySelector("a-console").setAttribute("visible", false)
// could update to toggle console too
// cf wristShortcut = "jxr toggleHideAllJXRCommands()"
}
setTimeout( _ => {
// color scheme testing, unfortunately can't do CSS "proper"
// generalizing selector/attribute pairs though
// could be a user provided JSON, ideally CSS though as that's more common
const styles = {
light : [
{selector:'#start_file_sloan_testtxt_end_file_hello_worldtxt', attribute:'line', value: 'color:blue'},
{selector:'a-sky', attribute:'color', value: 'gray'},
{selector:'.notes', attribute:'color', value: 'black'},
{selector:'.notes', attribute:'outline-color', value: 'white'},
{selector:'a-troika-text a-plane', attribute:'color', value: 'red'},
{selector:'a-troika-text a-triangle', attribute:'color', value: 'darkred'}
],
print : [
{selector:'#start_file_sloan_testtxt_end_file_hello_worldtxt', attribute:'line', value: 'color:brown'},
{selector:'a-sky', attribute:'color', value: '#EEE'},
{selector:'.notes', attribute:'color', value: 'black'},
{selector:'.notes', attribute:'outline-color', value: 'white'},
{selector:'a-troika-text a-plane', attribute:'color', value: 'lightyellow'},
{selector:'a-troika-text a-triangle', attribute:'color', value: 'orange'}
],
}
parametersViaURL(urlParams)
makeTargetLocationsVisible()
wristShortcut = "jxr toggleHideAllJXRCommands()"
document.getElementById("typinghud").setAttribute("material","opacity", .01)
// could also gradually hide away, or show only after typing
hideAllJXRCommands()
// overrides user-visibility component due to the delay
/* exploration on highlighting by color within text
let startColor = Math.floor(Math.random()*100)
let endColor = Math.floor(startColor + Math.random()*100)
let range = {}
range[0] = 0xffffff
range[startColor] = 0x0099ff
range[endColor] = 0xffffff
hightlightabletext.setAttribute("troika-text", {colorRanges: range})
// should map from the highlight result of the raycaster, only when it hits
// could try to rely on https://github.com/protectwise/troika/blob/main/packages/troika-three-text/src/selectionUtils.js
// could start indirect, with sliders to
// grow/shrink a selection
// move it's starting position
*/
if (username && username == "spreadsheetcolumns") {
toggleHideAllJXRCommands()
Array.from( spreadsheetcolumns.querySelectorAll("a-troika-text") ).map( el => {
el.setAttribute("onreleased", "snapClosest([spreadsheetcolumns.querySelector('a-box>a-box')])" )
// if close enough to
// top position should add it as executable content as top cell
// to other cell position, make it as content that it get executed on
// could add class then call the function
// applyFunctionToColumn("changeColorToBlue") on all such entities
})
}
if (username && username == "thicknesstesteruser") {
thicknesscommands.setAttribute("visible", true)
Array.from( thicknesscommands.children ).map( c => c.setAttribute("visible", true) )
}
if (username && username == "tabletest") {
setTimeout( _ => { // example of conditional hint
if ( selectedElements.filter( el => el.element.id == "virtualdesktopplanemovable" && el.primary ).length < 1 )
setFeedbackHUD('pinch from the center of the yellow element')
}, 30*1000 )
manuscript.setAttribute("visible", false)
virtualdesktopplanemovable.setAttribute("visible", "true")
virtualdesktopplanemovable.setAttribute("onpicked", "selectedElements.at(-1).element.setAttribute('wireframe', 'true')")
virtualdesktopplanemovable.setAttribute("onreleased", "let el = selectedElements.at(-1).element; el.setAttribute('wireframe', 'false'); el.setAttribute('rotation', '0 0 0'); ")
}
if (username && username == "jsonrefmanualtester") {
console.clear()
}
if (username && username == "refoncubetester") {
console.clear()
showFile("references_manual_v04.json")
setTimeout( _ => { roundedpageborders.setAttribute("visible", "false") }, 1000 )
let cube = addCubeWithAnimations()
cube.setAttribute("target", "")
// should reparent 1st 6 cards to faces
setTimeout( _ => {
let refs = Array.from( document.querySelectorAll(".reference-entry") )
// refs.map( r => { r.parentElement = cube // doesn't seem to have any impact })
refs.map( (r,i) => {
r.object3D.parent = cubetest.object3D;
r.object3D.translateZ(.5);
r.object3D.translateY(-1);
r.object3D.scale.setScalar(.01)
} )
// should offset them down too
Array.from( document.querySelectorAll(".reference-entry-card") ).map( el => el.setAttribute("visible", "false"))
}, 500 )
let testingCommands = ["unfoldCube()", "roomScaleCube()", "palmScaleCube()", "refoldCube()"]
testingCommands.map( (c,i) => addNewNote("jxr " + c, "0.5 "+(1+i/10)+" -1" ) )
}
if (username && username == "cubetester") {
addCubeWithAnimations()
console.clear()
}
if (username && username == "metatester13032025") {
AFRAME.scenes[0].setAttribute("timed-demos", "")
}
if (username && username == "metatester10032025") {
// see demoqueueq1 user instead and related q1_* users
// could consider grouping if same prefix
console.clear()
const prefixUrl = "?username="
const optionUsers = ["refoncubetester", "backgroundexplorationlowopacity", "backgroundexplorationlowwhite", "backgroundexplorationlowwhitegrids", "backgroundexplorationlowwhitestatic"]
optionUsers.map( (c,i) => addNewNote("jxr location.assign('index.html?username=" + c + "')", "0.5 "+(1+i/10)+" -1" ) )
// somehow ? doesn't get escaped
// see timed-demos component instead
}
if (username && username == "instructionsonhands") {
AFRAME.scenes[0].setAttribute("instructions-on-hands", "")
}
if (username && username == "poweruser") {
// could instead use a per user limited visibility e.g. rely on AFRAME.registerComponent('user-visibility') untested for now
toggleHideAllJXRCommands()
// --- to demo :
// startViewCheck()
// showHighlight() // older version without images
//addImagesViaXML()
window.pageastextviaxmlsrc = "https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml"
//window.pageastextviaxmlsrc = "https://companion.benetou.fr/augmented_paper_xml/augmented_paper.xml"
// to try with another one...
pageAsTextViaXML()
//pageAsTextViaXML(5)
highlightcommands.setAttribute("visible", true)
roundedpageborders.setAttribute("visible", true)
//recordercommands.setAttribute("visible", true)
//addRecentAudioFiles()
// recordings to try the binding to annotation
// should check if dropped nearby colored annotations
/*
setTimeout( _ => {
Array.from( document.querySelectorAll(".highlightabletext") )[0].setAttribute("color", "aqua")
Array.from( document.querySelectorAll(".audiorecordings") ).map( el => {
el.setAttribute("onreleased", "associateLatestDropRecordingClosestHighlight()")
})
}, 2000 )
*/
// to test
// ----------------------------------------- gesture vertical hand -------------------------------------------
/*
let myScene = AFRAME.scenes[0].object3D
setInterval( i => {
if ( myScene.getObjectByName("r_handMeshNode") && myScene.getObjectByName("r_handMeshNode").parent.getObjectByName("wrist").rotation._y > -0.1
&& myScene.getObjectByName("r_handMeshNode").parent.getObjectByName("wrist").rotation._y < 0.1)
console.log('right hand about straight up')
// could try to use this with a minimum amplitude and above threshold, e.g. > .3m under 1s, trigger action
// should find better visualization, e.g. position as color curve? rotation as oriented sphere?
}, 500 )
*/
}
// ----------------------------------------- skating demo -------------------------------------------
if (username && username == "skating_rings") {
cleanSlateUI() // hides a bunch of stuff
let odometerEl = document.createElement("a-troika-text")
odometerEl.setAttribute("odometer", "")
odometerEl.setAttribute("value", "odometer")
odometerEl.setAttribute("position", "0 -.5 -.9")
odometerEl.setAttribute("rotation", "-10 0 0")
player.appendChild( odometerEl )
groundfor360.setAttribute("visible", "false")
audio.src = "success-221935.mp3"
const winColor = "yellow"
let level = 1
Array.from(skating_rings.querySelectorAll("a-torus")).map( r => r.setAttribute("visible", "false") )
let rings = Array.from( skating_rings.querySelectorAll("a-torus[level='"+level+"']") )
let allVisiblePreview = true
rings.map( r => {
r.setAttribute("animation", "property: rotation.z; from: 0; to: 360; dur: 1000; startEvents:startAnimation;")
r.setAttribute("visible", "true" )
if (!allVisiblePreview) r.setAttribute("visible", "false" )
// harder difficulty, could show them all on first level
// for shorter players
// r.object3D.position.y = 1.2
// not ideal though as it forces everything to go down to a fixed position
// and breaks animations relative to position
})
rings[0].setAttribute("visible", "true" )
let pos_player = new THREE.Vector3()
let pos_ring = new THREE.Vector3()
let hintRadius = .2
let passRadius = .1
// can be increased/decreated to change difficulty
setInterval( _ => {
player.object3D.getWorldPosition( pos_player )
rings.map( (r,i) => {
r.object3D.getWorldPosition( pos_ring )
let mode3D = false // should be property of ring
if (!mode3D){
pos_ring.y = 0 // projecting to the ground
pos_player.y = 0
}
// could show/hide based on difficulty
let d = pos_ring.distanceTo( pos_player )
// visual feedback on proximity
r.setAttribute("wireframe", (d<hintRadius) )
if (d<passRadius) {
let color = r.getAttribute("color")
// could also use this for level reset, i.e. if ring is loseColor (e.g. "red") then reset the color of all winColor rings
if (color != winColor){
r.setAttribute("color", winColor)
// ding sound
audio.play()
// restart every time
r.emit('startAnimation', null, false)
if (rings[i+1]){
rings[i+1].setAttribute("visible", "true")
} else {
// could check first if all rings on that level are the right color
if ( rings.filter( r => r.getAttribute("color") == winColor).length == rings.length )
console.log( 'actually cleared level' )
setFeedbackHUD('you win! next level?')
// should play better sound
audio.src = "skating_rings_goodresult-82807.mp3"
// consider starting a new interval instead
rings.map( r => r.setAttribute("visible", "false" ) )
level++
rings = Array.from( skating_rings.querySelectorAll("a-torus[level='"+level+"']") )
rings.map( r => r.setAttribute("visible", "true" ) )
// should take into account the fact that we roughlty start from the end of the previous level
setTimeout( _ => audio.src = "success-221935.mp3", 3000 ) // easier than event listener on ended
// not ideal because it plays it too
}
}
}
})
}, 100 )
}
// ----------------------------------------- demo queue Q2 customizations -------------------------------------------
if (username && username == "ring_discovery_with_keyboard") {
rings.setAttribute("visible", "true")
Array.from( rings.querySelectorAll("a-troika-text") ).map( el => el.setAttribute("visible", "true"))
cleanSlateUI() // hides a bunch of stuff
AFRAME.scenes[0].setAttribute("instructions-on-hands", "")
addKeyboardRings()
}
if (username && username == "ring_discovery") {
rings.setAttribute("visible", "true")
Array.from( rings.querySelectorAll("a-troika-text") ).map( el => el.setAttribute("visible", "true"))
cleanSlateUI() // hides a bunch of stuff
AFRAME.scenes[0].setAttribute("instructions-on-hands", "")
}
if (username && username == "icon_tags") {
cleanSlateUI() // hides a bunch of stuff
AFRAME.scenes[0].setAttribute("instructions-on-hands", "")
toggleHideAllJXRCommands()
document.querySelector("a-console").setAttribute("visible", true)
}
if (username && username == "ring_highlights") {
rings.setAttribute("visible", "true")
rings.object3D.translateZ(.3)
Array.from( rings.querySelectorAll("a-troika-text") ).map( el => el.setAttribute("visible", "true"))
cleanSlateUI() // hides a bunch of stuff
AFRAME.scenes[0].setAttribute("instructions-on-hands", "")
roundedpageborders.setAttribute("visible", "true")
setTimeout( _ => {
Array.from( roundedpageborders.querySelectorAll("a-troika-text") ).map( el => {
el.setAttribute("onpicked", "startRingCheck()")
// el.setAttribute("onreleased", el.getAttribute("onreleased") + ';' + "endRingCheck()") // should also snap back
el.setAttribute("onreleased", "let el = selectedElements.at(-1).element; el.setAttribute('rotation', ''); el.setAttribute('position', el.getAttribute('originalposition') ); endRingCheck()")
// somehow still apply green over...
})
}, 2000 )
Array.from( roundedpageborders.querySelectorAll("[color='#43A367']") ).map( el => el.setAttribute("color", "green"))
Array.from( highlighterA.querySelectorAll("[color='gray']") ).map( el => el.setAttribute("color", "#CCC"))
Array.from( highlighterB.querySelectorAll("[color='gray']") ).map( el => el.setAttribute("color", "#CCC"))
window.pageastextviaxmlsrc = "https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml"
pageAsTextViaXML()
// sequentialFiltersInteractionOnReleased = [] // skipped for this example
document.querySelector("a-console").setAttribute("visible", true)
}
if (username && username == "q2_visualmetaexport") {
cleanSlateUI() // hides a bunch of stuff
applyToClass("flatuioptions", el => el.style.visibility = "hidden" )
showFile("q2_visualmetaexport_test.visualmetaexport.json")
// not showFile("test.visualmetaexport.json") as we always use this as a user
addEventListener("paste", (event) => { hudTextEl.setAttribute("value", event.clipboardData.getData("text") ) })
}
if (username && username == "q2_pasting") {
cleanSlateUI() // hides a bunch of stuff
document.querySelector("a-console").setAttribute("visible", true)
addEventListener("paste", (event) => { hudTextEl.setAttribute("value", event.clipboardData.getData("text") ) })
}
if (username && username == "q2_immersive_console") {
setTimeout( _ => {
// TODO add sphere with color and id to then show/hide for a while
// this method will proxy your custom method with the original one
function proxy(context, method, message) {
return function() {
method.apply(context, [message].concat(Array.prototype.slice.apply(arguments)))
showLogSphere(message)
}
}
function showLogSphere(message){
let color = "pink"
if (message.includes("Error:")) color = "red"
if (message.includes("Log:")) color = "green"
if (message.includes("Warning:")) color = "yellow"
setFeedbackHUD( message + ' (in ' + color + ')' )
let sphereEl = document.createElement("a-sphere")
sphereEl.setAttribute("radius", "20")
sphereEl.setAttribute("wireframe", "true")
sphereEl.setAttribute("segments-height", 8)
sphereEl.setAttribute("segments-width", 8)
sphereEl.setAttribute("color", color)
AFRAME.scenes[0].appendChild( sphereEl )
setTimeout( _ => sphereEl.remove(), 2000 ) // same timing as setFeedbackHUD
return sphereEl
}
// let's do the actual proxying over originals
window.console.log = proxy(console, console.log, 'Log:')
window.console.error = proxy(console, console.error, 'Error:')
window.console.warn = proxy(console, console.warn, 'Warning:')
// somehow lost content itself but visible in the console still
}, 3000)
}
if (username && username == "q2_ring_keyboard") {
cleanSlateUI() // hides a bunch of stuff
rings.setAttribute("visible", "true")
Array.from( rings.querySelectorAll("a-troika-text") ).map( el => el.setAttribute("visible", "true"))
// need example test only, not more
basiccommands.setAttribute("visible", "true")
document.querySelector("a-console").setAttribute("visible", "true")
startRingCheck(); setTimeout( _ => endRingCheck(), 500 )
// somehow seems to start with another text?!
let el = addNewNote( "type with me", "-0.7 1.1 -0.5")
//el.setAttribute("onpicked", "startRingCheck()") // low precision
el.setAttribute("onpicked", "startRingCheck(.02, .01)") // high precision
el.setAttribute("onreleased", "endRingCheck()")
el.id = "typingnoteexample"
AFRAME.scenes[0].setAttribute("instructions-on-hands", "")
// adapting addKeyboardRings() , cf fabien@debian2080ti:~/ring_keyboard_improvement.svg
const columns = 3 // should be dynamic as on a real keyboard, e.g. 10,9,7 thus based on keyboard line length
const step = 20
const xOffset = -.5
const yOffset = 1.4
const zOffset = -.5
const scale = .5
const xStep = .05
// need something to pull through
"qwertyuiop,asdfgthjkl,zxcvbnm".split('').reverse().map( (c,i) => {
// TODO // qwertyuiop,asdfgthjkl,zxcvbnm layout
let x = (i%columns)/step + xOffset
let y = (i/columns)/step + yOffset
let r = addRing(c, x+" "+y+ " " + zOffset, .03, .005, .005)
//function addRing(code, position="0 1.5 -.7", radius=.1, radiusTubular=.01, codeVerticalOffset=.15)
r.setAttribute("rotation", "0 30 0") // rotated 45deg
r.setAttribute("scale", ""+scale + " "+scale + " "+scale ) // smaller scale
r.classList.add('kbd_key')
} )
// TODO line to closest key
// can be based on 2 IDs, "only" changing idea of closest key
let closestKeyLine = document.createElement("a-entity")
closestKeyLine.setAttribute('line__closestkey', "start: 0 0 0; end: 0.1 0.1 0.1")
AFRAME.scenes[0].appendChild( closestKeyLine )
// should ideally piggyback on startRingCheck()
setInterval( _ => {
let closestDistance = 1000
let closestEl = null
Array.from( document.querySelectorAll('.kbd_key') ).map( k => {
let startPos = document.getElementById( el.id ).getAttribute("position")
let endPos = k.getAttribute("position")
let d = endPos.distanceTo( startPos )
if (d < closestDistance) {
closestDistance = d
closestKeyLine.setAttribute("line__closestkey", "start", AFRAME.utils.coordinates.stringify( startPos) )
closestKeyLine.setAttribute("line__closestkey", "end", AFRAME.utils.coordinates.stringify( endPos) )
}
})
}, 50)
// to happen after each virtual keypress
AFRAME.scenes[0].addEventListener('virtualkeypress', event => {
console.log( event)
applyToClass("kbd_key", el => el.object3D.position.x+= xStep ) // keyboard itself moved after each "typed" letter
// hudTextEl.setAttribute("value", hudTextEl.getAttribute("value") + keyPressedValue ) // append to HUD value
} )
}
if (username && username == "q2_remote_ntfy_keyboard") {
let textInput = document.createElement("input")
document.body.append( textInput )
textInput.focus()
textInput.id = "textinputforoskeyboard"
textInput.style = 'position:absolute; zIndex:99; top:100px; left:100px;'
let ntfy_keyboard_path = "remote_keyboard"
const remote_keyboard_group = urlParams.get('remote_keyboard_group'); // should be URL encoded, assuming for now alphanum only
if (remote_keyboard_group) ntfy_keyboard_path += remote_keyboard_group
// seems to work most times.... not clear when it does or does not
// needs refresh somehow, connecting both
const eventSourceConverted = new EventSource( `https://ntfy.benetou.fr/${ntfy_keyboard_path}/sse` )
eventSourceConverted.onmessage = (e) => {
let message = JSON.parse( JSON.parse( e.data ).message )
console.log('ntfy remote keyboard', message )
if (message.status == "change" )
addNewNote( message.value, "0 1.5 -0.7")
if (message.status == "input" )
hudTextEl.setAttribute("value", message.value )
}
textInput.onchange = e => {
parseKeys("keydown", "Enter")
fetch('https://ntfy.benetou.fr/'+ntfy_keyboard_path, { method: 'POST', body: JSON.stringify({status: "change", value:e.target.value}) })
hudTextEl.setAttribute("value", "" )
textInput.value = ""
}
textInput.oninput = e => {
fetch('https://ntfy.benetou.fr/'+ntfy_keyboard_path, { method: 'POST', body: JSON.stringify({status: "input", value:e.target.value}) })
}
}
if (username && username == "q2_os_keyboard") {
// does not work on Vision Pro, issue opened
// cf https://mastodon.pirateparty.be/@ada@mastodon.social/114574514396127528
// consider until then polyfilling https://github.com/AdaRoseCannon/aframe-htmlmesh/pull/25#issuecomment-2911887918
// namely here use e.g. q2_ring_keyboard or addKeyboardRings()
let textInput = document.createElement("input")
document.body.append( textInput )
textInput.focus()
textInput.id = "textinputforoskeyboard"
typinghud.setAttribute("material","opacity", .5)
textInput.onchange = e => {
console.log( "OS keyboard changed content", e.target.value )
hudTextEl.setAttribute("value", e.target.value )
// should then do {enter} keypress equivalent, namely addNewNote from that input and clear HUD
//let event = new KeyboardEvent('keypress', { 'keyCode': 13, });
//document.dispatchEvent(event);
// does not seem to work...
parseKeys("keydown", "Enter")
}
textInput.oninput = e => {
console.log( "OS keyboard content", e.target.value )
hudTextEl.setAttribute("value", e.target.value )
// kind of works... but
// HUD not visible anymore (ugh)
// enter key is not cought, so what is the "validate" OS keyboard equivalent?
}
let testingCommands = [
'document.getElementById("textinputforoskeyboard").focus()'
]
testingCommands.map( (c,i) => addNewNote("jxr " + c, "-.0 "+(1.2+i/10)+" -.45" ) )
}
if (username && username == "q2_onrelease_lookat") {
window.usernamesForQuarterlyReport.push(username)
// cleanSlateUI() // hides a bunch of stuff
let testingCommands = ["console.log('hello world')"]
let newCommandsEl = testingCommands.map( (c,i) => addNewNote("jxr " + c, "0 "+(1+i/10)+" -.5" ) )
newCommandsEl.at(-1).setAttribute("onreleased", 'selectedElements.at(-1).element.object3D.lookAt( player.object3D.position )' )
const debug = urlParams.get('showdebug');
if (debug){
document.querySelector("a-console").setAttribute("visible", true)
basiccommands.setAttribute("visible", true)
}
}
if (username && username == "q2_visualmetaexport_map_via_wordpress_with_lookat") {
// should use q2_visualmetaexport_map_via_wordpress with q2_onrelease_lookat
}
if (username && username == "q2_visualmetaexport_map_via_wordpress_with_keyboard") {
// mix of "q2_visualmetaexport_map_via_wordpress" and q2_keydrumsticks
cleanSlateUI() // hides a bunch of stuff
const showdemoexample = urlParams.get('showdemoexample');
if (showdemoexample) showFile('https://futuretextlab.info/wp-content/uploads/Frode-dynamicviewvisualmetaexport.jsons_.zip')
applyToClass("flatuioptions", el => el.style.visibility = "hidden" )
setTimeout( _ => {
let lines = Array.from( document.querySelectorAll("[line]") )
.filter( el => el.id.startsWith("start_visualmetaexport") && el.id.includes("_end_visualmetaexport_") )
lines.map( el => el.setAttribute("line", "color", "black")) // can be highlighted as lightgray or black
lines.map( el => el.setAttribute("visible", "false") )
// lines.map( el => el.setAttribute("opacity", .1) ) // does nothing
let keywordElements = Array.from( document.querySelectorAll(".notes") ).filter( el => el.id.startsWith("visualmetaexport_") )
keywordElements.map( el => {
el.setAttribute("troika-text", "fontSize", ".1" )
el.setAttribute("onpicked", 'Array.from( document.querySelectorAll("[line]") ).filter( el => el.id.includes( selectedElements.at(-1).element.id.toLowerCase() ) ).map( el => el.setAttribute("visible", "true")) ')
el.setAttribute("onreleased", 'Array.from( document.querySelectorAll("[line]") ).filter( el => el.id.includes("visualmetaexport_")).map( el => el.setAttribute("visible", "false")); selectedElements.at(-1).element.object3D.lookAt( player.object3D.position ) ')
const forcelookat = urlParams.get('forcelookat');
if (forcelookat) el.object3D.lookAt( player.object3D.position )
})
/*
let keywords = keywordElements.map( el => el.getAttribute("value" ) )
// example of dynamic filtering
let keyword = keywords.at(keywords.length*Math.random())
lines.filter( el => el.id.includes("visualmetaexport_"+keyword.toLowerCase() ) )
.map( el => el.setAttribute("line", "color", "lightgray")) // can be highlighted as lightgray or black
*/
}, 1000 )
const debug = urlParams.get('showdebug');
if (debug){
document.querySelector("a-console").setAttribute("visible", true)
basiccommands.setAttribute("visible", true)
}
document.getElementById("otherbox").setAttribute("opacity", 1)
// note mode -------------------------------------------------------------
const primaryPinchSingleHandedDistanceThreshold = 0.02 // 2 cm
const secondaryPinchSingleHandedDistanceThreshold = 0.05 // need to be much looser, maybe due to privacy limitations or camera position (hidden fingers)
let secondaryPinchSingleHanded = setInterval( el => {
if ( !AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode") ) return
let j1 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-tip").position
let j2 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("index-finger-tip").position
let j3 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("pinky-finger-tip").position
let j4 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("middle-finger-tip").position
let d1 = j1.distanceTo( j2 )
let d2 = j1.distanceTo( j3 )
let d3 = j1.distanceTo( j4 )
// console.log( 'normal pinch distance', d1 )
// console.log( 'secondary pinch distance', d2 )
document.querySelector("a-sky").setAttribute("color", "gray")
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 > secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-not', {hand: 'right'})
// console.log( 'normal pinch' ) // just for testing
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 < secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch', {hand: 'right'})
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d3 < primaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-middle-finger', {hand: 'right'})
}, 10)
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch', evt => {
//addNewNote('added here...', AFRAME.utils.coordinates.stringify( pinches.filter( p => p.primary ).at(-1).position ) )
} )
let lastSecondaryPinchExecuted = Date.now()
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch-middle-finger', evt => {
if ( Date.now() - lastSecondaryPinchExecuted < 500 ){
// console.warn('ignoring, executed secondary pinch during the last 500ms already')
let x = 42 // added only to comment the previous line
} else {
lastSecondaryPinchExecuted = Date.now()
//console.log('mid pinched after 500ms...')
let el = addNewNote('note', AFRAME.utils.coordinates.stringify( AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-tip").position ) )
el.setAttribute("troika-text", "outlineWidth", 0)
setTimeout( _ => el.object3D.lookAt( player.object3D.position ), 500 )
}
} )
wristShortcut = 'jxr applyToClass("keys_from_drumsticks", el => el.setAttribute("visible", "false") )'
otherWristShortcut = 'jxr applyToClass("keys_from_drumsticks.layer_0", el => el.setAttribute("visible", "true") )'
// get keymap
showFile('fabien_corneish_zen.keymap')
const keyclass = "keys_from_drumsticks"
// transform keymap to keys of keyboard
AFRAME.scenes[0].addEventListener("keymaploaded", e => {
applyToClass("keymap_layer", el => el.setAttribute("visible", "false") )
// should get other layers
Array.from( document.querySelectorAll('.keymap_layer') ).map( (layerToAdd, layerNumber) => {
layerToAdd.getAttribute("value").split('\n').map( (l,y) => {
l.split("|").map( (k,x) => {
if (k.trim()) {
// zIndex could be once deep once shallow to potentially go faster between keys, kind of straggered vs ortho
let xOffset = -.2
// layer 1 and 2 get strange offsets
let yOffset = 1.3
let zOffset = -.4
let ratio = 1/20
if (l.length < 80) xOffset += .15
let labelEl = document.createElement("a-troika-text")
labelEl.setAttribute("value",k.trim())
labelEl.setAttribute("position", "0 .51 0")
labelEl.setAttribute("font-size", "1")
labelEl.setAttribute("color", "black")
labelEl.setAttribute("rotation", "-90 0 0")
let keyEl = document.createElement("a-cylinder")
keyEl.setAttribute("segments-height", "2")
keyEl.setAttribute("segments-radial", "24")
keyEl.setAttribute("scale", ".01 .01 .01")
keyEl.setAttribute("rotation", "60 0 0")
keyEl.setAttribute("position", ""+(xOffset+x*ratio)+" " +(yOffset-y*ratio)+" "+(zOffset + y/50) )
keyEl.classList.add( keyclass )
keyEl.classList.add( "layer_"+ layerNumber )
AFRAME.scenes[0].appendChild( keyEl )
keyEl.appendChild( labelEl )
// keyEl.id = keyclass+'_'+k.trim() // not correct anymore as multiple layers can have the same key
keyEl.id = keyclass+'_'+layerNumber+'_'+k.trim()
// setTimeout( _ => keyEl.object3D.lookAt( new THREE.Vector3(0, 1.5, 0)), 100 )
// not great as they have 90 deg rotation already, could find a better way
// ... but arguably should be the opposite based on resting hand positions
}
})
})
})
keys_from_drumsticks_0_SHFT.setAttribute("wireframe", shiftFromVirtualKeyboard) // arguable, could do so for all layers
// hide all layers but the current one
Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") )
Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") )
keyBoardCheck()
})
const threshold = .02 // distance
const refractionPeriod = 500 // ms until next keypress
// add visible contact points
let jointTestEl1 = document.createElement("a-sphere")
jointTestEl1.setAttribute("radius", .01)
jointTestEl1.id = "jointtest1"
AFRAME.scenes[0].appendChild( jointTestEl1 )
let jointTestEl2 = document.createElement("a-sphere")
jointTestEl2.setAttribute("radius", .01)
jointTestEl2.id = "jointtest2"
AFRAME.scenes[0].appendChild( jointTestEl2 )
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test1", { hand: 'r_handMeshNode', finger: 'index-finger-tip', target: '#jointtest1' } )
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test2", { hand: 'l_handMeshNode', finger: 'index-finger-tip', target: '#jointtest2' } )
let pos1 = new THREE.Vector3()
let pos2 = new THREE.Vector3()
let shiftFromVirtualKeyboard = true
let layerFromVirtualKeyboard = 0
let keyboardTarget = typinghud // TODO replace in keyBoardCheck()
// check for potential contact
let lastKeypress = Date.now()
function keyBoardCheck() {
return setInterval( _ => {
jointTestEl1.object3D.getWorldPosition( pos1 )
jointTestEl2.object3D.getWorldPosition( pos2 )
// could also check only when jointTestEl1 / jointTestEl2 are visible, ignore otherwise
// to do with all keys instead
Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) )
.concat( [keys_from_drumsticks_0_LWR, keys_from_drumsticks_0_RSE] )
.filter( k => k.getAttribute("visible") )
.map( k => {
// should only look at visible keys, could limit via .filter( k => k.getAttribute("visible") == "true")
// this way one wouldn't type on an invisible keyboard
let d1 = k.object3D.position.distanceTo( pos1 )
let d2 = k.object3D.position.distanceTo( pos2 )
if ( d1 < threshold || d2 < threshold) {
k.setAttribute("color", "pink")
typinghud.setAttribute("material","opacity", .5)
if ( Date.now() - lastKeypress < refractionPeriod ){
// console.warn('ignoring, executed during the last 500ms already')
let x = 42 // added just to ignore
} else {
lastKeypress = Date.now()
let value = k.firstChild.getAttribute("value")
if ( typinghud.getAttribute("value") == "[]" )
typinghud.setAttribute("value", "" )
if (value == "SPC") value = " "
if (value == "ENT") {
parseKeys("keydown", "Enter")
typinghud.setAttribute("value", "" )
} else if (value == "SHFT") {
shiftFromVirtualKeyboard = !shiftFromVirtualKeyboard
// visual highlight, also note that is closer to CAPSLOCK behavior
keys_from_drumsticks_0_SHFT.setAttribute("wireframe", shiftFromVirtualKeyboard) // arguable, could do so for all layers
} else if (value == "RSE") {
if (layerFromVirtualKeyboard<2) layerFromVirtualKeyboard++ // hardcoded max
console.log('should raise layer', layerFromVirtualKeyboard) // a la CAPSLOCK too
Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") )
Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") )
// forcing visibility yet get ignored as on wrong layer
keys_from_drumsticks_0_LWR.setAttribute("visible", "true")
keys_from_drumsticks_0_RSE.setAttribute("visible", "true")
} else if (value == "LWR") {
if (layerFromVirtualKeyboard>0) layerFromVirtualKeyboard--
console.log('should lower layer', layerFromVirtualKeyboard) // a la CAPSLOCK too
Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") )
Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") )
keys_from_drumsticks_0_LWR.setAttribute("visible", "true")
keys_from_drumsticks_0_RSE.setAttribute("visible", "true")
} else if (value == "BKSP") {
typinghud.setAttribute("value", typinghud.getAttribute("value").slice(0,-1) )
} else {
if (!shiftFromVirtualKeyboard) value = value.toLowerCase()
typinghud.setAttribute("value", typinghud.getAttribute("value") + value )
}
}
} else if ( d1 < threshold*1.2 || d2 < threshold*1.2) { // arguably, not convinced it brings value more than confusion
k.setAttribute("color", "#ffe4e1")
} else {
k.setAttribute("color", "white")
}
})
}, 20)
}
el = addNewNote( 'jxr applyToClass("keys_from_drumsticks", k => k.object3D.translateZ(-.1) )', '0.5 1.35 0')
el.setAttribute("annotation", "content:"+ "move keyboard up")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'jxr applyToClass("keys_from_drumsticks", k => k.object3D.translateZ(.1) )', '0.5 1.3 0')
el.setAttribute("annotation", "content:"+ "move keyboard down")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'jxr applyToClass("keys_from_drumsticks", k => k.object3D.translateY(-.1) )', '0.5 1.25 0')
el.setAttribute("annotation", "content:"+ "move keyboard further")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'jxr applyToClass("keys_from_drumsticks", k => k.object3D.translateY(.1) )', '0.5 1.2 0')
el.setAttribute("annotation", "content:"+ "move keyboard closer")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'jxr applyToClass("keys_from_drumsticks", k => k.object3D.translateX(-.1) )', '0.5 1.15 0')
el.setAttribute("annotation", "content:"+ "move keyboard left")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'jxr applyToClass("keys_from_drumsticks", k => k.object3D.translateX(.1) )', '0.5 1.1 0')
el.setAttribute("annotation", "content:"+ "move keyboard right")
el.setAttribute("rotation", "90 -90 0")
}
if (username && username == "q2_visualmetaexport_map_via_wordpress") {
cleanSlateUI() // hides a bunch of stuff
const showdemoexample = urlParams.get('showdemoexample');
if (showdemoexample) showFile('https://futuretextlab.info/wp-content/uploads/Frode-dynamicviewvisualmetaexport.jsons_.zip')
applyToClass("flatuioptions", el => el.style.visibility = "hidden" )
setTimeout( _ => {
let lines = Array.from( document.querySelectorAll("[line]") )
.filter( el => el.id.startsWith("start_visualmetaexport") && el.id.includes("_end_visualmetaexport_") )
lines.map( el => el.setAttribute("line", "color", "black")) // can be highlighted as lightgray or black
lines.map( el => el.setAttribute("visible", "false") )
// lines.map( el => el.setAttribute("opacity", .1) ) // does nothing
let keywordElements = Array.from( document.querySelectorAll(".notes") ).filter( el => el.id.startsWith("visualmetaexport_") )
keywordElements.map( el => {
el.setAttribute("troika-text", "fontSize", ".1" )
el.setAttribute("onpicked", 'Array.from( document.querySelectorAll("[line]") ).filter( el => el.id.includes( selectedElements.at(-1).element.id.toLowerCase() ) ).map( el => el.setAttribute("visible", "true")) ')
el.setAttribute("onreleased", 'Array.from( document.querySelectorAll("[line]") ).filter( el => el.id.includes("visualmetaexport_")).map( el => el.setAttribute("visible", "false")); selectedElements.at(-1).element.object3D.lookAt( player.object3D.position ) ')
const forcelookat = urlParams.get('forcelookat');
if (forcelookat) el.object3D.lookAt( player.object3D.position )
})
/*
let keywords = keywordElements.map( el => el.getAttribute("value" ) )
// example of dynamic filtering
let keyword = keywords.at(keywords.length*Math.random())
lines.filter( el => el.id.includes("visualmetaexport_"+keyword.toLowerCase() ) )
.map( el => el.setAttribute("line", "color", "lightgray")) // can be highlighted as lightgray or black
*/
}, 1000 )
const debug = urlParams.get('showdebug');
if (debug){
document.querySelector("a-console").setAttribute("visible", true)
basiccommands.setAttribute("visible", true)
}
document.getElementById("otherbox").setAttribute("opacity", 1)
// note mode -------------------------------------------------------------
const primaryPinchSingleHandedDistanceThreshold = 0.02 // 2 cm
const secondaryPinchSingleHandedDistanceThreshold = 0.05 // need to be much looser, maybe due to privacy limitations or camera position (hidden fingers)
let secondaryPinchSingleHanded = setInterval( el => {
if ( !AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode") ) return
let j1 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-tip").position
let j2 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("index-finger-tip").position
let j3 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("pinky-finger-tip").position
let j4 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("middle-finger-tip").position
let d1 = j1.distanceTo( j2 )
let d2 = j1.distanceTo( j3 )
let d3 = j1.distanceTo( j4 )
// console.log( 'normal pinch distance', d1 )
// console.log( 'secondary pinch distance', d2 )
document.querySelector("a-sky").setAttribute("color", "gray")
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 > secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-not', {hand: 'right'})
// console.log( 'normal pinch' ) // just for testing
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 < secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch', {hand: 'right'})
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d3 < primaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-middle-finger', {hand: 'right'})
}, 10)
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch', evt => {
//addNewNote('added here...', AFRAME.utils.coordinates.stringify( pinches.filter( p => p.primary ).at(-1).position ) )
} )
let lastSecondaryPinchExecuted = Date.now()
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch-middle-finger', evt => {
if ( Date.now() - lastSecondaryPinchExecuted < 500 ){
// console.warn('ignoring, executed secondary pinch during the last 500ms already')
let x = 42 // added only to comment the previous line
} else {
lastSecondaryPinchExecuted = Date.now()
//console.log('mid pinched after 500ms...')
let el = addNewNote('note', AFRAME.utils.coordinates.stringify( AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-tip").position ) )
el.setAttribute("troika-text", "outlineWidth", 0)
setTimeout( _ => el.object3D.lookAt( player.object3D.position ), 500 )
}
} )
/*
let jointTestEl1 = addNewNote( "[menu]", "0 .03 -.0")
jointTestEl1.setAttribute("scale", ".05 .05 .05")
jointTestEl1.setAttribute("rotation", "0 90 0")
jointTestEl1.id = "jointtest1"
let jointTestEl2 = addNewNote( "[option]", "0 .03 -.0")
jointTestEl2.setAttribute("scale", ".05 .05 .05")
jointTestEl2.setAttribute("rotation", "0 -90 180")
jointTestEl2.id = "jointtest2"
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test1", { hand: 'l_handMeshNode', finger: 'index-finger-tip', target: '#jointtest1' })
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test2", { hand: 'l_handMeshNode', finger: 'thumb-tip', target: '#jointtest2' } )
*/
wristShortcut = 'jxr document.querySelector("a-console").setAttribute("visible", false)'
otherWristShortcut = 'jxr document.querySelector("a-console").setAttribute("visible", true)'
}
if (username && username == "q2_noneuclidian") {
// something else via wrists
//wristShortcut = 'jxr document.querySelector("a-console").setAttribute("visible", false)'
otherWristShortcut = 'jxr document.querySelector("a-console").setAttribute("visible", true)' // could toggle instead
// note mode -------------------------------------------------------------
const primaryPinchSingleHandedDistanceThreshold = 0.02 // 2 cm
const secondaryPinchSingleHandedDistanceThreshold = 0.05 // need to be much looser, maybe due to privacy limitations or camera position (hidden fingers)
let secondaryPinchSingleHanded = setInterval( el => {
if ( !AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode") ) return
let j1 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-tip").position
let j2 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("index-finger-tip").position
let j3 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("pinky-finger-tip").position
let j4 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("middle-finger-tip").position
let j1b = AFRAME.scenes[0].object3D.getObjectByName("l_handMeshNode")?.parent.getObjectByName("thumb-tip").position
let j2b = AFRAME.scenes[0].object3D.getObjectByName("l_handMeshNode")?.parent.getObjectByName("index-finger-tip").position
let j3b = AFRAME.scenes[0].object3D.getObjectByName("l_handMeshNode")?.parent.getObjectByName("pinky-finger-tip").position
let j4b = AFRAME.scenes[0].object3D.getObjectByName("l_handMeshNode")?.parent.getObjectByName("middle-finger-tip").position
let d1 = j1.distanceTo( j2 )
let d2 = j1.distanceTo( j3 )
let d3 = j1.distanceTo( j4 )
let d1b = j1b.distanceTo( j2b )
let d2b = j1b.distanceTo( j3b )
let d3b = j1b.distanceTo( j4b )
// console.log( 'normal pinch distance', d1 )
// console.log( 'secondary pinch distance', d2 )
if ( d1b < primaryPinchSingleHandedDistanceThreshold && d2b > secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-not', {hand: 'left'})
// console.log( 'normal pinch' ) // just for testing
if ( d1b < primaryPinchSingleHandedDistanceThreshold && d2b < secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch', {hand: 'left'})
if ( d1b < primaryPinchSingleHandedDistanceThreshold && d3b < primaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-middle-finger', {hand: 'left'})
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 > secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-not', {hand: 'right'})
// console.log( 'normal pinch' ) // just for testing
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 < secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch', {hand: 'right'})
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d3 < primaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-middle-finger', {hand: 'right'})
}, 10)
let lastSecondaryPinchExecuted = Date.now()
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch-middle-finger', evt => {
if ( Date.now() - lastSecondaryPinchExecuted < 500 ){
// console.warn('ignoring, executed secondary pinch during the last 500ms already')
let x = 42 // added only to comment the previous line
} else {
// handling a single hand for now, should test for both
lastSecondaryPinchExecuted = Date.now()
console.log('mid pinched after 500ms...')
// could use right hand to push/pull entire rig
// could use left hand y axis to zoom in/out of world
}
} )
// -------------------------------------------------------------
// more options
let jointTestEl1 = addNewNote( "jxr resetZoom()", "0 .03 -.0")
jointTestEl1.setAttribute("scale", ".05 .05 .05")
jointTestEl1.setAttribute("rotation", "0 90 0")
jointTestEl1.id = "jointtest1"
let jointTestEl2 = addNewNote( "jxr resetRigPosition()", "0 .03 -.0")
jointTestEl2.setAttribute("scale", ".05 .05 .05")
jointTestEl2.setAttribute("rotation", "0 -90 180")
jointTestEl2.id = "jointtest2"
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test1", { hand: 'r_handMeshNode', finger: 'index-finger-tip', target: '#jointtest1' })
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test2", { hand: 'r_handMeshNode', finger: 'thumb-tip', target: '#jointtest2' } )
}
if (username && username == "q2_radialmenu") {
// <a-cylinder color="yellow" theta-start="50" theta-length="280" side="double"></a-cylinder>
let el = document.createElement("a-cylinder")
el.setAttribute("theta-start", "0")
el.setAttribute("theta-length", "80")
el.setAttribute("side", "double")
el.setAttribute("position", "0 1 -1")
el.setAttribute("color", "orange")
el.setAttribute("scale", "0.1 0.1 .1")
AFRAME.scenes[0].appendChild(el)
el = document.createElement("a-cylinder")
el.setAttribute("rotation", "0 90 0")
el.setAttribute("scale", "0.1 0.1 .1")
el.setAttribute("theta-start", "80")
el.setAttribute("theta-length", "80")
el.setAttribute("color", "red")
el.setAttribute("side", "double")
el.setAttribute("position", "0 1 -1")
AFRAME.scenes[0].appendChild(el)
el = document.createElement("a-cylinder")
el.setAttribute("rotation", "0 90 0")
el.setAttribute("scale", "0.1 0.1 .1")
el.setAttribute("theta-start", "160")
el.setAttribute("theta-length", "40")
el.setAttribute("color", "blue")
el.setAttribute("side", "double")
el.setAttribute("position", "0 1 -1")
AFRAME.scenes[0].appendChild(el)
el = document.createElement("a-cylinder")
el.setAttribute("rotation", "0 90 0")
el.setAttribute("scale", "0.1 0.1 .1")
el.setAttribute("theta-start", "200")
el.setAttribute("theta-length", "180")
el.setAttribute("color", "green")
el.setAttribute("side", "double")
el.setAttribute("position", "0 1 -1")
AFRAME.scenes[0].appendChild(el)
el = document.createElement("a-cylinder")
el.setAttribute("side", "double")
el.setAttribute("position", "0 1 -1")
el.setAttribute("scale", "0.01 0.2 .01")
AFRAME.scenes[0].appendChild(el)
}
if (username && username == "q2_visualmetaexport_map") {
cleanSlateUI() // hides a bunch of stuff
showFile('q2_visualmetaexport_map__dynamicviewvisualmetaexport.json')
applyToClass("flatuioptions", el => el.style.visibility = "hidden" )
setTimeout( _ => {
let lines = Array.from( document.querySelectorAll("[line]") )
.filter( el => el.id.startsWith("start_visualmetaexport") && el.id.includes("_end_visualmetaexport_") )
lines.map( el => el.setAttribute("line", "color", "darkgray")) // can be highlighted as lightgray or black
// lines.map( el => el.setAttribute("opacity", .1) ) // does nothing
let keywordElements = Array.from( document.querySelectorAll(".notes") ).filter( el => el.id.startsWith("visualmetaexport_") )
keywordElements.map( el => {
el.setAttribute("onpicked", 'Array.from( document.querySelectorAll("[line]") ).filter( el => el.id.includes( selectedElements.at(-1).element.id.toLowerCase() ) ).map( el => el.setAttribute("line", "color", "black")) ')
el.setAttribute("onreleased", 'Array.from( document.querySelectorAll("[line]") ).filter( el => el.id.includes("visualmetaexport_")).map( el => el.setAttribute("line", "color", "darkgray")) ')
})
/*
let keywords = keywordElements.map( el => el.getAttribute("value" ) )
// example of dynamic filtering
let keyword = keywords.at(keywords.length*Math.random())
lines.filter( el => el.id.includes("visualmetaexport_"+keyword.toLowerCase() ) )
.map( el => el.setAttribute("line", "color", "lightgray")) // can be highlighted as lightgray or black
*/
}, 1000 )
const debug = urlParams.get('showdebug');
if (debug){
document.querySelector("a-console").setAttribute("visible", true)
basiccommands.setAttribute("visible", true)
}
document.getElementById("otherbox").setAttribute("opacity", 1)
}
if (username && username == "q2_wrist_rotations") {
rightHand.removeAttribute("pinchprimary")
leftHand.removeAttribute("pinchsecondary")
/*
rightHand.setAttribute("pinchprimary_alt")
leftHand.setAttribute("pinchsecondary_alt")
*/
// replaced for now by ... q2_onrelease_lookat
}
if (username && username == "q2_keymap") {
showFile('fabien_corneish_zen.keymap')
}
if (username && username == "q2_ntfy_keyboard_with_keymap_visual_feedback") {
showFile('fabien_corneish_zen.keymap')
let textInput = document.createElement("input")
document.body.append( textInput )
textInput.focus()
textInput.id = "textinputforoskeyboard"
textInput.style = 'position:absolute; zIndex:99; top:100px; left:100px;'
let ntfy_keyboard_path = "remote_keyboard"
const remote_keyboard_group = urlParams.get('remote_keyboard_group'); // should be URL encoded, assuming for now alphanum only
if (remote_keyboard_group) ntfy_keyboard_path += remote_keyboard_group
// seems to work most times.... not clear when it does or does not
// needs refresh somehow, connecting both
const eventSourceConverted = new EventSource( `https://ntfy.benetou.fr/${ntfy_keyboard_path}/sse` )
eventSourceConverted.onmessage = (e) => {
let message = JSON.parse( JSON.parse( e.data ).message )
console.log('ntfy remote keyboard', message )
if (message.status == "change" ){
addNewNote( message.value, "0 1.5 -0.7")
}
if (message.status == "input" ){
hudTextEl.setAttribute("value", message.value )
let letter = message.value.at(-1).toUpperCase()
if (letter == ' ') letter = 'SPC' // somehow does not work
// seems to be 1 letter "behind", or very slow
setTimeout( _ => {
keymap_layer0.highlightLetter(letter)
setTimeout( _ => keymap_layer0.unhighlightLetter(letter), 500 )
}, 2000 )
}
}
textInput.onchange = e => {
parseKeys("keydown", "Enter")
fetch('https://ntfy.benetou.fr/'+ntfy_keyboard_path, { method: 'POST', body: JSON.stringify({status: "change", value:e.target.value}) })
hudTextEl.setAttribute("value", "" )
textInput.value = ""
}
textInput.oninput = e => {
fetch('https://ntfy.benetou.fr/'+ntfy_keyboard_path, { method: 'POST', body: JSON.stringify({status: "input", value:e.target.value}) })
}
}
if (username && username == "q2_arcade") {
// https://github.com/mrxz/fern-aframe-components/tree/main/effekseer
// ...
let script = document.createElement("script")
document.head.appendChild( script )
script.src = "https://cdn.jsdelivr.net/gh/mrxz/effekseer-sample-effects/effekseer-build/effekseer.min.js"
let script2 = document.createElement("script")
document.head.appendChild( script2 )
script2.src = "https://cdn.jsdelivr.net/npm/@zip.js/zip.js/dist/zip.min.js"
setTimeout( _ => {
let script3 = document.createElement("script")
document.head.appendChild( script3 )
script3.src = "https://cdn.jsdelivr.net/npm/@fern-solutions/aframe-effekseer/dist/aframe-effekseer.umd.min.js"
AFRAME.scenes[0].setAttribute("effekseer", "wasmPath: https://cdn.jsdelivr.net/gh/mrxz/effekseer-sample-effects/effekseer-build/effekseer.wasm" )
let el = document.createElement("a-assets")
AFRAME.scenes[0].appendChild(el)
setTimeout( _ => {
let elAsset = document.createElement("a-asset-item")
el.appendChild(elAsset)
elAsset.id="effect-asset"
elAsset.setAttribute("src", "https://cdn.jsdelivr.net/gh/mrxz/effekseer-sample-effects/effects/tktk02/Sword1.efkpkg")
elAsset.setAttribute("response-type", "arraybuffer")
let elEffect = document.createElement("a-entity")
AFRAME.scenes[0].appendChild(elEffect)
elEffect.setAttribute("effekseer", "src: #effect-asset")
elEffect.setAttribute("position","0 1.5 -10")
elEffect.id = "effect"
// command to do it again
addNewNote("jxr effect.components.effekseer.playEffect()", "-0.5 1 -.45" )
// could do that on pinch instead, AT pinch, a la q2_secondarypinch_singlehanded_spatial
// could generalize to run any command at location instead
fetch('https://cdn.jsdelivr.net/gh/mrxz/effekseer-sample-effects/effects/index.json').then( res => res.json() ).then( json => {
console.log(json)
let elAsset = document.createElement("a-asset-item")
el.appendChild(elAsset)
elAsset.id="effect-asset-dynamic"
let effect = json.effects[Math.floor(Math.random()*json.effects.length)]
elAsset.setAttribute("src", "https://cdn.jsdelivr.net/gh/mrxz/effekseer-sample-effects/effects/"+effect)
elAsset.setAttribute("response-type", "arraybuffer")
elEffect.setAttribute("effekseer", "src: #effect-asset-dynamic")
})
}, 1000 )
}, 1000 )
cleanSlateUI() // hides a bunch of stuff
document.querySelector("a-sky").setAttribute("color","black")
groundfor360.setAttribute("visible", "false")
}
if (username && username == "q2_spatialknowledgeobject") {
const query = urlParams.get('query');
console.log( query )
let el = document.createElement("a")
el.href = "mms://whatever/somedata"
//el.href = "web+spatialknowledgeobject:whatever/somedata"
el.innerText = "testing custom protocol"
flatuifeatures.appendChild(el)
// ref https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
// register
//navigator.registerProtocolHandler( "web+spatialknowledgeobject", "https://companion.benetou.fr/?username=q2_spatialknowledgeobject&query=%s",)
let registerEl = document.createElement("span")
registerEl.innerText = "register custom protocol"
registerEl.onclick = _ => {
console.log(navigator);
navigator.registerProtocolHandler( "mms", "https://companion.benetou.fr/?username=q2_spatialknowledgeobject&query=%s",)
}
flatuifeatures.appendChild(registerEl)
navigator.registerProtocolHandler( "mms", "https://companion.benetou.fr/?username=q2_spatialknowledgeobject&query=%s",)
// does not seem available on Quest
// test
//setTimeout( _ => window.open("web+spatialknowledgeobject:whatever/somedata"), 2000 )
//setTimeout( _ => window.open("web+spatialknowledgeobject:whatever/somedata"), 2000 )
}
if (username && username == "q2_bbox_per_filter_source") {
// make a non visible box
const box = new THREE.Box3();
const mesh = new THREE.Mesh( new THREE.SphereGeometry(), new THREE.MeshBasicMaterial());
mesh.geometry.computeBoundingBox();
// make that box visible
const helper = new THREE.Box3Helper( box, 0xffff00 )
AFRAME.scenes[0].object3D.add( helper )
// expand the box with all objects from the same source
//box.expandByObject( document.querySelector("a-console").object3D )
//box.expandByObject( document.querySelector("a-troika-text").object3D )
// etc
showFile( "https://fabien.benetou.fr/PersonalInformationStream/WithoutNotesMay2025?action=source" )
AFRAME.scenes[0].addEventListener("pmwikiloaded", e => {
console.log(e)
setTimeout( _ => {
Array.from( document.querySelectorAll('.filterimport.pmwikifilter') ).map( el => box.expandByObject( el.object3D ) )
},1000)
})
}
if (username && username == "q2_nouploadfile") {
// could try to use showfile anyway via specific metadata
// e.g. r.map( f => filesWithMetadata[f.name] = f.metadata )
// could then use data itself as a metadata e.g.
// filesWithMetadata[f.name].data = dataFromLoadedFile
let div = document.createElement("div")
document.body.append( div )
let span = document.createElement("span")
span.innerText = 'Private upload (no server) : '
div.appendChild( span )
let fileInput = document.createElement("input")
div.appendChild( fileInput )
fileInput.type = "file"
fileInput.id = "fileinputinline"
fileInput.name = "file"
fileInput.accept = ".png, .jpg, .jpeg"
div.style = 'position:absolute; zIndex:99; top:100px; left:100px;'
fileInput.onchange = e => {
let file = fileInput.files[0]
// console.log( e, file )
const reader = new FileReader();
reader.onload = (evt) => {
fileContent = evt.target.result
// console.log( fileContent )
// could then make new <a-image> with content a src
let el = document.createElement("a-image")
AFRAME.scenes[0].appendChild(el)
el.setAttribute("position", "0 "+(Math.random()+1)+" -0.5" )
el.setAttribute("rotation", "0 180 0")
el.setAttribute("scale", ".1 .1 .1")
el.setAttribute("src", fileContent)
el.setAttribute("target", "")
}
reader.readAsDataURL(file);
}
}
if (username && username == "q2_lego_map") {
// introspection related functions
// file listing via WebDAV
// filters listing via WebDAV
// experience listing via dedicated JSON (existing filter?)
// write dedicated filter for in XR exploration
const fileExampleLink = 'https://webdav.benetou.fr/fotsave/fot_sloan_companion_public/references_manual_v14.json'
const demoExampleLink = 'https://companion.benetou.fr/index.html?username=q2_json_collaborations'
const filtersLink = 'https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/filters'
const convertersLink = 'https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/backend/converters'
const images = [
'https://companion.benetou.fr/poweruser_screenshot_1739174489566.jpg',
'https://companion.benetou.fr/q1_step_refcards.png',
'https://companion.benetou.fr/q1_step_highlights.png',
'https://companion.benetou.fr/demoqueueq1.png'
]
// x = -1 left border (blue axis)
// z = -1 back border (red axis)
// baseplate = 25cm square, 32x32-stud
// see also MILS Baseplate with Customizable Border
let width = .25
let studSpacing = width/32
let xOffset = -width/2
let yOffset = .8 // table height
let zOffset = -1+width
let y = .8
let notesToPlaces = [
{xStuds:10, zStuds:10, text: "text at 10x10"},
{xStuds:32, zStuds:32, text: "text at 32x32"},
]
notesToPlaces.map( n =>
addNewNote( n.text, "" + (xOffset+n.xStuds*studSpacing) + " " + (yOffset) + " " + (zOffset-n.zStuds*studSpacing) )
)
addNewNote( 'jxr window.open("'+fileExampleLink+'", "_blank").focus()', "-.5 "+(y+.1)+" -.3" )
addNewNote( 'jxr window.open("'+fileExampleLink+'")', "-.5 "+(y+.2)+" -.3" )
// open in background
images.map( (img,i) => {
let el = document.createElement("a-image")
AFRAME.scenes[0].appendChild(el)
el.setAttribute("position", ""+ (0.5+i*.2) + " " +(y+.1)+" "+ zOffset )
el.setAttribute("scale", ".1 .05 .01")
el.setAttribute("src", img)
el.setAttribute("target", "")
})
addNewNote("Frontend", "-.3 "+(y)+" -.5" )
addNewNote("Backend", "-.4 "+(y)+" -.4" )
addNewNote("Files", "-.5 "+(y)+" -.3" )
manuscript.setAttribute("visible", "false")
groundfor360.setAttribute("visible", "false")
}
if (username && username == "q2_json_collaborations") {
// TODO showcase src="filters/rete.bitbybit.json.js"
//showFile('rete-runner.bitbybit')
// workspace-rete.bitbybit
// maybe nosave_rete-runner.bitbybit
showFile('https://video.benetou.fr/api/v1/search/videos?tagsOneOf=q2_fot_sloan')
// consider framing each output as loaded file
showFile('example26_05_2025_dynamicviewvisualmetaexport.json')
showFile('Mereological zoom selection on (wiki) filter-20250524-120707.md')
showFile('milestones.csv')
// can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
// consider relying on showFile() new parameter openingOptions to also explore
// indexability for full text search, e.g. indexable: true
// non working example : showFile("FoT_Sloan_WhitePaper_content.odt/content.xml", { indexable: true })
showFile( "https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml" )
// could try to move it after, alternatively could try to not overlap
showFile( "https://companion.benetou.fr/saved/pdfxml/augmented_paper.xml" )
AFRAME.scenes[0].addEventListener("pdfxmlloaded", e =>
document.querySelector(".page_from_pdf").object3D.position.x+=1.5
// should shift each new added page this way
)
let testingCommands = [
'AFRAME.scenes[0].setAttribute("useraddednote-append-to", "target:#manuscript")',
'AFRAME.scenes[0].removeAttribute("useraddednote-append-to")',
]
testingCommands.map( (c,i) => addNewNote("jxr " + c, "-1.0 "+(1+i/10)+" -.45" ) )
showFile("full.visualmetaexport.json")
showFile("tapestry/root.json")
showFile("FoT_Sloan_WhitePaper_content.odt/content.xml")
showFile("sample3_content.docx/word/document.xml")
showFile("sample3_content.docx/sample3.docx")
AFRAME.scenes[0].addEventListener("visualmetajsonloaded", e => console.log('file loaded',e.detail))
// via URL too...
showFile( "https://fabien.benetou.fr/PersonalInformationStream/WithoutNotesMay2025?action=source" )
showFile( "Horn.svg" )
AFRAME.scenes[0].addEventListener("svgloaded", e => {
let filename = "Horn.svg"
console.log('svg file loaded',e.detail)
let idFromFilename = filename.replaceAll('.','') // has to remove from proper CSS ID
document.getElementById( idFromFilename ).setAttribute("width", 2)
document.getElementById( idFromFilename ).setAttribute("position", "1.5 1.5 0")
document.getElementById( idFromFilename ).setAttribute("rotation", "0 -80 0")
fetch(filename).then( r => r.text() )
.then( str => new window.DOMParser().parseFromString(str, "text/xml"))
.then(data => {
let exampleFromSVG = addNewNote( data.querySelector("#tspan15852").innerHTML, "1.5 2.1 0.5")
exampleFromSVG.setAttribute("rotation", "0 -80 0")
})
})
}
if (username && username == "q2_most_recent_file") {
const partialfilename = urlParams.get('partialfilename');
//setTimeout( _ => getMostRecentFile(partialfilename).then( files => showFile(files[0].basename) ), 2000 )
//setTimeout( _ => getMostRecentFile(partialfilename).then( files => console.log( files ) ) , 2000 )
// waiting on WebDAV to get ready... but never ready?!
async function rd(){ return await webdavClient.getDirectoryContents(subdirWebDAV) }
rd().then( res => {
let files = res.filter(f => f.basename.includes(partialfilename))
.filter(f=>f.type=="file")
.sort( (a,b) => new Date(a.lastmod).getTime() < new Date(b.lastmod).getTime() ) // newest first
//console.log( partialfilename, files, res, files[0].basename )
showFile( files[0].basename )
console.log( 'file found:', files[0].basename )
})
}
if (username && username == "q2_annotated_bibliography_week2") {
addDrumKeyboard()
manuscript.removeAttribute("onreleased")
manuscript.setAttribute("scale", ".42 .58 .02")
// untested input methods for VisionPro due to lack of keyboard support
// keyboard rings
// see q2_ring_keyboard
// STT
// see q1_step_audio and recordercommands.setAttribute("visible", true)
/*
recordercommands.setAttribute("visible", true)
Array.from( recordercommands.querySelectorAll("a-troika-text")).map( el => el.setAttribute("visible", "true") )
// must have an Apple specific validation outside of XR
let styleEl = document.querySelector("[href='#recorder']").style
styleEl.position = "absolute"
styleEl.top = "300px"
styleEl.left = "300px"
styleEl.fontSize = "xxx-large"
// unfortunately on Vision Pro despite all that the audio is muted, no sound sent
*/
showFile("references_manual_v11.json")
setTimeout( _ => {
Array.from( document.querySelectorAll(".reference-entry") ).map( el => el.setAttribute("visible","false") )
Array.from( document.querySelectorAll(".reference-entry-card") ).map( el => el.setAttribute("visible","false") )
Array.from( document.querySelectorAll(".reference-entry-note") ).map( el => el.setAttribute("visible","false") )
Array.from( document.querySelectorAll(".reference-entry-annotate") ).map( el => el.setAttribute("visible","false") )
Array.from( document.querySelectorAll(".reference-entry-showfile") ).map( el => el.setAttribute("visible","false") )
},1000)
// showFile("saved/pdfxml/317426.317445.xml") // photo of text, no highlights
showFile("saved/pdfxml/3603163.3609075.xml")
AFRAME.scenes[0].addEventListener("pdfxmlloaded", e => {
// TODO untested
Array.from( AFRAME.scenes[0].querySelectorAll(".page_from_pdf") ).at(-1).object3D.translateX(1.5)
// does not seem to work
let id = "page_from_"+e.detail.replaceAll("/","_")
document.getElementById(id).setAttribute("scale", ".5 .5 .5")
document.getElementById(id).setAttribute("position", ".5 1.7 -.5")
//Array.from( AFRAME.scenes[0].querySelectorAll(".page_from_pdf") ).at(-1).object3D.position.x+=1.5
// does not work well
// should shift each new added page this way
Array.from( AFRAME.scenes[0].querySelectorAll(".page_from_pdf>a-troika-text") )[0].setAttribute("onreleased",
"let el = selectedElements.at(-1).element; el.object3D.parent = manuscript.object3D; el.object3D.position.set(0,0,0); el.object3D.rotation.set(0,0,0); el.object3D.translateZ(.9); "
+ "el.setAttribute('value', el.getAttribute('value') + Array.from( document.querySelectorAll('.reference-entry') ).filter( el => el.getAttribute('value').includes('SPORE') )[0].data['bibtex-data'].doi );"
)
// should add citation meta-data e.g. page number, page title, etc
// now loaded and hidden
})
const classNameToBeSaved = "tobesaved"
let testingCommands = [
{value:'AFRAME.scenes[0].removeAttribute("useraddednote-append-to")', annotation: "free flow"},
{value:'loadFromLocalStorage()', annotation: "load locally"},
{value:'saveToLocateStorage()', annotation: "save locally"},
{value:'loadFromRemoteStorage()', annotation: "load remotely"}, // should be a filter, a JSON layout from year 1
// need either a filename or latest file
{value:'saveToRemoteStorage()', annotation: "save remotely"},
{value:'AFRAME.scenes[0].setAttribute("useraddednote-append-to", "target:#manuscript")', annotation: "append to manuscript"}
]
let elements = testingCommands
.map( (c,i) => {
let el = addNewNote("jxr " + c.value, "-0.5 "+(0.8+i/10)+" -.45" )
el.setAttribute("annotation", "content:"+ testingCommands[i].annotation)
el.classList.add(classNameItemsToSave)
el.setAttribute("rotation", "90 0 0")
})
// menu to show/hide keyboard
let el = addNewNote( 'jxr applyToClass("keys_from_drumsticks", el => el.setAttribute("visible", "false") )', '.3 1.15 -.7')
el.setAttribute("annotation", "content:"+ "hide keys")
el.setAttribute("rotation", "90 0 0")
el = addNewNote( 'jxr applyToClass("keys_from_drumsticks.layer_0", el => el.setAttribute("visible", "true") )', '.3 1.1 -.7')
// TODO should do so only for current layer
el.setAttribute("annotation", "content:"+ "show keys")
el.setAttribute("rotation", "90 0 0")
el = addNewNote( 'jxr window.keyboardTarget = manuscript.children[0]', '.3 1.2 -.7')
el.setAttribute("annotation", "content:"+ "write to manuscript")
el.setAttribute("rotation", "90 0 0")
setTimeout( _ => { let x = [...document.querySelectorAll("[value]")].map( el => { el.setAttribute("troika-text", "outlineWidth", 0) } ) } , 1000 )
// MUST have a persistent id (not generated on the fly as hash per session)
// can search by value as in theory those are constant (but not necessarily unique, even though usually are)
window.saveToLocateStorage = function (savingClass=classNameItemsToSave){
let savingDataDemoQ2Week2 = []
applyToClass(savingClass, el => {
// ignore element when position is "perfect" e.g. not rounded (thus unmodified)
// if ( el.getAttribute('position').x.toFixed(2) != el.getAttribute('position').x ) // forcing saves for tests
savingDataDemoQ2Week2.push( { value: el.getAttribute("value"), position: el.getAttribute("position"), rotation: el.getAttribute("rotation") } )
})
localStorage.setItem("savingDataDemoQ2Week2", JSON.stringify( savingDataDemoQ2Week2 ) )
// could also make save to WebDAV for sharing, arguably different feature
}
window.loadFromLocalStorage = function(){
const savedDataFromPreviousSession = JSON.parse( localStorage.getItem("savingDataDemoQ2Week2") )
savedDataFromPreviousSession.map( savedData =>
Array.from( document.querySelectorAll('.'+classNameItemsToSave) )
.filter( noteEl => noteEl.getAttribute("value") == savedData.value )
// search by value as in theory those are constant (but not necessarily unique, even though usually are)
.map( foundNoteEl => {
foundNoteEl.setAttribute("position", AFRAME.utils.coordinates.stringify(savedData.position) )
foundNoteEl.setAttribute("rotation", AFRAME.utils.coordinates.stringify(savedData.rotation) )
} )
)
}
let remoteSaveFilename = 'test_q2layout.json'
window.saveToRemoteStorage = function (savingClass=classNameItemsToSave){
let savingDataDemoQ2Week2 = []
applyToClass(savingClass, el => {
// ignore element when position is "perfect" e.g. not rounded (thus unmodified)
// if ( el.getAttribute('position').x.toFixed(2) != el.getAttribute('position').x ) // forcing saves for tests
savingDataDemoQ2Week2.push( { value: el.getAttribute("value"), position: el.getAttribute("position"), rotation: el.getAttribute("rotation") } )
})
let content = JSON.stringify( savingDataDemoQ2Week2 )
async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, content) }
written = w(subdirWebDAV+usernamePrefix+remoteSaveFilename)
if (written){ fetch('https://ntfy.benetou.fr/fileuploadtowebdav', { method: 'POST', body: 'added '+usernamePrefix+remoteSaveFilename }) }
}
// fitler proper, could then be used with showFile in URL, and thus become sharable with others
// via e.g. https://companion.benetou.fr/index.html?username=q2_most_recent_file&partialfilename=_q2layout.json
// BUT also requires the content to layout to be loaded first
// e.g. done as https://companion.benetou.fr/index.html?username=q2_annotated_bibliography_week2&showfile=q2_annotated_bibliography_week2_test_q2layout.json
window.loadFromRemoteStorage = function (){
async function rd(){ return await webdavClient.getDirectoryContents(subdirWebDAV) }
rd().then( res => {
let files = res.filter(f => f.basename.includes(remoteSaveFilename))
.filter(f=>f.type=="file")
.sort( (a,b) => new Date(a.lastmod).getTime() < new Date(b.lastmod).getTime() ) // newest first
//console.log( partialfilename, files, res, files[0].basename )
showFile( files[0].basename )
// this goes in filter...
})
}
}
if (username && username == "q2_drop_for_graph") {
el0 = addNewNote( 'some text', '-.5 1.1 -.5')
el1 = addNewNote( 'some other text', '.5 1.1 -.5')
el2 = addNewNote( 'yet more text', '.1 1.5 -.5')
// to try in XR
el0.setAttribute("onreleased", "ifNearbyAddLiveLine()")
el1.setAttribute("onreleased", "ifNearbyAddLiveLine()")
el2.setAttribute("onreleased", "ifNearbyAddLiveLine()")
window.ifNearbyAddLiveLine = function (el = selectedElements.at(-1).element, threshold=.1){ // 10cm default
return Array.from( document.querySelectorAll("[value]") )
.filter( e => e.getAttribute("onreleased") && e.getAttribute("onreleased") == "ifNearbyAddLiveLine()" )
.filter( e => e != el )
.filter( e => e.object3D.position.distanceTo(el.object3D.position) < threshold )
.map( e => { el.setAttribute('live-selector-line__'+e.id+'_'+el.id, {start: el, end: e}); return e })
}
// testing
//el1.setAttribute('live-selector-line', {start: el1, end: el2})
//ifNearbyAddLiveLine(el0)
//setTimeout( _ => Array.from( document.querySelectorAll("[value]") ).at(-1).object3D.translateY(.1), 2000 ) // truly live validation
}
if (username && username == "q2_picker") {
// to be use for example to get a target for keyboard spatially
window.pickerValue = 'nothing picked'
let el = addNewNote( 'jxr console.log(pickerValue)', '-.5 1.5 -.3')
el.id = "pickertest"
el.setAttribute("raycaster", {showLine:true, far:.5, objects:".pickable"})
el.addEventListener('raycaster-intersection', e => console.log('Player hit something!', e.detail.els[0].getAttribute("value") ) )
el = addNewNote( 'pickable example', '0 1.35 -0.7')
el.classList.add("pickable")
}
if (username && username == "q2_keydrumsticks") {
// groundfor360.setAttribute("visible", "false")
// for AR demo
addDrumKeyboard()
// menu to show/hide keyboard
let el = addNewNote( 'jxr applyToClass("keys_from_drumsticks", el => el.setAttribute("visible", "false") )', '-.5 1.15 -.5')
el.setAttribute("annotation", "content:"+ "hide keys")
el.setAttribute("rotation", "90 0 0")
el = addNewNote( 'jxr applyToClass("keys_from_drumsticks.layer_0", el => el.setAttribute("visible", "true") )', '-.5 1.1 -.5')
// TODO should do so only for current layer
el.setAttribute("annotation", "content:"+ "show keys")
el.setAttribute("rotation", "90 0 0")
// menu to adjust position
el = addNewNote( 'keyboard.object3D.translateZ(-.1)', '0.5 1.35 0')
el.setAttribute("annotation", "content:"+ "move keyboard up")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'keyboard.object3D.translateZ(.1)', '0.5 1.3 0')
el.setAttribute("annotation", "content:"+ "move keyboard down")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'keyboard.object3D.translateY(-.1)', '0.5 1.25 0')
el.setAttribute("annotation", "content:"+ "move keyboard further")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'keyboard.object3D.translateY(.1)', '0.5 1.2 0')
el.setAttribute("annotation", "content:"+ "move keyboard closer")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'keyboard.object3D.translateX(-.1)', '0.5 1.15 0')
el.setAttribute("annotation", "content:"+ "move keyboard left")
el.setAttribute("rotation", "90 -90 0")
el = addNewNote( 'keyboard.object3D.translateX(.1)', '0.5 1.1 0')
el.setAttribute("annotation", "content:"+ "move keyboard right")
el.setAttribute("rotation", "90 -90 0")
// menu to type elsewhere
el = addNewNote( 'jxr window.keyboardTarget = testTypingNote', '-.5 0.95 -.5')
el.setAttribute("annotation", "content:"+ "write to test note")
el.setAttribute("rotation", "90 0 0")
el = addNewNote( 'jxr window.keyboardTarget = typinghud', '-.5 0.90 -.5')
el.setAttribute("annotation", "content:"+ "write to HUD")
el.setAttribute("rotation", "90 0 0")
el = addNewNote( 'this is a test note', '0 1.60 -.5')
el.id = 'testTypingNote'
el = addNewNote( 'jxr window.keyboardTarget = manuscript.children[0]', '-.5 0.85 -.5')
el.setAttribute("annotation", "content:"+ "write to manuscript")
el.setAttribute("rotation", "90 0 0")
el = addNewNote( 'jxr window.keyboardTarget = Array.from(document.querySelectorAll(".notes")).at(-1)', '-.5 0.80 -.5')
el.setAttribute("annotation", "content:"+ "write to last added note")
el.setAttribute("rotation", "90 0 0")
el = addNewNote( 'jxr window.keyboardTarget = postitnotetoedit', '-.5 0.75 -.5')
el.setAttribute("annotation", "content:"+ "write to postit")
el.setAttribute("rotation", "90 0 0")
let note = addNewNoteAsPostItNote( "Some note to edit", '-0.6 1.4 -0.5')
note.id = 'postitnotetoedit'
// type on executable ring
let newRingTest = addRing("jxr console.log('hi')", '-.6 1.6 -.5')
newRingTest.id = 'newringtest'
// cleanup other rings
rings.setAttribute("visible", "true")
Array.from( rings.children ).map( el => el.setAttribute("visible", "false"));
Array.from( active_rings.children ).map( el => el.setAttribute("visible", "false"));
active_rings.setAttribute("visible", "true")
newRingTest.setAttribute("visible", "true")
el = addNewNote( 'jxr window.keyboardTarget = newringtest.firstChild', '-.5 0.70 -.5')
el.setAttribute("annotation", "content:"+ "write to ring")
el.setAttribute("rotation", "90 0 0")
// insure this new snippet can activate new ring
el.setAttribute("onpicked", "startRingCheck()" )
el.setAttribute("onreleased", "endRingCheck()" )
let x = [...document.querySelectorAll("[value]")].map( el => {
el.setAttribute("troika-text", "outlineWidth", 0)
// should work for annotations too
} )
AFRAME.scenes[0].addEventListener("useraddednote", e => {
let el = e.detail.element
if ( el.getAttribute("value").slice(0,3) == "jxr" ) colorJxrEl( el )
el.setAttribute("troika-text", "outlineWidth", 0)
})
}
function addDrumKeyboard(){
// get keymap
showFile('fabien_corneish_zen.keymap')
const swiping = urlParams.get('swiping');
const keyclass = "keys_from_drumsticks"
// transform keymap to keys of keyboard
let keyboardEl = document.createElement("a-entity")
keyboardEl.id = 'keyboard'
AFRAME.scenes[0].appendChild( keyboardEl )
AFRAME.scenes[0].addEventListener("keymaploaded", e => {
applyToClass("keymap_layer", el => el.setAttribute("visible", "false") )
// alternatively other layers could be displayed but with lower opacity
// might be very busy but easier to recall
Array.from( document.querySelectorAll('.keymap_layer') ).map( (layerToAdd, layerNumber) => {
layerToAdd.getAttribute("value").split('\n').map( (l,y) => {
l.split("|").map( (k,x) => {
if (k.trim()) {
// zIndex could be once deep once shallow to potentially go faster between keys, kind of straggered vs ortho
let xOffset = -.2
// layer 1 and 2 get strange offsets
let yOffset = 1.3
let zOffset = -.4
let keysPerRow = l.split("|").length -1
let ratio = 1/20
zOffset -= Math.abs( keysPerRow/2 - x ) * ratio/5 // arguably more ergonomic
if (swiping){ zOffset = zOffset + Math.abs( x - keysPerRow/2 ) * ratio } // opposite from swiping
// somehow the center isn't... the center
if (l.length < 80) xOffset += .15
let labelEl = document.createElement("a-troika-text")
labelEl.setAttribute("value",k.trim())
labelEl.setAttribute("position", "0 .51 0")
labelEl.setAttribute("font-size", "1")
labelEl.setAttribute("color", "black")
labelEl.setAttribute("rotation", "-90 0 0")
let keyEl = document.createElement("a-cylinder")
keyEl.setAttribute("segments-height", "2")
keyEl.setAttribute("segments-radial", "24")
keyEl.setAttribute("scale", ".01 .01 .01")
keyEl.setAttribute("rotation", "60 0 0")
keyEl.setAttribute("position", ""+(xOffset+x*ratio)+" " +(yOffset-y*ratio)+" "+(zOffset + y/50) )
keyEl.classList.add( keyclass )
keyEl.classList.add( "layer_"+ layerNumber )
keyEl.appendChild( labelEl )
keyboardEl.appendChild( keyEl )
// keyEl.id = keyclass+'_'+k.trim() // not correct anymore as multiple layers can have the same key
keyEl.id = keyclass+'_'+layerNumber+'_'+k.trim()
// setTimeout( _ => keyEl.object3D.lookAt( new THREE.Vector3(0, 1.5, 0)), 100 )
// not great as they have 90 deg rotation already, could find a better way
// ... but arguably should be the opposite based on resting hand positions
}
})
})
})
keys_from_drumsticks_0_SHFT.setAttribute("wireframe", shiftFromVirtualKeyboard) // arguable, could do so for all layers
// hide all layers but the current one
Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") )
Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") )
keyBoardCheck()
})
const threshold = .02 // distance
const refractionPeriod = 500 // ms until next keypress
// add visible contact points
let jointTestEl1 = document.createElement("a-sphere")
jointTestEl1.setAttribute("radius", .01)
jointTestEl1.id = "jointtest1"
AFRAME.scenes[0].appendChild( jointTestEl1 )
let jointTestEl2 = document.createElement("a-sphere")
jointTestEl2.setAttribute("radius", .01)
jointTestEl2.id = "jointtest2"
AFRAME.scenes[0].appendChild( jointTestEl2 )
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test1", { hand: 'r_handMeshNode', finger: 'index-finger-tip', target: '#jointtest1' } )
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test2", { hand: 'l_handMeshNode', finger: 'index-finger-tip', target: '#jointtest2' } )
// thumb tip test
let jointTestEl3 = document.createElement("a-sphere")
jointTestEl3.setAttribute("radius", .01)
jointTestEl3.id = "jointtest3"
AFRAME.scenes[0].appendChild( jointTestEl3 )
let jointTestEl4 = document.createElement("a-sphere")
jointTestEl4.setAttribute("radius", .01)
jointTestEl4.id = "jointtest4"
AFRAME.scenes[0].appendChild( jointTestEl4 )
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test3", { hand: 'r_handMeshNode', finger: 'thumb-tip', target: '#jointtest3' } )
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test4", { hand: 'l_handMeshNode', finger: 'thumb-tip', target: '#jointtest4' } )
const forcecontrollers = urlParams.get('forcecontrollers');
if (forcecontrollers){
setTimeout( _ => {
jointtest1.object3D.parent = document.querySelector("[meta-touch-controls]").object3D
jointtest2.object3D.parent = document.querySelector("[oculus-touch-controls]").object3D
}, 2000 )
// TODO does not work with pen
// see <a-entity logiteck-mx-ink-controls="hand: right"></a-entity> that does not seem to show anything, even in AFrame 1.7.1 nor recent master build
}
// if done a la bind-element-to-finger consider
// const joint = AFRAME.scenes[0].object3D.getObjectByName(this.data.hand)?.parent.getObjectByName(this.data.finger)
// if ( joint && this.data.target.object3D.parent == AFRAME.scenes[0].object3D ) this.data.target.object3D.parent = joint
let pos1 = new THREE.Vector3()
let pos2 = new THREE.Vector3()
let pos3 = new THREE.Vector3()
let pos4 = new THREE.Vector3()
let shiftFromVirtualKeyboard = true
let layerFromVirtualKeyboard = 0
window.keyboardTarget = typinghud
// check for potential contact
let lastKeypress = Date.now()
function keyBoardCheck() {
return setInterval( _ => {
jointTestEl1.object3D.getWorldPosition( pos1 )
jointTestEl2.object3D.getWorldPosition( pos2 )
jointTestEl3.object3D.getWorldPosition( pos3 )
jointTestEl4.object3D.getWorldPosition( pos4 )
// could also check only when jointTestEl1 / jointTestEl2 are visible, ignore otherwise
// to do with all keys instead
Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) )
.concat( [keys_from_drumsticks_0_LWR, keys_from_drumsticks_0_RSE] )
.filter( k => k.getAttribute("visible") )
.map( k => {
// should only look at visible keys, could limit via .filter( k => k.getAttribute("visible") == "true")
// this way one wouldn't type on an invisible keyboard
let d1 = k.object3D.position.distanceTo( pos1 )
let d2 = k.object3D.position.distanceTo( pos2 )
let d3 = k.object3D.position.distanceTo( pos3 )
let d4 = k.object3D.position.distanceTo( pos4 )
if ( d1 < threshold || d2 < threshold || d3 < threshold || d4 < threshold) {
//if ( d1 < threshold || d2 < threshold) {
k.setAttribute("color", "pink")
if (keyboardTarget == typinghud) typinghud.setAttribute("material","opacity", .5)
if ( Date.now() - lastKeypress < refractionPeriod ){
// console.warn('ignoring, executed during the last 500ms already')
let x = 42 // added just to ignore
} else {
lastKeypress = Date.now()
let value = k.firstChild.getAttribute("value")
if ( keyboardTarget.getAttribute("value") == "[]" ) keyboardTarget.setAttribute("value", "" ) // for typinghud starting value
if (value == "TAB") console.warn('does not complete yet') // TODO add completion
if (value == "SPC") value = " "
if (value == "ENT") {
if (keyboardTarget == typinghud) {
parseKeys("keydown", "Enter")
keyboardTarget.setAttribute("value", "" )
} else {
keyboardTarget.setAttribute("value", keyboardTarget.getAttribute("value") + '\n' )
}
} else if (value == "SHFT") {
shiftFromVirtualKeyboard = !shiftFromVirtualKeyboard
// visual highlight, also note that is closer to CAPSLOCK behavior
keys_from_drumsticks_0_SHFT.setAttribute("wireframe", shiftFromVirtualKeyboard) // arguable, could do so for all layers
} else if (value == "RSE") {
if (layerFromVirtualKeyboard<2) layerFromVirtualKeyboard++ // hardcoded max
console.log('should raise layer', layerFromVirtualKeyboard) // a la CAPSLOCK too
Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") )
Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") )
// forcing visibility yet get ignored as on wrong layer
keys_from_drumsticks_0_LWR.setAttribute("visible", "true")
keys_from_drumsticks_0_RSE.setAttribute("visible", "true")
} else if (value == "LWR") {
if (layerFromVirtualKeyboard>0) layerFromVirtualKeyboard--
console.log('should lower layer', layerFromVirtualKeyboard) // a la CAPSLOCK too
Array.from( document.querySelectorAll(".keys_from_drumsticks") ).map( k => k.setAttribute("visible", "false") )
Array.from( document.querySelectorAll(".keys_from_drumsticks"+".layer_"+layerFromVirtualKeyboard) ).map( k => k.setAttribute("visible", "true") )
keys_from_drumsticks_0_LWR.setAttribute("visible", "true")
keys_from_drumsticks_0_RSE.setAttribute("visible", "true")
} else if (value == "BKSP") {
keyboardTarget.setAttribute("value", keyboardTarget.getAttribute("value").slice(0,-1) )
} else {
if (!shiftFromVirtualKeyboard) value = value.toLowerCase()
keyboardTarget.setAttribute("value", keyboardTarget.getAttribute("value") + value )
}
}
} else if ( d1 < threshold*1.2 || d2 < threshold*1.2) { // arguably, not convinced it brings value more than confusion
k.setAttribute("color", "#ffe4e1")
} else {
k.setAttribute("color", "white")
}
})
}, 20)
}
}
if (username && username == "q2_yubikeyotp") {
const password = "password from physical token"
// to set by the user rather than hard coded AND publicly visible (!) for tests
// rely on localStorage.getItem("ykpwd") if it exists
// we can't use a remote device as input to e.g pass via URL or ntfy otherwise the password is sent unencrypted over
// consider browser crypto to be actually secure https://developer.mozilla.org/en-US/docs/Web/API/Crypto
AFRAME.scenes[0].addEventListener("useraddednote", e => {
let el = e.detail.element.getAttribute("value")
console.log ('OTP?', el.length == 44, el.length )
console.log ('slot 1 pre-defined password?', el == password)
// rely on localStorage.getItem("ykpwd") if it exists
// still has to validate it but works!
// next step https://developers.yubico.com/Developer_Program/Guides/Touch_triggered_OTP.html
if (el == password ){
const savedDataFromPreviousSession = JSON.parse( localStorage.getItem("savingDataDemoQ2Week2") )
savedDataFromPreviousSession.map( savedData =>
Array.from( document.querySelectorAll('.'+classNameItemsToSave) )
.filter( noteEl => noteEl.getAttribute("value") == savedData.value )
// search by value as in theory those are constant (but not necessarily unique, even though usually are)
.map( foundNoteEl => {
foundNoteEl.setAttribute("position", AFRAME.utils.coordinates.stringify(savedData.position) )
foundNoteEl.setAttribute("rotation", AFRAME.utils.coordinates.stringify(savedData.rotation) )
} )
)
}
} )
// could be coupled with loadFromLocalStorage() to be only accessible this way
let testingCommands = [
{value:'loadFromLocalStorage()', annotation: "load locally"},
{value:'saveToLocateStorage()', annotation: "save locally"},
{value:'loadFromRemoteStorage()', annotation: "load remotely"}, // should be a filter, a JSON layout from year 1
// need either a filename or latest file
{value:'saveToRemoteStorage()', annotation: "save remotely"},
{value:'jxr localStorage.setItem("ykpwd", "YOUR PASSWORD")', annotation: "set local password (modify the passwords itself!"}
// could ask the user to type :
]
let elements = testingCommands
.map( (c,i) => addNewNote("jxr " + c.value, "-0.5 "+(0.8+i/10)+" -.45" ) )
console.log( elements )
elements.map( (c,i) => c.setAttribute("annotation", "content:"+ testingCommands[i].annotation) )
const classNameToBeSaved = "tobesaved"
elements.map( el => el.classList.add(classNameItemsToSave) )
addNewNote( 'commands here are disabled, to show only password loading!', '-.5 1.5 -.5')
manuscript.setAttribute("visible", false)
}
if (username && username == "q2_handswap") {
showFile("references_manual_v11.json")
AFRAME.scenes[0].addEventListener("enter-vr", e => {
leftHand.setAttribute("hand-tracking-controls", "hand: right;")
rightHand.setAttribute("hand-tracking-controls", "hand: left;")
})
// check rotation handle on hand, wrong offset as card is flipped
}
if (username && username == "q2_annotated_bibliography") {
showFile("references_manual_v11.json")
/*
adding some text snippet to each item of the loaded bibliography
core mechanism
how to link 1 text snippet to 1 item of the bibliography
add it
type via BT
STT
pull through rings (really awkward)
link to item of bibliography
positional?
rings via e.g. tagging?
icon_tags
give it a type among the 2 possible
note/annote
rings via e.g. tagging?
*/
icon_tags.setAttribute("visible", "true")
AFRAME.scenes[0].addEventListener("useraddednote", e => {
let el = e.detail.element
el.setAttribute("onpicked", "startRingCheck()" )
el.setAttribute("onreleased", "endRingCheck()" )
} )
Array.from( tagging_active_rings.querySelectorAll("a-troika-text") ).map( el => el.setAttribute("visible", "true"))
Array.from( tagging_active_rings.children ).map( el => el.object3D.translateX(1) )
//showFile( "https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml" )
// the PDF XML unpacked reader does not connect to the references JSON one
// could try to move it after, alternatively could try to not overlap
//showFile( "https://companion.benetou.fr/saved/pdfxml/augmented_paper.xml" )
AFRAME.scenes[0].addEventListener("pdfxmlloaded", e =>
// TODO untested
Array.from( AFRAME.scenes[0].querySelectorAll(".page_from_pdf") ).at(-1).object3D.translateX(1.5)
//Array.from( AFRAME.scenes[0].querySelectorAll(".page_from_pdf") ).at(-1).object3D.position.x+=1.5
// does not work well
// should shift each new added page this way
)
let testingCommands = [
'AFRAME.scenes[0].setAttribute("useraddednote-append-to", "target:#manuscript")',
'AFRAME.scenes[0].removeAttribute("useraddednote-append-to")',
// 'setTimeout( _ => saveScreenshot("screenshot_"+Date.now()+".png") , 1000 )'
// saveHighlights(filename="highlights.json")
// saveCSLJson(filename="example_bibliography.csl.json")
]
testingCommands.map( (c,i) => addNewNote("jxr " + c, "-1.0 "+(0.8+i/10)+" -.45" ) )
// save to JSON... not necessarily .layout.json as seems it's not supported anymore
// see https://git.benetou.fr/utopiah/text-code-xr-engine/src/branch/fot-sloan-companion/public/index.html#L726
// also https://git.benetou.fr/utopiah/text-code-xr-engine/src/branch/fot-sloan-companion/public/index.html#L996
// TODO untested
let savingDataDemoQ2 = []
document.addEventListener('keypress', event => {
if (event.keyCode == 13) {
let el = Array.from( AFRAME.scenes[0].querySelectorAll("a-troika-text") ).at(-1)
savingDataDemoQ2.push( { value: el.getAttribute("value"), position: el.getAttribute("position"), rotation: el.getAttribute("rotation") } )
localStorage.setItem("savingDataDemoQ2", JSON.stringify( savingDataDemoQ2 ) )
}
if (event.key == 'q') { // tab is problemaltic with Tridactyl
//if (event.keyCode == 9) { // tab is problemaltic with Tridactyl
// completion attempt
// could load a remote word dictionary... but for now :
const dictionaryForCompletion = `equitable add audio recorder capability adding 'raycaster-targets' to all potential targets on picked then removing onreleased via startLenseUpdate() and endLenseUpdate() instead advantage of spiral, namely dense without overlap already used... should try another visual property always saving the last recorded one arbitrary offset for panels, could be a parameter arguably could be ANOTHER middleware case, namely onreleased does one action per item... but could also do more for all items, with filtering per class, position, etc assumes single document open this way attach class name to all resolvedElements matching that condition filter available then as because direct rotation works (with target component) this is about constrained rotation bottom part do not move box can be done client side via threejs can be increased/decreated to change difficulty can fail by simplifying to 2 value instead of 3 when arriving at 0 can maybe be done client side via file reader can rely on a filter/ instead cf wristShortcut = "jxr toggleHideAllJXRCommands()" character append clone ring then append it to rings_stack (for follow up macro) consequently should be modifying the target component consider lastExecuted['viewerFullscreen'] equivalent yet without blocking when done programmatically, e.g. when used on load or layout consider target range then normalize over it could also be axis locked by only copying the x/y/z value of position could also check on geometry, i.e. box only for now could also use HUD on state change e.g. if (isInView) setFeedbackHUD('Out of view') could also use x/y/z offsets could also wait for conversion could be a user provided JSON, ideally CSS though as that's more common could be better to do it later on, at the transformation level, going through ring could be stored first using getHighlights() first, safer could be use with timer on selectedElements which already includes timestamps and primary/secondary could instead emit back to the snapping element could instead make snippets with addNewNote(i.title) then add the right class, etc could instead use a per user limited visibility e.g. rely on AFRAME.registerComponent('user-visibility') untested for now could try apply on last/next picked item instead could try applying to only that segment... requires a bit of troika text syntax (and assumptions, e.g. only appear once) could try to move it after, alternatively could try to not overlap could try to offset down from here could try to rely on https://github.com/protectwise/troika/blob/main/packages/troika-three-text/src/selectionUtils.js could until then prevent picking, e.g. removing the target dirty mix of threejs and AFrame... doesn't seem to get world coordinates, probably due to parenting e.g. drop jxr on ring, bind them so that next time an element goes through, it's applied via binded ring e.g. selected pages 1, 2, 4, 5 (in that orqder) (in that order) el.setAttribute("animation__vertical", "property: position; to: " + layouts["vertical"].at(-1) + "; dur: 1000; startEvents:startAnimation; autoplay:false;") elFilename.setAttribute("annotation", "content", name) eraser as black cube, same principle as highlighters eraser as black cube, same principle as highlighters even while trying to get world coordinates the distance seems of, close to 1 when it should be 0. example of direct layout but with incorrect fonts examples of adjustements fileContent.map( i => { return {title:i.title, note:i.note.split('\n')}}) filtered on within a specific volume, e.g. within unit cube around center but .5m above floor and in front flatten back for truly remote debugging (or when its not otherwise possible, e.g. on the go BUT with connectivity) generalizing selector/attribute pairs though generate new class name based on filter value and UUID-ish grow/shrink a selection here we are considering a sequential "positive" flow highlighters https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image if it is jxr should then rotate within if used as contextual menu, could reset/hide at endRingCheck() in in turn available as https://companion.benetou.fr/poweruser_example_bibliography.csl.json inView(targetSelector) indexing on 0 so no need for +1 on page number instead BUT assumes 1 screenshot per username, thus URL is position via center of element so should offset it let testingCommands = ['setTimeout( _ => saveScreenshot("screenshot_"+Date.now()+".jpg") , 1000 )' ] make that new class name available for further processing manipulable raycasters to wireframe messes up direct picking after, so could do an interaction filter on pick for this class move it's starting position move title/desc to the side move title/desc to the side need refractory period or maximum amount of times added not actually used not throroughly tested not to be downloaded though, with live URL, still usable object related, should be axis bound offset from current ring position on pick re-parent to scene then parent back on released only applied to sphere... despire printing correctly the right id and intersection point for manuscript and box out panel affordances possibly using a tree where each demos has an optional next one, then following that instead (can become a cycling graph...) potentially from glTF with external images then https://github.com/donmccurdy/glTF-Transform probably need some refractory period too, otherwise stacking transformations that are probably not required problematic as it can execute multiple times, leading to overlapping yet hidden viewers or content quite wonky... safer to keep the initial rotation recordings to try the binding to annotation recordings to try the binding to annotation recordings to try the binding to annotation removes a step for the user save to JSON... not necessarily .layout.json as seems it's not supported anymore saveToCompanion() with emailing after see "q1_step_refcards" see also roundedpageborders see applyFunctionToSelection() ... which does not exist see demoqueueq1 user instead and related q1_* users seems it needs an offset, maybe due to the cone initial rotation (i.e. pointing up) sequentialFiltersInteractionOnReleased = [] // skipped for this example should be based on currently / lastly picked highlighter should be more general, this is a weird default should check if dropped nearby colored annotations should check if lastPick is including in vf.snappedOn then remove it should emit an event when done, for now just console.log which isn't programmatic should reparent 1st 6 cards to faces show cards show rings for queries show/unhide/unfold the "following" rings based on context showHighlight() // older version without images simplistic, should instead be using e.g. https://threejs.org/docs/#api/en/math/Box3.containsPoint skipping for now to test for perf without saving somehow ? doesn't get escaped somehow needed twice for table anchor square layout startViewCheck() tagEl.setAttribute("rotation", window.currentTag.rotation) tagEl.setAttribute("scale", window.currentTag.scale) the PDF XML unpacked reader does not connect to the references JSON one then 'jxr newContent('+filename+')' as value to make it executable on left pinch to open (default function) then visually highlight metadata with dedicated affordances this itself should enable the creation of a 4th ring, etc this.el.setAttribute( "wireframe", "true" ) thus showcasing composability to test to try with another one... todo random layout without overlap top part move alongside another moving face, e.g. front face trailing line try to get the selection value and append to it, if none make new one undefined on tagEl? used for preview compatible elements via URL too... would also allow to show/hide entire set would be better to have its own parent, unrelated to active_rings written = w(subdirWebDAV+usernamePrefix+filename) zoomable-interface, e.g title only, card, documents (assuming open-access)`
let uniqueWords = [...new Set( dictionaryForCompletion.split(' ') )]
uniqueWords.filter( w => w.startsWith( typinghud.getAttribute("value") ) ).map( (w,i) => addNewNote(w, "0 "+(1+i/20)+" -0.3") )
// could
// keep stopwords
// frequency count (sort then iterate)
// keep uniq only
// addNewNote with each proposal
}
})
const savedDataFromPreviousSession = localStorage.getItem("savingDataDemoQ2");
console.log( "savedDataFromPreviousSession:", savedDataFromPreviousSession )
}
if (username && username == "q2_lense") {
// manipulable raycasters to wireframe
// adding 'raycaster-targets' to all potential targets on picked then removing onreleased via startLenseUpdate() and endLenseUpdate() instead
manuscript.classList.add("lensable") // .lensable
// examples of adjustements
let rays = Array.from( document.querySelectorAll("[raycaster]") ).filter( el => el.getAttribute("raycaster").objects.includes("lensable") )
//rays.map( el => el.setAttribute("raycaster", "showLine", "true") )
rays.map( el => el.setAttribute("raycaster", "far", "10") )
}
if (username && username == "q2_step_volumetric_frames") {
cleanSlateUI() // hides a bunch of stuff
//basiccommands.setAttribute("visible", true)
//document.querySelector("a-console").setAttribute("visible", true)
// show cards
showFile("references_manual_v04.json")
// zoomable-interface, e.g title only, card, documents (assuming open-access)
// HUD to skim through cards
// show rings for queries
volumetricframes.setAttribute("visible", "true")
Array.from(volumetricframes.children).map( vf => {
vf.snappedOn = []
})
setTimeout( _ => {
Array.from( document.querySelectorAll(".reference-entry") ).map( el => {
el.setAttribute("target", "")
el.setAttribute("onpicked", "puckFromVolumetricFrame()")
el.setAttribute("onreleased", "snapClosest(Array.from(volumetricframes.children))" )
// extend snapClosest() to append to target panel
el.addEventListener('snapped', evt => {
console.log( 'snapped to', evt.detail.closest.id)
evt.detail.el.object3D.translateX( -1/2 )
evt.detail.el.object3D.translateY( 1/2 )
let vf = document.getElementById( evt.detail.closest.id )
// should support removal too using onpicked
const maxItemsPerSnapTarget = 3
if ( vf.snappedOn.length > maxItemsPerSnapTarget) {
let r = Array.from( volumetricframes.querySelectorAll(".panel") ).map( el => el.id )
let id = evt.detail.closest.id
id = r[ r.indexOf(id) + 1 ] // if unfound, loops back to first item
// let vf = document.getElementById( evt.detail.closest.id )
console.log( 'should instead apppend to', id )
vf = document.getElementById(id)
// should attach to other first
let finalPos = new THREE.Vector3()
vf.object3D.getWorldPosition( finalPos )
el.object3D.position.copy( finalPos )
// could try to offset down from here
el.object3D.rotation.copy( vf.object3D.rotation )
el.object3D.translateX( -1/2 )
el.object3D.translateY( 1/2 )
}
// sequencial pannels, i.e. if appending to one linked to another, append to the other
// can visually add <a-tube> between each linked panels
vf.snappedOn.push( evt.detail.el )
evt.detail.el.object3D.translateY( -vf.snappedOn.length/10 )
// onpicked="startVolumetricFrameMoving()"
// onreleased="endVolumetricFrameMoving()"
// might prefer to re-parent instead
// a la r.object3D.parent = cubetest.object3D;
})
// should keep track in order to e.g translateY(-.1)
})
}, 1000 ) // assuming showFile finished successfully, should rely on event instead
// panel affordances
// changing layout within panel
// move panel (becomes target)
// be named (can't be renamed)
// export to JSON layout
// sequencial pannels, i.e. if appending to one linked to another, append to the other
}
if (username && username == "q2_step_start") {
rings.setAttribute("visible", "true")
Array.from( rings.querySelectorAll("a-troika-text") ).map( el => el.setAttribute("visible", "true"))
cleanSlateUI() // hides a bunch of stuff
// Change text on hands to refer to wrists. Then provide results on wrist taps.
// Once code snippet appears then show text (if possible to change) to say move and pinch.
AFRAME.scenes[0].setAttribute("instructions-on-hands", "")
// Make cube less transparent, or fully opaque. Also slightly larger please.
// see cubetest
// e.g username "refoncubetester"
}
if (username && username == "q2_step_highlight") {
cleanSlateUI() // hides a bunch of stuff
// Change frame from green to dark grey, slightly darker than pens.
Array.from( roundedpageborders.querySelectorAll("[color='#43A367']") ).map( el => el.setAttribute("color", "green"))
Array.from( highlighterA.querySelectorAll("[color='gray']") ).map( el => el.setAttribute("color", "#CCC"))
Array.from( highlighterB.querySelectorAll("[color='gray']") ).map( el => el.setAttribute("color", "#CCC"))
// Remove Manuscript.
// see "q1_step_refcards"
}
if (username && username == "q2_step_contextuallayouts") {
cleanSlateUI() // hides a bunch of stuff
rings.setAttribute("visible", "true")
groundfor360.setAttribute("visible", "false")
Array.from( rings.querySelectorAll("a-troika-text") ).map( el => el.setAttribute("visible", "true"))
addContextualRings( rings.children[0] )
document.querySelector("a-console").setAttribute("visible", true)
}
if (username && username == "q2_step_jsonedit") {
cleanSlateUI() // hides a bunch of stuff
editJSON()
document.querySelector("a-console").setAttribute("visible", true)
}
if (username && username == "q2_step_end") {
cleanSlateUI() // hides a bunch of stuff
// Remove lines of text, show only Knowledge Objects/Citation cards.
// On pinching cards make hand more transparent.
// On pinching cards make the initial movement more of a pivot towards the user on the Y axis.
// Currently I have to twist my hand clockwise to compensate.
// Knowledge Objects as small shapes (Cube, Pyramid etc.), in configuration,
// with the ability to move them in the space, see connections and expand for the user to ‘read’ the object.
}
if (username && username == "q2_step_layout_animationtests") {
// todo random layout without overlap
// square layout
addKeyboardRings()
active_rings.setAttribute("visible", "true")
rings.setAttribute("visible", "true")
setTimeout( _ => {
let res = layoutsSwitch()
//console.log( res )
let layoutNames = [ "horizontal", "vertical", "spiral", "circle" ]
layoutNames = [] // disabled
layoutNames.map( (ln, i) =>
setTimeout( _ => Array.from( document.querySelectorAll("[animation__"+ln+"]") ).map( el => el.emit("startAnimation")), i*2000 )
)
const keys = Array.from( document.querySelectorAll( '.kbd_key' ) )
setTimeout( _ => { keys.map( (el,i) =>{ el.setAttribute("animation__h", "property: position; to: " + res["horizontal"][i] + "; dur: 1000;") })
}, 5000 )
setTimeout( _ => { keys.map( (el,i) =>{ el.setAttribute("animation__v", "property: position; to: " + res["vertical"][i] + "; dur: 1000;") })
}, 7000 )
setTimeout( _ => { keys.map( (el,i) =>{ el.setAttribute("animation__c", "property: position; to: " + res["circle"][i] + "; dur: 1000;") })
}, 9000 )
setTimeout( _ => { keys.map( (el,i) =>{ el.setAttribute("animation__s", "property: position; to: " + res["spiral"][i] + "; dur: 1000;") })
}, 11000 )
}, 500)
}
if (username && username == "temple_test") {
document.querySelector("a-sky").remove()
let el = document.createElement("a-entity")
el.setAttribute("light", "type: ambient; color: #BBB" )
AFRAME.scenes[0].appendChild( el )
el = document.createElement("a-entity")
el.setAttribute("light", "type: directional; color: #FFF; intensity: 0.6")
el.setAttribute("position", "0.7 0.4 -1" )
AFRAME.scenes[0].appendChild( el )
}
if (username && username == "q2_step_refcards_filtering") {
showFile("references_manual_v04.json")
setTimeout( _ => { roundedpageborders.setAttribute("visible", "false") }, 1000 )
let cube = addCubeWithAnimations()
cube.setAttribute("target", "")
cube.setAttribute("visible", "false")
rings.setAttribute("visible", "true")
rings.object3D.translateZ(.3)
Array.from( rings.querySelectorAll("a-troika-text") ).map( el => el.setAttribute("visible", "true"))
cleanSlateUI() // hides a bunch of stuff
manuscript.setAttribute("visible", false)
addRing( "filterBibtex")
let refCardsClassName = addNewNote( ".reference-entry-card" )
refCardsClassName.setAttribute("onpicked", "startRingCheck()")
refCardsClassName.setAttribute("onreleased", "endRingCheck()" )
}
if (username && username == "q2_fingersmenu") {
cleanSlateUI() // hides a bunch of stuff
document.querySelector("a-console").setAttribute("visible", true)
//AFRAME.scenes[0].setAttribute("instructions-on-hands-more-fingers", "")
// deprecated
// consider instead a list of
// hand side r_handMeshNode / l_handMeshNode
// finger part e.g index-finger-phalanx-proximal or thumb-metacarpal etc
// unique selector or directly element e.g. show_instructions
// generalized version as bind-element-to-finger component
let jointTestEl1 = addNewNote( "jxr document.querySelector('a-sky').setAttribute('color','purple')", "0 .03 -.05")
jointTestEl1.setAttribute("scale", ".05 .05 .05")
jointTestEl1.setAttribute("rotation", "0 -90 0")
jointTestEl1.setAttribute("target", "")
jointTestEl1.id = "jointtest1"
let jointTestEl2 = addNewNote( "jxr document.querySelector('a-sky').setAttribute('color','gray')", "0 .03 -.05")
jointTestEl2.setAttribute("scale", ".05 .05 .05")
jointTestEl2.setAttribute("position", "0 .03 -.05")
jointTestEl2.setAttribute("rotation", "0 -90 0")
jointTestEl2.id = "jointtest2"
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test1", { hand: 'r_handMeshNode', finger: 'index-finger-tip', target: '#jointtest1' })
AFRAME.scenes[0].setAttribute("bind-element-to-finger__test2", { hand: 'r_handMeshNode', finger: 'thumb-tip', target: '#jointtest2' } )
}
if (username && username == "q2_secondarypinch_singlehanded_spatial") {
const primaryPinchSingleHandedDistanceThreshold = 0.02 // 2 cm
const secondaryPinchSingleHandedDistanceThreshold = 0.05 // need to be much looser, maybe due to privacy limitations or camera position (hidden fingers)
let secondaryPinchSingleHanded = setInterval( el => {
if ( !AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode") ) return
let j1 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-tip").position
let j2 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("index-finger-tip").position
let j3 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("pinky-finger-tip").position
let j4 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("middle-finger-tip").position
let d1 = j1.distanceTo( j2 )
let d2 = j1.distanceTo( j3 )
let d3 = j1.distanceTo( j4 )
// console.log( 'normal pinch distance', d1 )
// console.log( 'secondary pinch distance', d2 )
document.querySelector("a-sky").setAttribute("color", "gray")
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 > secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-not', {hand: 'right'})
// console.log( 'normal pinch' ) // just for testing
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 < secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch', {hand: 'right'})
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d3 < primaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-middle-finger', {hand: 'right'})
}, 10)
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch', evt => {
//addNewNote('added here...', AFRAME.utils.coordinates.stringify( pinches.filter( p => p.primary ).at(-1).position ) )
} )
let lastSecondaryPinchExecuted = Date.now()
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch-middle-finger', evt => {
if ( Date.now() - lastSecondaryPinchExecuted < 500 ){
console.warn('ignoring, executed secondary pinch during the last 500ms already')
} else {
lastSecondaryPinchExecuted = Date.now()
console.log('mid pinched after 500ms...')
let el = addNewNote('added here...', AFRAME.utils.coordinates.stringify( AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-tip").position ) )
el.setAttribute("troika-text", "outlineWidth", 0)
setTimeout( _ => el.object3D.lookAt( player.object3D.position ), 100 )
}
} )
}
if (username && username == "q2_secondarypinch_singlehanded") {
const primaryPinchSingleHandedDistanceThreshold = 0.02 // 2 cm
const secondaryPinchSingleHandedDistanceThreshold = 0.05 // need to be much looser, maybe due to privacy limitations or camera position (hidden fingers)
let secondaryPinchSingleHanded = setInterval( el => {
if ( !AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode") ) return
let j1 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-tip").position
let j2 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("index-finger-tip").position
let j3 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("pinky-finger-tip").position
let j4 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("middle-finger-tip").position
let d1 = j1.distanceTo( j2 )
let d2 = j1.distanceTo( j3 )
let d3 = j1.distanceTo( j4 )
// console.log( 'normal pinch distance', d1 )
// console.log( 'secondary pinch distance', d2 )
document.querySelector("a-sky").setAttribute("color", "gray")
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 > secondaryPinchSingleHandedDistanceThreshold ) document.querySelector("a-sky").setAttribute("color", "purple")
// just for testing
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d2 < secondaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch', {hand: 'right'})
if ( d1 < primaryPinchSingleHandedDistanceThreshold && d3 < primaryPinchSingleHandedDistanceThreshold )
AFRAME.scenes[0].emit('secondary-singled-handed-pinch-middle-finger', {hand: 'right'})
}, 10)
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch', evt => {
document.querySelector("a-sky").setAttribute("color", "blue")
} )
AFRAME.scenes[0].addEventListener('secondary-singled-handed-pinch-middle-finger', evt => {
document.querySelector("a-sky").setAttribute("color", "green")
} )
}
// ----------------------------------------- demo queue Q1 customizations -------------------------------------------
if (username && username == "demoqueueq1") {
instructions.setAttribute("visible", false)
// could be use with timer on selectedElements which already includes timestamps and primary/secondary
// selectedElements.filter( a => a.primary ).length
// selectedElements.filter( a => a.secondary ).length
// could use timestamp to show if after 30s either is still 0
// consider also setFeedbackHUD('hi')
manuscript.setAttribute("visible", false)
basiccommands.setAttribute("visible", false)
middlecommands.setAttribute("visible", false)
topsidecommands.setAttribute("visible", false)
document.querySelector("a-console").setAttribute("visible", false)
AFRAME.scenes[0].setAttribute("instructions-on-hands", "")
// not throroughly tested
}
if (username && username == "q1_step_urlcustom") {
const testUrlParams = new URLSearchParams( "set_IDmanuscript_color=lightyellow" )
parametersViaURL(testUrlParams)
}
if (username && username == "q1_step_refcards") {
manuscript.setAttribute("visible", false)
showFile("references_manual_v04.json")
setTimeout( _ => { roundedpageborders.setAttribute("visible", "false") }, 1000 )
let cube = addCubeWithAnimations()
cube.setAttribute("target", "")
cube.setAttribute("visible", "false")
setTimeout( _ => {
demoMetaDataName.object3D.translateX(-.9)
demoMetaDataDescription.object3D.translateX(-.9)
}, 1000)
}
if (username && username == "q1_step_showfile") {
const testUrlParams = new URLSearchParams( "showfile=175235.175237.pdf-0.jpg" )
parametersViaURL(testUrlParams)
}
if (username && username == "q1_step_highlights") {
window.pageastextviaxmlsrc = "https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml"
//window.pageastextviaxmlsrc = "https://companion.benetou.fr/augmented_paper_xml/augmented_paper.xml"
// to try with another one...
pageAsTextViaXML()
//pageAsTextViaXML(5)
highlightcommands.setAttribute("visible", true)
roundedpageborders.setAttribute("visible", true)
highlighterA.setAttribute("visible", true)
highlighterB.setAttribute("visible", true)
// eraser as black cube, same principle as highlighters
// move title/desc to the side
setTimeout( _ => {
demoMetaDataName.object3D.translateX(-.7)
demoMetaDataDescription.object3D.translateX(-.7)
}, 1000)
}
if (username && username == "q1_step_audio") {
recordercommands.setAttribute("visible", true)
addRecentAudioFiles()
// recordings to try the binding to annotation
// should check if dropped nearby colored annotations
}
if (username && username == "q1_step_screenshot") {
middlecommands.setAttribute("visible", false)
basiccommands.setAttribute("visible", false)
topsidecommands.setAttribute("visible", false)
let testingCommands = ['setTimeout( _ => saveScreenshot("screenshot_"+Date.now()+".png") , 1000 )' ]
// let testingCommands = ['setTimeout( _ => saveScreenshot("screenshot_"+Date.now()+".jpg") , 1000 )' ]
//let testingCommands = ["toggleShowCube()", 'setTimeout( _ => saveScreenshot("screenshot_"+Date.now()+".jpg") , 1000 )' ]
testingCommands.map( (c,i) => addNewNote("jxr " + c, "-0.5 "+(1+i/10)+" -.55" ) )
//recordercommands.setAttribute("visible", true)
//addRecentAudioFiles()
// recordings to try the binding to annotation
// should check if dropped nearby colored annotations
window.pageastextviaxmlsrc = "https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml"
//window.pageastextviaxmlsrc = "https://companion.benetou.fr/augmented_paper_xml/augmented_paper.xml"
// to try with another one...
pageAsTextViaXML()
//pageAsTextViaXML(5)
highlightcommands.setAttribute("visible", true)
roundedpageborders.setAttribute("visible", true)
highlighterA.setAttribute("visible", true)
highlighterB.setAttribute("visible", true)
// eraser as black cube, same principle as highlighters
// move title/desc to the side
setTimeout( _ => {
demoMetaDataName.object3D.translateX(-.7)
demoMetaDataDescription.object3D.translateX(-.7)
}, 2000)
}
// ----------------------------------------- constrained move -------------------------------------------
/*
virtualdesktopplanemovableblue.setAttribute("onpicked", "constraintsMoveSingleAxis(selectedElements.at(-1).element, 'x')")
virtualdesktopplanemovableblue.setAttribute("onreleased", "clearConstraintsMoveSingleAxis('x')")
virtualdesktopplanemovablegreen.setAttribute("onpicked", "constraintsMoveSingleAxis(selectedElements.at(-1).element, 'y')")
virtualdesktopplanemovablegreen.setAttribute("onreleased", "clearConstraintsMoveSingleAxis('y')")
virtualdesktopplanemovablered.setAttribute("onpicked", "constraintsMoveNoRotation(selectedElements.at(-1).element)")
virtualdesktopplanemovablered.setAttribute("onreleased", "clearConstraintsMoveNoRotation()")
const axesHelper = new THREE.AxesHelper(.1)
const colorGreen = new THREE.Color( 'green' )
const colorRed = new THREE.Color( 'red' )
const colorBlue = new THREE.Color( 'blue' )
const colorGray = new THREE.Color( 'gray' )
axesHelper.setColors(colorGreen, colorGray, colorGray)
cylinderorange.object3D.add( axesHelper )
// object related, should be axis bound
cylinderorange.setAttribute("onpicked", "constraintsTranslateX(selectedElements.at(-1).element)")
cylinderorange.setAttribute("onreleased", "clearConstraintsTranslateX()")
// rotation edit widget : donut + cone as arrow (interactable)
let helperDonut = document.createElement("a-torus")
helperDonut.setAttribute("radius", ".02")
helperDonut.setAttribute("rotation", "0 90 0")
helperDonut.setAttribute("radius-tubular", ".001")
helperDonut.setAttribute("segments-height", 8)
helperDonut.setAttribute("segments-width", 8)
helperDonut.setAttribute("opacity", .3)
cylinderpurple.appendChild(helperDonut)
let helperCone = document.createElement("a-cone")
helperCone.setAttribute("opacity", .3)
helperCone.setAttribute("rotation", "0 90 0")
helperCone.setAttribute("position", "0 0 .02")
helperCone.setAttribute("height", ".001")
helperCone.setAttribute("radius-top", ".001")
helperCone.setAttribute("radius-bottom", ".005")
helperCone.setAttribute("segments-height", 8)
helperCone.setAttribute("segments-width", 8)
cylinderpurple.appendChild(helperCone)
helperCone.setAttribute("target", "") // works via the console... but while in XR the target for direct rotation (on object) and this are too close
// could try apply on last/next picked item instead
helperCone.id = "cylinderpurplecone"
cylinderpurplecone.setAttribute("onpicked", "constraintsRotationX(selectedElements.at(-1).element)")
//cylinderpurplecone.setAttribute("onreleased", "clearConstraintsMoveNoRotation()")
// cylinderpurple.object3D.rotateX(.1)
// because direct rotation works (with target component) this is about constrained rotation
// e.g. snapping at 90deg angles
// onreleased could also be added from within onpicked... not necessarily clearer though
*/
}, 500)
AFRAME.registerComponent('user-visibility', {
schema: { username: {type: 'string'}, },
init: function(){
if (!this.data.username){ console.warn('username required'); return }
if (username && username == this.data.username){
this.el.setAttribute("visible", true)
} else {
this.el.setAttribute("visible", false)
}
}
})
// -----------------------------------------------------------------------------------------------------------------------------------------------------------
// TODO move to filter
function addImagesViaXML(src = "https://companion.benetou.fr/augmented_paper_xml/augmented_paper.xml"){
fetch( src ).then( r => r.text() ).then( txt => {
const parser = new DOMParser();
const doc = parser.parseFromString(txt, "application/xml");
Array.from( doc.querySelectorAll("image") ).map( i => showFile('/augmented_paper_xml/'+i.attributes.src.value));
setTimeout( _ => {
Array.from( doc.querySelectorAll("image") ).map( i => {
let el = document.getElementById('/augmented_paper_xml/'+i.attributes.src.value.replace('.',''));
el.setAttribute("width", i.attributes.width.value/1000);
el.setAttribute("height", i.attributes.height.value/1000);
el.setAttribute("position", ""+ i.attributes.left.value/1000+" "+ (i.attributes.top.value/1000+1)+ " "+(Math.random()/1000-.5));
})
}, 1000)
})
}
function pageAsTextViaXML(page=0){
let src = window.pageastextviaxmlsrc
fetch( src ).then( r => r.text() ).then( txt => {
Array.from( roundedpageborders.querySelectorAll(".highlightimagefromxmlitem,.highlightabletext") ).map( e => e.remove() )
targets = targets.filter( el => !el.classList.contains("highlightabletext"))
// assumes single document open this way
// probably safer performance-wise, otherwise rely on (high quality) image instead, no interaction needed
const parser = new DOMParser();
let doc = parser.parseFromString(txt, "application/xml")
const scalingFactor = 1/1000 // used for position of text and images
// could also use x/y/z offsets
// probably easier to append to an entity, either empty or used as (white) background
const xOffset = 0
const yOffset = 1
const zPos = -.5
// see also roundedpageborders
//Array.from( doc.children[0].children[page].querySelectorAll("text") ).map( (l,n) => addNewNote(l.textContent, ""+(l.attributes.left.value*scalingFactor+xOffset) + " " + (1-l.attributes.top.value*scalingFactor+yOffset) + " "+zPos, "0.045 0.045 0.045", "highlighttextfromxml_"+n, "highlighttextfromxmlitem" ) )
Array.from( doc.children[0].children[page].querySelectorAll("text") ).map( (l,n) => {
// addNewNote(l.textContent, ""+(l.attributes.left.value*scalingFactor+xOffset) + " " + (1-l.attributes.top.value*scalingFactor+yOffset) + " "+zPos, "0.045 0.045 0.045", "highlighttextfromxml_"+n, "highlighttextfromxmlitem" ) )
let tktxt = document.createElement("a-troika-text")
let pos = ""+(l.attributes.left.value*scalingFactor+xOffset) + " " + (1-l.attributes.top.value*scalingFactor+yOffset) + " "+zPos
let scale = "0.045 0.045 0.045"
tktxt.setAttribute("position", pos)
tktxt.setAttribute("originalposition", pos)
tktxt.setAttribute("originalpage", page)
// FIXME
//tktxt.setAttribute("originalsource", fileContent.meta.metadata['dc:title'])
//tktxt.setAttribute("originalidentifier", fileContent.meta.metadata['dc:identifier'])
tktxt.setAttribute("font-size", "0.009")
tktxt.setAttribute("color", "black")
tktxt.setAttribute("target", "")
tktxt.classList.add("highlightabletext")
tktxt.setAttribute("onpicked", "console.log(selectedElements.at(-1).element.getAttribute('value'))")
tktxt.setAttribute("onreleased", "let el = selectedElements.at(-1).element; if (true) el.setAttribute('color', highlightColor); el.setAttribute('rotation', ''); el.setAttribute('position', el.getAttribute('originalposition') )")
// resets back...
// change color
// only if above a certain threshold, e.g. held a long time, or released close to specific other item
// could also toggle coloring
// can be based on coloring pick with jxr
tktxt.setAttribute("value", l.textContent)
tktxt.setAttribute("anchor", "left")
roundedpageborders.appendChild(tktxt)
})
Array.from( doc.children[0].children[page].querySelectorAll("image") ).map( (l,n) => {
let el = document.createElement("a-box")
// is position via center of element so should offset it
el.setAttribute("src", "/augmented_paper_xml/"+l.attributes.src.value); // somehow set to #transparent...
el.setAttribute("width", l.attributes.width.value*scalingFactor);
el.setAttribute("height", l.attributes.height.value*scalingFactor);
el.setAttribute("depth", .01);
el.setAttribute("target", "")
el.id = "highlightimagefromxml_"+n
el.classList.add("highlightimagefromxmlitem")
el.classList.add("highlightabletext")
let w = l.attributes.width.value*scalingFactor
let h = l.attributes.height.value*scalingFactor
el.setAttribute("position", ""+ ""+(w/2+l.attributes.left.value*scalingFactor+xOffset)+" "+ (-h/2+1-l.attributes.top.value*scalingFactor+yOffset)+ " "+zPos)
roundedpageborders.appendChild(el)
})
} );
}
function previousPageForXMLText(){
if (pageNumberShownForHighlight>0)
changePageForXMLText(--pageNumberShownForHighlight)
return pageNumberShownForHighlight
}
function nextPageForXMLText(){
//if (pageNumberShownForHighlight<contentFromDocumentAsJSON.pages.length-1) changePageXMLText(++pageNumberShownForHighlight)
// needs the doc parsed
if (pageNumberShownForHighlight<11) changePageForXMLText(++pageNumberShownForHighlight) // hard coded and wrong...
return pageNumberShownForHighlight
}
function changePageForXMLText(pageNumber){
Array.from( document.querySelectorAll(".highlightabletext") ).map(el => el.remove())
// expected to be saved first by the user
// could be stored first using getHighlights() first, safer
highlightsBetweenPageChanges.push( getHighlights() )
pageAsTextViaXML(pageNumber)
}
let contentFromDocumentAsJSON
let pageNumberShownForHighlight = 0
function showHighlight(){
const originalDoc = "augmented_paper.pdf"
const contentJSON = originalDoc+".json"
const renderedFirstPage = originalDoc+"-0.jpg"
fetch(contentJSON).then( r => r.json() ).then( fileContent => {
let pageBackground = document.createElement("a-box")
pageBackground.id = 'pagebackground'
pageBackground.setAttribute("position", "0.1 1.7 -0.51")
pageBackground.setAttribute("scale", ".0011 .0011 .001")
pageBackground.setAttribute("width", "612")
pageBackground.setAttribute("height", "792")
AFRAME.scenes[0].appendChild(pageBackground)
contentFromDocumentAsJSON = fileContent
showPageForHighlight(fileContent, pageNumberShownForHighlight)
})
showFile(renderedFirstPage)
// adapting ratio based on page width/height
setTimeout( _ => {
let renderedFirstPageEl = document.getElementById( renderedFirstPage.replaceAll('.',''))
renderedFirstPageEl.setAttribute("scale", 612/792+" 1 0.1") // hard coded based on data from parsed JSON from PDFExtract
}, 200)
}
function previousPageForHighlight(){
if (pageNumberShownForHighlight>0)
changePageForHighlight(--pageNumberShownForHighlight)
return pageNumberShownForHighlight
}
function nextPageForHighlight(){
if (pageNumberShownForHighlight<contentFromDocumentAsJSON.pages.length-1)
changePageForHighlight(++pageNumberShownForHighlight)
return pageNumberShownForHighlight
}
let highlightsBetweenPageChanges = []
function changePageForHighlight(pageNumber){
Array.from( document.querySelectorAll(".highlightabletext") ).map(el => el.remove())
// expected to be saved first by the user
// could be stored first using getHighlights() first, safer
highlightsBetweenPageChanges.push( getHighlights() )
showPageForHighlight(contentFromDocumentAsJSON, pageNumber)
}
function showPageForHighlight(fileContent, pageNumber){
// example of direct layout but with incorrect fonts
const scale = 1/1000
const xOffset = -.2
const yOffset = 2.1
//let pageNumber = 0
fileContent.pages[pageNumber].content.map( str => {
let tktxt = document.createElement("a-troika-text")
tktxt.setAttribute("position", ""+(xOffset+str.x*scale) + " " + (yOffset-str.y*scale) + " -0.5")
tktxt.setAttribute("originalposition", ""+(xOffset+str.x*scale) + " " + (yOffset-str.y*scale) + " -0.5")
tktxt.setAttribute("originalpage", pageNumber)
tktxt.setAttribute("originalsource", fileContent.meta.metadata['dc:title'])
tktxt.setAttribute("originalidentifier", fileContent.meta.metadata['dc:identifier'])
tktxt.setAttribute("font-size", "0.005")
tktxt.setAttribute("color", "black")
tktxt.setAttribute("target", "")
tktxt.classList.add("highlightabletext")
tktxt.setAttribute("onpicked", "console.log(selectedElements.at(-1).element.getAttribute('value'))")
tktxt.setAttribute("onreleased", "let el = selectedElements.at(-1).element; if (true) el.setAttribute('color', highlightColor); el.setAttribute('rotation', ''); el.setAttribute('position', el.getAttribute('originalposition') )")
// resets back...
// change color
// only if above a certain threshold, e.g. held a long time, or released close to specific other item
// could also toggle coloring
// can be based on coloring pick with jxr
tktxt.setAttribute("value", str.str)
tktxt.setAttribute("anchor", "left")
AFRAME.scenes[0].appendChild(tktxt)
// somehow looks more demanding than own addNewNote based on it...
// can then process for all str that is NOT the default color, i.e. black
// ".highlightabletext"
// then become exportable JSON
})
}
function nonBlackHighlitableTexts(){
return Array.from( document.querySelectorAll(".highlightabletext") ).filter( el => el.getAttribute("color") != "black" )
}
function getHighlights(){
let data = {}
let foundHighlights = nonBlackHighlitableTexts()
data.source = foundHighlights.map( el => el.getAttribute("originalsource")).slice(-1) // assuming single source for now!
data.identifier = foundHighlights.map( el => el.getAttribute("originalidentifier")).slice(-1) // assuming single source for now!
data.highlights = foundHighlights.map( el => { return {page: el.getAttribute("originalpage"), color: el.getAttribute("color"), content: el.getAttribute("value")}})
return data
}
let highlightColor = 'aqua'
// ----------------------------------------------------------------------------------------------------------------------------------
function constraintsRotationX(el){
el.object3D.rotateX(.1)
}
// relative translation, using translateX/Y/Z
// amount based on distance between current position and starting position
// becomes tricky due to on-going rotation... so using ghost object
function constraintsTranslateX(el){
window.cmv_value = new THREE.Vector3()
el.object3D.getWorldPosition( window.cmv_value ) // goes back to initial position, not new one after being picked...
window.cmv_el_original = el
window.cmv_el_clone = el.cloneNode(true) // can not override the hand picking pose use a "ghost" object
window.cmv_el_clone.setAttribute("opacity", .3)
// arguably this can be swapped, namely the opacity of the original could be lower and the clone higher, then swap on release
window.cmv_el_clone.classList.add("ghost_element")
AFRAME.scenes[0].appendChild(window.cmv_el_clone)
window.cmv = setInterval( _ => {
window.cmv_el_clone.object3D.translateX( window.cmv_el_original.object3D.position.distanceTo( window.cmv_value )/100 )
// might try sqrt() or log() to avoid going away too fast
// quite wonky... safer to keep the initial rotation
},10)
}
function clearConstraintsTranslateX(){
clearInterval( window.cmv )
window.cmv_el_original.object3D.position.copy(cmv_el_clone.object3D.position)
window.cmv_el_original.object3D.rotation.copy(cmv_el_clone.object3D.rotation)
Array.from( document.querySelectorAll('.ghost_element')).map( el => el.remove())
}
function constraintsMoveSingleAxis(el, axis){
// could visually add a colored helper arrow on the axis
// ArrowHelper, GridHelper, AxesHelper, PlaneHelper
window.cmv_value = AFRAME.utils.coordinates.stringify( el.getAttribute("rotation") )
window.cmv_el_original = el
window.cmv_el_clone = el.cloneNode(true) // can not override the hand picking pose use a "ghost" object
window.cmv_el_clone.setAttribute("opacity", .3)
// arguably this can be swapped, namely the opacity of the original could be lower and the clone higher, then swap on release
window.cmv_el_clone.classList.add("ghost_element")
AFRAME.scenes[0].appendChild(window.cmv_el_clone)
window.cmv = setInterval( _ => {
window.cmv_el_clone.object3D.position[axis] = window.cmv_el_original.object3D.position[axis]
window.cmv_el_clone.object3D.rotation.copy(cmv_el_original.object3D.rotation)
// could also be axis locked by only copying the x/y/z value of position
},10)
}
function clearConstraintsMoveSingleAxis(axis){
clearInterval( window.cmv )
if (axis!="x") window.cmv_el_original.object3D.position.x = window.cmv_el_clone.object3D.position.x
if (axis!="y") window.cmv_el_original.object3D.position.y = window.cmv_el_clone.object3D.position.y
if (axis!="z") window.cmv_el_original.object3D.position.z = window.cmv_el_clone.object3D.position.z
Array.from( document.querySelectorAll('.ghost_element')).map( el => el.remove())
}
function constraintsMoveNoRotation(el){
window.cmv_value = AFRAME.utils.coordinates.stringify( el.getAttribute("rotation") )
window.cmv_el_original = el
window.cmv_el_clone = el.cloneNode(true) // can not override the hand picking pose use a "ghost" object
window.cmv_el_clone.setAttribute("opacity", .3)
// arguably this can be swapped, namely the opacity of the original could be lower and the clone higher, then swap on release
window.cmv_el_clone.classList.add("ghost_element")
AFRAME.scenes[0].appendChild(window.cmv_el_clone)
window.cmv = setInterval( _ => {
window.cmv_el_clone.setAttribute("position", AFRAME.utils.coordinates.stringify( window.cmv_el_original.getAttribute("position") ) )
// could also be axis locked by only copying the x/y/z value of position
},10)
}
function clearConstraintsMoveNoRotation(){
clearInterval( window.cmv )
Array.from( document.querySelectorAll('.ghost_element')).map( el => el.remove())
window.cmv_el_original.setAttribute("rotation", window.cmv_value)
}
function toggleHideAllJXRCommands(){
// could be good to filter out commands that are on ring, as there we do need to see the content before applying their transformation to content
let jxrCommands = Array.from( document.querySelectorAll("a-troika-text") ).filter( c => c.getAttribute("value").startsWith("jxr"))
if (!jxrCommands) return
let visible = jxrCommands[0].getAttribute("visible")
hideAllJXRCommands(!visible)
}
function hideAllJXRCommands(visible="false"){
Array.from( document.querySelectorAll("a-troika-text") ).filter( c => c.getAttribute("value").startsWith("jxr")).map( c => c.setAttribute("visible", visible))
}
function bumpSelection(invert = false){
let newStart = Number(Object.entries( hightlightabletext.getAttribute("troika-text").colorRanges )[1][0])+1
if (invert) newStart-=2
if (newStart<0) newStart = 0
let lengthString = hightlightabletext.getAttribute("troika-text").value.length
if (newStart>lengthString) newStart = lengthString-1
let end = Number(Object.entries( hightlightabletext.getAttribute("troika-text").colorRanges )[2][0])
// can fail by simplifying to 2 value instead of 3 when arriving at 0
let range = {}
range[0] = 0xffffff
range[newStart] = 0x0099ff
range[end] = 0xffffff
hightlightabletext.setAttribute("troika-text", {colorRanges: range})
}
function growSelection(invert = false){
let start = Number(Object.entries( hightlightabletext.getAttribute("troika-text").colorRanges )[1][0])
let newEnd = Number(Object.entries( hightlightabletext.getAttribute("troika-text").colorRanges )[2][0])+1
if (invert) newEnd-=2
if (newEnd<0) newStart = 0
let lengthString = hightlightabletext.getAttribute("troika-text").value.length
if (newEnd>lengthString) newStart = lengthString-1
let range = {}
range[0] = 0xffffff
range[start] = 0x0099ff
range[newEnd] = 0xffffff
hightlightabletext.setAttribute("troika-text", {colorRanges: range})
}
let selections = []
function extractSelection(){
// should have a refractory period, i.e. don't repeat if done less than 1s ago
if ( Date.now() - lastExecuted['getSelectionWithRefractoryPeriod'] < 500 ){
console.warn('ignoring, executed during the last 500ms already')
return
}
lastExecuted['getSelectionWithRefractoryPeriod'] = Date.now()
let startPos = Number(Object.entries( hightlightabletext.getAttribute("troika-text").colorRanges )[1][0])
let endPos = Number(Object.entries( hightlightabletext.getAttribute("troika-text").colorRanges )[2][0])
let text = hightlightabletext.getAttribute("troika-text").value.substring(startPos, endPos)
addNewNote(text)
selections.push(text)
document.querySelector('#jsondownload').href=URL.createObjectURL( new Blob([JSON.stringify(selections)], { type:`text/json` }) )
}
AFRAME.registerComponent('raycaster-targets-wireframe-lense', {
init: function () {
// Use events to figure out what raycaster is listening so we don't have to hardcode the raycaster.
this.el.addEventListener('raycaster-intersected', evt => { this.raycaster = evt.detail.el; this.el.setAttribute("wireframe", "true") });
this.el.addEventListener('raycaster-intersected-cleared', evt => { this.raycaster = null; this.el.setAttribute("wireframe", "false") });
},
tick: function () {
if (!this.raycaster) {
return;
} // Not intersecting.
let intersection = this.raycaster.components.raycaster.getIntersection(this.el);
if (!intersection) {
//this.el.setAttribute( "wireframe", "false" )
} else {
console.log(this.el.id, intersection.point);
// this.el.setAttribute( "wireframe", "true" )
// only applied to sphere... despire printing correctly the right id and intersection point for manuscript and box
}
}
})
// trying to find a convenient way to be responsive with controllers to interact, not just hand tracking
AFRAME.registerComponent('raycaster-targets', {
init: function () {
// Use events to figure out what raycaster is listening so we don't have to hardcode the raycaster.
this.el.addEventListener('raycaster-intersected', evt => { this.raycaster = evt.detail.el; });
this.el.addEventListener('raycaster-intersected-cleared', evt => { this.raycaster = null; });
},
tick: function () {
if (!this.raycaster) { return; } // Not intersecting.
let intersection = this.raycaster.components.raycaster.getIntersection(this.el);
if (!intersection) { return; }
console.log(intersection.point);
this.el.setAttribute( "color", "red" )
}
})
AFRAME.registerComponent('setupable', {
init: function () {
let setupableEl = this.el
if (!setupableEl.id){
console.warn('setupable fail, target element needs unique ID')
// could also check on geometry, i.e. box only for now
return
}
let w = setupableEl.getAttribute("width")
let h = setupableEl.getAttribute("height")
let d = setupableEl.getAttribute("depth")
let controlPointEl1 = document.createElement("a-sphere")
controlPointEl1.setAttribute("position", "" + w/2 + " " + h/2 + " " + d/2)
// should be based on element width/height/depth
controlPointEl1.setAttribute("radius", ".02")
controlPointEl1.setAttribute("wireframe", "true")
controlPointEl1.setAttribute("segments-height", 8)
controlPointEl1.setAttribute("segments-width", 8)
controlPointEl1.setAttribute("color", "yellow")
controlPointEl1.setAttribute("target", "")
controlPointEl1.setAttribute("controlpoint", setupableEl.id)
controlPointEl1.id = "controlPointEl1_"+setupableEl.id
controlPointEl1.setAttribute("onpicked", "selectedElements.at(-1).element.setAttribute('wireframe', 'false')")
controlPointEl1.setAttribute("onreleased", "setupEntity('"+setupableEl.id+"')")
// AFRAME.scenes[0].appendChild(controlPointEl1)
setupableEl.appendChild(controlPointEl1)
let controlPointEl2 = document.createElement("a-sphere")
controlPointEl2.setAttribute("position", "" + -w/2 + " " + -h/2 + " " + -d/2)
// should be based on element width/height/depth
controlPointEl2.setAttribute("radius", ".02")
controlPointEl2.setAttribute("wireframe", "true")
controlPointEl2.setAttribute("segments-height", 8)
controlPointEl2.setAttribute("segments-width", 8)
controlPointEl2.setAttribute("color", "yellow")
controlPointEl2.setAttribute("target", "")
controlPointEl2.setAttribute("controlpoint", setupableEl.id)
controlPointEl2.setAttribute("onreleased", "setupEntity('"+setupableEl.id+"')")
controlPointEl2.id = "controlPointEl2_"+setupableEl.id
// AFRAME.scenes[0].appendChild(controlPointEl1)
setupableEl.appendChild(controlPointEl2)
// console.log('setupable', AFRAME.scenes) // very weird... scene should be loaded when component register and initiate
},
//tick: function () { }
})
function setupEntity( id ){
// assuming some world/axis alignment
let setupableEl = document.getElementById(id)
let controlPointEl1 = document.getElementById( "controlPointEl1_"+setupableEl.id )
let controlPointEl2 = document.getElementById( "controlPointEl2_"+setupableEl.id )
let middlePos = new THREE.Vector3()
middlePos = controlPointEl1.object3D.position.clone()
middlePos.add(controlPointEl2.object3D.position.clone())
middlePos.divideScalar(2)
middlePos.add( setupableEl.object3D.position.clone() ) // becoming world coordinates instead, assuming that the parent object has no parent
setupableEl.setAttribute("position", AFRAME.utils.coordinates.stringify(middlePos) )
let w = Math.abs( controlPointEl1.object3D.position.x - controlPointEl2.object3D.position.x )
let h = Math.abs( controlPointEl1.object3D.position.y - controlPointEl2.object3D.position.y )
let d = Math.abs( controlPointEl1.object3D.position.z - controlPointEl2.object3D.position.z )
setupableEl.setAttribute("width", w)
setupableEl.setAttribute("height", h)
setupableEl.setAttribute("depth", d)
// could also some rotations as we can assume a table to be flat
}
function loadOnPanels(){
let url ="https://webdav.benetou.fr/fotsave/fot_sloan_companion_public/augmented_paper.pdf-"
let extension = ".jpg"
let selector = "#deskpanels"
Array.from( document.querySelector(selector).children )
.map( (p,i) => {
p.setAttribute("wireframe", "false");
p.setAttribute("depth", "1");
p.setAttribute("src", url+i+extension)
})
}
function toggleAnchors(){
let anchorsFound = false
targets.map( t => { el = t.querySelector("a-sphere[color=blue]"); if (el) anchorsFound = true} )
if (anchorsFound){
targets.map( t => { el = t.querySelector("a-sphere[color=blue]"); if (el) el.remove()} )
targets.map( t => { el = t.querySelector("a-sphere[color=blue]"); if (el) el.remove()} )
// somehow needed twice for table anchor
} else {
makeAnchorsVisibleOnTargets()
}
}
function noteFromMetaData(){
f = Object.values(filesWithMetadata)[0]
addNewNote( "First file\n\n"+Object.getOwnPropertyNames( f )
.filter( p => (p == "size" || p.endsWith("timeMs")) )
.map( p => p + "\t: " + f[p]).join('\n') )
// should instead take any filename, ideally any object, including interface ones
// then visually highlight metadata with dedicated affordances
// consider also metadata on non-files, e.g. every thing in <a-scene> with an ID
}
/* PDF reflow demo
loadOnPanels() // should make them target too
r = Array.from( document.querySelector("#deskpanels").children ).sort( (a,b) => a.object3D.position.z > b.object3D.position.z ).map( el => el.getAttribute("src")).map( url => url.replace(/.*pdf-/,'').replace('.jpg','')).map( r => Number(r)+1)
// could also filter
// in
// only objects with a class or property (e.g. target)
// out
// object with a class (e.g. only .document_part)
// outside of a volume
// e.g. z < -1 || z > 1 || x < -1 ...
// this could be made visible
// could also provide feedback as preview
// e.g. selected pages 1, 2, 4, 5 (in that orqder) (in that order)
fetch("/save-as-new-pdf/augmented_paper.pdf/"+JSON.stringify(r))
// should send back URL after, maybe via ntfy
// jpg (montage) or
// https://stackoverflow.com/questions/37709879/how-to-generate-a-collage-image-like-shown
// via fetch("/save-as-new-montage/augmented_paper.pdf/"+JSON.stringify(r))
// indexing on 0 so no need for +1 on page number
// HTML working
// alternatively copy this converter than make
// epub, PmWiki, MarkDown, etc
// not to be downloaded though, with live URL, still usable
// SVG (using <image href"">)
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
// can maybe be done client side via file reader
// glb
// potentially from glTF with external images then https://github.com/donmccurdy/glTF-Transform
// or https://gltf-transform.dev/cli
// can be done client side via threejs
// cf e.g. https://git.benetou.fr/utopiah/text-code-xr-engine/issues/24
*/
let mediaRecorder
let chunks = []
// could instead be an array of audio elements
let audioElements = []
// then create element and push
let audioRecordingBlob
// will request permissions, potentially kicking out of XR
// probably safer to do so via a microphone emoji in 2D
// could also call setupRecorder() if mediaRecorder is still null
// removes a step for the user
function setupRecorder(){
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
console.log("getUserMedia supported.");
navigator.mediaDevices
.getUserMedia( { audio: true, },)
// Success callback
.then((stream) => {
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (e) => {
chunks.push(e.data);
};
mediaRecorder.onstop = (e) => {
const audioBlob = new Blob(chunks, { type: "audio/ogg; codecs=opus" });
audioRecordingBlob = audioBlob
chunks = [];
const audio = document.createElement("audio")
// this shouldn't be replaced on Vision Pro
// but also prevents from having multiple recordings
audio.src = window.URL.createObjectURL(audioBlob)
audio.play()
audioElements.push(audio)
addXRAudioWidget( audioElements.length-1 )
// should add a new AFrame entity too, so that it has a player
// ideally it's also play/pause as toggle, not just play (as of right now)
}
})
// Error callback
.catch((err) => {
console.error(`The following getUserMedia error occurred: ${err}`);
});
} else {
console.error("getUserMedia not supported on your browser!");
}
}
let latest_audio_id
// for Apple support on Vision Pro
// cf https://stackoverflow.com/questions/31776548/why-cant-javascript-play-audio-files-on-iphone-safari
const audio = new Audio();
audio.src = "success-221935.mp3"
audio.autoplay = true;
function latestAudioPlay(){
// onClick of first interaction on page before I need the sounds
// (This is a tiny MP3 file that is silent and extremely short - retrieved from https://bigsoundbank.com and then modified)
audio.src = "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"
if (latest_audio_id) audio.src = latest_audio_id.src
audio.play()
}
async function addRecentAudioFiles(){
const contents = await webdavClient.getDirectoryContents(subdirWebDAV);
// consider instead search https://github.com/perry-mitchell/webdav-client#search
contents.filter(f => f.basename.endsWith('.ogg'))
.sort( (a,b) => new Date(a.lastmod).getTime() < new Date(b.lastmod).getTime() ) // newest first
.slice(0,4) // top or last?
.map(a => {
const audio = document.createElement("audio")
audio.src = a.basename
audioElements.push(audio)
addXRAudioWidget( audioElements.length-1 )
})
}
function addXRAudioWidget(n){
// should become available via showFile() thus become a filter
let rootEl = document.getElementById("audiowidgets")
let el = document.createElement("a-entity")
let color= Math.random().toString(16).substr(-6)
let miniID = Math.random().toString(36).slice(-1).toUpperCase()+Math.floor(Math.random()*10)
el.id = color + '_' + miniID
latest_audio_id = el.id
el.innerHTML =
//`<a-troika-text anchor=left target value="jxr latestAudioPlay()" rotation="0 90 0" position="0 ${-n*.1} 0" scale="0.1 0.1 0.1">
`<a-troika-text class="audiorecordings" anchor=left target value="jxr audioElements[${n}].play()" rotation="0 90 0" position="0 ${-n*.1} 0" scale="0.1 0.1 0.1">
<a-entity scale=".2 .2 .2" class="icon speaker" position="-.5 0 0">
<a-cylinder color=gray radius=.3 rotation="0 0 90"></a-cylinder>
<a-cone color=gray radius-top=".1" rotation="0 0 90"></a-cone>
<a-box color=${'#'+color} scale="" position="1 0 0"></a-box>
<a-troika-text value="${miniID}" position="1 0 .51" font-size=1></a-troika-text>
</a-entity>
</a-troika-text>`
rootEl.appendChild(el)
// <a-sphere color=purple radius=".5" position="1 0 0"></a-sphere>
// this audioElements...play() does not work on Safari due to audio limitations, should instead swap content and play from same allow audio element
}
function associateLatestDropRecordingClosestHighlight(){
let el = selectedElements.at(-1).element
// check distance to all highlights
let foundHighlights = nonBlackHighlitableTexts()
// find closest under threshold...
foundHighlights.map( h => {
// doesn't seem to get world coordinates, probably due to parenting
let p1 = new THREE.Vector3()
el.object3D.getWorldPosition( p1 )
let p2 = new THREE.Vector3()
h.object3D.getWorldPosition( p2 )
let dist = p1.distanceTo( p2 )
console.log( dist )
// even while trying to get world coordinates the distance seems of, close to 1 when it should be 0.
// it does seems to be proportional though, i.e. it increases while moving objects away
// could visually show what has been tested, i.e. last 2 elements and a line between both
})
}
function saveAudioFile(filename="audiofile.ogg"){
// this could support multiple audio element as input, not "just" audioRecordingBlob, i.e the last one
const reader = new FileReader();
reader.onloadend = (evt) => {
fileContent = evt.target.result
async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, fileContent); }
written = w(subdirWebDAV+usernamePrefix+filename)
if (written){
fetch('https://ntfy.benetou.fr/fileuploadtowebdav', { method: 'POST', body: 'added '+usernamePrefix+filename })
// available then as https://webdav.benetou.fr/fotsave/fot_sloan_companion_public/audiofile.ogg
}
};
reader.readAsArrayBuffer(audioRecordingBlob);
// always saving the last recorded one
}
function saveHighlights(filename="highlights.json"){
async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, JSON.stringify(getHighlights())); }
// note that this only single page saving, should instead consider highlightsBetweenPageChanges but after dedup
console.log('browsable in 2D at https://companion.benetou.fr/highlights_example.html')
written = w(subdirWebDAV+usernamePrefix+filename)
if (written){
fetch('https://ntfy.benetou.fr/fileuploadtowebdav', { method: 'POST', body: 'added '+usernamePrefix+filename })
// available then as https://webdav.benetou.fr/fotsave/fot_sloan_companion_public/highlights.json
}
}
// TODO annotations via screenshot richer UX
// add filter/screenshotux.js
// add audio recorder capability
// highlighters
function saveScreenshot(filename="screenshot_test.jpg"){
// https://git.benetou.fr/utopiah/text-code-xr-engine/commit/d5bc01251ecb2380c9be0b456d2a7b68fd16e4f2
document.querySelector('a-scene').components.screenshot.getCanvas('perspective').toBlob( blob => {
let imgBlob = new File([blob], filename, { type: "image/jpeg"});
const reader = new FileReader();
reader.onloadend = (evt) => {
fileContent = evt.target.result
async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, fileContent); }
written = w(subdirWebDAV+usernamePrefix+filename)
if (written){
fetch('https://ntfy.benetou.fr/fileuploadtowebdav', { method: 'POST', body: 'added '+usernamePrefix+filename })
// available then as https://webdav.benetou.fr/fotsave/fot_sloan_companion_public/screenshot_test.jpg
}
};
reader.readAsArrayBuffer(imgBlob);
}, "image/jpeg", 0.8);
}
function saveCSLJson(filename="example_bibliography.csl.json"){
// could get again get snippets of a certain type, e.g. .bibliographyitem only
// filtered on within a specific volume, e.g. within unit cube around center but .5m above floor and in front
// so not centered on 0 0 0 but rather on 0 1 -.5 (can be visualized via e.g. wireframe)
// can rely on a filter/ instead
let readExample = "https://webdav.benetou.fr/fotsave/ExportedItems-FromZoteroAsCSLJSON.json"
fetch(readExample).then( r => r.json() ).then( fileContent => {
// fileContent.map( i => { return {title:i.title, note:i.note.split('\n')}})
fileContent.map( (i,n) => addNewNote( i.title, position=`-0.2 ${1+n*.1} -0.1`, "0.1 0.1 0.1", "bibliographyitem_"+n, "bibliographyitem" ) )
// could instead make snippets with addNewNote(i.title) then add the right class, etc
setTimeout( _ => {
console.log( Array.from( document.querySelectorAll(".bibliographyitem") )
.filter( i => i.object3D.position.y > 0.5 && i.object3D.position.y < 1.5 )
.sort( (a,b) => a.object3D.position.y > b.object3D.position.y )
)
// could then save back
}, 500)
async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, JSON.stringify(fileContent)); }
// written = w(subdirWebDAV+usernamePrefix+filename)
// in turn available as https://companion.benetou.fr/poweruser_example_bibliography.csl.json
})
}
// requires running locally, e.g. OLLAMA_HOST=0.0.0.0 OLLAMA_ORIGINS=* ollama serve on the same machine (hence does not work remotely or on HMD)
function ollamaToNote(llmPrompt = "Why is the sky blue?" ){
// could be used based on the transcription of an audio recording
fetch('http://localhost:11434/api/generate', { method: 'POST',body:
'{\n "model": "deepseek-r1:1.5b",\n "prompt": "'+llmPrompt+'",\n "stream": false\n}'
})
.then( res => res.json() ).then( res => addNewNote(res.response))
//.then( res => res.json() ).then( res => addNewNote(res.response.replace(/think[\s\S]*?think/,'')))
// here with deepseek could remove the <think></think> part
}
function startViewCheck(){
let visualArrow = document.createElement("a-cone")
visualArrow.id = 'visualarrow'
visualArrow.setAttribute("opacity", .3)
visualArrow.setAttribute("position", "0 0.5 -1")
visualArrow.setAttribute("scale", "0.1 0.1 0.1")
visualArrow.setAttribute("segments-height", 8)
visualArrow.setAttribute("segments-width", 8)
player.appendChild(visualArrow)
setInterval( i => inView(document.querySelector("a-console")), 100 )
}
// -----=========== snap closest =============--------------------------------------------------------------------------------------------
function snapClosest(snappable=null){ // not selector but array
// now using world positions
let lastPick = selectedElements.at(-1).element
let pos = new THREE.Vector3()
lastPick.object3D.getWorldPosition( pos )
let closestEl
let smallestDistance
let rotationOffset = false
if (!snappable) {
snappable = Array.from( deskpanels.children )
// should be more general, this is a weird default
rotationOffset = true
}
snappable.map( p => {
let targetPos = new THREE.Vector3()
p.object3D.getWorldPosition( targetPos )
let d = pos.distanceTo( targetPos )
//let d = lastPick.object3D.position.distanceTo( p.object3D.position )
if (!closestEl) {
closestEl = p
smallestDistance = d
}
if (d < smallestDistance) {
closestEl = p
smallestDistance = d
}
})
if (closestEl){
//lastPick.object3D.position.copy( closestEl.object3D.position )
let finalPos = new THREE.Vector3()
closestEl.object3D.getWorldPosition( finalPos )
lastPick.object3D.position.copy( finalPos )
// could try to offset down from here
lastPick.object3D.rotation.copy( closestEl.object3D.rotation )
// arbitrary offset for panels, could be a parameter
if ( rotationOffset ) lastPick.object3D.rotateX(-Math.PI/2)
// can't read the text... it is backward
lastPick.emit('snapped', {closest: closestEl, el: lastPick})
return closestEl
// could instead emit back to the snapping element
}
return null
}
// -----=========== HUD visibility =============--------------------------------------------------------------------------------------------
function toggleHUDVisibility(){
let opacity = typinghud.getAttribute("material").opacity
if ( opacity > .1 )
typinghud.setAttribute("material","opacity", .1)
else
typinghud.setAttribute("material","opacity", .5)
}
// -----=========== Cube =============--------------------------------------------------------------------------------------------
function toggleShowCube(){
if ( cubetest.getAttribute("visible") )
cubetest.setAttribute("visible", "false")
else
cubetest.setAttribute("visible", "true")
}
function addCubeWithAnimations(){
let cube = document.createElement("a-entity")
cube.id = "cube"
cube.setAttribute("position", "0.2 1.2 -.7")
//cube.setAttribute("position", "0 1 -1")
cube.setAttribute("target", "")
AFRAME.scenes[0].appendChild(cube)
function cubeFace(parentElement){
let face = document.createElement("a-box")
face.setAttribute("scale", ".1 .1 .01")
face.setAttribute("wireframe", "true")
let axis = document.createElement("a-entity")
// not actually used
axis.appendChild(face)
parentElement.appendChild(axis)
return face
}
let elFaceName
let face_number = 0
let f = cubeFace(cube)
f.id = "face_"+"ABCDEF"[face_number++]
elFaceName = document.createElement("a-troika-text")
elFaceName.setAttribute("value", f.id )
f.appendChild(elFaceName)
const targetAngle = .03
const animate = false
// https://animejs.com/documentation/#JSobject
// https://threejs.org/docs/#api/en/core/Object3D.rotateOnAxis
if (animate) AFRAME.ANIME({targets: cube.children[0].object3D, update: function(){ cube.children[0].object3D.rotateX(-targetAngle)} })
//AFRAME.ANIME({targets: cube.children[0].object3D, update: function(){ cube.children[0].object3D.translateY(-.001)} })
//AFRAME.ANIME({targets: cube.children[0].object3D, update: function(){ cube.children[0].object3D.translateY(-.002)} })
f = cubeFace(cube)
f.id = "face_"+"ABCDEF"[face_number++]
elFaceName = document.createElement("a-troika-text")
elFaceName.setAttribute("value", f.id )
f.appendChild(elFaceName)
// adding visible name to help identify on movement and animations
f.setAttribute("position", ".05 0 .05")
f.setAttribute("rotation", "0 90 0")
if (animate) AFRAME.ANIME({targets: cube.children[1].object3D, update: function(){ cube.children[1].object3D.rotateZ(-targetAngle)} })
f = cubeFace(cube)
f.id = "face_"+"ABCDEF"[face_number++]
elFaceName = document.createElement("a-troika-text")
elFaceName.setAttribute("value", f.id )
f.appendChild(elFaceName)
f.setAttribute("position", "0 0 .1")
if (animate) AFRAME.ANIME({targets: cube.children[2].object3D, update: function(){ cube.children[2].object3D.rotateX(targetAngle)} })
f = cubeFace(cube)
f.id = "face_"+"ABCDEF"[face_number++]
elFaceName = document.createElement("a-troika-text")
elFaceName.setAttribute("value", f.id )
f.appendChild(elFaceName)
f.setAttribute("position", "-0.05 0 .05")
f.setAttribute("rotation", "0 90 0")
if (animate) AFRAME.ANIME({targets: cube.children[3].object3D, update: function(){ cube.children[3].object3D.rotateZ(targetAngle)} })
// bottom face
f = cubeFace(cube)
f.id = "face_"+"ABCDEF"[face_number++]
elFaceName = document.createElement("a-troika-text")
elFaceName.setAttribute("value", f.id )
f.appendChild(elFaceName)
f.setAttribute("rotation", "90 0 0")
f.setAttribute("position", "0 -0.05 0.05")
// top face
f = cubeFace(cube)
f.id = "face_"+"ABCDEF"[face_number++]
elFaceName = document.createElement("a-troika-text")
elFaceName.setAttribute("value", f.id )
f.appendChild(elFaceName)
f.setAttribute("rotation", "90 0 0")
f.setAttribute("position", "0 0.05 0.05")
cube.id = "cubetest"
return cube
}
// both functions should be toggable as ways to revert
function unfoldCube(){
// should save rotation/positions first
Array.from( cubetest.querySelectorAll("a-box") ).map( (f,i) => {
f.formerRotation = f.getAttribute("rotation")
f.setAttribute("rotation", "0 0 0")
f.formerPosition = AFRAME.utils.coordinates.stringify( f.getAttribute("position") )
f.setAttribute("position", i/(10-1)+" 0 0")
} )
// each face has a parent with also element with an offset position
}
function refoldCube(){
// should save rotation/positions first
Array.from( cubetest.querySelectorAll("a-box") ).map( (f,i) => {
f.setAttribute("rotation", f.formerRotation )
f.setAttribute("position", f.formerPosition )
} )
// each face has a parent with also element with an offset position
}
function roomScaleCube(){
cubetest.setAttribute("scale", "20 20 20"); cubetest.setAttribute("position", "0 1 -1"); cubetest.setAttribute("rotation", "0 0 0")
}
function palmScaleCube(){
cubetest.setAttribute("scale", "1 1 1"); cubetest.setAttribute("position", "0 1 -1")
}
function addCube(){
let cube = document.createElement("a-entity")
cube.setAttribute("position", "1 1 -.5")
cube.setAttribute("target", "")
AFRAME.scenes[0].appendChild(cube)
let cubeloweraxis = document.createElement("a-entity")
cubeloweraxis.setAttribute("position", "-.1 0 0")
cubeloweraxis.setAttribute("animation__rot", "property:rotation.z; to:90")
cubeloweraxis.setAttribute("animation__pos", "property:position; to:-.1 -.1 0")
let sll = document.createElement("a-box")
sll.setAttribute("scale", ".1 .1 .01")
sll.setAttribute("position", "0.05 0 0")
sll.setAttribute("rotation", "0 90 0")
sll.setAttribute("wireframe", "true")
cubeloweraxis.appendChild(sll)
cube.appendChild(cubeloweraxis)
// too complicated... should make 1 face, rotate along it's bottom axis then duplicate it and rotate accordingly
// note that 2 faces are specials in regard to animations
// bottom part do not move
// top part move alongside another moving face, e.g. front face
let ssra = document.createElement("a-entity")
ssra.setAttribute("position", ".1 0 0")
ssra.setAttribute("animation__rot", "property:rotation.z; to:-90")
ssra.setAttribute("animation__pos", "property:position; to:.1 0 0")
let srr = document.createElement("a-box")
srr.setAttribute("scale", ".1 .1 .01")
srr.setAttribute("position", ".05 0 0")
srr.setAttribute("rotation", "0 90 0")
srr.setAttribute("wireframe", "true")
ssra.appendChild(srr)
cube.appendChild(ssra)
let sl = document.createElement("a-box")
sl.setAttribute("scale", ".1 .1 .01")
sl.setAttribute("position", "0 -.05 0")
sl.setAttribute("rotation", "90 0 0")
sl.setAttribute("wireframe", "true")
cube.appendChild(sl)
let sr = document.createElement("a-box")
sr.setAttribute("scale", ".1 .1 .01")
sr.setAttribute("position", "0 .05 0")
sr.setAttribute("rotation", "90 0 0")
sr.setAttribute("wireframe", "true")
cube.appendChild(sr)
let sb = document.createElement("a-box")
sb.setAttribute("scale", ".1 .1 .01")
sb.setAttribute("position", "0 0 -.05")
sb.setAttribute("wireframe", "true")
cube.appendChild(sb)
let sf = document.createElement("a-box")
sf.setAttribute("scale", ".1 .1 .01")
sf.setAttribute("position", "0 0 .05")
sf.setAttribute("wireframe", "true")
cube.appendChild(sf)
return cube
}
// -----=========== indicator in view =============--------------------------------------------------------------------------------------------
let isInView = false
function inView(targetSelector){
// https://stackoverflow.com/a/69955650/1442164
const frustum = new THREE.Frustum()
const camera = AFRAME.scenes[0].camera
const matrix = new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
frustum.setFromProjectionMatrix(matrix)
isInView = frustum.containsPoint(targetSelector.object3D.position)
if (isInView){
// could also use HUD on state change e.g. if (isInView) setFeedbackHUD('Out of view')
visualarrow.setAttribute("opacity", .01)
} else {
visualarrow.setAttribute("opacity", .3)
visualarrow.object3D.lookAt( targetSelector.object3D.position )
// seems it needs an offset, maybe due to the cone initial rotation (i.e. pointing up)
visualarrow.object3D.rotateX(1)
// still looks vertically off
}
}
// -----=========== adjust pinch thickness line =============--------------------------------------------------------------------------------------------
function adjustPinchThicknessLines(thickness){
document.querySelector("#rightHand").object3D.traverse( o => { if (o.material) o.material.wireframeLinewidth=thickness } )
}
// -----=========== demos management =============--------------------------------------------------------------------------------------------
let demos
function nextDemo(){
// basically just caching at this point...
if ( demos ) {
moveToNextDemo()
} else {
fetch('/demo_q1.json').then( r => r.json() ).then( r => {
demos = r["content"]
moveToNextDemo()
})
}
}
function moveToNextDemo(){
let internalOrigin = "&sourceFromNextDemo=true" // should be use to display a welcome message, clarifying the name of the demo and what can be done
let availableDemos = [].concat( ...demos.filter( d => d.usernames ).map( d => d.usernames ) )
// too complicated data structure... (due to not having 1-1 URL/demo matching, should simplify that)
// possibly using a tree where each demos has an optional next one, then following that instead (can become a cycling graph...)
// if modifying though then must modify demos_example.html too (which is short so no problem)
let pos = availableDemos.indexOf( username )
if (pos > -1 && pos < availableDemos.length-1)
location.href = "/index.html?username=" + availableDemos[++pos] + internalOrigin
if (pos == -1)
location.href = "/index.html?username=" + availableDemos[0] + internalOrigin
}
function addDemoScreenshot( demo ){
let el = document.createElement("a-image")
AFRAME.scenes[0].appendChild(el)
el.setAttribute("position", "0 "+(Math.random()+1)+" 0.5" )
el.setAttribute("rotation", "0 180 0")
el.setAttribute("scale", ".1 .1 .1")
el.setAttribute("src", demo.screenshot)
el.setAttribute("target", "")
// consider child of
// addNewNote("jxr location.href='/index.html?username="+u+"'", "0.5 "+(1+i/10)+" .5", "0 180 0" )
// instead BUT assumes 1 screenshot per username, thus URL
}
function addMetaDataCurrentDemo(){
let matches = demos.filter( d => d.usernames?.includes(username) )
matches.map( currentDemo => {
if (currentDemo.name) {
let nameEl = addNewNote(currentDemo.name, "0.1 1.8 -.4")
nameEl.id = "demoMetaDataName"
}
if (currentDemo.description) {
let descriptionEl = addNewNote(currentDemo.description, "0.1 1.7 -.5")
descriptionEl.id = "demoMetaDataDescription"
}
})
}
AFRAME.registerComponent('current-demo-metadata', {
init: function () {
if ( demos ) {
addMetaDataCurrentDemo()
} else {
fetch('/demo_q1.json').then( r => r.json() ).then( r => {
demos = r["content"]
addMetaDataCurrentDemo()
})
}
}
})
AFRAME.registerComponent('timed-demos', {
init: function () {
fetch('/demo_q1.json').then( r => r.json() ).then( r => {
demos = r["content"]
const baseURL = r["configuration"].prefixurl
r["content"].filter( c => c.usernames?.length>0 ).map( c=> {
if (c.screenshot) addDemoScreenshot( c )
c.usernames.map( u => console.log( baseURL+u ) )
c.usernames.map( (u,i) => {
addNewNote("jxr location.href='/index.html?username="+u+"'", "0.5 "+(1+i/10)+" .5", "0 180 0" )
})
})
// addNewNote("jxr nextDemo()", "-0.5 1 -.5" )
})
}
})
// -----=========== ... =============--------------------------------------------------------------------------------------------
// generalize snapClosest() to any selector
// use that as example on result of audio notes for manuscript editing
// could also test, if snapped on manuscript, then append content to it
// -----=========== sequential filters on interactions, e.g. on drop and on release =============--------------------------------------------------------------------------------------------
// should also do on move but slightly different logic
// should generalize to any event
// on drop meta data optional... an in VR debug mode in order to show e.g. position/rotation of the last dropped element
// arguably could be ANOTHER middleware case, namely onreleased does one action per item... but could also do more for all items, with filtering per class, position, etc
// could NOT be added to 'onreleased' or 'onpicked' component within the try/catch block, before and after the eval
// because it means it would only apply to such elements
// should instead be applied to ALL targets
// consequently should be modifying the target component
// test case : onreleased : if (el.getAttribute("color") == "red") console.log( el.getAttribute("position") )
let currentFilterOnPicked = null
let currentFilterOnReleased = null
function applyNextFilterInteraction( element, filters, filter ){
filters.map( f => f(element) ) // simplified version, no next() as done with filters
console.log( "done filtering for" )
}
function colorChangeJXROnly( el ){
if ( el.getAttribute("value")?.includes("jxr") ) el.setAttribute("color", "cyan")
}
sequentialFiltersInteractionOnReleased.push( colorChangeJXROnly )
function colorChangeSpecificCommandNameOnly( el ){
if ( el.getAttribute("value")?.includes("location.reload") ){
el.setAttribute("color", "pink")
// could try applying to only that segment... requires a bit of troika text syntax (and assumptions, e.g. only appear once)
el.setAttribute("scale", ".2 .2 .2")
}
}
sequentialFiltersInteractionOnReleased.push( colorChangeSpecificCommandNameOnly )
function colorChangeSpecificPerId( el ){
if ( el.id == "virtualdesktopplanemovable" ) el.setAttribute("color", "#ddd")
if ( el.id == "manuscript" ) el.setAttribute("color", "white")
}
sequentialFiltersInteractionOnReleased.push( colorChangeSpecificPerId )
// could it be a lambda?
// should try on absolute position, relative position, etc
// should show non overlapping consequences, e.g. one filter change color, the next changes scale
// thus showcasing composability
// then add those filters as examples to remix
// -----=========== binding to joints =============--------------------------------------------------------------------------------------------
AFRAME.registerComponent('bind-element-to-finger', {
multiple: true,
schema: {
hand: {type: 'string', default: 'r_handMeshNode'},
finger: {type: 'string', default: 'index-finger-tip'},
target : {type: 'selector'},
},
init: function () {
},
tick: function (time, timeDelta) {
const joint = AFRAME.scenes[0].object3D.getObjectByName(this.data.hand)?.parent.getObjectByName(this.data.finger)
if ( joint && this.data.target.object3D.parent == AFRAME.scenes[0].object3D ) this.data.target.object3D.parent = joint
// unfortunately reparenting for now breaks properly picking...
// could check in core for whom the parent is and reparent only on release
// or potentially not at all, maybe the user expects to be able to pick the instruction away from the hand
// would also then need an explicit mechanism to bind back
// a la wrist shortcut
// warning that some parenting is done "right" namely by removing the offset then become an A-Frame child with that offset
// allowing thus the expected behavior in that context
// consequently might have to distinguish the 2 situations
}
})
AFRAME.registerComponent('instructions-on-hands-more-fingers', {
init: function () {
let showEl = addNewNote( "jxr console.log('show')" ) // TODO better command
showEl.setAttribute("scale", ".05 .05 .05")
showEl.setAttribute("position", "0 .03 -.05")
showEl.setAttribute("rotation", "0 -90 0")
showEl.id = "show_instructions"
let layoutEl = addNewNote( "jxr console.log('layout')" ) // TODO better command
layoutEl.setAttribute("scale", ".05 .05 .05")
layoutEl.setAttribute("position", "0 .03 -.05")
layoutEl.setAttribute("rotation", "0 -90 0")
layoutEl.id = "layout_instructions"
let rightEl = addNewNote( "pinch to move" )
rightEl.setAttribute("scale", ".05 .05 .05")
rightEl.setAttribute("position", "0 .03 -.05")
rightEl.setAttribute("rotation", "0 -90 0")
rightEl.id = "right_hand_instruction"
let leftEl = addNewNote( "pinch to execute" )
leftEl.setAttribute("scale", ".05 .05 .05")
leftEl.setAttribute("position", "0 .03 .02")
leftEl.setAttribute("rotation", "0 90 0")
leftEl.id = "left_hand_instruction"
},
tick: function (time, timeDelta) {
// definitley overkill... should listen to a connected event instead
const r_hand = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-metacarpal")
const l_hand = AFRAME.scenes[0].object3D.getObjectByName("l_handMeshNode")?.parent.getObjectByName("thumb-metacarpal")
if ( r_hand && right_hand_instruction.object3D.parent == AFRAME.scenes[0].object3D ) right_hand_instruction.object3D.parent = r_hand
if ( l_hand && left_hand_instruction.object3D.parent == AFRAME.scenes[0].object3D ) left_hand_instruction.object3D.parent = l_hand
// break expected target behavior
// on pick re-parent to scene then parent back on released
const i1 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("pinky-finger-phalanx-proximal")
const i2 = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("index-finger-phalanx-proximal")
if ( i1 && show_instructions.object3D.parent == AFRAME.scenes[0].object3D ) show_instructions.object3D.parent = i1
if ( i2 && layout_instructions.object3D.parent == AFRAME.scenes[0].object3D ) layout_instructions.object3D.parent = i2
}
})
AFRAME.registerComponent('instructions-on-hands', {
init: function () {
let rightEl = addNewNote( "pinch to move" )
rightEl.setAttribute("scale", ".05 .05 .05")
rightEl.setAttribute("position", "0 .03 -.05")
rightEl.setAttribute("rotation", "0 -90 0")
rightEl.id = "right_hand_instruction"
let leftEl = addNewNote( "pinch to execute" )
leftEl.setAttribute("scale", ".05 .05 .05")
leftEl.setAttribute("position", "0 .03 .02")
leftEl.setAttribute("rotation", "0 90 0")
leftEl.id = "left_hand_instruction"
},
tick: function (time, timeDelta) {
// definitley overkill... should have a connected event instead
const r_hand = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")?.parent.getObjectByName("thumb-metacarpal")
const l_hand = AFRAME.scenes[0].object3D.getObjectByName("l_handMeshNode")?.parent.getObjectByName("thumb-metacarpal")
if ( r_hand && right_hand_instruction.object3D.parent == AFRAME.scenes[0].object3D ) right_hand_instruction.object3D.parent = r_hand
if ( l_hand && left_hand_instruction.object3D.parent == AFRAME.scenes[0].object3D ) left_hand_instruction.object3D.parent = l_hand
// break expected target behavior
// on pick re-parent to scene then parent back on released
}
})
// -----=========== ... =============--------------------------------------------------------------------------------------------
AFRAME.registerComponent('useraddednote-append-to', {
schema: { target : {type: 'selector'}, },
events: {
useraddednote: function (e) {
let noteEl = e.detail.element
if ( noteEl.getAttribute("value").startsWith("jxr ") ) return
noteEl.classList.add("manuscriptnote")
// dirty mix of threejs and AFrame...
noteEl.object3D.parent = this.data.target.object3D;
//noteEl.setAttribute("position", manuscript.children[0].getAttribute("position") )
//noteEl.setAttribute("rotation", manuscript.children[0].getAttribute("rotation") )
setTimeout( _ => noteEl.object3D.position.set( -.4, .4 + - document.querySelectorAll(".manuscriptnote").length/10, .51), 100 )
// messes up direct picking after, so could do an interaction filter on pick for this class
// relatively complex to keep track of but should work
// could until then prevent picking, e.g. removing the target
},
}
})
// -----=========== ring system =============--------------------------------------------------------------------------------------------
/* AFrame example
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='another example of text to tag' position="-0.4 1.20 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='.selected_via_tag' position="-0.4 1.60 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-entity id=tagging_active_rings>
<a-torus color="blue" position="0 1.8 -.8" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr applyFunctionToSelection("changeColorToBlue")' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
</a-entity>
*/
function sceneIdsToElementForRings(){
// does not actually traverse the scene, only gets the top level entities
Array.from( document.querySelector("a-scene").childNodes ).filter( el => el.id && !el.id.startsWith("note_") ).reverse().map( (el,i) => {
let noteEl = addNewNote("#"+el.id, "1 "+(1+i/50)+" -1")
noteEl.setAttribute("onpicked","startRingCheck()")
noteEl.setAttribute("onreleased","endRingCheck()")
})
}
function getElementsCurrentTag(){
// including rings...
if (!window.currentTag || !window.currentTag.model) {
console.warn('tag missing')
return []
}
let res = Array.from( document.querySelectorAll("a-gltf-model[src='"+window.currentTag.model+"']") )
// should filter out with internal_tag
res = res.filter( el => el.getAttribute("tag") != "internal_tag" ) // special tag value to ignore
Array.from( document.querySelectorAll(".selected_via_tag") ).map( i => i.classList.remove("selected_via_tag") )
res.map( i => i.parentNode.classList.add( 'selected_via_tag' ) )
console.log(res)
return res
// then used with resolvedElements (and possibly clipboard-ish), approach to unify
}
let ringCheckInterval = null
let ringAdded = false
let ringsUsedInLastSession = []
let trailingLineEnabled = false
function startRingCheck( feedbackDistance = .2, activationDistance = .1 ){
const refractoryPeriodRing = 500 // ms, consider lower value for keystrokes equivalent
// document.querySelector("a-console").setAttribute("visible", true)
let pos = new THREE.Vector3()
let el = selectedElements.at(-1)?.element
if (!el) return
let historyPositions = []
document.querySelector("a-console").setAttribute("visible", "true")
ringCheckInterval = setInterval( el => {
el = selectedElements.at(-1)?.element
el.object3D.getWorldPosition( pos )
let handAvailable = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")
// trailing line
if ( trailingLineEnabled && handAvailable ){
historyPositions.push( AFRAME.utils.coordinates.stringify( AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode").parent.getObjectByName("thumb-tip").position ) )
if (historyPositions.length > 10){
let path = historyPositions.slice(-10).join(", ")
// console.log( path )
rings.querySelector("a-tube").setAttribute("path", path)
// console.log( rings.querySelector("a-tube") )
}
}
Array.from( active_rings.children )
.concat( Array.from( tagging_active_rings.children ) ) // mixing ring sets
.concat( [ring_dial_test] ) // should find a more generalizable way... but convenient too
.concat( [ ring_dial_color, ring_dial_color_r, ring_dial_color_g, ring_dial_color_b, ] )
.filter( r => {
// hidden rings should not be active (probably has a threejs helper...)
let hidden = false
r?.object3D?.traverseAncestors( a => { if (!a.visible) hidden = true } )
// causing errors with ring dial tests, hence the ?. way
return !hidden
})
.map( r => {
r.setAttribute("animation", "property: rotation.z; from: 0; to: 360; dur: 1000; startEvents:startAnimation;")
let pos_ring = new THREE.Vector3()
r.object3D.getWorldPosition( pos_ring )
let d = pos.distanceTo( pos_ring )
// visual feedback on proximity
if (d<feedbackDistance) { r.setAttribute("wireframe", "true") } else { r.setAttribute("wireframe", "false") }
if (d<activationDistance) {
if ( ringsUsedInLastSession.length == 0 || r != ringsUsedInLastSession.at(-1).ring || (Date.now() - ringsUsedInLastSession.at(-1).timestamp) > refractoryPeriodRing){
console.log('+++ safe to re-run')
ringsUsedInLastSession.push( { ring: r, target: el, timestamp: Date.now() } )
executeRing( r, el )
// tested just a bit, seems fine
// append as clone of r to rings_stack done in endRingCheck()
} else {
console.log('!!! ignored, too fast and same ring')
}
}
})
}, 100)
}
function executeRing( r, el ){
// already used... should try another visual property
// r.setAttribute("opacity", .5)
r.setAttribute("wireframe", "true")
// ding sound
audio.play()
// restart every time
r.emit('startAnimation', null, false)
let elValue = el.getAttribute("value")
// to clean up otherwise always expect a value, e.g. on gltf-model
// ---------------------------------------- resolving -----------------------------------------------------
let resolvedElements = []
// consider threejs named objects e.g.
// myScene.getObjectByName("r_handMeshNode").parent.getObjectByName("wrist")
if ( elValue && elValue.length > 0 && elValue.startsWith(".") || elValue.startsWith("#") || elValue.startsWith("a-") ){
console.log('elValue', elValue)
getElementsCurrentTag() // implicitly called
if ( elValue.startsWith(".") ){
resolvedElements = Array.from( AFRAME.scenes[0].querySelectorAll(elValue) )
setFeedbackHUD('got class '+ elValue+' element numbers: '+ resolvedElements.length )
} else {
el = document.querySelector(elValue)
// assuming "a-..." is unique, e.g. a-sky, which isn't true for others, e.g. a-troika-text
}
}
let value = r.querySelector("a-troika-text").getAttribute("value")
// should instead "just" apply the jxr, done here just for testing
// probably need some refractory period too, otherwise stacking transformations that are probably not required
// ---------------------------------------- this should be based on value itself, i.e. another eval() -----------------------------------------------------
// see applyFunctionToSelection() ... which does not exist
// can be replaced by something more generic, there are already jxr shortcuts e.g. sa attributeName attributeValue
// could be done as filters are done, i.e. filters.map( f => f(element) )
if (resolvedElements.length == 0) resolvedElements.push(el)
resolvedElements.map( rEl => {
if (value.includes("scaleUp") ) rEl.setAttribute("scale", ".01 .02 .02")
if (value.includes("White") ) rEl.setAttribute("color", "white")
if (value.includes("Blue") ) rEl.setAttribute("color", "blue")
if (value.includes("Red") ) rEl.setAttribute("color", "red")
if (value.includes("Green") ) rEl.setAttribute("color", "green")
if (value.includes("setFontSize") ) rEl.setAttribute("font-size", Number(r.querySelector("a-troika-text").getAttribute("value").split("(")[1].split(")")[0])/10 )
if (value.includes("setColorFull") ) rEl.setAttribute("color", r.querySelector("a-troika-text").getAttribute("value").split('"')[1] )
// update from 3 values of R/G/B done at the ring dial level
})
if (value.includes("filterBibtex") ) {
// generate new class name based on filter value and UUID-ish
const newClassName = value.replace(/\W/g, '')+Date.now()
// attach class name to all resolvedElements matching that condition filter
resolvedElements
.filter( el => ! (el.data["bibtex-data"].year > 2000) ) // hardcoded for now
.map( el => el.classList.add( newClassName ) )
// make that new class name available for further processing
let newNoteFromNewClassName = addNewNote( '.'+newClassName )
newNoteFromNewClassName.setAttribute("onpicked", "startRingCheck()")
newNoteFromNewClassName.setAttribute("onreleased", "endRingCheck()" )
}
// could include resolving too, e.g .collidable doesn't get appended as-is but rather its resulting elements do
// could be better to do it later on, at the transformation level, going through ring
if (value.includes("addToRingSelection") ) {
// try to get the selection value and append to it, if none make new one
const selectionNodeId = "selectionnote"
let selectionNoteEl = document.getElementById( selectionNodeId )
if (!selectionNoteEl){
let newPos = r.getAttribute("position").clone()
newPos.y += .3
selectionNoteEl = addNewNote( elValue, AFRAME.utils.coordinates.stringify( newPos ), "0.1 0.1 0.1", selectionNodeId)
// text only, not 3D model, for this would try to get its ID instead
selectionNoteEl.setAttribute("onpicked", "startRingCheck()")
selectionNoteEl.setAttribute("onreleased", "endRingCheck()")
} else {
selectionNoteEl.setAttribute("value", selectionNoteEl.getAttribute("value") + "\n" + elValue )
}
}
if (value.includes("addRingFromCode") ) {
let newRing = addRing( elValue )
// if it is jxr should then rotate within
if ( elValue && elValue.startsWith("jxr "))
setTimeout( _ => {
let textEl = newRing.querySelector("a-troika-text")
textEl.setAttribute("curve-radius","1")
textEl.setAttribute("rotation","90 0 0")
textEl.setAttribute("position",".0 .1 .0")
})
}
// mixing up set of active rings
if (value.includes("addTag")) {
if (! window.currentTag ) {
setFeedbackHUD('tag missing, set one first')
} else {
addOrUpdateTagToElement( el )
}
}
if (value.includes("setAsActiveTagging")) {
// "type" check, not everything can be the right input
let modelSrc = el.getAttribute("gltf-model")
if (!modelSrc) {
setFeedbackHUD('tag not set, use a 3D model tag')
} else {
window.currentTag = {}
window.currentTag.model = el.getAttribute("gltf-model")
window.currentTag.scale = el.getAttribute("scale")
window.currentTag.rotation = el.getAttribute("rotation") // gets overwritten, should be offset applied also while moving it
// TODO should get the initial one, not the current rotation
// visual impact
setFeedbackHUD('tag set as', window.currentTag.model) // something is of, as it is empty
addOrUpdateTagToElement( r, "internal_tag" )
}
}
if (value.length==1) {
el.setAttribute("value", el.getAttribute("value")+value)
// character append
// could then make a keyboard this way...
// but 26 characters make for very large movements
// assuming a meta-ring but the layout itself can be totally different
// can try much smaller rings
// also need a refractory period
// add another ring to
// create new word (how?)
// split existing word on " "
AFRAME.scenes[0].emit('virtualkeypress', value )
}
// ---------------------------------------- -----------------------------------------------------
// should then
// clone ring then append it to rings_stack (for follow up macro)
// this should be permanent, i.e. JSON WebDAV save, and movable, i.e add target attribute (done)
// show/unhide/unfold the "following" rings based on context
// special rings ideas
// key aspect of UI/UX
// could also be limited to a specific ring
if (!ringAdded){
// here we are considering a sequential "positive" flow
// but could also be negative, a la ! or conditional e.g. && or ||
// so rings could have a type or return value equivalent
// if used as contextual menu, could reset/hide at endRingCheck()
ringAdded = true
/*
// need refractory period or maximum amount of times added
let newRing = addRing("jxr console.log('Red')", r.getAttribute("position") )
newRing.setAttribute("color", "red")
setTimeout( _ => newRing.object3D.position.z += .3, 100 )
// offset from current ring position
*/
// this itself should enable the creation of a 4th ring, etc
addContextualRings( r )
}
}
function layoutsSwitch( elements=[] ){
// elements = "abcdefghijklmnopqrstuvwxyz ,.{}()[]".split('').reverse()
elements = Array.from( document.querySelectorAll( '.kbd_key' ) )
// ignored parameter for testing, should be an array
let hOffset = 1
let dOffset = -1.3
let layouts = {}
layouts.horizontal = []
layouts.vertical = []
layouts.spiral = []
layouts.circle = []
let layoutNames = [ "horizontal", "vertical", "spiral", "circle" ]
//layoutNames = [ "horizontal" ]
// layoutNames = [ "vertical" ]
elements.map( (el,i) => {
layouts.horizontal.push( ""+ i/10+ " 1 -1" )
layouts.vertical.push( "0 "+ i/10+ " -1" )
theta = i*360/elements.length
x = hOffset * Math.cos(theta*Math.PI/180)
y = hOffset * Math.sin(theta*Math.PI/180)
z = dOffset
layouts.circle.push( "" + x + " " + y + " " + z )
x = i/10 * hOffset * Math.cos(theta*Math.PI/180)
y = i/10 * hOffset * Math.sin(theta*Math.PI/180)
z = i/10 * dOffset
layouts.spiral.push( "" + x + " " + y + " " + z )
/*
layoutNames.map( ln =>
el.setAttribute("animation__"+ln,
"property: position; to: "
+ layouts[ln].at(-1)
+ "; dur: 1000; startEvents:startAnimation;"
)
)
*/
//el.setAttribute("animation__horizontal", "property: position; to: " + layouts["horizontal"].at(-1) + "; dur: 1000; startEvents:startAnimation; autoplay:false;")
// el.setAttribute("animation__vertical", "property: position; to: " + layouts["vertical"].at(-1) + "; dur: 1000; startEvents:startAnimation; autoplay:false;")
})
return layouts
}
// consider passing another parameter with the set of code (and optional annotation) to populate that menu
/* e.g.
addContextualRings( r, [
'jxr applyFunctionToSelection("changeColorToBlue")',
'jxr applyFunctionToSelection("changeColorToWhite")',
'jxr applyFunctionToSelection("changeColorToRed")',
'jxr applyFunctionToSelection("changeColorToGreen")',
])
*/
function addContextualRings( r ){
active_rings.setAttribute("visible", "true") // might be overwhelming in other contexts, e.g. ring tags
// would be better to have its own parent, unrelated to active_rings
// would also allow to show/hide entire set
let hOffset = .2 // horizontal offset
let dOffset = .3 // depth offset, i.e. closer to the person assuming they are facing the z axis
let contextualRings = []
let code = [
'jxr applyFunctionToSelection("changeColorToBlue")',
'jxr applyFunctionToSelection("changeColorToWhite")',
'jxr applyFunctionToSelection("changeColorToRed")',
'jxr applyFunctionToSelection("changeColorToGreen")',
]
let spiral = true
spiral = false
if (spiral) code = "abcdefghijklmnopqrstuvwxyz ,.{}()[]".split('').reverse()
if (spiral) hOffset = 2.2 // for spiral testing
let layoutParts = []
slices = code.length;
// consider multiple loops
// advantage of spiral, namely dense without overlap
// and predictable, linear despite being in 2D
// repeat in N loops where each new loop start its radius beyond previous one
for (i=0;i<slices; i++) {
theta = i*360/slices
x = hOffset * Math.cos(theta*Math.PI/180)
y = hOffset * Math.sin(theta*Math.PI/180)
z = dOffset
if (spiral) x = i/10 * hOffset * Math.cos(theta*Math.PI/180)
if (spiral) y = i/10 * hOffset * Math.sin(theta*Math.PI/180)
if (spiral) z = i/10 * dOffset
layoutParts.push( new THREE.Vector3(x, y, z ) )
}
console.log( layoutParts )
code.map( (c,i) =>
contextualRings.push( addRing(c, r.getAttribute("position").clone().add( layoutParts[i]) ) )
)
contextualRings.map( cr => cr.classList.add( "contextual_ring" ) )
if (spiral) contextualRings.map( cr => cr.setAttribute("color", "purple") ) // spiral testing
return contextualRings
}
function removeContextualRings(){
Array.from( document.querySelectorAll(".contextual_ring") ).map( el => el.remove())
}
function addOrUpdateTagToElement( el, tagValue = "tagged" ){
console.log('tagging with', window.currentTag.model )
//let hasTag = el.querySelector("a-gltf-model") // querySelector on [tag] doesnt work
let hasTag = el.querySelector("[tag='"+tagValue+"'") // querySelector on [tag] doesnt work
let tagEl
if (!hasTag) {
console.log('no tag found on', el, 'adding one' )
tagEl = document.createElement("a-gltf-model")
el.appendChild( tagEl )
tagEl.setAttribute("position", ".2 .2 0") // trying to be in the top right corner... but totally depends on the volume of the element
tagEl.setAttribute("src", window.currentTag.model)
// parenting surely messes up the scale!
// tagEl.setAttribute("scale", window.currentTag.scale)
//console.log('scale set', window.currentTag.scale)
//console.log('scale set', AFRAME.utils.coordinates.stringify( window.currentTag.scale) )
tagEl.setAttribute("scale", ".001 .001 .001" ) // somehow rotation works but not scale..?
// tagEl.setAttribute("rotation", window.currentTag.rotation)
tagEl.setAttribute("tag", tagValue)
} else {
console.log('tag found on', el, 'replacing it' )
el.querySelector("a-gltf-model").setAttribute("src", window.currentTag.model)
// undefined on tagEl?
}
}
function addKeyboardRings(){
// addRing(code, position)
return "abcdefghijklmnopqrstuvwxyz ,.{}()[]".split('').reverse().map( (c,i) => {
let x = (i%3)/10 +.5
let y = 1+(i/3)/10
let r = addRing(c, x+" "+y+ " -.5", .03, .005, .005)
r.classList.add('kbd_key')
return r
} )
// could probably be done once they repositioned instead via a well known parent with id (or unique selector)
}
function addRing(code, position="0 1.5 -.7", radius=.1, radiusTubular=.01, codeVerticalOffset=.15){
let el = document.createElement("a-torus")
el.setAttribute("position", position)
el.setAttribute("segments-radial", "4")
el.setAttribute("segments-tubular", "12")
el.setAttribute("opacity", ".3")
el.setAttribute("radius", radius)
el.setAttribute("radius-tubular", radiusTubular)
let elCode = document.createElement("a-troika-text")
// consider binding an existing jxr command to an existing ring to have a similar behavior
// e.g. drop jxr on ring, bind them so that next time an element goes through, it's applied via binded ring
elCode.setAttribute("anchor", "left")
elCode.setAttribute("target", "")
elCode.setAttribute("value", code)
elCode.setAttribute("position", ".0 "+codeVerticalOffset+" .0")
elCode.setAttribute("scale", "0.1 0.1 0.1")
el.appendChild( elCode )
active_rings.appendChild( el )
return el
}
function endRingCheck(){
console.log('rings used', ringsUsedInLastSession)
// TODO test
if (ringsUsedInLastSession.length > 1){
console.log('more than 1 ring used, preparing stack', ringsUsedInLastSession.length)
ringsUsedInLastSession.map( currentRing => {
let r = currentRing.ring
let value = r.querySelector("a-troika-text").getAttribute("value")
if (value.length>1) { // ignoring virtual keypresses
console.log('cloned ', r, ' on stack', rings_stack )
let clonedRing = r.cloneNode(true)
rings_stack.appendChild( clonedRing )
// they are really combined though, they should have a single parent entity with a target on it
// and a way to tag or name
// could be another ring
}
})
}
// keeping visually track of what has been executed so far
// ringsUsedInLastSession.map( r => r.ring.setAttribute("opacity", 1) )
// already used... should try another visual property
ringsUsedInLastSession.map( r => r.ring.setAttribute("wireframe", "false") )
ringsUsedInLastSession = [] // ending the session
// if used as contextual menu, reset/hide here
removeContextualRings()
ringAdded = false
clearInterval(ringCheckInterval)
}
let ringDialUpdateInterval = null
function startRingDialUpdate(){
ringDialUpdateInterval = setInterval( _ => {
let el = selectedElements.at(-1)?.element
let z = el.getAttribute('rotation').z.toFixed(2)
// consider target range then normalize over it
// consider also acceptable input range, e.g. -30deg to +30deg, not 360 as it's ergonomically impossible
let value = el.querySelector('a-troika-text').getAttribute('value')
el.querySelector('a-troika-text').setAttribute('value', value.split('(')[0]+'('+z+')')
if ( el == ring_dial_color_r || el == ring_dial_color_g || el == ring_dial_color_b ){
let channels = ['r', 'g', 'b']
let colors = []
channels.map( c => {
colors.push( Math.round( Number( document.querySelector("#ring_dial_color_"+c).querySelector("a-troika-text").getAttribute("value").split("(")[1].split(")")[0] ) ).toString(16) )
if ( (colors.at(-1).includes("-")) || (colors.at(-1).length < 2)) colors[colors.length-1] = "00"
})
// TODO seems there is a problem in here,
let jxrValue = ring_dial_color.querySelector("a-troika-text").getAttribute("value").split("(")[0]
ring_dial_color.querySelector("a-troika-text").setAttribute("value", jxrValue+'("'+'#'+colors.join("")+'")' )
ring_dial_color.querySelector("a-troika-text").setAttribute("color", '#'+colors.join("") )
// maybe to here...
ring_dial_color.setAttribute("color", '#'+colors.join("") )
}
}, 100)
}
function endRingDialUpdate(){
clearInterval(ringDialUpdateInterval)
let el = selectedElements.at(-1)?.element
if (el) el.object3D.rotation.z = 0
// ring rotation reset so that value is readible after
}
// -----=========== end of ring system =============--------------------------------------------------------------------------------------------
// --------------------------------------------------- skating specific ----------------------------------------------------
let positions = []
AFRAME.registerComponent('odometer',{
init: function () {
this.tick = AFRAME.utils.throttleTick(this.tick, 50, this);
},
tick: function () {
let el = this.el
let pos = document.getElementById('player').getAttribute('position').clone()
positions.push( pos )
if (positions.length > 20) {
let dist = pos.distanceTo( positions[positions.length-20] )
//console.log(dist) // could be done via setFeedbackHUD() instead
el.setAttribute('value', dist.toFixed(2))
}
}})
function prepareCloneAndReposition(){
window.startingElementValues = selectedElements.at(-1).element.cloneNode(true)
}
const classNameItemsToSave = "itemsclonedtosave"
function cloneAndReposition(){
let lastElement = selectedElements.at(-1).element
let cloned = lastElement.cloneNode(true)
cloned.classList.add( classNameItemsToSave )
AFRAME.scenes[0].appendChild( cloned )
lastElement = window.startingElementValues
// not tested deeply
}
const itemsfile = urlParams.get('itemsfile');
if ( itemsfile ) setTimeout( _ => loadItemsClonedViaWebDAV( itemsfile ) , 1000 )
// should do so only after scene has loaded
function loadItemsClonedViaWebDAV(filename){
fetch('../content/'+filename).then( r => r.json() ).then( r => {
// fetch('https://companion.benetou.fr/'+filename).then( r => r.json() ).then( r => {
// fails due to CORS...
r.map( e => {
let cloned = document.getElementById(e.templating_id).cloneNode(true)
cloned.classList.add( classNameItemsToSave )
cloned.setAttribute("position", e.position)
cloned.setAttribute("rotation", e.rotation)
AFRAME.scenes[0].appendChild( cloned )
})
})
}
function saveItemsClonedViaWebDAV(classtosave=classNameItemsToSave){
// should prevent from saving multiples times a second (due to pinching)
// let data = Array.from( document.querySelectorAll("."+classtosave) ).map( i => { return {nodeName:i.nodeName, attributes:i.attributes} } ) // somehow empty attributes... or rather plenty of values, e.g 10, but empty
let data = Array.from( document.querySelectorAll("."+classtosave) ).map( i => { return {templating_id:i.id, position:i.getAttribute("position"), rotation:i.getAttribute("rotation"), } } )
// somehow saves the templating element too rather than the first cloned element
// assuming other properties are important here because they can't be modified by the user
// from saveHighlights()
let filename = "gameitems_"+Date.now()+".json"
async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, JSON.stringify(data)); }
// note that this only single page saving, should instead consider highlightsBetweenPageChanges but after dedup
written = w(subdirWebDAV+usernamePrefix+filename)
if (written){
fetch('https://ntfy.benetou.fr/fileuploadtowebdav', { method: 'POST', body: 'added '+usernamePrefix+filename })
}
let url = '?itemsfile='+usernamePrefix+filename
setTimeout( _ => window.open(url, '_blank'), 1000 )
return usernamePrefix+filename
}
// --------------------------------------------------- end of skating specific ----------------------------------------------------
// --------------------------------------------------- in-XR JSON editing ----------------------------------------------------
function wordsToNotes( words ){
console.log(words)
words.split(' ').map( (w,i) => {
let el = addNewNote( w, "-.5 "+ (1+i/10) +" -.5" )
el.setAttribute("onreleased", "setFeedbackHUD( selectedElements.at(-1).element.getAttribute('value'))" )
})
}
// default test URL
function editJSON( url = 'https://companion.benetou.fr/demo_q1.json' ){
fetch( url ).then( res => res.json() ).then( res => {
keys = []
findKeys ( res )
let content = []
keys.map( (k,i) => { t = getValueByDottedKeys( res, k )
if (typeof t == "string" && t.length > 42) t = t.substring(0, 42)+"..."
let line = k + " = " + t.replaceAll('\n','')
content.push( line )
let y = 2-i/50
let el = addNewNote( line, "0 "+ y +" -1" )
let range = {}
range[0] = 0xffffff
range[k.length + " = ".length ] = 0x0099ff
// range[end] = 0xffffff
el.setAttribute("troika-text", {colorRanges: range})
// el.setAttribute("onreleased", "setFeedbackHUD( selectedElements.at(-1).element.getAttribute('value'))" )
// could add as selection JSON property to replace
// includes everything, should keep only the property name with path
// could have recursive precision edition depth
// i.e. edit selection or word in it, or character in word
el.setAttribute("onreleased", "wordsToNotes(selectedElements.at(-1).element.getAttribute('value'))" )
} )
})
var keys = []
const findKeys = (object, prevKey = '') => {
Object.keys(object).forEach((key) => {
const nestedKey = prevKey === '' ? key : `${prevKey}.${key}`
if (typeof object[key] !== 'object') return keys.push(nestedKey)
findKeys(object[key], nestedKey)
})
}
// from https://stackoverflow.com/a/65345406/1442164
const getValueByDottedKeys = (obj, strKey)=>{
let keys = strKey.split(".")
let value = obj[keys[0]];
for(let i=1;i<keys.length;i++){
value = value[keys[i]]
}
return value
}
// from https://stackoverflow.com/a/74738513/1442164
}
// --------------------------------------------------- end of in XR JSON editing ----------------------------------------------------
// --------------------------------------------------- volumetric frame ----------------------------------------------------
function puckFromVolumetricFrame(){
let lastPick = selectedElements.at(-1).element
// should remove from its current frame
// invert of vf.snappedOn.push( evt.detail.el )
Array.from(volumetricframes.children).map( vf => {
// should check if lastPick is including in vf.snappedOn then remove it
})
}
function endVolumetricFrameMoving(){
// should stop moving all associating volumetric frames
}
function startVolumetricFrameMoving(){
// should move all associating volumetric frames
}
// --------------------------------------------------- end of volumetric frames ----------------------------------------------------
async function getMostRecentFile(partialfilename=''){
const contents = await webdavClient.getDirectoryContents(subdirWebDAV);
// consider instead search https://github.com/perry-mitchell/webdav-client#search
return contents.filter(f => f.basename.includes(partialfilename))
.filter(f=>f.type=="file")
.sort( (a,b) => new Date(a.lastmod).getTime() < new Date(b.lastmod).getTime() ) // newest first
}
// once working could update addRecentAudioFiles() accordingly (even though takes last 4 for now)
</script>
<a-scene list-files-sorted xr-mode-ui="enabled: true; enterAREnabled: true; XRMode: xr;">
<!-- useraddednote-append-to="target:#manuscript" forcing flat mode -->
<a-entity id="rig">
<a-entity id="player" hud camera look-controls wasd-controls waistattach="target: .movebypinch" position="0 1.6 0"></a-entity>
<a-entity laser-controls="hand: left" raycaster="objects: .collidable; far: .5"></a-entity>
<a-entity oculus-touch-controls="hand: right"></a-entity>
<a-entity id="rightHand" pinchprimary wristattachprimary="target: #otherbox" hand-tracking-controls="hand: right;"></a-entity>
<a-entity id="leftHand" pinchsecondary wristattachsecondary="target: #box" hand-tracking-controls="hand: left;"></a-entity>
<a-entity logiteck-mx-ink-controls="hand: right"></a-entity>
</a-entity>
<a-sphere visible=true id=groundfor360 scale="2 .1 2" color="#ccc"></a-sphere>
<a-sphere segments-width=12 segments-height=12 pressable="" start-on-press="" id="box" radius="0.033" color="gray"></a-sphere>
<a-box pressable="" start-on-press-other="" id="otherbox" scale=".05 .05 .05" opacity=.3 wireframe=false color="white"></a-box>
<a-troika-text value="Knowledge Space" anchor="left" outline-width="5%" font="https://fabien.benetou.fr/pub/home/future_of_text_demo/content/ChakraPetch-Regular.ttf" position="-5.26197 6.54224 -1.81284"
scale="4 4 5" rotation="90 0 0" troika-text="outlineWidth: 0.01; strokeColor: #ffffff" material="flatShading: true; blending: additive; emissive: #c061cb"></a-troika-text>
<a-troika-text value="(based on SpaSca : Spatial Scaffolding)" anchor="left" outline-width="5%" font="https://fabien.benetou.fr/pub/home/future_of_text_demo/content/ChakraPetch-Regular.ttf" position="-5.26197 6.54224 -2.81284"
scale="2 2 2" rotation="90 0 0" troika-text="outlineWidth: 0.01; strokeColor: #ffffff" material="flatShading: true; blending: additive; emissive: #c061cb"></a-troika-text>
<a-sky id="environmentsky" class="hidableenvironment" hide-on-enter-ar color="darkgray"></a-sky>
<a-entity visible="false" hide-on-enter-ar="" id="environment" rotation="0 -90 0" position="0 .65 0" scale='' gltf-model="url(Apartment.glb)" class="hidableenvironment" ></a-entity>
<a-troika-text id=instructions anchor="left" target value="instructions : \n--right pinch to move\n--left pinch to execute" position="0 1.65 -2" scale="0.1 0.1 0.1"></a-troika-text>
<a-entity id=basiccommands position="0 0 0.3">
<a-troika-text anchor=left target id="locationreload" value="jxr location.reload()" rotation="0 90 0" position="-.5 .9 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target id="toggleAnchors" value="jxr toggleAnchors()" rotation="0 90 0" position="-.5 1.25 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target id="bumptableup" value="jxr virtualdesktopplanemovable.object3D.position.y+=.1" annotation="content:bump table" rotation="90 90 0" position="-.5 1.15 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target id="bumptabledown" value="jxr virtualdesktopplanemovable.object3D.position.y-=.1" annotation="content:bump table down" rotation="90 90 0" position="-.5 1.0 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target id="bumptabledown" value="jxr loadOnPanels()" annotation="content:load on panels" rotation="90 90 0" position="-.5 1.05 0" scale="0.1 0.1 0.1"></a-troika-text>
</a-entity>
<a-console target position="-1 1.3 0" rotation="-45 90 0" font-size="34" height="0.5" skip-intro="true"></a-console>
<a-box visible=false id="virtualdesktopplane" wireframe=true position="0 .9 -.5" height=.01 depth=.4></a-box>
<a-box visible=false id="virtualdesktopplanemovable" target setupable position="0 1.4 -.5" color="yellow" width=1 height=.01 depth=.02></a-box>
<!--
<a-box id="virtualdesktopplanemovablered" target position="0 1.6 -.3" color="red" width=.1 height=.01 depth=.1></a-box>
<a-box id="virtualdesktopplanemovablegreen" target position="-.5 1.6 -.3" color="green" width=.1 height=.01 depth=.1></a-box>
<a-box id="virtualdesktopplanemovableblue" target position=".5 1.6 -.3" color="blue" width=.1 height=.01 depth=.1></a-box>
<a-cylinder id="cylinderorange" target position=".25 1.7 -.3" color="orange" rotation="45 0 45" height=.1 radius=.01></a-cylinder>
<a-cylinder id="cylinderpurple" target position="-.25 1.7 -.3" color="purple" rotation="45 0 45" height=.1 radius=.01></a-cylinder>
-->
<a-entity visible=true id=middlecommands>
<a-troika-text anchor=left target value="jxr deskpanels.setAttribute('visible', 'true')" rotation="45 0 0" annotation="content:show panels" position="-.1 1.20 -.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value="jxr deskpanels.setAttribute('visible', 'false')" rotation="45 0 0" annotation="content:hide panels" position="-.1 1.10 -.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value="jxr environment.setAttribute('visible', 'true')" rotation="45 0 0" annotation="content:show background" position="-.5 1.20 -.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value="jxr environment.setAttribute('visible', 'false')" rotation="45 0 0" annotation="content:hide background" position="-.5 1.10 -.4" scale="0.1 0.1 0.1"></a-troika-text>
</a-entity>
<a-entity visible=false id=selectioncommands>
<a-troika-text anchor=left target value="jxr bumpSelection()" rotation="45 0 0" annotation="content:push selection" position=".5 1.20 -.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value="jxr bumpSelection(true)" rotation="45 0 0" annotation="content:pull selection" position=".5 1.15 -.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value="jxr growSelection()" rotation="45 0 0" annotation="content:grow selection" position=".5 1.10 -.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value="jxr growSelection(true)" rotation="45 0 0" annotation="content:shrink selection" position=".5 1.05 -.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value="jxr extractSelection()" rotation="45 0 0" annotation="content:clone selection" position=".5 1.00 -.4" scale="0.1 0.1 0.1"></a-troika-text>
</a-entity>
<a-entity visible=false target onpicked="startVolumetricFrameMoving()" onreleased="endVolumetricFrameMoving()" id=volumetricframes>
<a-box id=vf1 class=panel position="0 1.3 -1" rotation="0 0 0" wireframe=true depth=.2>
<a-troika-text anchor=left target value='volumetric frame 1' position="-.5 .55 .1" scale="0.2 0.2 0.2"></a-troika-text>
</a-box>
<a-box id=vf2 class=panel position="1 1.3 0" rotation="0 -90 0" wireframe=true depth=.2>
<a-troika-text anchor=left target value='volumetric frame 2' position="-.5 .55 .1" scale="0.2 0.2 0.2"></a-troika-text>
</a-box>
<a-box id=vf3 class=panel position="-1 1.3 0" rotation="0 90 0" wireframe=true depth=.2>
<a-troika-text anchor=left target value='volumetric frame 3' position="-.5 .55 .1" scale="0.2 0.2 0.2"></a-troika-text>
</a-box>
<a-box id=vf4 class=panel position="0 1.3 1" rotation="0 180 0" wireframe=true depth=.2>
<a-troika-text anchor=left target value='volumetric frame 4' position="-.5 .55 .1" scale="0.2 0.2 0.2"></a-troika-text>
</a-box>
</a-entity>
<a-entity visible=false id=deskpanels>
<a-box class=panel position="1 1.3 -1" rotation="45 -45 0" wireframe=true height=.01 depth=.4></a-box>
<a-box class=panel position="1 1.3 0" rotation="45 -90 0" wireframe=true height=.01 depth=.4></a-box>
<a-box class=panel position="1 1.3 1" rotation="45 -135 0" wireframe=true height=.01 depth=.4></a-box>
<a-box class=panel position="0 1.3 1.5" rotation="45 180 0" wireframe=true height=.01 depth=.4></a-box>
<a-box class=panel position="-1 1.3 1" rotation="-45 -45 0" wireframe=true height=.01 depth=.4></a-box>
<a-box class=panel position="-1 1.3 0" rotation="45 90 0" wireframe=true height=.01 depth=.4></a-box>
<a-box class=panel position="-1 1.3 -1" rotation="45 45 0" wireframe=true height=.01 depth=.4></a-box>
</a-entity>
<a-entity id=topsidecommands>
<a-troika-text anchor=left target value='jxr setTimeout( _ => saveScreenshot("screenshot_"+Date.now()+".jpg") , 1000 )' rotation="0 90 0" position="-.7 1.50 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr startViewCheck()' rotation="0 90 0" position="-.7 1.70 .2" scale="0.1 0.1 0.1"></a-troika-text>
</a-entity>
<a-entity visible=false id=highlightcommands>
<a-troika-text anchor=left target color="black" value='jxr highlightColor="black"' rotation="0 -90 0" position=".7 1.70 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target color="yellow" value='jxr highlightColor="yellow"' rotation="0 -90 0" position=".7 1.60 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target color="orange" value='jxr highlightColor="orange"' rotation="0 -90 0" position=".7 1.50 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target color="aqua" value='jxr highlightColor="aqua"' rotation="0 -90 0" position=".7 1.40 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target color="lime" value='jxr highlightColor="lime"' rotation="0 -90 0" position=".7 1.30 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr console.log( getHighlights() )' rotation="0 -90 0" position=".7 1.10 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr saveHighlights()' rotation="0 -90 0" position=".7 1.00 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr nextPageForXMLText()' rotation="0 -90 0" position=".7 0.80 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr previousPageForXMLText()' rotation="0 -90 0" position=".7 0.70 .2" scale="0.1 0.1 0.1"></a-troika-text>
<!--
<a-troika-text anchor=left target value='jxr nextPageForHighlight()' rotation="0 -90 0" position=".7 0.80 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr previousPageForHighlight()' rotation="0 -90 0" position=".7 0.70 .2" scale="0.1 0.1 0.1"></a-troika-text>
-->
</a-entity>
<a-entity visible=false id=recordercommands>
<a-troika-text anchor=left target value="jxr setupRecorder()" rotation="0 90 0" position="-.7 1.30 .2" scale="0.1 0.1 0.1">
<a-entity scale=".2 .2 .2" class="icon microphone" position="-.5 0 0">
<a-sphere color=black segments-width=8 segments-height=8 position="0 1 0"></a-sphere>
<a-box color=gray scale="" position=""></a-box>
<a-box color=white scale=".4 2 .4" position="0 -1 0"></a-box>
</a-entity>
</a-troika-text>
<a-troika-text anchor=left target value="jxr mediaRecorder.start(); microphone_recording_indicator.emit('start')" rotation="0 90 0" position="-.7 1.20 .2" scale="0.1 0.1 0.1">
<a-entity scale=".2 .2 .2" class="icon recordmicrophone" position="-.5 0 0">
<a-sphere id=microphone_recording_indicator
animation="property: opacity; from: 1; to: 0; loop: true; dir: alternate; easing: easeInExpo; autoplay: false; startEvents:start; pauseEvents:pause;"
color=red scale=".7 .7 .1" segments-width=8 segments-height=8 position="0 1 1"></a-sphere>
<a-sphere color=black segments-width=8 segments-height=8 position="0 1 0"></a-sphere>
<a-box color=gray scale="" position=""></a-box>
<a-box color=white scale=".4 2 .4" position="0 -1 0"></a-box>
</a-entity>
</a-troika-text>
<a-troika-text anchor=left target value="jxr mediaRecorder.stop(); microphone_recording_indicator.emit('pause')" rotation="0 90 0" position="-.7 1.10 .2" scale="0.1 0.1 0.1">
<a-entity scale=".2 .2 .2" class="icon nonmicrophone" position="-.5 0 0">
<a-sphere color=black segments-width=8 segments-height=8 position="0 1 0"></a-sphere>
<a-box color=gray scale="" position=""></a-box>
<a-box color=white scale=".4 2 .4" position="0 -1 0"></a-box>
<a-box color=red scale=".4 3 .4" rotation="0 0 45" position="0 0 .5"></a-box>
<a-box color=red scale=".4 3 .4" rotation="0 0 -45" position="0 0 .5"></a-box>
</a-entity>
</a-troika-text>
<a-troika-text anchor=left target value="jxr saveAudioFile(latest_audio_id+'.ogg')" rotation="0 90 0" position="-.7 1.00 .2" scale="0.1 0.1 0.1">
<a-entity scale=".2 .2 .2" class="icon audiofile" position="-.5 0 0">
<a-box color=white scale="2 2 .1" position="0 0 0"></a-box>
<a-cone color=gray radius-top=".1" rotation="0 0 -90" scale="1 1 .4" position="0 0 0"></a-cone>
</a-entity>
</a-troika-text>
<a-entity id=audiowidgets position="-.7 .90 .2"></a-entity>
</a-entity>
<a-entity visible=false position="-.4 0 -.5" target user-visibility="username:thicknesstesteruser" id="thicknesscommands">
<a-troika-text anchor=left target value='jxr adjustPinchThicknessLines(1)' position=".7 1.40 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr adjustPinchThicknessLines(2)' position=".7 1.30 .2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr adjustPinchThicknessLines(3)' position=".7 1.20 .2" scale="0.1 0.1 0.1"></a-troika-text>
</a-entity>
<a-entity id="roundedpageborders" target visible=false>
<a-entity position="0.5 1.4 -0.51">
<a-image position="0 0 .001" scale="1 1 .1" class="drawable" raycaster-listen src="#transparent" ></a-image>
<a-box id='pagebackgroundxml' scale=".0015 .0015 .001" width=612 height=792></a-box>
<a-torus position=".39 .5 0" color="#43A367" arc="90" radius=".1" radius-tubular="0.01"></a-torus>
<a-torus position="-.39 .5 0" rotation="0 0 90" color="#43A367" arc="90" radius=".1" radius-tubular="0.01"></a-torus>
<a-torus position=".39 -.5 0" rotation="180 0 0" color="#43A367" arc="90" radius=".1" radius-tubular="0.01"></a-torus>
<a-torus position="-.39 -.5 0" rotation="180 0 90" color="#43A367" arc="90" radius=".1" radius-tubular="0.01"></a-torus>
<a-cylinder position="-.49 0 0" color="#43A367" radius=".02" ></a-cylinder>
<a-cylinder position=".49 0 0" color="#43A367" radius=".02" ></a-cylinder>
<a-cylinder position="0 .6 0" height=.8 rotation="0 0 90" color="#43A367" radius=".02" ></a-cylinder>
<a-cylinder position="0 -.6 0" height=.8 rotation="0 0 90" color="#43A367" radius=".02" ></a-cylinder>
</a-entity>
</a-entity>
<a-entity visible=false position="-.4 1 -.5" target user-visibility="username:poweruser" id=highlighterA>
<a-entity rotation="-30 0 30">
<a-entity raycaster="direction:0 1 0; objects: .drawable; showLine: true; far: .03; lineColor: purple; lineOpacity: 0.5"></a-entity>
<a-cone color=gray radius-top="0" radius-bottom=".01" height=.05></a-cone>
<a-cylinder position="0 -.07 0" height=.1 color=gray radius=".01" ></a-cylinder>
</a-entity>
</a-entity>
<a-entity visible=false position="-.2 1 -.5" target user-visibility="username:poweruser" id=highlighterB>
<a-entity rotation="-30 0 30">
<a-entity raycaster="direction:0 1 0; objects: .drawable; showLine: true; far: .03; lineColor: #0cc; lineOpacity: 0.5"></a-entity>
<a-cone color=gray radius-top="0" radius-bottom=".01" height=.05></a-cone>
<a-cylinder position="0 -.07 0" height=.1 color=gray radius=".01" ></a-cylinder>
</a-entity>
</a-entity>
<a-box id=manuscript position="-.3 1.5 -.5" target onreleased="snapClosest()" scale=".21 .29 .01">
<a-troika-text anchor=left value='Manuscript...' color=black position="-0.4 .4 .51" scale="0.1 0.1 0.1"></a-troika-text>
</a-box>
<!-- cube grids, low opacity, no color, etc -->
<a-entity visible=false user-visibility="username:backgroundexploration" id=backgroundexploration>
<a-sphere wireframe=true animation="property: rotation.x; from: 0; to: 360; loop: true; dur: 100000;" color=red segments-width=8 radius=250 segments-height=8 ></a-sphere>
<a-sphere wireframe=true animation="property: rotation.y; from: 0; to: 360; loop: true; dur: 100000;" color=green segments-width=8 radius=250 segments-height=8 ></a-sphere>
<a-sphere wireframe=true animation="property: rotation.z; from: 0; to: 360; loop: true; dur: 100000;" color=blue segments-width=8 radius=250 segments-height=8 ></a-sphere>
</a-entity>
<a-entity visible=false user-visibility="username:backgroundexplorationlowopacity" id=backgroundexploration>
<a-sphere wireframe=true animation="property: rotation.x; from: 0; to: 360; loop: true; dur: 100000;" opacity=.1 color=red segments-width=8 radius=250 segments-height=8 ></a-sphere>
<a-sphere wireframe=true animation="property: rotation.y; from: 0; to: 360; loop: true; dur: 100000;" opacity=.1 color=green segments-width=8 radius=250 segments-height=8 ></a-sphere>
<a-sphere wireframe=true animation="property: rotation.z; from: 0; to: 360; loop: true; dur: 100000;" opacity=.1 color=blue segments-width=8 radius=250 segments-height=8 ></a-sphere>
</a-entity>
<a-entity visible=false user-visibility="username:backgroundexplorationlowwhitestatic" id=backgroundexploration>
<a-sphere wireframe=true segments-width=8 radius=250 segments-height=8 ></a-sphere>
</a-entity>
<a-entity visible=false user-visibility="username:backgroundexplorationlowwhite" id=backgroundexploration>
<a-sphere wireframe=true animation="property: rotation.x; from: 0; to: 360; loop: true; dur: 100000;" segments-width=8 radius=250 segments-height=8 ></a-sphere>
<a-sphere wireframe=true animation="property: rotation.y; from: 0; to: 360; loop: true; dur: 100000;" segments-width=8 radius=250 segments-height=8 ></a-sphere>
<a-sphere wireframe=true animation="property: rotation.z; from: 0; to: 360; loop: true; dur: 100000;" segments-width=8 radius=250 segments-height=8 ></a-sphere>
</a-entity>
<a-entity opacity=.1 gridplace visible=false user-visibility="username:backgroundexplorationlowwhitegrids" id=backgroundexploration>
</a-entity>
<a-troika-text visible=false user-visibility="username:demoqueueq1" id=demoqueueq1 anchor=left target value='jxr nextDemo()' position=".7 1.30 .5" rotation="0 180 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-entity visible=false user-visibility="username:spreadsheetcolumns" id=spreadsheetcolumns>
<a-troika-text anchor=left target value='jxr applyFunctionToColumn("changeColorToBlue")' position=".3 1.30 -.5" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='some text' position=".3 1.20 -.5" scale="0.1 0.1 0.1"></a-troika-text>
<a-box width=.1 depth=.1 target wireframe=true position="0 1 -.5">
<a-box width=.1 depth=.1 height=.1 position="0 .55 0" wireframe=true></a-box>
</a-box>
</a-entity>
<a-entity visible=false id=rings>
<!-- consider applyNextFilterInteraction() -->
<!-- sequence of rings, namely no "absolute" list, in addition to apply a function to the content currently selected, it opens up the "next" rings and stacks itself to the applied rings -->
<a-troika-text anchor=left target value= "jxr addKeyboardRings()" position="0 1.10 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value= 'jxr Array.from( active_rings.querySelectorAll("a-torus") ).map( el => { el.setAttribute("radius", ".01"); el.setAttribute("radius-tubular", ".001") })' position="0.4 1.20 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value= "jxr trailingLineEnabled = !trailingLineEnabled" position="0 1.10 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value= "jxr sceneIdsToElementForRings()" position="0 1.20 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='example text' position="0 1.30 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='a-sky' position="0 1.40 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='.collidable' position="0 1.60 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='jxr applyFunctionToSelection("changeColorToBlue")' position="0 1.50 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.3 1.35 -.7" scale='.01 .01 .01' gltf-model="url(Apartment.glb)"></a-entity>
<a-entity id=active_rings>
<a-torus color="blue" position="0 1 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr applyFunctionToSelection("changeColorToBlue")' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus color="white" position="0 1 -.3" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr applyFunctionToSelection("changeColorToWhite")' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus color="white" position=".5 1 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr scaleUp()' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus color="white" position="-.5 1 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr addRingFromCode()' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus color="white" position="-.5 1.5 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr addToRingSelection()' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
</a-entity>
<!-- see history of transformations via voice, and cancelling it, older PoC -->
<a-entity id=rings_stack target position="-1 1 -1.5">
<a-troika-text anchor=left target value='previous transformations' position="-.1 .3 .1" scale="0.1 0.1 0.1"></a-troika-text>
<a-torus wireframe=true color="#43A367" position="0 0 -.1" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text curve-radius=1 rotation="90 0 0" anchor=left target value='jxr applyFunctionToSelection("changeColorToRed")' position=".0 .1 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus wireframe=true color="#43A367" position="0 0 0" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr applyFunctionToSelection("changeColorToBlue")' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
</a-entity>
<a-tube path="1 0 5, 5 0 5, -5 0 5" radius=0.01 material="color: blue"></a-tube>
</a-entity>
<a-entity visible=false user-visibility="username:skating_rings" id=skating_rings>
<a-troika-text anchor=left target value='jxr Array.from(skating_rings.querySelectorAll("a-torus")).map( r => r.object3D.position.y = 1.2 )' position="0 1.5 -1.0" scale="0.1 0.1 0.1"></a-troika-text>
<a-torus level=1 color="blue" position="0 1.6 -.5" segments-radial=4 segments-tubular=12 radius=".1" radius-tubular="0.01"></a-torus>
<a-torus level=1 color="blue" position="0 1.6 -.8" animation__move="property: position; from: 0 1.6 -0.8; to: -0.5 1.6 -0.8; loop: true; dir:alternate; dur: 1000;" segments-radial=4 segments-tubular=12 radius=".1" radius-tubular="0.01"></a-torus>
<a-torus level=1 color="blue" position="0.5 1.6 -1.1" rotation="0 -30 0" segments-radial=4 segments-tubular=12 radius=".1" radius-tubular="0.01"></a-torus>
<a-torus level=1 color="green" position="1.0 1.6 -1.1" rotation="0 -80 0" segments-radial=4 segments-tubular=12 radius=".1" radius-tubular="0.01"></a-torus>
<a-torus level=2 color="blue" position="0 1.6 -.5" segments-radial=4 segments-tubular=12 radius=".1" radius-tubular="0.01"></a-torus>
<a-torus level=2 color="blue" position="-0.2 1.6 -.8" segments-radial=4 segments-tubular=12 radius=".1" radius-tubular="0.01"></a-torus>
<a-torus level=2 color="blue" position="-0.5 1.6 -1.1" rotation="0 -30 0" segments-radial=4 segments-tubular=12 radius=".1" radius-tubular="0.01"></a-torus>
<a-torus level=2 color="green" position="-1.0 1.2 -1.1" rotation="0 -80 0" segments-radial=4 segments-tubular=12 radius=".1" radius-tubular="0.01"></a-torus>
</a-entity>
<a-entity visible=false user-visibility="username:icon_tags" id=icon_tags>
<!-- one ring could be "set tag" then one would take one of those and it would become the tag to be set
then one would take any knowledge object, e.g. text or code snippet, or even another 3D model, then pull through that new ring, and get that knowledge object tagged
pinching on the original tag would then visually highlight were the objects matching that tag are
(potentially then bringing them to the clipboard, letting the user do so, nothing implicit there)
-->
<a-troika-text anchor=left target value='Icon for tagging' position="0.3 1.5 -0.7" scale="0.1 0.1 0.1"></a-troika-text>
<a-entity position="0.3 1.35 -.7" >
<!-- could become a dedicated JSON file, that is a filter proper, a la <script src="filters/json_ref_manual.js"></script> and which can be edited, a la editor.html or even a WedDAV directory reader to remain dynamic without server side code -->
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.1 0.0 0.0" rotation="0 90 0" scale='.0001 .0001 .0001' gltf-model="url(https://fabien.benetou.fr/pub/home/future_of_text_demo/content/IconTags/Star.glb)"></a-entity>
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.2 0.0 0.0" rotation="0 0 0" scale='.0001 .0001 .0001' gltf-model="url(https://fabien.benetou.fr/pub/home/future_of_text_demo/content/IconTags/Heart.glb)"></a-entity>
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.3 0.0 0.0" rotation="0 -90 0" scale='.05 .05 .05' gltf-model="url(https://fabien.benetou.fr/pub/home/future_of_text_demo/content/IconTags/Clipboard.glb)"></a-entity>
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.4 0.0 0.0" rotation="0 0 0" scale='.2 .2 .2' gltf-model="url(https://fabien.benetou.fr/pub/home/future_of_text_demo/content/IconTags/FileFolder.glb)"></a-entity>
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.5 0.0 0.0" rotation="0 -90 0" scale='.1 .1 .1' gltf-model="url(https://fabien.benetou.fr/pub/home/future_of_text_demo/content/IconTags/SimplePadlock.glb)"></a-entity>
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.6 0.0 0.0" rotation="0 180 0" scale='.02 .02 .02' gltf-model="url(https://fabien.benetou.fr/pub/home/future_of_text_demo/content/IconTags/RecycleBin.glb)"></a-entity>
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.7 0.0 0.0" rotation="0 0 0" scale='.09 .09 .09' gltf-model="url(https://fabien.benetou.fr/pub/home/future_of_text_demo/content/IconTags/QuestionBlock.glb)"></a-entity>
<a-entity onpicked="startRingCheck()" onreleased="endRingCheck()" target value="" position="0.8 0.0 0.0" rotation="0 0 0" scale='.01 .01 .01' gltf-model="url(https://fabien.benetou.fr/pub/home/future_of_text_demo/content/IconTags/ExclamationBlock.glb)"></a-entity>
</a-entity>
<a-troika-text id=colortesting2 onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='example text to tag' position="-0.4 1.30 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text id=colortesting onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='another example of text to tag' position="-0.4 1.20 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='.selected_via_tag' position="-0.4 1.60 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-entity id=tagging_active_rings>
<a-torus color="#43A367" position="0 1.4 -.8" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr addTag()' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus color="#43A367" position="0 1 -.8" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr setAsActiveTagging()' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus color="blue" position="0 1.8 -.8" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr applyFunctionToSelection("changeColorToBlue")' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
</a-entity>
</a-entity>
<a-entity visible=false user-visibility="username:ring_dial" id=ring_dial>
<a-troika-text onpicked="startRingCheck()" onreleased="endRingCheck()" anchor=left target value='example text' position="0 1.30 -.8" scale="0.1 0.1 0.1"></a-troika-text>
<a-torus id=ring_dial_test target onpicked="startRingDialUpdate()" onreleased="endRingDialUpdate()" color="purple" position="0 1.4 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr setFontSize(1)' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus id=ring_dial_color_r target onpicked="startRingDialUpdate()" onreleased="endRingDialUpdate()" position="-0.4 1.8 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr setColorR(1)' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus id=ring_dial_color_g target onpicked="startRingDialUpdate()" onreleased="endRingDialUpdate()" position="0 1.8 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr setColorG(1)' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus id=ring_dial_color_b target onpicked="startRingDialUpdate()" onreleased="endRingDialUpdate()" position="0.4 1.8 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr setColorB(1)' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
<a-torus id=ring_dial_color position="0.2 1.6 -.5" segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr setColorFull("#000000")' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
</a-torus>
</a-entity>
<a-entity visible=false user-visibility="username:q2_lense">
<a-box id=lense_test_box class=lensable position="0 1 -4"></a-box>
<a-sphere id=lense_test_sphere class=lensable position="-3 1 -4"></a-sphere>
<a-box position="0.2 1.6 -.5" opacity=.3 id=lense_handle
target
onpicked='applyToClass("lensable", el => el.setAttribute("raycaster-targets-wireframe-lense", true) )'
onreleased='applyToClass("lensable", el => el.removeAttribute("raycaster-targets-wireframe-lense") )'
width=.02 depth=.02 height=.1 >
<a-torus position="0 0.15 0" id=lense
segments-radial=4 segments-tubular=12 opacity=.3 radius=".1" radius-tubular="0.01">
<a-troika-text anchor=left target value='jxr showWireframe("#000000")' position=".0 .15 .0" scale="0.1 0.1 0.1"></a-troika-text>
<a-entity raycaster="direction:0 0 -1; objects: .lensable; far: .1; lineOpacity: 0.5"></a-entity>
<a-entity position=".05 .05 0" raycaster="direction:0 0 -1; objects: .lensable; far: .1; lineOpacity: 0.5"></a-entity>
<a-entity position="-.05 .05 0" raycaster="direction:0 0 -1; objects: .lensable; far: .1; lineOpacity: 0.5"></a-entity>
<a-entity position=".05 -.05 0" raycaster="direction:0 0 -1; objects: .lensable; far: .1; lineOpacity: 0.5"></a-entity>
<a-entity position="-.05 -.05 0" raycaster="direction:0 0 -1; objects: .lensable; far: .1; lineOpacity: 0.5"></a-entity>
</a-torus>
</a-box>
<!-- unfortunately off center target is problematic due to now onpicked/onreleased... maybe? -->
</a-entity>
<a-entity visible=false user-visibility="username:temple_test">
<a-gltf-model src="Complex_Shelf.glb" target="" rotation="0 90 0" gltf-model="Complex_Shelf.glb" class="collidable" scale="2 2 2" position="0 1.2 1.61146"></a-gltf-model>
<a-gltf-model src="Temple.glb" target="" gltf-model="Temple.glb" class="collidable" position="0 2.7 2" scale="5 5 5"></a-gltf-model>
<a-gltf-model src="Macintosh_Classic.glb" target="" gltf-model="Macintosh_Classic.glb" class="collidable" position="0 1.34281 1.5" scale="6 6 6" rotation="0 180 0"></a-gltf-model>
<a-simple-sun-sky sun-position="0.7 0.4 -1"></a-simple-sun-sky>
<!--
Macintosh Classic by Charlie
okshelf by CreativeTrio
The Temple of Reflection by Duncan Anderson
temple for tilt integration by kris pilcher
[CC-BY] via Poly Pizza
<a-gltf-model src="The_Temple_of_Reflection.glb" target="" gltf-model="The_Temple_of_Reflection.glb" class="collidable" position="8.09868 8.93651 -21.48136"></a-gltf-model>
<a-gltf-model src="Bookshelf.glb" target="" gltf-model="Bookshelf.glb" class="collidable" position="-0.43035 0.02628 2.24855"></a-gltf-model>
-->
</a-entity>
<a-entity visible=false user-visibility="username:visual_prototype">
<!-- could be based on image set in directory, filled from it, being either svg/png/jpg etc -->
<a-box target onreleased="let el = selectedElements.at(-1).element; let pos = el.getAttribute('position'); el.setAttribute('rotation', ''); el.setAttribute('position', pos.x.toFixed(1) + ' ' + pos.y.toFixed(1) + ' ' + pos.z.toFixed(1));"
position="0.1 1.5 -.3" wireframe=true scale=".1 .1 .1">
<a-image src="https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/gitea_logo.svg" position="0 0 -.5"></a-image>
</a-box>
<a-box target onreleased="let el = selectedElements.at(-1).element; let pos = el.getAttribute('position'); el.setAttribute('rotation', ''); el.setAttribute('position', pos.x.toFixed(1) + ' ' + pos.y.toFixed(1) + ' ' + pos.z.toFixed(1));"
position="0.3 1.5 -.3" wireframe=true scale=".1 .1 .1">
<a-image src="https://fabien.benetou.fr//pub/rss_logo_small.png" position="0 0 -.5"></a-image>
</a-box>
<a-box target onreleased="let el = selectedElements.at(-1).element; let pos = el.getAttribute('position'); el.setAttribute('rotation', ''); el.setAttribute('position', pos.x.toFixed(1) + ' ' + pos.y.toFixed(1) + ' ' + pos.z.toFixed(1));"
position="0.5 1.5 -.3" wireframe=true scale=".1 .1 .1">
<a-image src="https://fabien.benetou.fr//pub/images/by_3.0_88x31.png" position="0 0 -.5"></a-image>
</a-box>
<a-box target onreleased="let el = selectedElements.at(-1).element; let pos = el.getAttribute('position'); el.setAttribute('rotation', ''); el.setAttribute('position', pos.x.toFixed(1) + ' ' + pos.y.toFixed(1) + ' ' + pos.z.toFixed(1));"
position="-0.1 1.5 -.3" wireframe=true scale=".1 .1 .1">
<a-image src="https://fabien.benetou.fr//pub/images/lal.png" position="0 0 -.5"></a-image>
</a-box>
<!--
<a-box target onreleased="let el = selectedElements.at(-1).element; let pos = el.getAttribute('position'); el.setAttribute('rotation', ''); el.setAttribute('position', pos.x.toFixed(1) + ' ' + pos.y.toFixed(1) + ' ' + pos.z.toFixed(1));"
position="-0.1 1.5 -.3" wireframe=true scale="2 1 .1">
<a-image src="Horn.svg" position="0 0 -.5"></a-image>
</a-box>
-->
</a-entity>
<a-entity visible=false user-visibility="username:bricks">
<script>
let script = document.createElement("script")
//script.src = "https://cdn.jsdelivr.net/npm/aframe-plug-socket@0.0.1/dist/plug-socket.min.js"
script.src = "https://companion.benetou.fr/plug-socket.min.js"
document.head.appendChild( script )
let script2 = document.createElement("script")
script2.src = "https://companion.benetou.fr/dynamic-snap.min.js"
document.head.appendChild( script2 )
setTimeout( _ => {
if (!AFRAME.components['socket']) require('aframe-plug-socket')
if (!AFRAME.components['dynamic-snap']) require('aframe-dynamic-snap')
AFRAME.scenes[0].setAttribute("socket", "snapDistance: 0.2; debug: true")
brickforsocketfabric.setAttribute("socket-fabric", "") // TODO consider event rather than auto
be1.setAttribute("socket", "")
be2.setAttribute("socket", "")
be3.setAttribute("plug", "")
be4.setAttribute("plug", "")
// potentially add onreleased and remove onpicked
// consider also https://diarmidmackenzie.github.io/aframe-components/components/dynamic-snap/
// should setAttribute() removeAttribute() on both
//brickforsocketfabric.setAttribute("onpicked", 'brickforsocketfabric2.removeAttribute("socket-fabric")')
//brickforsocketfabric.setAttribute("onreleased", 'brickforsocketfabric2.setAttribute("socket-fabric", "")')
brickforsocketfabric2.setAttribute("socket-fabric", "")
//brickforsocketfabric2.setAttribute("onpicked", 'brickforsocketfabric2.removeAttribute("socket-fabric")')
//brickforsocketfabric2.setAttribute("onreleased", 'brickforsocketfabric2.setAttribute("socket-fabric", "")')
be1_1.setAttribute("socket", "")
be2_1.setAttribute("socket", "")
be3_1.setAttribute("plug", "")
be4_1.setAttribute("plug", "")
},1000)
</script>
<a-box id=brickforsocketfabric position = "0 1.4 -.5" height=.1 depth=.1 width=.2 target color="red">
<a-entity id=be1 position = "-0.05 -0.05 0"></a-entity>
<a-entity id=be2 position = "0.05 -0.05 0"></a-entity>
<a-entity id=be3 position = "-0.05 0.05 0"></a-entity>
<a-entity id=be4 position = "0.05 0.05 0"></a-entity>
</a-box>
<a-box id=brickforsocketfabric2 position = "-.5 1.4 -.5" height=.1 depth=.1 width=.2 target color="green">
<a-entity id=be1_1 position = "-0.05 -0.05 0"></a-entity>
<a-entity id=be2_1 position = "0.05 -0.05 0"></a-entity>
<a-entity id=be3_1 position = "-0.05 0.05 0"></a-entity>
<a-entity id=be4_1 position = "0.05 0.05 0"></a-entity>
</a-box>
</a-entity>
<a-entity visible=false user-visibility="username:chemistry">
<!-- front dot does not help much... still need to grab in front of it -->
<a-troika-text anchor=left class=chemicalelement target onpicked="startChemistryCheck()" onreleased="endChemistryCheck()" value='•H' position=".0 1.15 -.5"></a-troika-text>
<a-troika-text anchor=left class=chemicalelement target onpicked="startChemistryCheck()" onreleased="endChemistryCheck()" value='•H' position=".3 1.55 -.6"></a-troika-text>
<a-troika-text anchor=left class=chemicalelement target onpicked="startChemistryCheck()" onreleased="endChemistryCheck()" value='•O' position="-.3 1.25 -.6"></a-troika-text>
<script>
function endChemistryCheck(){
let el = selectedElements.at(-1).element
let pos = el.getAttribute('position')
el.setAttribute('rotation', '')
el.setAttribute('position', pos.x.toFixed(2) + ' ' + pos.y.toFixed(2) + ' ' + pos.z.toFixed(2))
applyToClass("chemistrylink", chl => chl.remove() )
Array.from( el.closest("[user-visibility]").querySelectorAll(".chemicalelement") ).map( (c,i) => {
c.setAttribute("troika-text", "outlineWidth", 0)
})
const linksNeeded = 2
let linksAdded = 0
Array.from( el.closest("[user-visibility]").querySelectorAll(".chemicalelement") ).filter( c => c != el ).map( (c,i) => {
// contained within top user-visibility, here "username:chemistry" rather than whole document
let start = el.getAttribute("position").clone()
let end = c.getAttribute("position").clone()
if ( start.distanceTo( end ) < 0.2 ){
// consider instead https://diarmidmackenzie.github.io/aframe-components/components/plug-socket/
let elLink = document.createElement("a-tube")
elLink.setAttribute("radius", 0.01)
let mid = new THREE.Vector3().copy( start ).add ( end ).divideScalar(2)
mid.y -= .1 // TODO going "around" other chemicalelement if close enough
let path = [start, mid, end].map( p => AFRAME.utils.coordinates.stringify( p ) ).join(", ")
elLink.setAttribute("path", path )
elLink.classList.add("chemistrylink")
AFRAME.scenes[0].appendChild(elLink)
linksAdded++
}
})
if (linksNeeded == linksAdded) chemistryresult.setAttribute("position", "0 1.5 -0.3")
}
function startChemistryCheck(){
// used for preview compatible elements
let el = selectedElements.at(-1).element
Array.from( el.closest("[user-visibility]").querySelectorAll(".chemicalelement") ).filter( c => c != el ).map( (c,i) => {
c.setAttribute("troika-text", "outlineWidth", .01)
})
}
</script>
<a-gltf-model id=chemistryresult src="Potion_Bottle.glb" target="" class="collidable" position="0 1000 0"></a-gltf-model>
</a-entity>
<a-entity visible=false user-visibility="username:q2_scale_test">
<!-- front dot does not help much... still need to grab in front of it -->
<a-troika-text anchor=left class=scalable target value='•H' position=".0 1.15 -.49"></a-troika-text>
<a-box class=scalable color=blue target position="1 1.15 -.5"></a-box>
<a-box class=scalable color=red target position="4 2.15 -.5"></a-box>
<a-sphere class=scalable target position="-1 -1.15 -.5"></a-sphere>
<a-image color=grey class=scalable target position=".0 1.15 -.5"></a-image>
<a-troika-text anchor=left target value='jxr scaleUp()' position=".0 0.95 -0.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr scaleDown()' position=".0 5.95 -1.4" scale="0.1 0.1 0.1"></a-troika-text>
<!-- supposedly used only once each way... but can do more! -->
<a-troika-text anchor=left target value='jxr scaleUp()' position=".0 6.95 -1.4" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left target value='jxr scaleDown()' position=".0 0.75 -0.4" scale="0.1 0.1 0.1"></a-troika-text>
<script>
function scaleDown(position="0 0 0"){
scaleScalableElements(1/10)
// reset position (but should instead be the inverse offset of scale up)
rig.setAttribute("position", position)
groundfor360.setAttribute("position", position)
}
function scaleScalableElements(scalar){
manuscript.classList.add("scalable")
applyToClass("notes", el => el.classList.add("scalable") )
// not sure if that works... might work but scale below 1 so too low? does work for rest of the Manuscript though
applyToClass("scalable", el => {
el.setAttribute("position", AFRAME.utils.coordinates.stringify( el.getAttribute("position").multiplyScalar(scalar) ) )
el.setAttribute("scale", AFRAME.utils.coordinates.stringify( el.getAttribute("scale").multiplyScalar(scalar) ) )
} )
}
function scaleUp(position="0 5 -1"){
scaleScalableElements(10)
// arbitrary position
rig.setAttribute("position", position)
groundfor360.setAttribute("position", position)
}
// consider making any new addNewNote / addNewNoteAsPostItNote scalable too
// can be done by ...
// document.querySelectorAll(".notes")
</script>
</a-entity>
</a-scene>
</body>
</html>
<script>
// for https://companion.benetou.fr/demos_example.html?filename=demo_q2.json
// ----------------------------------------- demo queue Q2 customizations -------------------------------------------
const q2_steps = [
"icon_tags",
"q2_annotated_bibliography",
"q2_annotated_bibliography_week2",
"q2_immersive_console",
"q2_json_collaborations",
"q2_lense",
"q2_os_keyboard",
"q2_pasting",
"q2_remote_ntfy_keyboard",
"q2_ring_keyboard",
"q2_step_contextuallayouts",
"q2_step_end",
"q2_step_highlight",
"q2_step_jsonedit",
"q2_step_layout_animationtests",
"q2_step_refcards_filtering",
"q2_step_start",
"q2_step_volumetric_frames",
"q2_visualmetaexport",
"q2_visualmetaexport_map",
"q2_visualmetaexport_map_via_wordpress",
"q2_wrist_rotations",
"ring_discovery",
"ring_discovery_with_keyboard",
"ring_highlights",
"temple_test",
"q2_most_recent_file",
]
// ------------------------------------------------- test scenarii --------------------------------------------------------------
// ( enabled by &emulatexr=true )
const q2_steps_with_emulation_testing = [
"q2_lense",
"q2_step_refcards_filtering",
"q2_step_volumetric_frames",
"q2_step_layout_animationtests",
]
</script>