|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<title>SpaSca : Spatial Scaffolding</title>
|
|
|
|
<head>
|
|
|
|
<!-- Suggestions? https://git.benetou.fr/utopiah/text-code-xr-engine/issues/ -->
|
|
|
|
|
|
|
|
<script src='dependencies/aframe.offline.min.js'></script>
|
|
|
|
<script src="dependencies/a-console.js"></script>
|
|
|
|
<script src='dependencies/aframe-troika-text.min.js'></script>
|
|
|
|
|
|
|
|
<script src="dependencies/shiki0.14.1.js"></script>
|
|
|
|
<!-- bit demanding but it IS about code too, so arguably important enough -->
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.0/dist/aframe-extras.min.js"></script>
|
|
|
|
|
|
|
|
<script src='jxr-core.js'></script>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
// goal : being able to modify the bindings in XR, hence the manager part
|
|
|
|
|
|
|
|
const gestures = [];
|
|
|
|
// start with : left pinch, right pinch, right index tap
|
|
|
|
var gesturesTargets = []
|
|
|
|
// start with target (updated by target component) and selectors e.g #box (maybe semantically clarified, i.e follow wrist)
|
|
|
|
|
|
|
|
var boundGestures = [];
|
|
|
|
// start with
|
|
|
|
// left pinch ON [target] (correct selector)
|
|
|
|
// right pinch ON [target]
|
|
|
|
// right index tap ON #box
|
|
|
|
|
|
|
|
AFRAME.registerComponent('empty', {
|
|
|
|
init: function(){
|
|
|
|
console.log('empty component example')
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
function listBoundGestures(){
|
|
|
|
console.log('could start with : left pinch, right pinch, box tap')
|
|
|
|
console.log(boundGestures, gestures, gesturesTargets)
|
|
|
|
console.log('pinchprimary pinchsecondary wristattachsecondary="target: #box" pressable start-on-press')
|
|
|
|
}
|
|
|
|
|
|
|
|
AFRAME.registerComponent('sumvectors', {
|
|
|
|
schema: {
|
|
|
|
v1: {type: 'selector'},
|
|
|
|
v2: {type: 'selector'}
|
|
|
|
},
|
|
|
|
init: function(){
|
|
|
|
updateSumVector( this.data.v1, this.data.v2 )
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
function updateSumVector( sv1, sv2 ){
|
|
|
|
console.log('updating sum vector', sv1.id, sv2.id)
|
|
|
|
let existingVector = document.getElementById( "vectorsum_" + sv1.id + "_" + sv2.id )
|
|
|
|
if ( existingVector ) existingVector.remove()
|
|
|
|
|
|
|
|
let v1 = sv1.getAttribute("path").split(',')
|
|
|
|
let v1_a = new THREE.Vector3().copy( AFRAME.utils.coordinates.parse( v1[0] ) )
|
|
|
|
let v1_b = new THREE.Vector3().copy( AFRAME.utils.coordinates.parse( v1[1] ) )
|
|
|
|
let v2 = sv2.getAttribute("path").split(',')
|
|
|
|
let v2_a = new THREE.Vector3().copy( AFRAME.utils.coordinates.parse( v2[0] ) )
|
|
|
|
let v2_b = new THREE.Vector3().copy( AFRAME.utils.coordinates.parse( v2[1] ) )
|
|
|
|
|
|
|
|
let vs_a = v1_a.clone()
|
|
|
|
let vs_b = vs_a.clone()
|
|
|
|
.add( v1_b.sub( v1_a ) )
|
|
|
|
.add( v2_b.sub( v2_a ) )
|
|
|
|
|
|
|
|
let tubeEl = document.createElement("a-tube")
|
|
|
|
tubeEl.id = "vectorsum_" + sv1.id + "_" + sv2.id
|
|
|
|
tubeEl.setAttribute("path", AFRAME.utils.coordinates.stringify(vs_a) +', '+AFRAME.utils.coordinates.stringify(vs_b) )
|
|
|
|
tubeEl.setAttribute("radius", "0.005")
|
|
|
|
AFRAME.scenes[0].appendChild(tubeEl)
|
|
|
|
let coneEl = document.createElement("a-cone")
|
|
|
|
coneEl.setAttribute("scale", ".02 .02 .02")
|
|
|
|
coneEl.setAttribute("position", AFRAME.utils.coordinates.stringify(vs_b) ) // missing rotation
|
|
|
|
tubeEl.appendChild(coneEl) // parenting is kind of "fake" as it doesn't inherent position/rotation
|
|
|
|
}
|
|
|
|
|
|
|
|
AFRAME.registerComponent('generate-anchors', {
|
|
|
|
init: function(){
|
|
|
|
return
|
|
|
|
let points = this.el.getAttribute('path').split(',')
|
|
|
|
let id = this.el.id
|
|
|
|
points.map( (p,i) => {
|
|
|
|
let anchorEl = document.createElement("a-entity")
|
|
|
|
anchorEl.id = id + "_" + i
|
|
|
|
anchorEl.setAttribute("target", "")
|
|
|
|
anchorEl.setAttribute("binding-anchor", "") // could use this value instead of
|
|
|
|
anchorEl.setAttribute("onreleased", "moveAnchorPointTarget('"+anchorEl.id+"')" )
|
|
|
|
anchorEl.setAttribute("position", p.trim() )
|
|
|
|
anchorEl.setAttribute("scale", ".2 .2 .2" )
|
|
|
|
AFRAME.scenes[0].appendChild(anchorEl)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
function moveAnchorPointTarget(id){
|
|
|
|
console.log('updating the position of'+id+' with new hand position')
|
|
|
|
let [targetId,numberOnPath] = id.split('_')
|
|
|
|
let targetEl = document.getElementById(targetId)
|
|
|
|
let path = targetEl.getAttribute("path").split(',')
|
|
|
|
path[numberOnPath] = AFRAME.utils.coordinates.stringify( document.getElementById(id).getAttribute('position') )
|
|
|
|
targetEl.setAttribute("path", path.join(",") )
|
|
|
|
|
|
|
|
Array.from( document.querySelectorAll("[id*='vectorsum_']") )
|
|
|
|
.filter( el => el.id.includes(targetId) )
|
|
|
|
.map( el => {
|
|
|
|
let [sv1, sv2] = el.id.replace( "vectorsum_", '').split( '_' )
|
|
|
|
console.log(targetId, sv1, sv2)
|
|
|
|
updateSumVector( document.getElementById(sv1), document.getElementById(sv2) )
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// switchSide() showcase a selector/attribute model. Maybe others do work this way.
|
|
|
|
// might be possible to have a onreleased neary a selector, e.g #rightHand as a way to manage gestures
|
|
|
|
// e.g onreleased="" on #rightHand or #leftHand could add/remove the attribute, e.g pinchprimary/pinchsecondary
|
|
|
|
// it could also remove it for any other entity if it's an exclusive attribute (can only be used once)
|
|
|
|
|
|
|
|
// need to list which entities can actually receive an attribute related to gesture
|
|
|
|
// for example pinchprimary/pinchsecondary need to be on entities with hand-tracking-controls
|
|
|
|
// it would not make sense to expect a pinch from a sphere nor a single finger of a hand
|
|
|
|
|
|
|
|
// draw a line between a selector and its instancing
|
|
|
|
// e.g between "#rightHand" and actually the element with this id
|
|
|
|
|
|
|
|
var tips = {
|
|
|
|
left : {},
|
|
|
|
right : {},
|
|
|
|
}
|
|
|
|
|
|
|
|
AFRAME.registerComponent('draw-on-board', {
|
|
|
|
init: function () {
|
|
|
|
// get right hand fingertip, if "beyond" board, add something
|
|
|
|
// boardEl.setAttribute("position", "0 1.5 -1.2")
|
|
|
|
this.z = document.getElementById("board").getAttribute("position").z
|
|
|
|
this.z = -1.2 // cheating, needed as it does yet exist
|
|
|
|
this.throttledFunction = AFRAME.utils.throttle(this.draw, 50, this);
|
|
|
|
this.p = document.querySelector('[pinchprimary]')
|
|
|
|
this.tip = new THREE.Vector3(); // create once an reuse it
|
|
|
|
this.lastPoint = null
|
|
|
|
},
|
|
|
|
draw: function () {
|
|
|
|
let tip = this.tip
|
|
|
|
this.p.object3D.traverse( e => { if (e.name == 'index-finger-tip' ) tip = e.position })
|
|
|
|
if (tip.z) {
|
|
|
|
if (tip.z < this.z){
|
|
|
|
let newPos = new THREE.Vector3( tip.x, (tip.y+1.5), (this.z+.01) )
|
|
|
|
if ( this.lastPoint && this.lastPoint.distanceTo( newPos ) > .01 ){
|
|
|
|
// merge nearby point
|
|
|
|
let drawEl = document.createElement("a-box")
|
|
|
|
drawEl.setAttribute("width", .01)
|
|
|
|
drawEl.setAttribute("depth", .02)
|
|
|
|
drawEl.setAttribute("height", .01)
|
|
|
|
drawEl.setAttribute("color", "red")
|
|
|
|
drawEl.setAttribute("position", "" + tip.x + " " + (tip.y+1.5) + " " + (this.z+.01) )
|
|
|
|
// tip.y seems to be too low... there is an offset too
|
|
|
|
AFRAME.scenes[0].appendChild( drawEl )
|
|
|
|
}
|
|
|
|
this.lastPoint = newPos
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
tick: function (t, dt) {
|
|
|
|
this.throttledFunction(); // Called once a second.
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
AFRAME.registerComponent('save-on-exit-xr', {
|
|
|
|
init: function () {
|
|
|
|
var sceneEl = this.el;
|
|
|
|
sceneEl.addEventListener('exit-vr', _ => {
|
|
|
|
saveBoard() // only when exiting VR proper, NOT "multitasking" so added onreleased to enable that workflow too
|
|
|
|
showBoardFromHash()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
function saveBoard(){
|
|
|
|
let positions = [] // ordered is preserved as the elements are created in order too (bit risky)
|
|
|
|
getArrayFromClass("pannels").map( p => {
|
|
|
|
let v = p.getAttribute("position")
|
|
|
|
|
|
|
|
let o = new THREE.Vector3()
|
|
|
|
o.x = v.x.toFixed(3)
|
|
|
|
o.y = v.y.toFixed(3)
|
|
|
|
o.z = v.z.toFixed(3)
|
|
|
|
|
|
|
|
positions.push( o )
|
|
|
|
})
|
|
|
|
|
|
|
|
let data = {}
|
|
|
|
data.positions = positions
|
|
|
|
|
|
|
|
window.location.hash = JSON.stringify(data)
|
|
|
|
// generate URL and append the optional forced 2D overlay parameter
|
|
|
|
// warning : a URI has a maximum length which might be too short due to text content
|
|
|
|
// we can rely on the index position of the array ASSUMING that the .json file does not change between both moments
|
|
|
|
}
|
|
|
|
|
|
|
|
//const billBoardSourceURL = "../content/nlnetproposal.json"
|
|
|
|
const billBoardSourceURL = "../content/snippets.json"
|
|
|
|
|
|
|
|
function showBoardFromHash(){ // the flattening... not very useful for now, ideally could be brought to a better canvas
|
|
|
|
let positions = JSON.parse( decodeURI( window.location.hash.substring(1) ) ).positions
|
|
|
|
|
|
|
|
//could already do a preview
|
|
|
|
let canvas2DpreviewEl = document.createElement('div')
|
|
|
|
document.body.appendChild( canvas2DpreviewEl )
|
|
|
|
canvas2DpreviewEl.style = "zIndex:99; position:absolute; top:0; left:0; width:90%; height:90%; background-color:black;"
|
|
|
|
|
|
|
|
// done again, should be cached instead
|
|
|
|
fetch(billBoardSourceURL).then( r => r.json() ).then( json => {
|
|
|
|
let count = json.length // can get too high, thus unreachable, sticking to 1m total
|
|
|
|
json.map( (l,i) => {
|
|
|
|
let content = l
|
|
|
|
// trim very long texts (should evoque the memory of instead, other cluttering)
|
|
|
|
if (count > 500) content = l.substring(0,500) + "\n..."
|
|
|
|
let boardEl = document.createElement('span')
|
|
|
|
boardEl.innerText = content
|
|
|
|
boardEl.draggable = true
|
|
|
|
let pos = positions[i]
|
|
|
|
let x = 500 + pos.x * 1000
|
|
|
|
let y = pos.y * 500 - 200
|
|
|
|
let z = Math.round( pos.z*100 + 999 )
|
|
|
|
boardEl.style = "z-index:"+z+"; position:absolute; top:"+y+"px; left:"+x+"px; color:white; width: 1000px;"
|
|
|
|
boardEl.className = "boardElement2D"
|
|
|
|
canvas2DpreviewEl.appendChild( boardEl )
|
|
|
|
dragElement( boardEl )
|
|
|
|
})
|
|
|
|
|
|
|
|
// add 2D button on corner to copy data to clipboard
|
|
|
|
let boardButtonData = document.createElement( "input" )
|
|
|
|
boardButtonData.id = "boardbuttondata"
|
|
|
|
let content = []
|
|
|
|
getArrayFromClass("boardElement2D").map( p => content.push( {
|
|
|
|
zIndex: p.style.zIndex,
|
|
|
|
x: p.style.left,
|
|
|
|
y: p.style.top
|
|
|
|
})
|
|
|
|
)
|
|
|
|
boardButtonData.style = "z-index:999; position:absolute; top:0px; left:0px; color:black; width: 100px;"
|
|
|
|
boardButtonData.value = JSON.stringify( content )
|
|
|
|
canvas2DpreviewEl.appendChild( boardButtonData )
|
|
|
|
|
|
|
|
|
|
|
|
/* from https://visual-meta.info */
|
|
|
|
let textareaEl = document.createElement( "textarea" )
|
|
|
|
textareaEl.id = "textareavisualmeta"
|
|
|
|
textareaEl.value = "@{visual-meta-start}"
|
|
|
|
getArrayFromClass("boardElement2D").map( p =>
|
|
|
|
textareaEl.value += `\ncontent={${p.innerText}}\nposition={${p.style.left.replace('px','')},${p.style.top.replace('px','')},${p.style.zIndex}}`
|
|
|
|
)
|
|
|
|
textareaEl.value += "\n@{visual-meta-start}"
|
|
|
|
textareaEl.style = "z-index:999; position:absolute; bottom:0px; left:0px; color:black; width: 800px; height:400px;"
|
|
|
|
canvas2DpreviewEl.appendChild( textareaEl )
|
|
|
|
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateBoardButtonData(){
|
|
|
|
let boardButtonData = document.getElementById("boardbuttondata")
|
|
|
|
let content = []
|
|
|
|
getArrayFromClass("boardElement2D").map( p => content.push( {
|
|
|
|
zIndex: p.style.zIndex,
|
|
|
|
x: p.style.left,
|
|
|
|
y: p.style.top
|
|
|
|
})
|
|
|
|
)
|
|
|
|
boardButtonData.value = JSON.stringify( content )
|
|
|
|
|
|
|
|
let textareaEl = document.getElementById("textareavisualmeta")
|
|
|
|
textareaEl.value = "@{visual-meta-start}"
|
|
|
|
getArrayFromClass("boardElement2D").map( p =>
|
|
|
|
textareaEl.value += `\ncontent={${p.innerText}}\nposition={${p.style.left.replace('px','')},${p.style.top.replace('px','')},${p.style.zIndex}}`
|
|
|
|
)
|
|
|
|
textareaEl.value += "\n@{visual-meta-start}"
|
|
|
|
textareaEl.style = "z-index:999; position:absolute; bottom:0px; left:0px; color:black; width: 800px; height:400px;"
|
|
|
|
|
|
|
|
// JSON Canvas support
|
|
|
|
|
|
|
|
var JSONCanvas = { nodes : [], edges : [] }
|
|
|
|
getArrayFromClass("boardElement2D").map( (p,i) => JSONCanvas.nodes.push( {
|
|
|
|
x: Number(p.style.left.replace('px','')),
|
|
|
|
y: Number(p.style.top.replace('px','')),
|
|
|
|
text: p.innerText,
|
|
|
|
type: "text",
|
|
|
|
width: 200,
|
|
|
|
height: 200,
|
|
|
|
id: "nodeid_"+i // as there is not set id for now
|
|
|
|
})
|
|
|
|
)
|
|
|
|
// edges could be updated based on links as <a-line> or <a-tube> but this isn't done in XR yet.
|
|
|
|
console.log( JSONCanvas )
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function dragElement(elmnt) { // from https://www.w3schools.com/howto/howto_js_draggable.asp
|
|
|
|
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
|
|
|
elmnt.onmousedown = dragMouseDown;
|
|
|
|
|
|
|
|
function dragMouseDown(e) {
|
|
|
|
e = e || window.event;
|
|
|
|
e.preventDefault();
|
|
|
|
// get the mouse cursor position at startup:
|
|
|
|
pos3 = e.clientX;
|
|
|
|
pos4 = e.clientY;
|
|
|
|
document.onmouseup = closeDragElement;
|
|
|
|
// call a function whenever the cursor moves:
|
|
|
|
document.onmousemove = elementDrag;
|
|
|
|
}
|
|
|
|
|
|
|
|
function elementDrag(e) {
|
|
|
|
e = e || window.event;
|
|
|
|
e.preventDefault();
|
|
|
|
// calculate the new cursor position:
|
|
|
|
pos1 = pos3 - e.clientX;
|
|
|
|
pos2 = pos4 - e.clientY;
|
|
|
|
pos3 = e.clientX;
|
|
|
|
pos4 = e.clientY;
|
|
|
|
// set the element's new position:
|
|
|
|
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
|
|
|
|
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
|
|
|
|
}
|
|
|
|
|
|
|
|
function closeDragElement() {
|
|
|
|
// stop moving when mouse button is released:
|
|
|
|
document.onmouseup = null;
|
|
|
|
document.onmousemove = null;
|
|
|
|
// note that this should probably save back to the hash too, otherwise no persistance.
|
|
|
|
// could also consider saving to JSON or whatever useful format (probably https://jsoncanvas.org )
|
|
|
|
updateBoardButtonData()
|
|
|
|
}
|
|
|
|
} // done
|
|
|
|
|
|
|
|
AFRAME.registerComponent('billboard-content', {
|
|
|
|
init: function(){
|
|
|
|
let positions = false
|
|
|
|
if (window.location.hash)
|
|
|
|
positions = JSON.parse( decodeURI( window.location.hash.substring(1) ) ).positions
|
|
|
|
// assume JSON is correct...
|
|
|
|
|
|
|
|
let boardEl = document.createElement("a-box")
|
|
|
|
boardEl.setAttribute("width", 2)
|
|
|
|
boardEl.setAttribute("depth", .01)
|
|
|
|
boardEl.setAttribute("height", 1)
|
|
|
|
boardEl.setAttribute("color", "#303030")
|
|
|
|
boardEl.setAttribute("position", "0 1.5 -1.2")
|
|
|
|
boardEl.id = "board"
|
|
|
|
AFRAME.scenes[0].appendChild( boardEl )
|
|
|
|
var grid = new THREE.GridHelper(1, 10, 0xFFFFFF, 0x888888 );
|
|
|
|
grid.rotateX(Math.PI/2)
|
|
|
|
grid.translateY(.01)
|
|
|
|
grid.translateX(.5)
|
|
|
|
boardEl.object3D.add(grid);
|
|
|
|
var grid = new THREE.GridHelper(1, 10, 0xFFFFFF, 0x888888 );
|
|
|
|
grid.rotateX(Math.PI/2)
|
|
|
|
grid.translateY(.01)
|
|
|
|
grid.translateX(-.5)
|
|
|
|
boardEl.object3D.add(grid);
|
|
|
|
fetch('../content/DynamicView_260924.json').then( r => r.json() ).then( json => {
|
|
|
|
let positionedNodes = {}
|
|
|
|
console.log( json.nodes.map( l => l.title ) )
|
|
|
|
json.nodes.map( (l,i) => {
|
|
|
|
let noteEl
|
|
|
|
let pos = json.layout.nodePositions[l.identifier]
|
|
|
|
let x = pos.x/1000
|
|
|
|
let y = 1.2-pos.y/1000
|
|
|
|
noteEl = addNewNote(l.title, ""+x+" "+y+ " -1", scale=".1 .1 .1", null, "pannels") // should add a class for 2D export
|
|
|
|
noteEl.setAttribute("max-width", 10)
|
|
|
|
positionedNodes[l.title] = ""+x+" "+y+ " -1"
|
|
|
|
})
|
|
|
|
fetch('../content/glossary.json').then( r => r.json() ).then( glossaryJson => {
|
|
|
|
let nodesNames = json.nodes.map( l => l.title )
|
|
|
|
let found = []
|
|
|
|
let entries = Object.values(glossaryJson.entries)
|
|
|
|
json.nodes.map( n => {
|
|
|
|
found.push( entries.filter(e=>e.entry.includes(n.title)).map( e => { return {title: n.title, entry: e.phrase} }) )
|
|
|
|
})
|
|
|
|
found = found.filter( e=> e.length ).map( m => { // list of pairs, but not all entry are nodes, so must remove them
|
|
|
|
return m.filter( n => nodesNames.includes(n.entry) )
|
|
|
|
})
|
|
|
|
// might have to remove duplicates as some definition have multiple entries
|
|
|
|
// e.g. Glossary or Visual-Meta
|
|
|
|
found.map( f => {
|
|
|
|
f.map( fe => {
|
|
|
|
let tubeEl = document.createElement("a-tube")
|
|
|
|
tubeEl.setAttribute("path", positionedNodes[fe.title] +', '+positionedNodes[fe.entry])
|
|
|
|
tubeEl.setAttribute("radius", "0.001")
|
|
|
|
tubeEl.setAttribute("opacity", "0.1")
|
|
|
|
AFRAME.scenes[0].appendChild(tubeEl)
|
|
|
|
// won't get updated live while moving though, assuming the nodes are NOT picked
|
|
|
|
// could rely on the live-selector-line component instead, relying on selectors
|
|
|
|
// present in the companion exploration
|
|
|
|
// ~/Prototypes/fot_sloan_companion/public/index.html
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
})
|
|
|
|
})
|
|
|
|
// bringing content in
|
|
|
|
fetch(billBoardSourceURL).then( r => r.json() ).then( json => {
|
|
|
|
let count = json.length // can get too high, thus unreachable, sticking to 1m total
|
|
|
|
json.map( (l,i) => {
|
|
|
|
let content = l
|
|
|
|
// trim very long texts (should evoque the memory of instead, other cluttering)
|
|
|
|
if (count > 500) content = l.substring(0,500) + "\n..."
|
|
|
|
// could filter out or highlight when l begins with "TODO:"
|
|
|
|
let noteEl
|
|
|
|
if (positions)
|
|
|
|
noteEl = addNewNote(content, positions[i], scale=".1 .1 .1", null, "pannels") // should add a class for 2D export
|
|
|
|
else
|
|
|
|
noteEl = addNewNote(content, "-0.5 "+ (i/count+0.7)+" "+(-0.5-Math.random()), scale=".1 .1 .1", null, "pannels") // should add a class for 2D export
|
|
|
|
noteEl.setAttribute("max-width", 10)
|
|
|
|
noteEl.setAttribute("jsonID", i)
|
|
|
|
noteEl.setAttribute("onreleased", "saveBoard()")
|
|
|
|
// could also possible insure that it's in front of the board, always
|
|
|
|
})
|
|
|
|
billboarding = true
|
|
|
|
makeAnchorsVisibleOnTargets()
|
|
|
|
})
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
AFRAME.registerComponent('selector-line', {
|
|
|
|
init: function(){
|
|
|
|
this.newLine = document.createElement("a-entity")
|
|
|
|
this.newLine.setAttribute("line", "start: 0, 0, 0; end: 0 0 0.01; color: red")
|
|
|
|
AFRAME.scenes[0].appendChild( this.newLine )
|
|
|
|
this.worldPosition=new THREE.Vector3()
|
|
|
|
this.hr=new THREE.Vector3()
|
|
|
|
this.hl=new THREE.Vector3()
|
|
|
|
|
|
|
|
document.querySelector('a-scene').addEventListener('enter-vr', _ => {
|
|
|
|
// seems there is a slight delay to get children for hands and no hasLoaded event to catch
|
|
|
|
setTimeout( _ => {
|
|
|
|
["left", "right"].map( (side, i) =>
|
|
|
|
[ "index-finger", "middle-finger", "ring-finger", "pinky-finger", "thumb"].map( (part, j) => {
|
|
|
|
document.querySelector('[hand-tracking-controls="hand: '+side+';"]').object3D.traverse( e => { if (e.name == part+"-tip") tips[side][part] = e })
|
|
|
|
addNewNote("#"+side+"_"+part+"_tip", "-1 "+ (j/10+1)+" "+(-1+i/10)) // for surfacing affordances as selectors
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}, 500
|
|
|
|
)
|
|
|
|
// risky bet, maybe hand are occluded when enter VR
|
|
|
|
} )
|
|
|
|
// might also want to "remove" them on leaving VR (works for AR and VR)
|
|
|
|
},
|
|
|
|
tick: function(){
|
|
|
|
let dist = 0
|
|
|
|
if (tips.left.thumb) { // assuming we are getting both hands which is not necessarily true
|
|
|
|
let proximityPairs = [
|
|
|
|
{ pair : [ tips.left.thumb, tips.right.thumb ], msg : 'thumbs touching-ish', eventName: "touchingThumbs"},
|
|
|
|
{ pair : [ tips.left["pinky-finger"], tips.right["pinky-finger"] ], msg : 'pinkies touching-ish', eventName: "touchingPinkies" },
|
|
|
|
{ pair : [ tips.right["pinky-finger"], tips.right["thumb"] ], msg : 'right pinky to thumb touching-ish', eventName: "rightPinkyToThumbTouching" },
|
|
|
|
// for the gesture manager those pairs and msg/events directly manipulable as selectors (cf this overall component)
|
|
|
|
]
|
|
|
|
|
|
|
|
proximityPairs.map( rule => {
|
|
|
|
dist = 0
|
|
|
|
dist = rule.pair[0].position.distanceTo(rule.pair[1].position)
|
|
|
|
let threshold = .04 // tricky threshold, assuming with better camera and CV it will improve over time
|
|
|
|
if ( dist > 0 && dist < threshold) {
|
|
|
|
console.log( rule.msg )
|
|
|
|
AFRAME.scenes[0].emit( rule.eventName )
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
// note that is about RELATIVE position, not absolute position!
|
|
|
|
|
|
|
|
// unfortunately here it does not work as it's not the entity itself for the hand that has a position... (cf NAF discussions)
|
|
|
|
let worldPosition = this.worldPosition
|
|
|
|
document.querySelector( this.el.getAttribute("value") ).object3D.traverse( e => { if (e.name == "wrist") {
|
|
|
|
worldPosition.copy(e.position);e.parent.updateMatrixWorld();e.parent.localToWorld(worldPosition)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
let startPos = this.el.getAttribute("position") // does not seem to update..?
|
|
|
|
this.newLine.setAttribute("line", "end", AFRAME.utils.coordinates.stringify( worldPosition ) )
|
|
|
|
this.newLine.setAttribute("line", "start", AFRAME.utils.coordinates.stringify( startPos) )
|
|
|
|
|
|
|
|
//console.log( startPos, worldPosition)
|
|
|
|
},
|
|
|
|
|
|
|
|
// try pinching modifier, e.g pinch while holding the SHIFT key
|
|
|
|
})
|
|
|
|
|
|
|
|
</script>
|
|
|
|
<a-scene startfunctions save-on-exit-xr billboard-content draw-on-board>
|
|
|
|
<!-- Apartment 2 by Gabriele Romagnoli [CC-BY] via Poly Pizza -->
|
|
|
|
<a-gltf-model hide-on-enter-ar="" id="environment" rotation="0 90 0" position="-.3 .8 4" scale="8 8 8" gltf-model="../content/Apartment2.glb"></a-gltf-model>
|
|
|
|
<a-box color="gray" position="0 0 -1.6" scale="10 0.1 0.2"></a-box>
|
|
|
|
<a-box color="gray" position="0 2.7 -1.6" scale="10 0.1 0.2"></a-box>
|
|
|
|
<a-box color="gray" position="3.2 1.3 -1.6" scale="0.1 3 0.2"></a-box>
|
|
|
|
<a-box color="gray" position="-3.1 1.3 -1.6" scale="0.1 3 0.2"></a-box>
|
|
|
|
|
|
|
|
<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 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-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="lightblue"></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-console position="-1 1.5 0" rotation="0 45 0" font-size="34" height=1 skip-intro=true></a-console>
|
|
|
|
|
|
|
|
<a-troika-text anchor=left target selector-line value="#rightHand" position="1 1.30 -0.4" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
|
|
|
|
<a-troika-text anchor=left target id="listboundgestures" value="jxr listBoundGestures()" position="1 1.40 -0.4" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<a-troika-text anchor=left target id="switchside" value="jxr switchSide()" position="1 1.35 -0.4" scale="0.1 0.1 0.3"></a-troika-text>
|
|
|
|
<a-troika-text anchor=left target id="togglebillboarding" value="jxr toggleBillboarding()" position="1 1.55 -0.4" scale="0.1 0.1 0.3"></a-troika-text>
|
|
|
|
|
|
|
|
<a-troika-text anchor=left target id="locationreload" value="jxr location.reload()" position="1 1.20 -0.4" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<a-troika-text anchor=left target id="makeAnchorsVisibleOnTargets" value="jxr makeAnchorsVisibleOnTargets()" position="1 1.05 -0.4" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
|
|
|
|
<a-troika-text anchor=left target id="clearhash" value="jxr window.location.hash=''" position="-1 2.05 -1" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
|
|
|
|
<!-- need to be able to bind each anchor point as targets that update the matching a-tube path-->
|
|
|
|
|
|
|
|
<a-entity visible=false>
|
|
|
|
<a-tube generate-anchors id="number1" path="-0.3 1.5 -1, -0.2 1.6 -1, -0.2 1.4 -1" radius="0.01" material="color: red"></a-tube>
|
|
|
|
<a-tube generate-anchors id="number4" path="0.1 1.6 -1, 0 1.5 -1, 0.1 1.5 -1, 0.1 1.4 -1" radius="0.01" material="color: red"></a-tube>
|
|
|
|
|
|
|
|
<a-tube generate-anchors id="vector1" path="0.3 1.4 -1, 0.3 1.6 -1" radius="0.005" material="color: green">
|
|
|
|
<a-cone scale=".02 .02 .02" color="green" position="0.3 1.6 -1"></a-cone><!-- should stick to end -->
|
|
|
|
</a-tube>
|
|
|
|
|
|
|
|
<a-tube generate-anchors id="vector2" path="0.3 1.4 -1, 0.5 1.6 -1" radius="0.005" material="color: blue">
|
|
|
|
<a-cone scale=".02 .02 .02" color="blue" position="0.5 1.6 -1"></a-cone><!-- should stick to end, also rotate -->
|
|
|
|
</a-tube>
|
|
|
|
|
|
|
|
<a-entity NOsumvectors="v1:#vector1; v2:#vector2"></a-entity>
|
|
|
|
</a-entity>
|
|
|
|
|
|
|
|
</a-scene>
|
|
|
|
</body>
|
|
|
|
</script>
|
|
|
|
</html>
|