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.
328 lines
14 KiB
328 lines
14 KiB
<!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/webdav.js'></script>
|
|
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.1.0/dist/aframe-extras.min.js"></script>
|
|
<script src='jxr-core.js?12345'></script>
|
|
<script src='jxr-postitnote.js?13235'></script>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
|
|
AFRAME.registerComponent('startfunctions', {
|
|
init: function(){
|
|
|
|
let newEl = document.createElement('a-entity')
|
|
newEl.setAttribute('instruction-machine', '')
|
|
AFRAME.scenes[0].appendChild(newEl)
|
|
|
|
let el = document.createElement('a-sphere')
|
|
el.setAttribute('radius', '.01')
|
|
el.setAttribute('target', '')
|
|
el.setAttribute('position', '0 1 -.5')
|
|
AFRAME.scenes[0].appendChild(el)
|
|
el.setAttribute("onpicked", "window.pfb = selectedElements.at(-1).element.getAttribute('position').clone();")
|
|
el.setAttribute("onreleased", "twoPosToBox(window.pfb, selectedElements.at(-1).element.getAttribute('position'));")
|
|
// event detail is available within that code as a e.detail e.g e.detail.element
|
|
}
|
|
})
|
|
|
|
function twoPosToBox(A, B){
|
|
let center = A.clone()
|
|
center.add(B)
|
|
center.divideScalar(2)
|
|
let lengthes = A.clone()
|
|
lengthes.sub(B)
|
|
let el = document.createElement("a-box")
|
|
el.setAttribute("position", center )
|
|
el.setAttribute('target', '')
|
|
el.setAttribute("scale", lengthes.toArray().map( i => Math.abs(i) ).join(" ") )
|
|
AFRAME.scenes[0].appendChild(el)
|
|
}
|
|
|
|
// import from https://fabien.benetou.fr/PIMVRdata/SpaSca3DPrinterInstructions?action=source
|
|
// but as text editable format, not code
|
|
// would be safer to do few examples BEFORE synthesizing to a flexible enough format, just with mini games before
|
|
//___________________________________________________________________________________________________________________________________
|
|
AFRAME.registerComponent('instruction-machine', {
|
|
// other content next, ideally combinable
|
|
// tournevie metal workshop
|
|
// laser cutter at the EP
|
|
// todo
|
|
// split UI versus content
|
|
// visual timeline and where we are on it, e.g 1/4 steps
|
|
// a la Lego instruction, i.e horizontal bar with dot on current position
|
|
// could use a cylinder and a moving sphere
|
|
// could be helpful to persist once position is ok, after step 1 then
|
|
// https://aframe.io/docs/1.5.0/components/anchored.html
|
|
// timeout for increasingly explicit feedback
|
|
// jump to specific step via hash to better discussion
|
|
// port a text version, i.e no 6DoF or hand tracking, to Monocle/Frame
|
|
// adapt from wiki sequential instructions part, possibly with some images
|
|
// consider https://threejs.org/docs/?q=grid#api/en/helpers/GridHelper too
|
|
// hiding panel, like whiteboards/blackboards in order to focus on solely the task at hand
|
|
// optional blinders managed by the user
|
|
// overall guiding principle, finding the sweet spot of signal/noise ratio
|
|
// starting with a lot of blinders then gradually removing them
|
|
// potentially leading to open a set of other instructions next to it
|
|
// and find combinatorial opportunities
|
|
// wall aligning before starting, this way snap would make sense too
|
|
// could be done via GridHelper rather than primitive
|
|
// requirement, i.e this instruction needs that other instruction to be completed before
|
|
// leading to a graph Sasha Kaurov (IAS) as discussed a while ago with X and experienced via Khan Academy (esp. in math)
|
|
// can also list optional requirements
|
|
// get model of space https://twitter.com/utopiah/status/1712340651453411808
|
|
// can prepare remotely, i.e in VR, to either train virtually in-situ
|
|
// to build the instruction sets
|
|
|
|
|
|
schema: {default:
|
|
"https://fabien.benetou.fr/PIMVRdata/SpaSca3DPrinterInstructions?action=source"
|
|
}, // type: "string"
|
|
init: function(){
|
|
|
|
// shortcut to navigate through steps, 2D only
|
|
onwheel = (event) => {
|
|
event.deltaY < 0 ?
|
|
document.querySelector("["+generatorName+"]").emit('previousStep')
|
|
// does not yet exist
|
|
: document.querySelector("["+generatorName+"]").emit('nextStep')
|
|
}
|
|
|
|
let generatorName = this.attrName
|
|
const idSuffix = "_framing_box"
|
|
this.buttonSuffix = "_step_button"
|
|
let el = this.el
|
|
|
|
//---------------------------CONTENT--------------------------
|
|
this.scale = 1/10
|
|
// size of the initial bounding box
|
|
|
|
// main element
|
|
let newEl = document.createElement('a-box')
|
|
newEl.setAttribute("scale", ""+this.scale+" "+this.scale+" "+this.scale)
|
|
newEl.setAttribute("wireframe", "true")
|
|
newEl.setAttribute("position", "0 1.3 -.5")
|
|
newEl.setAttribute("target","true")
|
|
// newEl.setAttribute("onpicked", "document.querySelector('["+generatorName+"]').emit('check')")
|
|
// newEl.setAttribute("onreleased", "document.querySelector('["+generatorName+"]').emit('reset')")
|
|
// should now also handle event details properly e.g
|
|
// newEl.setAttribute("onreleased", 'document.querySelector("['+generatorName+']").emit("check",{color:"'+color+'"})')
|
|
// then in event
|
|
// let box = this.el.querySelector("a-box[color="+evt.detail.color+"]")
|
|
newEl.classList.add( generatorName )
|
|
newEl.id = generatorName+idSuffix
|
|
el.appendChild(newEl)
|
|
|
|
// button for step 1 with hinting arrow
|
|
let buttonEl = document.createElement('a-box')
|
|
buttonEl.setAttribute("scale", ".2 .1 .1")
|
|
buttonEl.setAttribute("wireframe", "true")
|
|
buttonEl.setAttribute("color", "red")
|
|
buttonEl.setAttribute("position", "0.1 0.1 0.5")
|
|
buttonEl.setAttribute("target","true")
|
|
buttonEl.setAttribute("visible","false")
|
|
buttonEl.classList.add( generatorName )
|
|
buttonEl.classList.add( generatorName+this.buttonSuffix)
|
|
buttonEl.id = generatorName+this.buttonSuffix+"_1"
|
|
newEl.appendChild(buttonEl)
|
|
this.addArrow([-4.5, 0, 0], buttonEl)
|
|
|
|
// cylinder example for step 2 with hinting arrow
|
|
buttonEl = document.createElement('a-cylinder')
|
|
buttonEl.setAttribute("scale", ".5 .5 .5")
|
|
buttonEl.setAttribute("wireframe", "true")
|
|
buttonEl.setAttribute("color", "blue")
|
|
buttonEl.setAttribute("rotation", "90 0 0")
|
|
buttonEl.setAttribute("position", "0 0 -0.5")
|
|
buttonEl.setAttribute("target", "true")
|
|
buttonEl.setAttribute("visible","false")
|
|
buttonEl.classList.add( generatorName )
|
|
buttonEl.classList.add( generatorName+this.buttonSuffix)
|
|
buttonEl.id = generatorName+this.buttonSuffix+"_2"
|
|
newEl.appendChild(buttonEl)
|
|
this.addArrow([-4.5, 0, 0], buttonEl)
|
|
|
|
//---------------------------END OF CONTENT--------------------------
|
|
// generic UI
|
|
|
|
this.steps = []
|
|
//this.steps = ["pick the grey box and align it with your 3D printer", "press change filament button", "remove the filament spool"]
|
|
|
|
fetch(this.data)
|
|
.then( r => r.text() )
|
|
.then( r => {
|
|
this.steps = r.split('\n')
|
|
this.steps.push( this.endOfInstructions )
|
|
document.querySelector("["+generatorName+"]").emit('reset')
|
|
})
|
|
|
|
let n = addNewNote("jxr qs #"+newEl.id+" sa rotation 0", "0.5 0.9 -.5")
|
|
n.setAttribute("rotation", "90 0 0")
|
|
n.setAttribute("annotation", "content: snap")
|
|
|
|
// symplistic, could instead be a directed graph of named nodes, each with optionally multiple outgoing edges
|
|
this.endOfInstructions = "You have finished the instructions. Feel free to reset and try again."
|
|
this.steps.push( this.endOfInstructions )
|
|
const confirmationStep = "Done" // could also consider a "done" area, moving the text there
|
|
|
|
let timelinegEl = document.createElement('a-cylinder')
|
|
timelinegEl.setAttribute("height", "1")
|
|
timelinegEl.setAttribute("radius", ".01")
|
|
timelinegEl.setAttribute("color", "white")
|
|
timelinegEl.setAttribute("rotation", "0 0 90")
|
|
timelinegEl.setAttribute("position", "0 1.15 -0.5")
|
|
timelinegEl.setAttribute("target","true")
|
|
timelinegEl.classList.add( generatorName )
|
|
timelinegEl.id = generatorName+"_timeline"
|
|
el.appendChild(timelinegEl)
|
|
let tlcursorgEl = document.createElement('a-sphere')
|
|
tlcursorgEl.setAttribute("radius", ".02")
|
|
tlcursorgEl.setAttribute("color", "red")
|
|
tlcursorgEl.setAttribute("position", "0 .5 0")
|
|
tlcursorgEl.classList.add( generatorName )
|
|
tlcursorgEl.id = generatorName+"_timelinecursor"
|
|
timelinegEl.appendChild(tlcursorgEl)
|
|
|
|
let actions = [
|
|
"translateX(.1)", "translateX(-.1)", "translateY(.1)", "translateY(-.1)", "translateZ(.1)", "translateZ(-.1)",
|
|
"rotateX(-.1)", "rotateX(.1)", "rotateY(.1)", "rotateY(-.1)", "rotateZ(.1)", "rotateZ(-.1)",
|
|
]
|
|
actions.reverse().map( (a,i) => {
|
|
let n = addNewNote("jxr qs #"+newEl.id+" .object3D."+a, ".5 "+(1+i/20)+" -.5")
|
|
n.setAttribute("rotation", "90 0 0")
|
|
n.setAttribute("annotation", "content: "+a.replace(/xxx/,'')) // could be simplified a bit
|
|
})
|
|
el.appendChild(newEl)
|
|
|
|
n = addNewNote("jxr qs #"+newEl.id+" .emit('nextStep')" , "0 1.1 -.5")
|
|
n.setAttribute("rotation", "90 0 0")
|
|
n.setAttribute("annotation", "content: confirm")
|
|
|
|
n = addNewNote("jxr qs #"+newEl.id+" .emit('previousStep')" , "0 1.0 -.5")
|
|
n.setAttribute("rotation", "90 0 0")
|
|
n.setAttribute("annotation", "content: go backward")
|
|
|
|
n = addNewNote("jxr qs #"+newEl.id+" .emit('reset')" , "0 0.8 -.5")
|
|
n.setAttribute("rotation", "90 0 0")
|
|
n.setAttribute("annotation", "content: restart")
|
|
|
|
this.currentInstructionStep = 0
|
|
this.currentInstruction = addNewNote(this.steps[this.currentInstructionStep], "-.3 1.2 -.5")
|
|
|
|
},
|
|
events: {
|
|
reset: function (evt) {
|
|
console.log(this.attrName, 'component was resetted!');
|
|
let generatorName = this.attrName
|
|
this.currentInstructionStep=-1
|
|
document.querySelector("["+generatorName+"]").emit('nextStep')
|
|
document.getElementById( generatorName+"_timelinecursor" ).object3D.position.y = .5
|
|
},
|
|
nextStep: function (evt) {
|
|
let generatorName = this.attrName
|
|
if (this.currentInstructionStep<this.steps.length-1){
|
|
this.currentInstruction.setAttribute("value",this.steps[++this.currentInstructionStep])
|
|
Array.from( document.querySelectorAll( "."+generatorName+this.buttonSuffix) ).map( el => el.setAttribute("visible", "false" ) )
|
|
document.getElementById( generatorName+this.buttonSuffix +"_"+this.currentInstructionStep )?.setAttribute("visible", "true" )
|
|
document.getElementById( generatorName+"_timelinecursor" ).object3D.translateY( -1/(this.steps.length-1) )
|
|
}
|
|
},
|
|
previousStep: function (evt) {
|
|
// going back after the end does NOT work as expected!
|
|
let generatorName = this.attrName
|
|
if (this.currentInstructionStep>0){
|
|
this.currentInstruction.setAttribute("value",this.steps[--this.currentInstructionStep])
|
|
Array.from( document.querySelectorAll( "."+generatorName+this.buttonSuffix) ).map( el => el.setAttribute("visible", "false" ) )
|
|
document.getElementById( generatorName+this.buttonSuffix +"_"+this.currentInstructionStep )?.setAttribute("visible", "true" )
|
|
document.getElementById( generatorName+"_timelinecursor" ).object3D.translateY( 1/(this.steps.length-1) )
|
|
} else {
|
|
document.querySelector("["+generatorName+"]").emit('reset')
|
|
}
|
|
},
|
|
},
|
|
addArrow: function(relativePosition, parentEl, color=0xffff00){
|
|
const dir = new THREE.Vector3( 1, 0, 0 )
|
|
|
|
//normalize the direction vector (convert to vector of length 1)
|
|
dir.normalize()
|
|
|
|
const origin = new THREE.Vector3( ...relativePosition )
|
|
const length = 4
|
|
const hex = color
|
|
|
|
const arrowHelper = new THREE.ArrowHelper( dir, origin, length, hex )
|
|
parentEl.object3D.add( arrowHelper )
|
|
}
|
|
})
|
|
|
|
function cloneTarget(target){
|
|
let el = target.cloneNode(true)
|
|
if (!el.id)
|
|
el.id = "clone_" + crypto.randomUUID()
|
|
else
|
|
el.id += "_clone_" + crypto.randomUUID()
|
|
AFRAME.scenes[0].appendChild(el)
|
|
}
|
|
|
|
function deleteTarget(target){
|
|
targets = targets.filter( e => e != target)
|
|
target.remove()
|
|
}
|
|
|
|
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") ) )
|
|
}
|
|
|
|
</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/">
|
|
<img style='position:fixed;left:10px;' title='code repository' src='gitea_logo.svg'>
|
|
</a>
|
|
</div>
|
|
|
|
<a-scene startfunctions >
|
|
|
|
<a-assets>
|
|
<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-entity id="rig">
|
|
<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 wristattachsecondary="target: #box" hand-tracking-controls="hand: left;"></a-entity>
|
|
</a-entity>
|
|
|
|
<a-troika-text value="SpaSca : Spatial Scaffolding" anchor="left" outline-width="5%" font="../content/ChakraPetch-Regular.ttf" position="-3 5 -2"
|
|
scale="3 3 3" rotation="80 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 1.65 -0.2" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<a-troika-text anchor=left value="jxr location.reload()" target position="-.5 1.30 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>
|
|
|
|
<a-console position="2 2 0" rotation="0 -45 0" font-size="34" height=1 skip-intro=true></a-console>
|
|
|
|
<a-box id="box" visible="false"></a-box>
|
|
<!-- bug if #box missing, so hiding for now -->
|
|
<!-- bug in start-on-press after XR init, as mentioned there -->
|
|
|
|
</a-scene>
|
|
</body>
|
|
</script>
|
|
</html>
|
|
|