@ -4,214 +4,34 @@
< head >
<!-- Suggestions? https://git.benetou.fr/utopiah/text - code - xr - engine/issues/ -->
< script src = 'dependencies/aframe.offline.min.js' > < / script >
< script >
// testing spatial introspection, starting with console output
// see #spatial-introspection-test
// example of normal output
// which should print to the main console AND to the local one (might actually be the same output at first)
// for that could modifiy captureConsole of < a-console >
// itself passed to grabAllLogs() https://github.com/kylebakerio/a-console/blob/master/a-console.js#L680
// might require too much rework, could be interesting to have own equivalent instead
// define a new console, from https://stackoverflow.com/questions/7042611/override-console-log-for-production
/*
var console=(function(oldCons){
return {
debug: function(text){
oldCons.debug(text);
// Your code
},
log: function(text){
oldCons.log("modified!"+text, arguments.caller);
// this does though rewrite for ALL, so might not help much to find provenance, i.e which functions did call for that output
// could try to get a strack trace from here and only filter from what has gone through interpretJXR / onreleased / onpicked
// does NOT get picked by < a-console > though
// Your code
},
info: function (text) {
oldCons.info(text);
// Your code
},
warn: function (text) {
oldCons.warn(text);
// Your code
},
error: function (text) {
oldCons.error(text);
// Your code
}
};
}(window.console));
// for now could add mini console after each secondary pinch
//Then redefine the old console
window.console = console;
*/
< / script >
< script src = "dependencies/a-console.js" > < / script >
< script src = 'dependencies/aframe-troika-text.min.js' > < / script >
< script src = 'dependencies/webdav.js' > < / script >
< script src = 'jxr-core.js?12' > < / script >
< script src = 'jxr-core.js?1234' > < / script >
< script src = 'jxr-postitnote.js?13235' > < / script >
< / head >
< body >
< script >
/*
SpaSca circular menu inspired by https://www.olegfrolov.design/spatialcomputing or the Meta OS menu
on empty pinch moving, if (moving duration above threshold 500ms)
then
show menu nearby position with JXR commands that hides on released,
on moving
highlight closest snippet
rewrite nearby getClosestTargetElements() to be only over a subset or targets, i.e the ones from active menu
on release
if (nearby one of the created JXR snippets), execute it
remove those JXR commands
var forceXaxis
// setInterval( _ => console.log(forceXaxis), 1000)
var translatingTargets = false
var clearRot
function toggleTranslateTargets(){
translatingTargets = !translatingTargets
let scene = AFRAME.scenes[0].object3D
if (translatingTargets){
let anchor = new THREE.Object3D()
let latest = selectedElements[selectedElements.length-1].element
latest.object3D.add( anchor )
// also inherits rotation, could try cancel it as the opposite of latest rotation
// might be easier to copy the position only every few ms instead
anchor.position.sub( latest.object3D.position )
//targets.map( t => anchor.attach(t.object3D) )
// should attach all BUT the current moving entity!
Array.from(document.querySelectorAll('.mab')).map( t => anchor.attach(t.object3D) )
// they don't move... despite
} else {
clearInterval( clearRot )
Array.from(document.querySelectorAll('.mab')).map( t => scene.attach(t.object3D) )
//targets.map( t => scene.attach(t.object3D) )
// could delete anchor, cleaner
}
}
var attachToPlayer = false
function toggleAttachToSelf(){
attachToPlayer = !attachToPlayer
attachToPlayer ? parent=document.querySelector("#player") : parent=AFRAME.scenes[0]
targets.map( t => parent.object3D.attach(t.object3D) )
}
function checkIntersection(latest, nearby){
//let latest = selectedElements[selectedElements.length-1].element
//let nearby = getClosestTargetElements( latest.getAttribute('position') )
// https://threejs.org/docs/?q=box#api/en/math/Box3.containsBox
// https://threejs.org/docs/?q=box#api/en/math/Box3.expandByObject
let a = new THREE.Box3().expandByObject( latest.object3D ) // consider mesh.geometry.computeBoundingBox() first
let b = new THREE.Box3().expandByObject( nearby.object3D )
console.log(a,b, a.containsBox(b))
// testable as checkIntersection( document.querySelector("[color='yellow']"), document.querySelector("[color='purple']") )
// < a-box scale = ".1 .1 .1" position = ".5 .8 -.3" color = "purple" > < / a-box >
// < a-box scale = ".2 .2 .2" position = ".5 .8 -.3" color = "yellow" > < / a-box >
}
setTimeout( _ => {
let newPostIt = addNewNoteAsPostItNote("jxr console.log(222);", "0 1.2 -.5")
.setAttribute("onreleased", "grammarBasedSnap()")
let otherPostIt = addNewNoteAsPostItNote("jxr console.log(111);", "0 1.4 -.5")
.setAttribute("onreleased", "grammarBasedSnap()")
let postIt = addNewNoteAsPostItNote("hi this is a post-it note.", "0 1.6 -.5")
.setAttribute("onreleased", "runClosestJXR(); grammarBasedSnap()") // dunno how to share the event context back here...
// .setAttribute("onreleased", "snapNext()") // does NOT support multiple instances for now
// see https://aframe.io/docs/1.5.0/core/component.html#multiple
// maybe bind could help
//let cloneMe = addNewNote('jxr clone me from corner', '0 0 .1', '1 1 1', 'cmd')
// should rebind parent...
//setTimeout( _ => { _ => cloneMe.object3D.parent = postIt.object3D }, 1000 )
// should try object3D.attach() instead
//.addEventListener('loaded',
// entityIndexes( document.querySelector("[color='blue']").object3D.children[0] )
}, 1000 )
// e.g document.querySelector("[color='blue']").object3D.children[0]
function entityIndexes(mesh){ // needs a mesh with a geometry, not a group
// could also traverse
let gp = mesh.geometry.attributes.position;
let wPos = [];
for(let i = 0;i < gp.count ; i + + ) {
let p = new THREE.Vector3().fromBufferAttribute(gp, i); // set p from `position`
mesh.localToWorld(p); // p has wordl coords
wPos.push(p);
}
// many are duplicates, i.e a a cube will return 24 indexes (4 per 6 faces), not 8
//let l = [...new Set(wPos)].length; console.log( l )
[...new Set(wPos)].map( p => addNewNote("x", p))
console.log( [...new Set(wPos)].length )
// seems to add the duplicates again
// try to "de-dup" via .distanceTo() below a threshold instead
}
function snapToGrid(gridSize=1){ // default as 1 decimeter
let latest = selectedElements[selectedElements.length-1].element
latest.setAttribute("rotation", "0 0 0")
let pos = latest.getAttribute("position")
pos.multiplyScalar(gridSize*10).round().divideScalar(gridSize*10)
latest.setAttribute("position", pos )
}
// deeper question, making the rules themselves manipulable? JXR?
// So the result of the grammar becomes manipulable, but could you make the rules of the grammar itself visual? Even manipulable?
// could start by visualizing examples first e.g https://writer.com/wp-content/uploads/2024/03/grammar-1.webp
function snapMAB(){
// multibase arithmetic blocks aka MAB cf https://en.wikipedia.org/wiki/Base_ten_block
let latest = selectedElements[selectedElements.length-1].element
let nearby = getClosestTargetElements( latest.getAttribute('position') )
let linked = []
if (nearby.length>0){
latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("rotation") ) )
latest.setAttribute("position", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("position") ) )
latest.object3D.translateX( 1/10 )
linked.push( latest )
linked.push( nearby[0].el )
let overlap = Array.from( document.querySelectorAll(".mab") ).filter( e => e.object3D.position.distanceTo( latest.object3D.position ) < 0.01 & & e ! = latest )
while (overlap.length > 0 ){
latest.object3D.translateX( 1/10 )
linked.push( overlap[0] )
overlap = Array.from( document.querySelectorAll(".mab") ).filter( e => e.object3D.position.distanceTo( latest.object3D.position ) < 0.01 & & e ! = latest )
}
// do something special if it becomes 10, e.g become a single line, removing the "ridges"
if (linked.length > 3)
linked.map( e => Array.from( e.querySelectorAll("a-box") ).setAttribute("color", "orange") )
// also need to go backward too to see if it's the latest added
}
}
function snapRightOf(){
let latest = selectedElements[selectedElements.length-1].element
let nearby = getClosestTargetElements( latest.getAttribute('position') )
if (nearby.length>0){
latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("rotation") ) )
latest.setAttribute("position", AFRAME.utils.coordinates.stringify( nearby[0].el.getAttribute("position") ) )
latest.object3D.translateX( 1/10 )
// somehow... works only the 2nd time, not the 1st?!
}
}
function grammarBasedSnap(){
// verify if snappable, e.g of same type (or not)
// e.g check if both have .getAttribute('value').match(prefix) or not
let latest = selectedElements[selectedElements.length-1].element
let nearby = getClosestTargetElements( latest.getAttribute('position') )
if (nearby.length>0){
let closest = nearby[0].el
let latestTypeJXR = latest.getAttribute('value').match(prefix)
let closestTypeJXR = latest.getAttribute('value').match(prefix)
latest.setAttribute("rotation", AFRAME.utils.coordinates.stringify( closest.getAttribute("rotation") ) )
latest.setAttribute("position", AFRAME.utils.coordinates.stringify( closest.getAttribute("position") ) )
if ( latestTypeJXR & & closestTypeJXR )
latest.object3D.translateX( 1/10 ) // same JXR type, snap close
else
latest.object3D.translateX( 2/10 ) // different types, snap away
// somehow... works only the 2nd time, not the 1st?!
}
}
< a-troika-text id = "spatial-introspection-test" anchor = left value = "console.log('executing from secondary pinch');"
onreleased="console.log('run on released')"
onpicked="console.log('run on picked')"
target position=" -0.3 1.35 0" rotation="0 40 0" scale="0.1 0.1 0.1">
< / a-troika-text >
*/
function cloneTarget(target){
let el = target.cloneNode(true)
@ -230,28 +50,11 @@ function deleteTarget(target){
function runClosestJXR(){
// ideally this would come from event details
let latest = selectedElements[selectedElements.length-1].element
let nearby = getClosestTargetElements( latest.getAttribute('position') )
// if (nearby.length>0){ interpretJXR( nearby[0].el.getAttribute("value") ) }
nearby.map( n => interpretJXR( n.el.getAttribute("value") ) )
}
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 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 el = getClosestTargetElement( latest.getAttribute('position') )
if (el) interpretJXR( el.getAttribute("value") )
}
// should move to jxr-core.js
AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent with event name
init: function(){
AFRAME.scenes[0].addEventListener('enter-vr', e => {
@ -259,10 +62,32 @@ AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent
document.querySelector("[cursor]").setAttribute("visible", "true")
document.querySelector("[camera]").setAttribute("cursor", "")
})
this.menuTargets = []
this.menuEl = document.createElement("a-box")
this.menuEl.setAttribute("scale", ".01 .01 .01")
this.menuEl.setAttribute("opacity", 0)
this.menuEl.setAttribute("color", "red")
this.menuEl.setAttribute("wireframe", "true")
this.menuOpacity = 0
AFRAME.scenes[0].appendChild(this.menuEl)
let colors =["green", "blue", "orange", "yellow", "brown", "black"].map( (c,i) => {
let newEl = document.createElement("a-box")
newEl.setAttribute("position", "" + ((i+1)*3) + " 0 0")
newEl.setAttribute("opacity", 0)
newEl.setAttribute("wireframe", "true")
newEl.setAttribute("color", c )
this.menuEl.appendChild( newEl )
this.menuTargets.push( newEl )
})
this.menuTargets.push( this.menuEl )
},
// could support multi
events: {
emptypinch: function (e) {
this.menuEl.setAttribute("position", e.detail.position)
// works with AFRAME.scenes[0].emit('emptypinch', {position:"0 0 0"})
let code = this.el.getAttribute('onemptypinch')
// if multi, should also look for onreleased__ not just onreleased
@ -271,7 +96,43 @@ AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent
} catch (error) {
console.error(`Evaluation failed with ${error}`);
}
},
emptypinchmoved: function (e) {
let foundElement = getClosestElement( e.detail.position, threshold=0.01, this.menuTargets )
this.menuTargets.map( mt => mt.setAttribute("wireframe", "true") )
if ( foundElement ) foundElement.setAttribute("wireframe", "false")
if (this.menuOpacity < 1 ) {
this.menuOpacity += .01
this.menuTargets.map( mt => mt.setAttribute("opacity", this.menuOpacity) )
}
let code = this.el.getAttribute('onemptypinchmoved')
// if multi, should also look for onreleased__ not just onreleased
try {
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
} catch (error) {
console.error(`Evaluation failed with ${error}`);
}
},
emptypinchreleased: function (e) {
// could also check if above this.menuOpacity threshold, e.g not doing below .3 to avoid unexpected action
let foundElement = getClosestElement( e.detail.position, threshold=0.02, this.menuTargets )
// does not seem to execute anymore after teleporting, did work before though
if ( foundElement ){
document.getElementById("box").setAttribute("color", foundElement.getAttribute("color") )
}
this.menuOpacity = 0
this.menuEl.setAttribute("opacity", this.menuOpacity)
this.menuTargets.map( mt => mt.setAttribute("opacity", this.menuOpacity) )
let code = this.el.getAttribute('onemptypinchreleased')
// if multi, should also look for onreleased__ not just onreleased
try {
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
} catch (error) {
console.error(`Evaluation failed with ${error}`);
}
},
}
})
@ -298,16 +159,6 @@ AFRAME.registerComponent('teleporter', {
}
});
let page = "Wiki.VirtualRealityInterface";
// should do then only once graph loaded instead, should emit event
let pageFromParam = AFRAME.utils.getUrlParameter('page')
if (pageFromParam) page = pageFromParam
setTimeout( _ => {
Array.from( document.querySelectorAll("[value='"+page+"']") ).map( n =>
n.setAttribute("onreleased", "console.log('dropped, should toggle display children,"+n.id+"')"));
Array.from( document.querySelectorAll("[value='"+page+"']>a-sphere") ).map( n => n.setAttribute("color", "purple"))
}, 5000)
< / script >
< 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/" >
@ -358,36 +209,6 @@ setTimeout( _ => {
< a-troika-text anchor = left value = "jxr setTimeout( _ => toggleAttachToSelf(), 1000); toggleAttachToSelf()" target position = " -0.3 1.25 0" rotation = "0 40 0" scale = "0.1 0.1 0.1" > < / a-troika-text >
< a-box scale = ".07 .07 .07" class = "mab" target position = ".3 1.6 -.5" color = "brown" onreleased = "snapMAB()" >
< a-box scale = "1.5 1.5 1" color = "brown" > < / a-box >
< a-box scale = "1 1.5 1.5" color = "brown" > < / a-box >
< a-box scale = "1.5 1 1.5" color = "brown" > < / a-box >
< / a-box >
< a-box scale = ".07 .07 .07" class = "mab" target position = ".1 1.6 -.5" color = "brown" onreleased = "snapMAB()" >
< a-box scale = "1.5 1.5 1" color = "brown" > < / a-box >
< a-box scale = "1 1.5 1.5" color = "brown" > < / a-box >
< a-box scale = "1.5 1 1.5" color = "brown" > < / a-box >
< / a-box >
< a-box scale = ".07 .07 .07" class = "mab" target position = ".5 1.6 -.5" color = "brown" onreleased = "snapMAB()" >
< a-box scale = "1.5 1.5 1" color = "brown" > < / a-box >
< a-box scale = "1 1.5 1.5" color = "brown" > < / a-box >
< a-box scale = "1.5 1 1.5" color = "brown" > < / a-box >
< / a-box >
< a-box scale = ".07 .07 .07" class = "mab" target position = "-.5 1.6 -.5" color = "brown" onreleased = "snapMAB()" >
< a-box scale = "1.5 1.5 1" color = "brown" > < / a-box >
< a-box scale = "1 1.5 1.5" color = "brown" > < / a-box >
< a-box scale = "1.5 1 1.5" color = "brown" > < / a-box >
< / a-box >
< a-box scale = ".1 .1 .1" target position = ".5 1.6 -.3" color = "blue" onreleased = "snapToGrid()"
annotation="content:could also show/hide grid with gridHelper on pinch started and hide on release"
>< / a-box >
< a-box scale = ".1 .1 .1" target position = ".5 1.8 -.3" color = "blue" onreleased = "snapToGrid()" > < / a-box >
< a-box scale = ".1 .1 .1" position = ".5 .8 -.3" color = "purple" > < / a-box >
< a-box scale = ".2 .2 .2" position = ".5 .8 -.3" color = "yellow" > < / a-box >
< a-box teleporter height = ".1" class = "teleportable" material = "color: cyan" position = "3.5 0 -3.5" > < / a-box >
< a-box teleporter height = ".1" class = "teleportable" material = "color: cyan" position = "-4 0 4" > < / a-box >
< a-box teleporter height = ".1" class = "teleportable" material = "color: cyan" position = "3 3 4" >