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
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>
|
|
|