moving from ondrop to onreleased, proper UUID on notes

list-editing
Fabien Benetou 9 months ago
parent 1caa31f9b7
commit 958acd82f1
  1. 290
      index.html
  2. 26
      jxr.js

@ -30,8 +30,7 @@
<!-- still experimenting, see webdav.html --> <!-- still experimenting, see webdav.html -->
<script src='dependencies/webdav.js'></script> <script src='dependencies/webdav.js'></script>
<script src='jxr.js?123456'></script>
<script src='jxr.js?12345'></script>
<!-- replacing with local copies as CDNs are like unpkg tend to be slow <!-- replacing with local copies as CDNs are like unpkg tend to be slow
<script type="module" src="https://unpkg.com/immers-client/dist/destination.bundle.js"></script> <script type="module" src="https://unpkg.com/immers-client/dist/destination.bundle.js"></script>
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script> <script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
@ -58,204 +57,92 @@
function loadFile(element){ function loadFile(element){
const file = element.files[0] const file = element.files[0]
const reader = new FileReader(); const reader = new FileReader();
let gltfEl = document.createElement('a-gltf-model')
gltfEl.setAttribute('position', '0 1 -1')
AFRAME.scenes[0].appendChild(gltfEl)
reader.addEventListener( "load", () => { reader.addEventListener( "load", () => {
gltfEl.addEventListener("model-loaded", e => inspectModel(gltfEl.object3D) ) writecsljsonback(reader.result)
// never seems to fire
gltfEl.setAttribute('src', reader.result)
gltfEl.setAttribute('animation-mixer', "")
setTimeout( _ => {
console.log( inspectModel(gltfEl.object3D) )
}, 1000)
}, false,); }, false,);
if (file) { if (file) {
reader.readAsDataURL(file); reader.readAsText(file);
}
}
function inspectModel(selectedModel){
let animations=[];
let foundBones=[];
let skinnedMeshes=[];
let morphTargets=[];
selectedModel.traverse( t => {
if (t.animations?.length) animations.push(t)
if (t.type == "Bone") foundBones.push(t)
if (t.type == "SkinnedMesh") skinnedMeshes.push(t)
if (t.morphTargetInfluences) morphTargets.push(t) // modelInspected.morphTargets.map(mt => mt.morphTargetInfluences.fill(0))
});
return {animations:animations, bones:foundBones, skinnedMeshes: skinnedMeshes, morphTargets:morphTargets}
} }
function manualAnimate(selector="#biggu"){
inspectModel( document.querySelector(selector).object3D ).animations[0].animations.map( (a,n) =>
addNewNote('jxr document.querySelector('+selector+').setAttribute("animation-mixer", "clip:'+a.name+';loop:once")', '-1 '+(n/10+1)+' -1')
)
} }
let sessionId = self.crypto.randomUUID() const libraryURL = 'https://webdav.benetou.fr/fotsave/ExportedItems-FromZoteroAsCSLJSON.json'
AFRAME.registerComponent('getcsljson', {
// see https://biggu-backend-collab.glitch.me/ to insure steps are done correctly schema: {
function shareLiveEvent(eventName, eventData, server='https://biggu-backend-collab.glitch.me/'){ url: {type: 'string', default: libraryURL },
if (!eventName) return },
let data = { eventName, sessionId } init: function(){
if (eventData) data.eventData = eventData let generatorName = this.attrName
let playername = AFRAME.utils.getUrlParameter('playername') fetch(this.data.url).then(res => res.json() ).then(res => { notesFromArray(res, generatorName, "title", 2, -1/10) })
if (playername) data.playername = playername //fetch(this.data.url).then(res => res.json() ).then(res => { notesFromArray(res, generatorName) })
fetch(server+'/newevent/'+JSON.stringify(data)) // could use citeproc instead
// see also https://citation.js.org
} }
// should then become a container hosted on benetou.fr
function saveTargets(server='https://biggu-backend-collab.glitch.me/'){
// might try to generate a hash as ID, should be reproducible though
let data = []
Array.from( document.querySelectorAll("[target]") )
.filter( el => el.id != '')
.map( el => {
// limited to location/position for now as that's only what target does modify
if( hasBeenManipulated(el.getAttribute('position')) || hasBeenManipulated(el.getAttribute('rotation')) )
data.push({id:el.id, position:el.getAttribute('position'), rotation:el.getAttribute('rotation')})
}) })
if (data.length > 0) fetch(server+'/save?data='+JSON.stringify(data))
return data
}
function animateThenIdle(mainCharacter, animationName, timeScale='1'){ function notesFromArray(data, generatorName="", field="title", offset=1, ratio=1/10, depth=-.5 ){
mainCharacter.setAttribute('animation-mixer', "clip:"+animationName+";loop:once; timeScale:"+timeScale) data.slice(0,maxItemsFromSources).map( (n,i) => {
mainCharacter.addEventListener('animation-finished', _ => { addNewNote( n[field], "0 "+(offset+i*ratio)+" "+depth, ".1 .1 .1", null, generatorName )
mainCharacter.setAttribute('animation-mixer', "clip:bigguaction_idle; loop:true;") .setAttribute("onreleased","spreadItemsFromCollection('getcsljson', 1.5)")
}) })
// could return the animation duration or an event when done
}
function checkExerciseCompletion(targetNumber=2){
console.log('checkExerciseCompletion', targetNumber)
// does not seem to happen...
// does ondrop work still?
const instructions = document.querySelector("#instructions>a-troika-text")
let counter = 0
const mainCharacter = document.getElementById("biggu")
mainCharacter.addEventListener("animation-finished", _ => console.log('anim done'))
Array.from( document.querySelector("#fishes").children ).map( f => {
if (f.object3D.position.distanceTo( document.querySelector('#plate').object3D.position ) < .3 ) counter++ })
if (counter == targetNumber) {
instructions.emit('win')
shareLiveEvent('win')
document.getElementById("biggubravojulia").play()
animateThenIdle(mainCharacter, 'bigguaction_win')
} else {
document.getElementById("biggucontinu").play()
instructions.emit('failed', {counter:counter})
shareLiveEvent('failed', {counter:counter})
animateThenIdle(mainCharacter, 'bigguaction_yes')
// anims = [ "bigguaction_no", "Bigguaction_pl", "bigguaction_pr", "bigguaction_talk", "bigguaction_win", "bigguaction_win", "bigguaction_yes" ]
}
} }
// much too complex function getDataToSaveBack(){
AFRAME.registerComponent('exercise', { let dataToSave = []
schema: { let unsorted = []
instructions: {type: 'string'}, fetch(libraryURL).then(res => res.json() ).then(res => {
win: {type: 'string'}, // assume unique title always present
failed: {type: 'string'}, getArrayFromClass('getcsljson').map(i=>{
next: {type: 'selector'}, unsorted.push( {data: res.filter(citation=>citation.title==i.getAttribute('value'))[0],
first: {type: 'boolean', default: false}, position: i.getAttribute('position')
},
init: function(){
const mainCharacter = document.getElementById("biggu")
mainCharacter.setAttribute('animation-mixer', "clip:bigguaction_idle; loop:true;")
const emittedExerciseId = this.el.id
const instructions = document.querySelector("#instructions>a-troika-text")
if (!this.data.first)
this.el.setAttribute('position', '0 0 9999')
else
instructions.setAttribute("value", this.data.instructions )
instructions.addEventListener('win', _ => {
instructions.setAttribute("value", this.data.win )
// use whatever last exercise set, not correct
// should setTimeout or let the player actively move on
this.el.setAttribute('position', '0 0 9999')
// must filter on id, making sure it's emited by matching excercise
if (this.data.next) {
console.log( this.data.next )
console.log( this.data.next.id )
this.data.next.setAttribute('position', '0 0 0')
}
}) })
instructions.addEventListener('failed', ev => {
instructions.setAttribute("value", this.data.failed )
console.log(emittedExerciseId, this.id)
}) })
// should be replaced by drawings or even scaled down models with "5" and arrow or hand model dataToSave = unsorted.sort((a,b)=>b.position.y-a.position.y)
} .map(i=> { if (!i.data.note) { i.data.note = JSON.stringify(i.position) } else {i.data.note += '\n' + JSON.stringify(i.position) } ; return i.data } )
// this is also where piggy-backing on CSL-JSON could be tested, e.g spatial position (or stringifoied pose) field added
// could consider appending to the .note field instead
// .position does get saved, and does not prevent to be loaded from Zotero, but does get lost after when exported again
// cf https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#cheater-syntax-for-odd-fields
writecsljsonback(JSON.stringify(dataToSave))
}) })
}
// for more complete supervision consider remote scrcpy //function bumpItemUp(id, generatorName){
AFRAME.registerComponent('start-with-supervision', { function bumpItemUp(id, generatorName){
init: function(){ if (!id || !generatorName) return
const server='https://biggu-backend-collab.glitch.me' // could use annotation symbols with jxr
// CORS enabled needed // sets x and z to 0 though
const source = new EventSource(server+'/events'); // requires to exist for each list item
shareLiveEvent('connected') // could bump on y by ratio*1.1
source.addEventListener('message', message => { document.getElementById(id).object3D.position.y += 1/10*1.1
console.log('Got', message); // assuming here they are already aligned, which is there case if they have onreleased already set with spreadItemsFromCollection()
let json = JSON.parse(message.data) spreadItemsFromCollection(generatorName, 1.5)
if (json && json.eventName == 'start-with-supervision') startExercise()
})
} }
})
function startExercise(){ function spreadItemsFromCollection( generatorName, offset=1, ratio=1/10, depth=-.5 ){
const maxFishes = 5 getArrayFromClass(generatorName).sort((a,b)=>a.getAttribute('position').y-b.getAttribute('position').y).map( (n,i) => {
for (let i=0;i<maxFishes;i++){ n.setAttribute('position', "0 "+(offset+i*ratio)+" "+depth)
let gltfEl = document.createElement('a-gltf-model') n.setAttribute('rotation', "0 0 0") // could also be based on the average of all items, the first item, last one, etc
gltfEl.setAttribute("ondrop","checkExerciseCompletion()") // see also snap-on-pinchended component
// setOnDropFromAttribute()
// might be problematic due to timing somehow... })
gltfEl.setAttribute("target","true")
gltfEl.setAttribute("scale",".001 .001 .001")
gltfEl.setAttribute("src","../content/winterset/Fish.glb")
let x = Math.random()*1-1
let y = Math.random()*1-.5
let z = Math.random()*1-1
gltfEl.setAttribute("position",`${x} ${y} ${z}`)
//console.log("position",`${x} ${y} ${z}`)
document.querySelector("#fishes").appendChild(gltfEl)
} // might want to appear from the start
setTimeout( _ => setOnDropFromAttribute(), 500)
// audio does not match asset, should be a white bowl, not black
document.getElementById("exerciseA").setAttribute("visible", false)
// hide everything until this is done, otherwise overwhelming
const mainCharacter = document.getElementById("biggu")
setTimeout(_=>{
document.getElementById("biggucestmoi").play()
// works in XR on headset, not on desktop (needs user action)
animateThenIdle(mainCharacter, 'bigguaction_talk', .5)
},1000)
// should focus on learning pinch (thumb and index) before doing the exercise itself
setTimeout(_=>{
document.getElementById("exerciseA").setAttribute("visible", true)
document.getElementById("bigguinstructions").play()
animateThenIdle(mainCharacter, 'bigguaction_talk', .5)
},5000)
} }
AFRAME.registerComponent('warmup', { // data could come from parsing back order from getArrayFromClass('getcsljson').map(i=>i.getAttribute('position').y)
init: function(){ // cf https://gist.github.com/Utopiah/26bae9fecc7a921f8bfd38cf5fc91612#file-logo_vr_hubs-js-L44
startExercise() // yet still needs the actual data itself and adding a comment field for position if to be used back here rather than e.g Zotero
function writecsljsonback(data){
const webdavurl = "https://webdav.benetou.fr";
const client = window.WebDAV.createClient(webdavurl)
async function w(path, data){ return await client.putFileContents(path, data); }
w("/fotsave/ExportedItems-FromZoteroAsCSLJSON.json", data )
setFeedbackHUD( "file saved" )
} }
})
</script> </script>
<input style='position:fixed;z-index:1; top: 0%; left: 20%; display:none' <input style='position:fixed;z-index:1; top: 0%; left: 20%; display:float'
type="file" name="file-input" accept=".gltf, .glb" id="file-input" onchange="loadFile(this)" /> type="file" name="file-input" accept=".json" id="file-input" onchange="loadFile(this)" />
<div style='position:fixed;z-index:1; top: 0%; left: 0%; border-bottom: 70px solid transparent; border-left: 70px solid #eee; '> <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/"> <a href="https://git.benetou.fr/utopiah/text-code-xr-engine/issues/">
<img style='position:fixed;left:10px;' title='code repository' src='gitea_logo.svg'> <img style='position:fixed;left:10px;' title='code repository' src='gitea_logo.svg'>
@ -264,7 +151,7 @@ AFRAME.registerComponent('warmup', {
<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> <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>
<a-scene startfunctions start-with-supervision> <a-scene startfunctions getcsljson>
<!-- screenstack dynamic-view selectionboxonpinches glossary timeline issues fot <!-- screenstack dynamic-view selectionboxonpinches glossary timeline issues fot
toolbox commands-from-external-json disable-components-via-url enable-components-via-url toolbox commands-from-external-json disable-components-via-url enable-components-via-url
physics="debug:true; friction: 0.01;" physics="debug:true; friction: 0.01;"
@ -272,10 +159,6 @@ AFRAME.registerComponent('warmup', {
refresh-text-content-from-wiki-page="pagename:TestingPairCollaboration" refresh-text-content-from-wiki-page="pagename:TestingPairCollaboration"
--> -->
<a-assets> <a-assets>
<audio id="biggucestmoi" src="../content/voicesBigguJulia/biggu-fem.mp3"></audio>
<audio id="biggubravojulia" src="../content/voicesBigguJulia/bravojulia.mp3"></audio>
<audio id="biggucontinu" src="../content/voicesBigguJulia/continu.mp3"></audio>
<audio id="bigguinstructions" src="../content/voicesBigguJulia/instructions.mp3"></audio>
<template id="avatar-template"></template> <template id="avatar-template"></template>
<template id="left-hand-default-template"> <template id="left-hand-default-template">
<a-entity networked-hand-controls="hand:left"></a-entity> <a-entity networked-hand-controls="hand:left"></a-entity>
@ -285,44 +168,13 @@ AFRAME.registerComponent('warmup', {
</template> </template>
</a-assets> </a-assets>
<!-- <a-gltf-model hide-on-enter-ar="" src="../content/MinimalisticJapaneseRoom.glb" rotation="0 -90 0" position="2 1 -1" scale="" ></a-gltf-model>
../content/winterset/Pinetree.glb <!-- from https://poly.pizza/m/8cWuXx5BASV -->
../content/winterset/Penguin.glb <!-- alt https://poly.pizza/m/cA_lcvRC4NA -->
../content/winterset/Mountains.glb
../content/winterset/Crystal.glb
<a-entity hide-on-enter-ar="" id="environment" class="hidableenvironment" ></a-entity>
<a-entity hide-on-enter-ar="" id="environmentsky" class="hidableenvironment" ></a-entity>
-->
<a-gltf-model hide-on-enter-ar="" src="../content/winterset/WinterIsland.glb" position="2.1 -4.5 -1.7" ></a-gltf-model>
<a-gltf-model src="../content/winterset/WinterSled.glb" position="0.11322 0.14197 0.27573" ></a-gltf-model>
<a-entity id="bubble" position="0 1 -1" scale=".1 .1 .1">
<a-sphere scale="2 1 .1" color="white"></a-sphere>
<a-cone scale="2 1 .1" position="-.5 -.7 0" rotation="0 0 45" color="white"></a-cone>
<a-troika-text value="C'est moi Biggu!" font-size=".4" outline-color="gray" outline-width=".02"
align="center" color="black" position="0 0 .2"></a-troika-text>
</a-entity>
<a-entity id="instructions" position="0.5 0.7 -1" rotation="0 -20 0" scale=".07 .07 .07"> <a-troika-text anchor=left target annotation="content:saves data back to Zotero library over WebDAV backend"
<a-sphere scale="2 1 .1" color="white"></a-sphere> value="jxr getDataToSaveBack()" position=" -0.3 0.60 -.5" rotation="0 0 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-cone scale="2 1 .1" position="-.5 -.7 0" rotation="0 0 45" color="white"></a-cone>
<a-troika-text value="" font-size=".3" outline-color="gray" outline-width=".02"
align="center" color="black" position="0 0 .2"></a-troika-text>
</a-entity>
<a-troika-text anchor=left target annotation="content:RECOMMENCER"
value="jxr startExercise()" position=" -0.3 0.60 .5" rotation="90 180 0" scale="0.1 0.1 0.1"></a-troika-text>
<a-entity id="exerciseA" exercise="first:true;next:#exerciseB;instructions:Pose 2 poissons\ndans l'assiette;win:Bravo Julia!;failed:presque Julia, continue;">
<a-entity id="fishes"></a-entity>
<a-gltf-model id="plate" src="../content/winterset/BowlWhite.glb" position="-0.397 0.14354 -0.508" scale="0.1 0.05 0.1" ></a-gltf-model>
</a-entity>
<a-gltf-model id="biggu" src="../content/winterset/SK_Biggu_v029_optimized.glb" position="-0.3 0.5 -1">
<!-- <a-sound src="#bigguinstructions"></a-sound> -->
</a-gltf-model>
<a-gltf-model src="../content/winterset/Crystal_iPoly3D.glb" position="-0.29409 -0.23524 -0.83481" scale="0.1 0.1 0.1"></a-gltf-model>
<a-gltf-model src="../content/winterset/FruitBowl.glb" position="-0.29409 -.23524 -0.83481" scale="0.1 0.1 0.1"></a-gltf-model>
<a-entity id="rig"> <a-entity id="rig">
<a-entity id="player" networked="template:#avatar-template;attachTemplateToLocal:false;" <a-entity id="player" networked="template:#avatar-template;attachTemplateToLocal:false;"
hud camera look-controls wasd-controls waistattach="target: .movebypinch" position="0 1.6 0"> hud camera look-controls wasd-controls waistattach="target: .movebypinch" position="0 1.6 0">
@ -343,6 +195,8 @@ AFRAME.registerComponent('warmup', {
<a-sky hide-on-enter-ar color="lightgray"></a-sky> <a-sky hide-on-enter-ar color="lightgray"></a-sky>
<a-troika-text anchor=left target value="instructions : \n--right pinch to move\n--left pinch to execute" position="0 0.65 -0.2" scale="0.1 0.1 0.1"></a-troika-text> <a-troika-text anchor=left target value="instructions : \n--right pinch to move\n--left pinch to execute" position="0 0.65 -0.2" scale="0.1 0.1 0.1"></a-troika-text>
<a-troika-text anchor=left value="jxr location.reload()" target position=" -0.3 1.30 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>
<!-- <!--
<a-troika-text anchor=left target id="startdraw2d" annotation="content:dessiner en 2D" <a-troika-text anchor=left target id="startdraw2d" annotation="content:dessiner en 2D"
value="jxr startDraw2D()" position="0 1.45 -0.1" scale="0.1 0.1 0.1"></a-troika-text> value="jxr startDraw2D()" position="0 1.45 -0.1" scale="0.1 0.1 0.1"></a-troika-text>
@ -354,8 +208,8 @@ AFRAME.registerComponent('warmup', {
<!-- somehow disable hand interaction despite, according to the documentation, it should rely on world position <!-- somehow disable hand interaction despite, according to the documentation, it should rely on world position
<a-text target value="jxr qs #rig sa position 0 0 10" position="0 1.55 .5" rotation="0 180 0" scale="0.1 0.1 0.1"></a-text> <a-text target value="jxr qs #rig sa position 0 0 10" position="0 1.55 .5" rotation="0 180 0" scale="0.1 0.1 0.1"></a-text>
<a-console position="2 2 0" rotation="0 -45 0" font-size="34" height=1 skip-intro=true></a-console>
--> -->
<a-console position="2 2 0" rotation="0 -45 0" font-size="34" height=1 skip-intro=true></a-console>
<!-- for Wolvic on Lynx support test --> <!-- for Wolvic on Lynx support test -->
<a-entity thumbstick-shifting oculus-touch-controls="hand: left"></a-entity> <a-entity thumbstick-shifting oculus-touch-controls="hand: left"></a-entity>

@ -1202,7 +1202,7 @@ function addNewNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=nu
if (id) if (id)
newnote.id = id newnote.id = id
else else
newnote.id = "note_" + Date.now() // not particularly descriptive but content might change later on newnote.id = "note_" + crypto.randomUUID() // not particularly descriptive but content might change later on
if (classes) if (classes)
newnote.className += classes newnote.className += classes
newnote.setAttribute("side", "double" ) newnote.setAttribute("side", "double" )
@ -3413,7 +3413,7 @@ function thumbToIndexAngle(){
console.log( 'r' ) console.log( 'r' )
p.emit('thumb2indexpush') p.emit('thumb2indexpush')
// could insert (with max threshold) a targe entity between tip and thumb // could insert (with max threshold) a targe entity between tip and thumb
// this entity could then ondrop add a new post it note or jxr element // this entity could then ondrop (now onreleased) add a new post it note or jxr element
} }
// could also check angle against head to insure it's facing the user // could also check angle against head to insure it's facing the user
}, 590) }, 590)
@ -3709,33 +3709,25 @@ AFRAME.registerComponent('gltf-jxr', {
*/ */
}); });
function setOnDropFromAttribute(){ // avoiding setOnDropFromAttribute() as it is not idiosyncratic and creates timing issues
// add to file descriptor from offtopus AFRAME.registerComponent('onreleased', { // changed from ondrop to be coherent with event name
// could also be prototyped with a URL instead, doesn't need offtopus even cached version events: {
// could also prototype by doing so via https://webdav.benetou.fr/fot-demo-day/mobydick-extract.txt released: function (e) {
console.log('setOnDropFromAttribute') let code = this.el.getAttribute('onreleased')
targets.map( el => {
if ( el.getAttribute('ondrop')?.length > 0 )
el.addEventListener('released', e => {
let code = el.getAttribute('ondrop')
console.log('do', code)
try { try {
eval( code ) eval( code )
} catch (error) { } catch (error) {
console.error(`Evaluation failed with ${error}`); console.error(`Evaluation failed with ${error}`);
} }
})
} )
// if dropped close enough to an editor, load file content in editor
// could try a jxr command...
} }
}
})
// used for testing, now that jxr.js is outside of index.html, could consider putting this back in index.html instead to keep behavior one would expect from a library // used for testing, now that jxr.js is outside of index.html, could consider putting this back in index.html instead to keep behavior one would expect from a library
// does indeed create problems, namely other pages relying on it do get this testing behavior // does indeed create problems, namely other pages relying on it do get this testing behavior
AFRAME.registerComponent('startfunctions', { AFRAME.registerComponent('startfunctions', {
init: function () { init: function () {
console.log('startfunctions') console.log('startfunctions')
setOnDropFromAttribute()
/* class clonableasset : Crystal.glb Fish.glb Mountains.glb Penguin.glb Pinetree.glb /* class clonableasset : Crystal.glb Fish.glb Mountains.glb Penguin.glb Pinetree.glb
consider also consider also
backend needed for caching backend needed for caching

Loading…
Cancel
Save