<!DOCTYPE html>
< html >
< title > SpaSca : Spatial Scaffolding< / title >
< head >
<!-- Suggestions? https://git.benetou.fr/utopiah/text - code - xr - engine/issues/ -->
<!-- <script src='dependencies/aframe.min.js'></script> -->
< script src = 'dependencies/aframe.offline.min.js' > < / script >
< script src = "dependencies/a-console.js" > < / script >
< script src = 'dependencies/aframe-html.js' > < / script >
< script src = 'dependencies/aframe-mirror.js' > < / script >
< script src = 'dependencies/aframe-troika-text.min.js' > < / script >
<!-- <script type="module" id=immersbundle src='dependencies/immers - client.js?save=true'></script> -->
<!-- <script type="module" id=immersbundle src="https://cdn.jsdelivr.net/npm/immers - client/dist/destination.bundle.js?role=modFull"></script> -->
<!-- <script src="https://threejs.org/examples/js/exporters/GLTFExporter.js"></script> -->
< script src = "https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.1.0/dist/aframe-extras.min.js" > < / script >
< script src = "dependencies/shiki0.14.1.js" > < / script >
<!-- for input sharing -->
< script src = 'dependencies/peerjs.min.js' > < / script >
<!-- for content sharing, using NAF
< script src = 'dependencies/socket.io.slim.js' > < / script >
< script src = "https://naf.benetou.fr/easyrtc/easyrtc.js" > < / script >
< script src = 'dependencies/networked-aframe.min.js' > < / script >
-->
< script src = "dependencies/aframe-physics-system.min.js" > < / script >
<!-- still experimenting, see webdav.html -->
< script src = 'dependencies/webdav.js' > < / script >
< script src = 'jxr.js?123456' > < / script >
<!-- 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 src = "https://aframe.io/releases/1.3.0/aframe.min.js" > < / script >
< script src = "https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js" > < / script >
< script src = "aframe-html.js" > < / script >
< script src = "https://unpkg.com/peerjs@1.4.5/dist/peerjs.min.js" > < / script >
< script src = "https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.4.0/socket.io.slim.js" > < / script >
< script src = "https://naf.benetou.fr/easyrtc/easyrtc.js" > < / script >
< script src = "https://unpkg.com/networked-aframe@^0.10.0/dist/networked-aframe.min.js" > < / script >
< script src = "https://cdn.jsdelivr.net/npm/aframe-mirror@latest/index.js" > < / script >
-->
< link rel = "apple-touch-icon" sizes = "180x180" href = "apple-touch-icon.png" >
< link rel = "icon" type = "image/png" sizes = "32x32" href = "favicon-32x32.png" >
< link rel = "icon" type = "image/png" sizes = "16x16" href = "favicon-16x16.png" >
< link rel = "manifest" href = "site.webmanifest" >
< / head >
< body >
< script >
function loadFile(element){
const file = element.files[0]
const reader = new FileReader();
reader.addEventListener( "load", () => {
writecsljsonback(reader.result)
}, false,);
if (file) {
reader.readAsText(file);
}
}
const libraryURL = 'https://webdav.benetou.fr/fotsave/ExportedItems-FromZoteroAsCSLJSON.json'
AFRAME.registerComponent('getcsljson', {
schema: {
url: {type: 'string', default: libraryURL },
},
init: function(){
let generatorName = this.attrName
fetch(this.data.url).then(res => res.json() ).then(res => { notesFromArray(res, generatorName, "title", 2, -1/10) })
//fetch(this.data.url).then(res => res.json() ).then(res => { notesFromArray(res, generatorName) })
// could use citeproc instead
// see also https://citation.js.org
}
})
function notesFromArray(data, generatorName="", field="title", offset=1, step=1/10, depth=-.5 ){
data.slice(0,maxItemsFromSources).map( (n,i) => {
addNewNote( n[field], "0 "+(offset+i*step)+" "+depth, ".1 .1 .1", null, generatorName )
.setAttribute("onreleased","spreadItemsFromCollection('getcsljson', 1.5)")
})
}
function getDataToSaveBack(){
let dataToSave = []
let unsorted = []
fetch(libraryURL).then(res => res.json() ).then(res => {
// assume unique title always present
getArrayFromClass('getcsljson').map(i=>{
unsorted.push( {data: res.filter(citation=>citation.title==i.getAttribute('value'))[0],
position: i.getAttribute('position')
})
})
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))
})
}
// assumption that a step here is 1/10
function bumpItemUp(id, generatorName){ bumpItem(id, generatorName, 1/10*1.1) }
function bumpItemDown(id, generatorName){ bumpItem(id, generatorName, -1/10*1.1) }
function bumpItem(id, generatorName, step){
if (!id || !generatorName) return
// could use annotation symbols with jxr
// sets x and z to 0 though
// requires to exist for each list item
document.getElementById(id).object3D.position.y += step
// assuming here they are already aligned, which is there case if they have onreleased already set with spreadItemsFromCollection()
spreadItemsFromCollection(generatorName, 1.5)
}
function spreadItemsFromCollection( generatorName, offset=1, step=1/10, depth=-.5 ){
getArrayFromClass(generatorName).sort((a,b)=>a.getAttribute('position').y-b.getAttribute('position').y).map( (n,i) => {
n.setAttribute('position', "0 "+(offset+i*step)+" "+depth)
n.setAttribute('rotation', "0 0 0") // could also be based on the average of all items, the first item, last one, etc
// see also snap-on-pinchended component
})
let items = getArrayFromClass(generatorName).sort((b,a)=>a.getAttribute('position').y-b.getAttribute('position').y).map( n => n.getAttribute('value') )
shareLiveEvent('modified list', items)
}
let sessionId = crypto.randomUUID()
const sse = 'https://jxr-backend-collab.glitch.me'
const sseEvents = '/events'
function shareLiveEvent(eventName, eventData, server=sse){
// should make a room based one e.g source URL (even though here with multiple branches on 1 URL it would not work)
// this could be forced via eventData then in turn ignored if the room name does not match
if (!eventName) return
let data = { eventName, sessionId }
if (eventData) data.eventData = eventData
fetch(server+'/newevent/'+JSON.stringify(data))
// using SSE but should use instead NAF broadcasting
}
const source = new EventSource(sse+sseEvents)
source.addEventListener('message', message => {
let json = JSON.parse(message.data)
if (json.eventData) console.log('Got', json.eventData);
// on first connecting getting an array of historical messages then after that an object for each new live event
})
// data could come from parsing back order from getArrayFromClass('getcsljson').map(i=>i.getAttribute('position').y)
// cf https://gist.github.com/Utopiah/26bae9fecc7a921f8bfd38cf5fc91612#file-logo_vr_hubs-js-L44
// 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 >
< input style = 'position:fixed;z-index:1; top: 0%; left: 20%; display:float'
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; ' >
< 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' >
< / a >
< / div >
< 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 getcsljson >
<!-- screenstack dynamic - view selectionboxonpinches glossary timeline issues fot
toolbox commands-from-external-json disable-components-via-url enable-components-via-url
physics="debug:true; friction: 0.01;"
networked-scene="serverURL: https://naf.benetou.fr/; adapter: easyrtc; audio: true;"
refresh-text-content-from-wiki-page="pagename:TestingPairCollaboration"
-->
< a-assets >
< template id = "avatar-template" > < / template >
< template id = "left-hand-default-template" >
< a-entity networked-hand-controls = "hand:left" > < / a-entity >
< / template >
< template id = "right-hand-default-template" >
< a-entity networked-hand-controls = "hand:right" > < / a-entity >
< / template >
< / 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 >
<!-- from https://poly.pizza/m/8cWuXx5BASV -->
<!-- alt https://poly.pizza/m/cA_lcvRC4NA -->
< a-troika-text anchor = left target annotation = "content:saves data back to Zotero library over WebDAV backend"
value="jxr getDataToSaveBack()" position=" -0.3 0.60 -.5" rotation="0 0 0" scale="0.1 0.1 0.1">< / a-troika-text >
< a-entity id = "rig" >
< a-entity id = "player" networked = "template:#avatar-template;attachTemplateToLocal:false;"
hud camera look-controls wasd-controls waistattach="target: .movebypinch" position="0 1.6 0">
< / a-entity >
< a-entity id = "rightHand" pinchprimary hand-tracking-controls = "hand: right;" > < / a-entity >
< a-entity id = "leftHand" pinchsecondary wristattachsecondary = "target: #box" hand-tracking-controls = "hand: left;" > < / a-entity >
< / a-entity >
< a-box pressable start-on-press id = "box" scale = "0.05 0.05 0.05" color = "pink" > < / a-box >
<!-- could attach functions here... BUT then they have to be activable with the other hand! -->
<!-- visual reminders of shortcuts, a poster on the far left/right of keyboard shortcuts -->
< a-entity light = "type: ambient; color: #BBB; intensity: 0.6" > < / a-entity >
< a-entity light = "type: directional; color: #FFF; intensity: 1.4" position = "-0.5 1 1" > < / a-entity >
< a-troika-text value = "SpaSca : Spatial Scaffolding" anchor = "left" outline-width = "5%" font = "../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-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 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 value = "jxr navigator.clipboard.readText().then( (clipText) => (addNewNote(clipText) ) )" target position = " -0.3 1.40 0" rotation = "0 40 0" scale = "0.1 0.1 0.1" > < / a-troika-text >
<!-- consider richer content, e.g images, 3D models, etc based on MIME type -->
<!--
< 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 >
< a-troika-text anchor = left target id = "displaypred" value = "jxr displayPred()" position = "0 1.40 -0.1" scale = "0.1 0.1 0.1" > < / a-troika-text >
< a-troika-text anchor = left target value = "jxr tiltUpId('codeditor')" position = " -0.3 1.65 0" rotation = "0 90 0" scale = "0.1 0.1 0.1" > < / a-troika-text >
< a-troika-text anchor = left target value = "jxr tiltDownId('codeditor')" position = " -0.3 1.60 0" rotation = "0 90 0" scale = "0.1 0.1 0.1" > < / a-troika-text >
-->
<!-- 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-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 -->
< a-entity thumbstick-shifting oculus-touch-controls = "hand: left" > < / a-entity >
< a-entity thumbstick-shifting oculus-touch-controls = "hand: right" > < / a-entity >
< / a-scene >
< / body >
< / script >
< / html >