SpaSca : open SCAffolding to SPAcially and textualy explore interfaces
https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/
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.
408 lines
15 KiB
408 lines
15 KiB
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<!-- Suggestions? https://git.benetou.fr/utopiah/text-code-xr-engine/issues/ -->
|
|
|
|
<script src='dependencies/aframe.offline.min.js'></script>
|
|
<script src='dependencies/aframe-troika-text.min.js'></script>
|
|
<!-- for content sharing, using NAF
|
|
<script src='dependencies/socket.io.slim.js'></script>
|
|
<script src='dependencies/networked-aframe.min.js'></script>
|
|
must also include specific /easyrtc/easyrtc.js
|
|
-->
|
|
</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='gitea_logo.svg'>
|
|
</a>
|
|
</div>
|
|
|
|
<script>
|
|
|
|
const prefix = /^jxr /
|
|
const codeFontColor = "lightgrey"
|
|
const fontColor= "white"
|
|
const maxItems = 10
|
|
const now = Math.round( +new Date()/1000 ) //ms in JS, seconds in UNIX epoch
|
|
var selectedElement = null;
|
|
var targets = []
|
|
const zeroVector3 = new THREE.Vector3()
|
|
var bbox = new THREE.Box3()
|
|
bbox.min.copy( zeroVector3 )
|
|
bbox.max.copy( zeroVector3 )
|
|
var selectionBox = new THREE.BoundingBoxHelper( bbox.object3D, 0x0000ff);
|
|
var groupHelpers = []
|
|
var primaryPinchStarted = false
|
|
var visible = true
|
|
var setupMode = false
|
|
var setupBBox = {}
|
|
var wristShortcut = "jxr switchToWireframe()"
|
|
var selectionPinchMode = false
|
|
var groupingMode = false
|
|
var sketchEl
|
|
var lastPointSketch
|
|
var hudTextEl // should instead rely on the #typinghud selector in most cases
|
|
const startingText = "[]"
|
|
var drawingMode = false
|
|
var added = []
|
|
const maxItemsFromSources = 20
|
|
let alphabet = ['abcdefghijklmnopqrstuvwxyz', '0123456789', '<>'];
|
|
var commandhistory = []
|
|
const savedProperties = [ "src", "position", "rotation", "scale", "value", ] // add newer properties e.g visibility and generator as class
|
|
var groupSelection = []
|
|
var cabin //storage for load/save. Should use a better name as this is a reminder of a past version rather than something semantically useful.
|
|
const url = "https://fabien.benetou.fr/PIMVRdata/CabinData?action="
|
|
var primarySide = 0
|
|
const sides = ["right", "left"]
|
|
// could be an array proper completed on each relevant component registration
|
|
var pinches = [] // position, timestamp, primary vs secondary
|
|
var dl2p = null // from distanceLastTwoPinches
|
|
var selectedElements = [];
|
|
|
|
AFRAME.registerComponent('target', {
|
|
init: function () {
|
|
targets.push( this.el )
|
|
}
|
|
})
|
|
|
|
function getClosestTargetElements( pos, threshold=0.05 ){
|
|
// TODO Bbox intersects rather than position
|
|
return targets.filter( e => e.getAttribute("visible") == true).map( t => { return { el: t, dist : pos.distanceTo(t.getAttribute("position") ) } })
|
|
.filter( t => t.dist < threshold )
|
|
.sort( (a,b) => a.dist > b.dist)
|
|
}
|
|
|
|
function getClosestTargetElement( pos, threshold=0.05 ){ // 10x lower threshold for flight mode
|
|
var res = null
|
|
const matches = getClosestTargetElements( pos, threshold)
|
|
if (matches.length > 0) res = matches[0].el
|
|
return res
|
|
}
|
|
|
|
function appendToHUD(txt){
|
|
const textHUD = document.querySelector("#typinghud").getAttribute("value")
|
|
if ( textHUD == startingText)
|
|
setHUD( txt )
|
|
else
|
|
setHUD( textHUD + " " + txt )
|
|
}
|
|
|
|
function getHUD(){
|
|
return document.querySelector("#typinghud").getAttribute("value")
|
|
}
|
|
|
|
function setHUD(txt){
|
|
document.querySelector("#typinghud").setAttribute("value",txt)
|
|
}
|
|
|
|
|
|
AFRAME.registerComponent('pinchsecondary', {
|
|
init: function () {
|
|
this.el.addEventListener('pinchended', function (event) {
|
|
selectedElement = getClosestTargetElement( event.detail.position )
|
|
selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:false})
|
|
// if close enough to a target among a list of potential targets, unselect previous target then select new
|
|
if (selectedElement) interpretJXR( selectedElement.getAttribute("value") )
|
|
selectedElement = null
|
|
});
|
|
this.el.addEventListener('pinchmoved', function (event) {
|
|
});
|
|
this.el.addEventListener('pinchstarted', function (event) {
|
|
});
|
|
},
|
|
remove: function() {
|
|
// should remove event listeners here. Requires naming them.
|
|
}
|
|
});
|
|
|
|
AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right one, should be switchable
|
|
init: function () {
|
|
var el = this.el
|
|
this.el.addEventListener('pinchended', function (event) {
|
|
// if positioned close enough to a target zone, trigger action
|
|
// see own trigger-box component. Could use dedicated threejs helpers instead.
|
|
// https://github.com/Utopiah/aframe-triggerbox-component/blob/master/aframe-triggerbox-component.js#L66
|
|
// could make trigger zones visible as debug mode
|
|
var closests = getClosestTargetElements( event.detail.position )
|
|
// unselect current target if any
|
|
selectedElement = null;
|
|
setTimeout( _ => primaryPinchStarted = false, 200) // delay otherwise still activate on release
|
|
|
|
var newPinchPos = new THREE.Vector3()
|
|
newPinchPos.copy(event.detail.position )
|
|
pinches.push({position:newPinchPos, timestamp:Date.now(), primary:true})
|
|
dl2p = distanceLastTwoPinches()
|
|
});
|
|
this.el.addEventListener('pinchmoved', function (event) {
|
|
// move current target if any
|
|
if (selectedElement) {
|
|
selectedElement.setAttribute("position", event.detail.position)
|
|
document.querySelector("#rightHand").object3D.traverse( e => {
|
|
if (e.name == "b_"+sides[primarySide][0]+"_wrist")
|
|
selectedElement.setAttribute("rotation", e.rotation.x*180/3.14 + " " + e.rotation.y*180/3.14 + " " + e.rotation.z*180/3.14)
|
|
})
|
|
// rotation isn't ideal with the wrist as tend not have wrist flat as we pinch
|
|
}
|
|
});
|
|
this.el.addEventListener('pinchstarted', function (event) {
|
|
primaryPinchStarted = true
|
|
|
|
selectedElement = getClosestTargetElement( event.detail.position )
|
|
selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:true})
|
|
// if close enough to a target among a list of potential targets, unselect previous target then select new
|
|
});
|
|
},
|
|
remove: function() {
|
|
// should remove event listeners here. Requires naming them.
|
|
}
|
|
});
|
|
|
|
function distanceLastTwoPinches(){
|
|
let dist = null
|
|
if (pinches.length>1){
|
|
dist = pinches[pinches.length-1].position.distanceTo( pinches[pinches.length-2].position )
|
|
}
|
|
return dist
|
|
}
|
|
|
|
function startSelectionVolume(){
|
|
selectionPinchMode = true
|
|
}
|
|
|
|
function parseKeys(status, key){
|
|
var e = hudTextEl
|
|
if (status == "keyup"){
|
|
if (key == "Control"){
|
|
groupingMode = false
|
|
groupSelectionToNewNote()
|
|
}
|
|
}
|
|
if (status == "keydown"){
|
|
var txt = e.getAttribute("value")
|
|
if (txt == "[]")
|
|
e.setAttribute("value", "")
|
|
if (key == "Backspace" && txt.length)
|
|
e.setAttribute("value", txt.slice(0,-1))
|
|
if (key == "Control")
|
|
groupingMode = true
|
|
if (key == "Shift" && selectedElement)
|
|
e.setAttribute("value", selectedElement.getAttribute("value") )
|
|
else if (key == "Enter") {
|
|
if ( selectedElement ){
|
|
var clone = selectedElement.cloneNode()
|
|
clone.setAttribute("scale", "0.1 0.1 0.1") // somehow lost
|
|
AFRAME.scenes[0].appendChild( clone )
|
|
targets.push( clone )
|
|
selectedElement = clone
|
|
} else {
|
|
if (txt.match(prefix)) interpretJXR(txt)
|
|
// check if text starts with jxr, if so, also interpret it.
|
|
addNewNote(e.getAttribute("value"))
|
|
e.setAttribute("value", "")
|
|
}
|
|
} else {
|
|
// consider also event.ctrlKey and multicharacter ones, e.g shortcuts like F1, HOME, etc
|
|
if (key.length == 1)
|
|
e.setAttribute("value", e.getAttribute("value") + key )
|
|
}
|
|
}
|
|
}
|
|
|
|
AFRAME.registerComponent('hud', {
|
|
init: function(){
|
|
var feedbackHUDel= document.createElement("a-troika-text")
|
|
feedbackHUDel.id = "feedbackhud"
|
|
feedbackHUDel.setAttribute("value", "")
|
|
feedbackHUDel.setAttribute("font", "#font")
|
|
feedbackHUDel.setAttribute("position", "-0.05 0.01 -0.2")
|
|
feedbackHUDel.setAttribute("scale", "0.05 0.05 0.05")
|
|
this.el.appendChild( feedbackHUDel )
|
|
var typingHUDel = document.createElement("a-troika-text")
|
|
typingHUDel.setAttribute("font", "#font")
|
|
typingHUDel.id = "typinghud"
|
|
typingHUDel.setAttribute("value", startingText)
|
|
typingHUDel.setAttribute("position", "-0.05 0 -0.2")
|
|
typingHUDel.setAttribute("scale", "0.05 0.05 0.05")
|
|
this.el.appendChild( typingHUDel )
|
|
hudTextEl = typingHUDel // should rely on the id based selector now
|
|
document.addEventListener('keyup', function(event) {
|
|
parseKeys('keyup', event.key)
|
|
});
|
|
document.addEventListener('keydown', function(event) {
|
|
parseKeys('keydown', event.key)
|
|
});
|
|
}
|
|
})
|
|
|
|
function addNewNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=null, classes=null, visible="true" ){
|
|
var newnote = document.createElement("a-troika-text")
|
|
newnote.setAttribute("font", "#font")
|
|
newnote.setAttribute("anchor", "left" )
|
|
newnote.setAttribute("outline-width", "5%" )
|
|
newnote.setAttribute("outline-color", "black" )
|
|
newnote.setAttribute("visible", visible )
|
|
|
|
if (id) newnote.id = id
|
|
if (classes) newnote.className += classes
|
|
newnote.setAttribute("side", "double" )
|
|
var userFontColor = AFRAME.utils.getUrlParameter('fontcolor')
|
|
if (userFontColor && userFontColor != "")
|
|
newnote.setAttribute("color", userFontColor )
|
|
else
|
|
newnote.setAttribute("color", fontColor )
|
|
if (text.match(prefix))
|
|
newnote.setAttribute("color", codeFontColor )
|
|
newnote.setAttribute("value", text )
|
|
//newnote.setAttribute("font", "sw-test/Roboto-msdf.json")
|
|
newnote.setAttribute("position", position)
|
|
newnote.setAttribute("scale", scale)
|
|
AFRAME.scenes[0].appendChild( newnote )
|
|
targets.push(newnote)
|
|
}
|
|
|
|
function addGltfFromURLAsTarget( url, scale=1 ){
|
|
var el = document.createElement("a-entity")
|
|
AFRAME.scenes[0].appendChild(el)
|
|
el.setAttribute("gltf-model", url)
|
|
el.setAttribute("position", "0 1.7 -0.3") // arbitrary for test
|
|
el.setAttribute("scale", scale + " " + scale + " " + scale)
|
|
targets.push(el)
|
|
}
|
|
|
|
function parseJXR( code ){
|
|
// should make reserved keywords explicit.
|
|
var newcode = code
|
|
newcode = newcode.replace("jxr ", "")
|
|
newcode = newcode.replace(/(\d)s (.*)/ ,`setTimeout( _ => { $2 }, $1*1000)`)
|
|
|
|
// qs X => document.querySelector("X")
|
|
newcode = newcode.replace(/qs ([^\s]+)/ ,`document.querySelector('$1')`)
|
|
|
|
// sa X Y => .setAttribute("X", "Y")
|
|
newcode = newcode.replace(/ sa ([^\s]+) ([^\s]+)/,`.setAttribute('$1','$2')`)
|
|
// problematic for position as they include spaces
|
|
|
|
newcode = newcode.replace(/obsv ([^\s]+)/ ,`newNoteFromObservableCell('$1')`)
|
|
|
|
// TODO
|
|
//<a-text target value="jxr observe selectedElement" position="0 1.25 -0.2" scale="0.1 0.1 0.1"></a-text>
|
|
newcode = newcode.replace(/observe ([^\s]+)/,`bindVariableValueToNewNote('$1')`)
|
|
// could proxy instead... but for now, the quick and dirty way :
|
|
|
|
// e.g qs a-sphere sa color red =>
|
|
// document.querySelector("a-sphere").setAttribute("color", "red")
|
|
|
|
newcode = newcode.replace(/lg ([^\s]+) ([^\s]+)/ ,`addGltfFromURLAsTarget('$1',$2)`)
|
|
// order matters, here we only process the 2 params if they are there, otherwise 1
|
|
newcode = newcode.replace(/lg ([^\s]+)/ ,`addGltfFromURLAsTarget('$1')`)
|
|
return newcode
|
|
}
|
|
|
|
function interpretJXR( code ){
|
|
if (code.length == 1) { // special case of being a single character, thus keyboard
|
|
if (code == ">") { // Enter equivalent
|
|
addNewNote( hudTextEl.getAttribute("value") )
|
|
setHUD("")
|
|
} else if (code == "<") { // Backspace equivalent
|
|
setHUD( hudTextEl.getAttribute("value").slice(0,-1))
|
|
} else {
|
|
appendToHUD( code )
|
|
}
|
|
}
|
|
if (!code.match(prefix)) return
|
|
var uninterpreted = code
|
|
var parseCode = ""
|
|
code.split("\n").map( lineOfCode => parseCode += parseJXR( lineOfCode ) + ";" )
|
|
// could ignore meta code e.g showhistory / saveHistoryAsCompoundSnippet
|
|
commandhistory.push( {date: +Date.now(), uninterpreted: uninterpreted, interpreted: parseCode} )
|
|
|
|
console.log( parseCode )
|
|
try {
|
|
eval( parseCode )
|
|
} catch (error) {
|
|
console.error(`Evaluation failed with ${error}`);
|
|
}
|
|
|
|
// unused keyboard shortcuts (e.g BrowserSearch) could be used too
|
|
// opt re-run it by moving the corresponding text in target volume
|
|
}
|
|
|
|
AFRAME.registerComponent('keyboard', {
|
|
init:function(){
|
|
let generatorName = this.attrName
|
|
const horizontaloffset = .5
|
|
const horizontalratio = 1/30
|
|
alphabet.map( (line,ln) => {
|
|
for (var i = 0; i < line.length; i++) {
|
|
var pos = i * horizontalratio - horizontaloffset
|
|
addNewNote( line[i], pos+" "+(1.6-ln*.03)+" -.4", ".1 .1 .1", null, generatorName)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
function cloneAndDistribute(){
|
|
el = document.querySelector("a-box[src]") // page
|
|
// trying instead to rely on previously selected matching element and dl2p
|
|
|
|
// lack visual feedback to show what is indeed lastly selected or the distance found
|
|
//el = selectedElements[selectedElements.length-2] // not current command
|
|
times = Math.floor(dl2p*10) // also assume it's been done properly
|
|
if (times < 2) times = 7
|
|
|
|
offset = .5
|
|
for (var i = 0; i < times ; i++) { // equivalent of Blender array modifier
|
|
let newEl = el.cloneNode()
|
|
AFRAME.scenes[0].appendChild(newEl) // takes time...
|
|
setTimeout( setZ, 100, {el: newEl, z: -1-i*offset} )
|
|
newEl.addEventListener('hasLoaded', function (event) {
|
|
//this.object3D.position.z = i*offset
|
|
console.log("loaded") // doesnt seem to happen
|
|
})
|
|
}
|
|
|
|
function setZ(params){
|
|
params.el.object3D.position.z = params.z
|
|
}
|
|
}
|
|
|
|
</script>
|
|
<a-scene keyboard >
|
|
<a-assets>
|
|
<template id="avatar-template"> <a-cylinder opacity=.3 scale=".2 1.2 .2" networked-audio-source></a-cylinder> </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-asset-item id="font" src="roboto.woff"></a-asset-item>
|
|
</a-assets>
|
|
<a-entity id="player" networked="template:#avatar-template;attachTemplateToLocal:false;"
|
|
hud camera look-controls wasd-controls position="0 1.6 0"></a-entity>
|
|
|
|
<a-entity id="rightHand" pinchprimary hand-tracking-controls="hand: right;"></a-entity>
|
|
<a-entity id="leftHand" pinchsecondary hand-tracking-controls="hand: left;"></a-entity>
|
|
|
|
<a-entity id="scaledworld" class="hidableassets" scale=".05 .05 .05" position="0 1.45 -1"><!-- can't be used for interactions otherwise becomes indirect-->
|
|
<a-box position="-0.1 1.2 -0.3" scale="0.5 0.5 0.5" rotation="0 45 0" color="#4CC3D9"></a-box>
|
|
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
|
|
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
|
|
</a-entity>
|
|
<a-entity hide-on-enter-ar id="environment" class="hidableenvironment" gltf-model="url(../content/Pond.glb)" scale="80 80 80" position="0 0.2 0.15" rotation="0 -90 0"></a-entity>
|
|
<a-entity hide-on-enter-ar class="hidableenvironment" gltf-model="url(../content/CabanaAndCurtains.glb)" scale=".010 .010 .010" position="0 0.2 0.15" rotation="0 0 0"></a-entity>
|
|
<a-sky hide-on-enter-ar color="#add8e6"></a-sky>
|
|
|
|
<a-troika-text target value="instructions : pinch letters to type text" font="#font" position="0 1.65 -0.2" scale="0.1 0.1 0.1"></a-troika-text>
|
|
<a-troika-text target value="jxr lg ../content/Clouds.glb 0.2" font="#font" position="0 1.45 -0.2" scale="0.1 0.1 0.1"></a-troika-text>
|
|
<a-troika-text target value="jxr lg Fox.glb 0.001" font="#font" position="0 1.55 -0.2" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<a-troika-text target value="jxr qs a-sphere sa color red" font="#font" position="0 1.35 -0.2" scale="0.1 0.1 0.1"></a-troika-text>
|
|
<a-troika-text target value="jxr qs a-sphere sa color blue" font="#font" position="0 1.25 -0.2" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
</a-scene>
|
|
</body>
|
|
</html>
|
|
|