<!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='jxr-core.js?123'></script> 
    <script src='jxr-postitnote.js?13235'></script> 
  </head>
  <body>

<script>
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?!
	}
}

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") ) )
}

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)
}

AFRAME.registerComponent('onemptypinch', { // changed from ondrop to be coherent with event name
  init: function(){
	AFRAME.scenes[0].addEventListener('enter-vr', e => {
		console.log('entered vr')
		document.querySelector("[cursor]").setAttribute("visible", "true")
		document.querySelector("[camera]").setAttribute("cursor", "") 
	})
  },
// could support multi
        events: {
                emptypinch: function (e) {
			// 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
                        try {   
                                eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
                        } catch (error) {
                                console.error(`Evaluation failed with ${error}`);
                        }
                }
        }
})

function onHoveredTeleport(){
	// iterate over targets
		// see instead of teleportable https://aframe.io/docs/1.5.0/components/cursor.html#configuring-the-cursor-through-the-raycaster-component 
	Array.from( document.querySelectorAll("[teleporter]") ).map( target => {
		if ( target.states.includes( "cursor-hovered" ) ){
			target.setAttribute("material", "color", "magenta") // visited
			document.getElementById('rig').setAttribute('position', target.getAttribute("position") )
		}
	})
}

AFRAME.registerComponent('teleporter', {
  init: function(){
	this.el.setAttribute("opacity", .5)
	if (window.location.hash && document.querySelector(window.location.hash+"[teleporter]"))
		document.getElementById('rig').setAttribute('position', document.querySelector(window.location.hash+"[teleporter]").getAttribute("position") )
  },
  events: {
    mouseenter: function (e) { this.el.setAttribute("opacity", .8) },
    mouseleave: function (e) { this.el.setAttribute("opacity", .5) },
    click: function (e) { document.getElementById('rig').setAttribute('position', this.el.getAttribute("position") ) }
    // makes it compatible with mouse on desktop ... but also somehow enable the wrist shortcut?!
  }
});

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/">
		<img style='position:fixed;left:10px;' title='code repository' src='gitea_logo.svg'>
	</a>
</div>

<a-scene startfunctions onemptypinch="onHoveredTeleport()">
      <a-gltf-model hide-on-enter-ar="" id="environment" src="../content/CubeRoom.glb" rotation="0 -90 0" position="0 0 1" scale="" ></a-gltf-model>
	<!-- Cube Room by Anonymous [CC-BY] via Poly Pizza -->

      <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 cursor position="0 0 -1"
				    geometry="primitive: ring; radiusInner: 0.005; radiusOuter: 0.01"
				    material="color: black; shader: flat; opacity:.05;"
				></a-entity>
		</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-console position="2 2 0" rotation="0 -45 0" font-size="34" height=1 skip-intro=true></a-console>
      </a-entity>

      <a-box pressable start-on-press id="box" scale="0.05 0.05 0.05" color="pink"></a-box>

      <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 0.65 -0.2" scale="0.1 0.1 0.1"></a-troika-text>

      <a-troika-text anchor=left value="jxr onNextPrimaryPinch(deleteTarget)" target position=" -0.3 1.50 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>
      <a-troika-text anchor=left value="jxr onNextPrimaryPinch(cloneTarget)" target position=" -0.3 1.60 0" rotation="0 40 0" 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 makeAnchorsVisibleOnTargets()" target position=" -0.3 1.20 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>

      <a-troika-text anchor=left value="forceXaxis toggling"
	onreleased="console.log('run on released');forceXaxis=!forceXaxis"
	onpicked="console.log('run on picked');forceXaxis=!forceXaxis"
	target position=" -0.3 1.45 0" rotation="0 40 0" scale="0.1 0.1 0.1"></a-troika-text>

      <a-troika-text anchor=left value="translate targets"
	onreleased="toggleTranslateTargets()"
	onpicked="toggleTranslateTargets()"
	target position=" 1 1.45 -.2" rotation="0 -40 0" scale="0.1 0.1 0.1"></a-troika-text>

      <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" >
		<a-troika-text anchor=left value="jxr location.reload()" target position="0 1.30 -.5" rotation="0 0 0" scale="0.1 0.1 0.1"></a-troika-text>
		    <!-- works to execute but not to move, should either reparent or take into account the parent offset while moving -->
		    <!-- see pinchmoved in primary pinch in jxr-core.js as potential solution -->
	</a-box>
	<a-box teleporter height=".1" class="teleportable" material="color: cyan" position="0 0 0" ></a-box>

	<a-box id="namedteleporter" teleporter height=".1" class="teleportable" material="color: cyan" position="2 0 2" ></a-box>

	<a-box target teleporter height=".1" depth=".1" width=".1" class="teleportable" material="color: red" position="0 1 -.5" ></a-box>
    </a-scene>
  </body>
</script>
</html>