<!DOCTYPE html>
<html>
  <title>SpaSca : Spatial Scaffolding</title>
  <head>
    <!-- Suggestions? https://git.benetou.fr/utopiah/text-code-xr-engine/issues/ -->

    <script src='dependencies/aframe.min.js'></script>
    <script src='dependencies/aframe-html.js'></script>
    <script src='dependencies/aframe-mirror.js'></script>
    <script src='dependencies/aframe-troika-text.min.js'></script>
    <script type="module" id=immersbundle src='dependencies/immers-client.js?role=modFull'></script>
    <!--<script type="module" id=immersbundle src="https://cdn.jsdelivr.net/npm/immers-client/dist/destination.bundle.js?role=modFull"></script>-->

    <!-- for input sharing -->
    <script src='dependencies/peerjs.min.js'></script>
    <!-- for content sharing, using NAF -->
    <script src='dependencies/socket.io.slim.js'></script>
    <script src="https://naf.benetou.fr/easyrtc/easyrtc.js"></script>
    <script src='dependencies/networked-aframe.min.js'></script>

    <!-- still experimenting, see webdav.html -->
    <script src='dependencies/webdav.js'></script> 

    <!-- replacing with local copies as CDNs are like unpkg tend to be slow
    <script type="module" src="https://unpkg.com/immers-client/dist/destination.bundle.js"></script>
    <script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>

    <script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
    <script src="aframe-html.js"></script>

    <script src="https://unpkg.com/peerjs@1.4.5/dist/peerjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.4.0/socket.io.slim.js"></script>
    <script src="https://naf.benetou.fr/easyrtc/easyrtc.js"></script>
    <script src="https://unpkg.com/networked-aframe@^0.10.0/dist/networked-aframe.min.js"></script>

    <script src="https://cdn.jsdelivr.net/npm/aframe-mirror@latest/index.js"></script>
	 -->

    <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
    <link rel="manifest" href="site.webmanifest">
  </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='https://git.benetou.fr/assets/img/logo.svg/'>
	</a>
</div>

<div>
	<div id="observablehq-numberOfPages-835aa7e9"></div>
	<div id="observablehq-result_as_html-ab4c1560"></div>
</div>

<script type="module">

// just text
import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js";
import define from "https://api.observablehq.com/@utopiah/from-pim-to-2d-to-3d-to-xr-explorations@2010.js?v=3";
import define2 from "https://api.observablehq.com/d/f219f0c440c6d5a2.js?v=3";
new Runtime().module(define, name => {
  if (name === "numberOfPages") return new Inspector(document.querySelector("#observablehq-numberOfPages-835aa7e9"));
  document.querySelector(".a-enter-vr").style.position = "fixed"
});

// HTML with interactable input
new Runtime().module(define2, name => {
  if (name === "viewof offsetExample") return new Inspector(document.querySelector("#observablehq-viewof-offsetExample-ab4c1560"));
  if (name === "result_as_html") return new Inspector(document.querySelector("#observablehq-result_as_html-ab4c1560"));
  return ["result_no_name","result"].includes(name);
});
// setTimeout( _ => document.querySelector("#gui3d").setAttribute("html", "html:#observablehq-key;cursor:#cursor;" ) , 2000)
  // <a-entity id="gui3d" class="observableui" position="0 1.5 -.4"></a-entity>
</script>

<script>

/*

motion to data
	- integer, e.g distance from beginning to end
	- curve, sampling N points between beginning and end

being able to use that in jxr commands, with example related to positioning entities
	see https://git.benetou.fr/utopiah/text-code-xr-engine/issues/52#issuecomment-229
	
warning that selectedElement will get overwritten once executing a command by pinching
	consequently in addition to have a history of executed commands
		there should be a history of selected elements
			and maybe their changed position states
*/

// motivated by https://git.benetou.fr/utopiah/text-code-xr-engine/issues/63
var reservedKeywords = ["selectedElement", "lastPointSketch ", "commandhistory", "groupSelection", "targets", "observe", "sa", "qs"]
// see generated file reserved-keywords for more yet not sufficient, see instead parseJXR()
// should also include some documentation

const prefix = /^jxr /
const codeFontColor = "lightgrey"
const fontColor= "white"
const wikiAsImages = "https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph.json"
const maxItems = 10
const now = Math.round( +new Date()/1000 ) //ms in JS, seconds in UNIX epoch
const baseCachedURL = "https://vatelier.benetou.fr/MyDemo/newtooling/textures/fabien.benetou.fr_" 
const baseLiveURL = "https://vatelier.benetou.fr/MyDemo/newtooling/web/renders/fabien.benetou.fr_"
const queryFormatBaseURL = "https://fabien.benetou.fr/"
const imageExtension = ".png"
const renderSuffix = "?action=serverrender"
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"]
var generators = "line-link-entities link screenstack dynamic-view selectionboxonpinches keyboard "
	+ "commands-from-external-json glossary timeline issues web-url background-via-url observableui hidableenvironmentfot fot"
// could be an array proper completed on each relevant component registration
var heightAdjustableClasses = ["commands-from-external-json"]
var pinches = [] // position, timestamp, primary vs secondary
var dl2p = null // from distanceLastTwoPinches
var selectedElements = [];

// could add a dedicated MakeyMakey mode with a fixed camera, e.g bird eye view, and an action based on some physical input that others, thanks to NAF, could see or even use.
	// ?inputmode=makeymakey

AFRAME.registerComponent('enable-components-via-url', { 
  init: function () {
	var src = AFRAME.utils.getUrlParameter('enable-components-via-url')
	if (src && src != "") {
		src.split(",").map( c => {
			this.el.setAttribute(c, "")
		})
	}
  }
})

AFRAME.registerComponent('disable-components-via-url', { 
  init: function () {
	var src = AFRAME.utils.getUrlParameter('disable-components-via-url')
	if (src && src != "") {
		src.split(",").map( c => {
			Array.from( document.querySelectorAll("["+c+"]") ).map( e => { e.removeAttribute(c) })
		})
	}
  }
})

// e.g background https://fabien.benetou.fr/pub/home/metaverse.png might have to allow options like scale to allow for modifying both size and ratio
AFRAME.registerComponent('background-via-url', { // non interactive mode
  init: function () {
	let generatorName = this.attrName
	var src = AFRAME.utils.getUrlParameter('background')
	if (src && src != "") {
		this.el.setAttribute( "visible", "true")
		this.el.setAttribute( "src", src )
		this.el.className += generatorName
		Array.from( document.querySelectorAll(".mural-instructions") ).map( i => {
			i.setAttribute("visible", "true") 
			i.className += generatorName
		})
	}
  }
})

AFRAME.registerComponent('web-url', {
// e.g <a-entity id=inbrowser web-url position="0 1.5 -2.4"></a-entity>
// motivated by https://glitch.com/edit/#!/aframe-lil-gui?path=observablewidget.html
  init: function () {
	const url = "https://fabien.benetou.fr/Fabien/Principle?action=webvr"
	var target = url
	var src = AFRAME.utils.getUrlParameter('url')
	// could also be a component parameter
	var el = this.el
	let generatorName = this.attrName
	if (src && src != "") target = src
	fetch(target).then( res => res.text() ).then( r => {
	  pageEl = document.createElement("div")
	  pageEl.id = "page"
	  pageEl.innerHTML = r
	  pageEl.style = "visibility:hidden;"
	  document.body.appendChild(pageEl)
	  el.setAttribute("html", "html:#page;cursor:#cursor;" )
	  el.className += generatorName
	  //backdrop
	  const geometry = new THREE.PlaneGeometry( el.object3D.children[0].geometry.parameters.width*1.1,
		el.object3D.children[0].geometry.parameters.height*1.1 );
	  const material = new THREE.MeshBasicMaterial( {color: 0xffffff, side: THREE.DoubleSide} );
	  const plane = new THREE.Mesh( geometry, material );
	  plane.position.z = -.1
	  el.object3D.add( plane );
	})
  }
})

var immersClient
// See dedicated issue https://git.benetou.fr/utopiah/text-code-xr-engine/issues/47 
document.querySelector("#immersbundle").addEventListener('load',(event) => { 
	immersClient = document.querySelector("immers-hud").immersClient
	document.querySelector("immers-hud").setAttribute("access-role", "modFull")
	document.querySelector("immers-hud").immersClient.addEventListener("immers-client-connected", _ => {
		immersClient.addEventListener("immers-client-new-message", e => addNewNote(e.detail.message.messageHTML) ) 
		immersClient.friendsList().then( r => {
			if (r.length>0) addNewNote( "Friends:", "-1 1.65 -0.5") 
			r.map( (u,i) => { 
				let friendData = u.profile.displayName
				if (u.locationName) friendData += " at " + u.locationName
				if (u.locationURL) friendData += " (" + u.locationURL + " )"
				addNewNote( friendData, "-1 " + (1.6-i/20) + " -0.5") // should make this interpretable to join there
			} )
		} ) 
	})
});

function ims(msg){ 
	if (!immersClient) { setFeedbackHUD("not connected via Immers"); return; }
	immersClient.sendChatMessage(msg, "public")
} // shorthand for jxr command, still requires parenthesis and quotes though, could be better to have a dedicated visual shorthand, e.g >>
// can send code too e.g immersClient.sendChatMessage("jxr loadPageRange(3,4)", "public")

/* not sure what's the right way... but timeout works, others don't.

        immers-client-friends-update or immers-client-new-message to keep track of conversations between recurring meeting? Say you join a room, spend a working session with colleagues then leave. Could these be used to in this context to send reminders to those who subscribed to that event?
*/

var polys
async function getPolyList(keyword){
	//return await fetch('/search?keyword='+keyword).then( res => res.json() ).then( res => return res )
	var response = await fetch('/search?keyword='+keyword);
	var polys = await response.json()
	return polys
}

// for testing purposes, disable when not local with asset caching server
//getPolyList("pizza").then( p => polys = p.results )

function cachePoly(res){
	var n = 0;
	res.map( i => { fetch(i.Thumbnail.replace("https://static.poly.pizza/","http://localhost:7777/getpoly?id=").replace(".webp","")) } ) ;
	// see await Promise.all()
}
// should properly wait. Only once all queries are done then try to load.

function loadPolyThumbnails(res){
	var n = 0;
	res.map( i => {
		var el = document.createElement("a-image");
		el.setAttribute("src", i.Thumbnail.replace("https://static.poly.pizza/","http://localhost:7777polydata/"));
		el.setAttribute("position", "0 1 "+n--/10);
			// could instead attach e.g 9 items to the wrist using wristattachsecondary on a palette
		el.setAttribute("scale", ".1 .1 .1")
		el.setAttribute("loadpolyfomthumbnail", "") // that could then be used to execute on pinch based on src property
		AFRAME.scenes[0].appendChild(el);
	} )
}

function loadFirstPolyModel(res){
	var n = 0;
	var i = res[n]
	var el = document.createElement("a-gltf-model");
	el.setAttribute("src", i.Thumbnail.replace("https://static.poly.pizza/","http://localhost:7777polydata/").replace("webp","glb")); 
	el.setAttribute("position", "0 1 "+n--);
	AFRAME.scenes[0].appendChild(el); 
	return el
}

function loadPolyModels(res){
// to load all models, rarely a good idea
	var n = 0;
	res.map( i => {
		var el = document.createElement("a-gltf-model");
		el.setAttribute("src", i.Thumbnail.replace("https://static.poly.pizza/","http://localhost:7777polydata/").replace("webp","glb")); 
		el.setAttribute("position", "0 1 "+n--);
		AFRAME.scenes[0].appendChild(el); 
		// optionally rescale e.g rescaleModelFromPoly(el) // probably has to wait for it to be properly loaded, cf modelHasLoaded event
			// e.g rescaleModelFromPoly ( loadFirstPolyModel(polys) ) won't work
	} )
}

function rescaleModelFromPoly(modelEl){
// rescale to fit in 1m3 box
	var bbox = new THREE.Box3().setFromObject( modelEl.object3D );
	var rescale = 1 / ( (( bbox.max.x - bbox.min.x) + (bbox.max.y - bbox.min.y) + (bbox.max.z - bbox.min.z) ) /3 );
	modelEl.setAttribute("scale", rescale+ " " + rescale + " " + rescale)
	// could also leave untouched if within boundaries, e.g > 0.1 and < 1
}

// SYNC WITH HMD EDITS before trying this
/*

source as URL e.g https://fabien.benetou.fr/Fabien/Principle?action=source (locally Fabien.Principle.pmwiki)
	usual parsing (e.g stop words)
	dedicated PmWiki cleanup e.g no URL
	clean up or rather highlight (e.g color not being black) with presence from https://github.com/wordset/wordset-dictionary
	could also have a short dictionnary of stop words based on popularity
		e.g https://en.wikipedia.org/wiki/Most_common_words_in_English#100_most_common_words
		but seems so short it might not help much, could try long and popular instead
WPM * distance travelled as metric, not just 1

can generate layout for keyboard as (Ctrl shortcut) copiable items to append to AR clipbard
	https://twitter.com/utopiah/status/1533690234424139779
		could also use an optional type in addition to target
			such items would be copied without pressing Ctrl

see remoteSave for saving to be able to use the result outside the HMD
*/

/*

use imagesFromURLs (used in screenstack) on https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph.json
       and line-link-entities="target:#instructionA; source:#instructionC" between pages

*/

// see https://remote-keyboard.glitch.me on how to provide a remote keyboard (no BT) for hud keydown/keyup events
	// consider alt server e.g 9000-peers-peerjsserver-bxw59h3fm87.ws-eu47.gitpod.io as peerjs isn't always reliable
	// to do the same offline could add to express too, cf https://github.com/peers/peerjs-server#combining-with-existing-express-app
//new Peer("2022xrkbd").on('connection', conn => conn.on('data', data => processRemoteInputData(data) ))
const altServer = "9000-peers-peerjsserver-bxw59h3fm87.ws-eu47.gitpod.io" 
//new Peer("2022xrkbd", {host: altServer}).on('connection', conn => conn.on('data', data => processRemoteInputData(data) ))
// disabled for now
function processRemoteInputData( data ){
	// .status : keydown keyup pointermove
		// on keydown or keyup, result un .key
		// on pointermove, result un .x and .y

	// could try to throw back as an event...
	parseKeys( data.status, data.key )
	if (data.status == "pointermove") parsePointer( data.x, data.y )
}

// for testing purposes, disable when not local with asset caching server
// SSE on a specific route to know if this file was updated, if so reload (would force leave VR) cf Inventing on Principle
/*
const source = new EventSource("streaming");
source.onmessage = message => { 
		console.log(message.data) // showing the updates without manually forcing a reload
		if ((JSON.parse(message.data)).status == "reload" ) location.reload(true); 
	} ;
*/
// monitored server-side, index.js with fs

/* extrusion and more generally compactness of 3D object description :

	Constructive Solid Geometry  (CSG) https://openjscad.nodebb.com/topic/235/threejs-integration/11 as example of integration of JSCAD (modelling with code basically) with threejs, naively looks like an interesting intersection, ideally with spacial editing after (i.e pinching a vertex to update the resulting geometry)
*/

// consider PinePhone keyboard as something more usable that BT rollable
	// PeerJS/WS(S) to share key.events
	// would also work with iPad keyboard ... or another other device with keyboard and browser.

// refactoring : consider pluggable execution models and targets e.g eval(), containers, Observable, etc
	// right now all mashed up together so creates both complexity and security risk

// consider also STT and translation experiment
// Codex by OpenAI (cf EP account) https://beta.openai.com/account/api-keys stored on ~/.openai-codex-test-xr used via backend
// with token e.g JWT could also consider ~/.bashrc ~/.bin or ~/Prototypes as commands
	// esp. those allowing to integrate with specific hardware

// load as page loads
// <a-text target observablecell="targetid:observablehq-numberOfPages-835aa7e9" position="0 1.55 -0.2" scale="0.1 0.1 0.1"></a-text>
// interactive
// <a-text target value="jxr obsv observablehq-numberOfPages-835aa7e9" position="0 1.55 -0.2" scale="0.1 0.1 0.1"></a-text>

function newNoteFromObservableCell( cell ){
	var targetEl = document.querySelector("#"+cell)
	var potentialRes = document.querySelector("#observablehq-numberOfPages-835aa7e9>span")
	if (potentialRes && potentialRes.children[1]){
	  addNewNote( potentialRes.children[1].innerText ) 
		return
	}

	let observer = new MutationObserver(mutationRecords => {
	  addNewNote( mutationRecords[0].addedNodes[0].children[1].innerText ) 
	});

	observer.observe(targetEl, {
	  childList: true, // observe direct children
	  subtree: true, // and lower descendants too
	  characterDataOldValue: true // pass old data to callback
	})
}

AFRAME.registerComponent('observablecell', { // non interactive mode
  schema: {
	  targetid: {type: 'string'}
  },
  init: function () {
	var el = this.el
	var targetEl = document.querySelector("#"+this.data.targetid)
	let observer = new MutationObserver(mutationRecords => {
	  addNewNote( mutationRecords[0].addedNodes[0].children[1].innerText )
	});

	observer.observe(targetEl, {
	  childList: true, // observe direct children
	  subtree: true, // and lower descendants too
	  characterDataOldValue: true // pass old data to callback
  });
}})

// might mess thing up on Quest somehow... like typing does not seem to work anymore since.

/*
const registerServiceWorker = async () => {
  if ('serviceWorker' in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(
        'sw-test/sw.js',
        {
          scope: '',
        }
      );
      if (registration.installing) {
        console.log('Service worker installing');
      } else if (registration.waiting) {
        console.log('Service worker installed');
      } else if (registration.active) {
        console.log('Service worker active');
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};
*/

// 2 modes : interact/display (see the .hidableenvironment class)
	// interact : small scale, 3D model of impact visible, keyboard visible, instruction visible
	// display : changeable scale, everything but content not visible

// could try new movements dedicated to modifying text, in particular dedicated to coding
	// e.g replacing a word by an expression el sa color red => dropping qs elname on el => qs elname sa color red

// for snapping display the current position (like now)
	// and future position as transparency when within a certain radius to target + offset
	// see getClosestTargetElement( pos ) when an object is already selected an moving
	// only snap there if within certain distance

// replace console with an in VR equivalent, at least during the try/catch of eval() to get some feedback

function displaySnappablePositions( position ){
	// to show while moving an object
	// has to be close enough (below threshold)
}

function coloredBlocksFromScreens(colors, el){
	// those are NOT updated at the moment
	colors.map( (u,i) => {
		var e = document.createElement("a-box")
		e.setAttribute("color", u.split(" ")[1])
		e.setAttribute("position",`2 1.8 -${i}`)
		e.setAttribute("width","0.2")
		e.setAttribute("depth","0.2")
		el.appendChild(e)
	})
}

function imagesFromURLs(urls, el, classes=null){
	urls.map( (u,i) => {
		var e = document.createElement("a-image")
		if (u.indexOf("http")>-1)
			e.setAttribute("src", u)
		else
			e.setAttribute("src", `screens/${u}`)
		//e.setAttribute("position",`0 1.8 -${i}`)
		e.setAttribute("position",`0 1.1 -${i/50}`) // flight mode
		e.setAttribute("rotation",`-30 0 0`) // flight mode
		e.setAttribute("scale", ".05 .05 .05") // have to scale down here otherwise move interactions aren't good
		// could instead rely on https://github.com/visjs/vis-timeline
		// as previously used  in https://mobile.twitter.com/utopiah/status/1110576751577567233
		e.setAttribute("width","2")
		if (classes) e.className += classes
		
		el.appendChild(e)
		targets.push(e)
	})
}

function URLs(urls, el){
	urls.map( (u,i) => {
		var e = document.createElement("a-text")
		e.setAttribute("value", u.split(" ")[1])
		e.setAttribute("position",`-1 1.25 -${i}`)
		// incorrect as screens (and their average color) are done per minute but URL done per change of tab
		//does not help, should be a text property instead 
		// e.setAttribute("width","10")
		e.setAttribute("text", "wrapCount","200")
		// el.appendChild(e) // disabled in flight mode
	})
}

function stringWithPositionFromCoordinates(pos){
	var el = getClosestTargetElement( pos, 0.5 )
	// loosen up the threshold as we normally pick from the top left

	// assumes a lot :
		// NO rotation of the text, at all!
		// single line of text
		// scale only of 1 depth and uniform scaling
		// left aligned
		// probably only positive values
	var selectedGlyph = {}
	selectedGlyph.index = -1 // if we get an empty string
	selectedGlyph.element = el
	if (!el) return selectedGlyph
	var glyph = el.object3D.children[0].geometry.visibleGlyphs
	const matches = glyph.map( (t,i) => {
			return {
				el: el,
				dist : Math.abs( pos.x - (
					el.object3D.position.x + t.position[0]/(150/el.object3D.scale.x) ) ),
				index : i
			}
		})
		.filter( t => t.dist < 0.5 )
		.sort( (a,b) => a.dist - b.dist )
		// https://twitter.com/utopiah/status/1532766336941686784
	if (matches.length > 0) {
		selectedGlyph.index = matches[0].index
	}
	return selectedGlyph
}

function plot(equation,variablename="x",scale=5,step=1){
	// could delete the past one document.querySelector("#plot")
	// but nice to compare curves... should rather avoid adding grids instead.
	var plot = document.querySelector("#plot")
	if (!plot){
		plot = document.createElement("a-entity")
		targets.push(plot) // adding only once
		plot.setAttribute("position", "0 1.5 -.5") // convenient position to test on desktop
		plot.setAttribute("scale", ".01 .01 .01")
		var idx = 0
		for (var i=-scale;i<=scale;i+=step){
			xl = `start: ${-scale} ${i} 0; end : ${scale} ${i} 0; opacity: 1;`
			// weirdest "trick"... something using only `` on setAttribute produces empty string
			// but indirecting once by setting a variable make the following one work?!
			plot.setAttribute("line__"+ ++idx, xl)
			plot.setAttribute("line__"+ ++idx, `start: ${i} ${-scale} 0; end : ${i} ${scale} 0; opacity: 1;`)
		}
		xaxis = `start: ${-scale} 0 0; end : ${scale} 0 0; opacity: 1; color:white;`
		plot.setAttribute("line__axis_x", xaxis)
		plot.setAttribute("line__axis_y", `start: 0 ${-scale} 0; end : 0 ${scale} 0; opacity: 1; color:white;`)
		plot.setAttribute("line__axis_z", `start: 0 0 ${-scale}; end : 0 0 ${scale}; opacity: 1; color:white;`)
		plot.id = "plot"
		AFRAME.scenes[0].appendChild( plot )
	}
	var previousPoint = null
	var curveId = +Date.now()
	idx = 0
	for (var i=-scale;i<=scale;i+=step/10){
		var pos = i + " " + eval( "x="+i +";"+ equation) + " .1"
		if (previousPoint) {
			plot.setAttribute("line__user"+curveId+"section"+ ++idx, 'start: ' + previousPoint+ '; end : ' + pos + '; color:red;')
		}
		previousPoint = pos
	}
	// variablename seems unused
}

AFRAME.registerComponent('target', {
  init: function () {
	targets.push( this.el )
  }
})

AFRAME.registerComponent('line-link-entities', {
  schema: {
    source: {type: 'selector'},
    target: {type: 'selector'},
    steps: {type: 'number', default: 1},
  },
  init: function () {
    let generatorName = this.attrName
    setTimeout( _ => { // stupid... but works. 
 	    if (!this.data.source || !this.data.target) return
	    var sourcePos = this.data.source.object3D.position
	    var targetPos = this.data.target.object3D.position
 	    if (!sourcePos || !targetPos) return // might not be needed anymore
	    // adding a gltf inside an element prevents the parent from having coordinates (fast enough?)
	    var step = 0
	    var points = cut ([sourcePos, targetPos], 0, ++step)
	    points = cut (points, 0, ++step)
	    points = cut (points, points.length-2, step)
	    var el = this.el
	    el.className += generatorName
	    points.map( (p,i,arr) => {
	      if (arr[i+1])
		el.setAttribute("line__"+i, "start:" + AFRAME.utils.coordinates.stringify( arr[i] ) + ";end: " +  AFRAME.utils.coordinates.stringify( arr[i+1] ) )      
	    })
    }, 100 ) // could check instead if both elements have loaded
    
    function cut(points, pos, step){
      var a = points[pos]
      var b = points[pos+1]
      var midPos = new THREE.Vector3()
      midPos.copy(a).add(b).divideScalar(2)
      midPos.z -= a.distanceTo(b)/(step*10) // smoothed out but axis aligned
      return [...points.slice(0,pos+1), midPos, ...points.slice(pos+1)]
    }
  }
});

function tryCachedImageOtherwiseRenderLive(pages){
	let urls = []
	pages.map( i => {
		let cached = baseCachedURL + i.group + "_" + i.name + imageExtension
		urls.push( cached )
		fetch( cached).then( res => res.status ).then( r => { if (r==404) 
			// console.log("try to get new one", r, cached)
			replaceCachedImageByLive(i.group, i.name)
		} )
	})
	return urls
}

function replaceCachedImageByLive(group, name){
	const live = baseLiveURL+group+"_"+name+imageExtension
	fetch( live ).then( res => res.status ).then( r => {
		if (r==200) 
		// check if in the "new" cache before doing a live query first
			document.querySelector("[src='"+baseCachedURL+group+"_"+name+imageExtension+"']")
				.setAttribute("src", live)
		else
			fetch( queryFormatBaseURL+group+"/"+name+renderSuffix ).then( res => res.json() )
			.then( document.querySelector("[src='"+baseCachedURL+group+"_"+name+imageExtension+"']")
				.setAttribute("src", live)
			)
	} )
}

AFRAME.registerComponent('screenstack', {
// this could be potentially be replaced with web-url
  init: function () {
	//load()
	//if (cabin && cabin.length > 0) return // test doesn't seem to work well on new page / 1st load
		  // see CEREMA project, seems to handle caching better
	var el = this.el
	let generatorName = this.attrName
	fetch(wikiAsImages).then(response => response.json()).then(data =>
		imagesFromURLs(
			tryCachedImageOtherwiseRenderLive(
				Object.entries(data.Nodes).map(( [k, v] ) => { return {group:v.Group, name:v.Label} } ).slice(0,maxItems)
			)
		, el, generatorName )
	)
	// example time sorting
	/* fetch('/screens').then(response => response.json()).then(data => console.log(
		  data.files.filter( i => i.indexOf("_000") < 0 ).map( i => Number(i.replace(".png", "")) ).filter( i => i > now-60 ) 
	) )
        */

	// works only locally for privacy reasons.
	//fetch('colors.txt').then(response => response.text()).then(data => coloredBlocksFromScreens(data.split("\n").splice(-maxItems), el))
	// timings should match as colors are generated from the screens

	//fetch('currentURL.txt').then(response => response.text()).then(data => URLs(data.split("\n").splice(-maxItems), el ))
	// could slice the array based on dates and e.g limit on current day or last 24hrs
  }
});

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
}
	
/*
alternatively could looks for the intersecting bounding boxes of all targets
then from those pick the closest one (again based on center)

Both work well... but without any depth/thickness the chance of intersection are null...
extruding the plane to a volume on a known axis, e.g z here (no rotation) is trivial but limiting.

Could rely on the bounding sphere instead but not ideal when text is beside other pieces of text

Note that in practice we plan to bring geometry with the text, a la Scratch, to showcase grammar and potential to combine.
Consequently those problems would probably go away by intersecting with that geometry instead.

That still means an efficient solution, e.g convex hull or BVH
*/

// e.g addBackgroundBoxToTextElements( targets.filter( e => e.localName == "a-text" ) )
// note that background is "just" for the user in the sense that an invisible bounding box is enough for interactions
function addBackgroundBoxToTextElements( textElements ){
	textElements.map( el => {

		addBoundingBoxToTextElement( el )

		var bbox = new THREE.Box3().setFromObject( el.object3D.children[0] )
			// the text element itself has no volume whereas its first children is a mesh
		var scale = el.getAttribute("scale").x // assume being uniform
		var box = document.createElement("a-box")
		el.appendChild( box )
		box.setAttribute("width", (bbox.max.x - bbox.min.x) * 1/scale)
		box.setAttribute("height", (bbox.max.y - bbox.min.y) * 1/scale)
		box.setAttribute("depth", scale)
		box.setAttribute("position", (bbox.max.x - bbox.min.x) * (1/scale) / 2 + " 0 " + -scale/2+0.0001 )

		setTimeout( _ => { // AFrame induced delay
			// should check instead how to make sure an object is indeed created.
				// maybe via el.getObject3D()
			var compoundbbox = new THREE.Box3().setFromObject( box.object3D )
			compoundbbox.expandByObject( el.object3D )
			var helperbox = new THREE.BoxHelper( box.object3D, 0x00ff00);
			helperbox.update();
			AFRAME.scenes[0].object3D.add(helperbox);
			/*
			var helpercompound = new THREE.BoundingBoxHelper( compoundbbox, 0x0000ff);
			helpercompound.update();
			AFRAME.scenes[0].object3D.add(helpercompound);
			*/
		}, 100)
	})
}

function addBoundingBoxToTextElement( el ){
	var meshEl = el.object3D.children.filter( e => (e.type == "Mesh") )[0]
	var helper = new THREE.BoundingBoxHelper(meshEl, 0xff0000);
		// otherwise doesn't work with icon...
	helper.update();
	AFRAME.scenes[0].object3D.add(helper);
	el.setAttribute("box-uuid", helper.uuid )
	groupHelpers.push( helper )
}

function removeBoundingBoxToTextElement( el ){
	var uuid = el.getAttribute("box-uuid")
	el.removeAttribute("box-uuid")
	//AFRAME.scenes[0].object3D.traverse( e => { if (e.uuid == uuid) e.removeFromParent() })
	//AFRAME.scenes[0].object3D.traverse( e => { if (e.uuid == uuid) AFRAME.scenes[0].object3D.remove(e) })
	AFRAME.scenes[0].object3D.traverse( e => { console.log(e.uuid == uuid) })
	AFRAME.scenes[0].object3D.traverse( e => { if (e.uuid == uuid) console.log("found", e)})
		// somehow removing did work before ...
}

function groupSelectionToNewNote(){
	var text = ""
	groupSelection.map( grpel => {
		//removeBoundingBoxToTextElement( grpel )
			// somehow fails...
		text += grpel.getAttribute("value") + "\n"
	})
	groupHelpers.map( e => e.removeFromParent() )
	groupHelpers = []
	groupSelection = []
	addNewNote( text )
}

function addToGroup( position ){
	var el = getClosestTargetElement( position )
	if (!el) return
	groupSelection.push( el )
	addBoundingBoxToTextElement( el )
}

function appendToFeedbackHUD(txt){
	setFeedbackHUD( document.querySelector("#feedbackhud").getAttribute("value") + " " + txt )
}

function setFeedbackHUD(txt){
	document.querySelector("#feedbackhud").setAttribute("value",txt)
	setTimeout( _ => document.querySelector("#feedbackhud").setAttribute("value","") , 2000) 
}

function appendToHUD(txt){
	const textHUD = document.querySelector("#typinghud").getAttribute("value") 
	if ( textHUD == startingText)
		setHUD( txt )
	else
		setHUD( textHUD + txt )
}

function setHUD(txt){
	document.querySelector("#typinghud").setAttribute("value",txt)
}

AFRAME.registerComponent('waistattach',{
  schema: {
    target: {type: 'selectorAll'},
  },
  init: function () {
	var el = this.el
	this.worldPosition=new THREE.Vector3();
  },
  tick: function () {
	var worldPosition=this.worldPosition;
	worldPosition.copy(this.el.object3D.position);this.el.object3D.parent.updateMatrixWorld();this.el.object3D.parent.localToWorld(worldPosition)
	Array.from( this.data.target ).map( t => {
		t.object3D.position.x = worldPosition.x
		t.object3D.position.z = worldPosition.z
	})
  },
});

AFRAME.registerComponent('attach',{
  schema: {
    target: {type: 'selector'},
  },
  init: function () {
	var el = this.el
	this.worldPosition=new THREE.Vector3();
  },
  tick: function () {
	  var worldPosition=this.worldPosition;
	  worldPosition.copy(this.el.position);
	  this.el.parent.updateMatrixWorld();
	  this.el.parent.localToWorld(worldPosition)
	  rotation = this.el.rotation.x*180/3.14 + " " + this.el.rotation.y*180/3.14 + " " + this.el.rotation.z*180/3.14
	  this.data.target.setAttribute("rotation", rotation)
	  this.data.target.setAttribute("position",
		  AFRAME.utils.coordinates.stringify( worldPosition ) )
  },
  remove: function() {
  }
});

AFRAME.registerComponent('wristattachsecondary',{
  schema: {
    target: {type: 'selector'},
  },
  init: function () {
	var el = this.el
	this.worldPosition=new THREE.Vector3();
  },
  tick: function () {
	// could check if it exists first, or isn't 0 0 0... might re-attach fine, to test
		  // somehow very far away... need to convert to local coordinate probably
		  // localToWorld?
	(primarySide == 0) ? secondarySide = 1 : secondarySide = 0
	var worldPosition=this.worldPosition;
	this.el.object3D.traverse( e => { if (e.name == "b_"+sides[secondarySide][0]+"_wrist") {
		worldPosition.copy(e.position);e.parent.updateMatrixWorld();e.parent.localToWorld(worldPosition)
		rotation = e.rotation.x*180/3.14 + " " + e.rotation.y*180/3.14 + " " + e.rotation.z*180/3.14
		this.data.target.setAttribute("rotation", rotation)
		this.data.target.setAttribute("position",
				AFRAME.utils.coordinates.stringify( worldPosition ) )
			  // doesnt work anymore...
		//this.data.target.setAttribute("rotation", AFRAME.utils.coordinates.stringify( e.getAttribute("rotation") )
	  }
	})
  },
  remove: function() {
	// should remove event listeners here. Requires naming them.
  }
});

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
		if (setupMode) setupBBox["B"] = event.detail.position
		if ( setupBBox["A"] && setupBBox["B"] ) {
			setupMode = false
			setFeedbackHUD( JSON.stringify(setupBBox))
		}
		/*
		selectionPinchMode = false
		setHUD( AFRAME.utils.coordinates.stringify( bbox.min ),
			AFRAME.utils.coordinates.stringify( bbox.max ) )
		bbox.min.copy( zeroVector3 )
		bbox.man.copy( zeroVector3 )
	       */
	});
	this.el.addEventListener('pinchmoved', function (event) {
		if (selectionPinchMode){
			bbox.min.copy( event.detail.position )
			setFeedbackHUD( "selectionPinchMode updated min")
			if (!bbox.max.equal(zeroVector3))
				selectionBox.update();
		}
	});
	this.el.addEventListener('pinchstarted', function (event) {
		if (!selectionPinchMode) bbox.min.copy( zeroVector3 )
		if (selectionPinchMode) setFeedbackHUD( "selectionPinchMode started")
	});
  },
  remove: function() {
	// should remove event listeners here. Requires naming them.
  }
});

AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right one, should be switchable

// consider instead https://github.com/AdaRoseCannon/handy-work/blob/main/README-AFRAME.md for specific poses
// or https://aframe.io/aframe/examples/showcase/hand-tracking/pinchable.js 

  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 )
		//if (closests && closests.length > 0) // avoiding self reference
		//	setFeedbackHUD("close enough, could stack with "+ closests[1].el.getAttribute("value") )
		var dist = event.detail.position.distanceTo( document.querySelector("#box").object3D.position )
		if (dist < .1){
			setFeedbackHUD("close enough, replaced shortcut with "+ selectedElement.getAttribute("value") )
			wristShortcut = selectedElement.getAttribute("value")
		}
		selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:true})
		// unselect current target if any
		selectedElement = null;
		save()
		if (setupMode) setupBBox["A"] = event.detail.position
			// somehow keeps on setting up... shouldn't once done.
		if ( setupBBox["A"] && setupBBox["B"] ) {
			setupMode = false
			setFeedbackHUD( JSON.stringify(setupBBox))
		}
		if ( drawingMode ) draw( event.detail.position )
		if ( groupingMode ) addToGroup( event.detail.position )
		selectionPinchMode = false
		/*
		setHUD( AFRAME.utils.coordinates.stringify( bbox.min ),
			AFRAME.utils.coordinates.stringify( bbox.max ) )
		bbox.min.copy( zeroVector3 )
		bbox.man.copy( zeroVector3 )
	       */
		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 (selectionPinchMode){
			bbox.max.copy( event.detail.position )
			if (!bbox.min.equal(zeroVector3))
				selectionBox.update();
		}
		if (selectedElement && !groupingMode) {
			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
		if (!selectionPinchMode) bbox.max.copy( zeroVector3 )

		//var clone = getClosestTargetElement( event.detail.position ).cloneNode()
		// might want to limit cloning to unmoved element and otherwise move the cloned one
		//AFRAME.scenes[0].appendChild( clone )
		//targets.push( clone )
		//selectedElement = clone

		selectedElement = getClosestTargetElement( event.detail.position )
		// is it truly world position? See https://github.com/aframevr/aframe/issues/5182
		setFeedbackHUD( AFRAME.utils.coordinates.stringify( event.detail.position ) )
		// 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.
  }
});

// testing on desktop
function switchToWireframe(){
	visible = !visible
	/*
	targets.map( e => {
			scale = 50// should be a variable instead
			e.setAttribute("scale", visible ? ".05 .05 .05" : ".1 .1 .1" )
			var pos = AFRAME.utils.coordinates.parse( e.getAttribute("position") )
			//visible ? pos.z *= scale : pos.z /= scale // might be the opposite but anyway give the principle
			e.setAttribute("position", AFRAME.utils.coordinates.stringify(pos))
			// should actually be just for src, not for text notes... even though could be interesting
	})
	*/
	var model = document.querySelector("#environment").object3D
	model.traverse( o => { if (o.material) {
			o.material.wireframe = visible;
			o.material.opacity = visible ? 0.05 : 1;
			o.material.transparent = visible;
	} })
}

// add (JXR) shortcuts as PIM function from e.g https://observablehq.com/@utopiah/from-pim-to-2d-to-3d-to-xr-explorations
	// allowing to search within PIM then show manipulable pages as preview.

// note that can be tested in VR also as jxr switchToWireframe()
	// could make for nice in VR testing setup as eved notes

function enterSetupMode(){
	// rely on 2 pinches to create a bounding box of safe interaction
	// https://threejs.org/docs/#api/en/math/Box3.containsBox
	setupMode = true
}

AFRAME.registerComponent('start-on-press', {
	// should become a property of the component instead to be more flexible.
	init: function(){
		var el = this.el
		this.el.addEventListener('pressedended', function (event) { 
			if (!primaryPinchStarted && wristShortcut.match(prefix)) interpretJXR(wristShortcut)
			// other action could possibly based on position relative to zones instead, i.e a list of bbox/functions pairs
		})
	}
})
//---- other components : ------
// could become like https://twitter.com/utopiah/status/1264131327269502976
	// can include a mini typing game to warm up finger placement

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
	// see setupBBox in pinchprimary and pinchsecondary
	// then addBoundingBoxToTextElement()
}
// note that the bbox with vertical position model is still interesting
	// (if within bounding box, try to execute code)
	// because it allows grouping and sequentially rather executing line by line
	// see https://threejs.org/docs/#api/en/math/Box3.containsBox

// save pose of targets and src locally and if available on PIM
/*
savingJSON = targets.map( e => {
	rot : e.getAttribute("rotation"),
	pos : e.getAttribute("position"), 
	scale : e.getAttribute("scale"), 
	src : e.getAttribute("src"), 
	value : e.getAttribute("value"), 
})
*/
// load alt set of items e.g from https://observablehq.com/@utopiah/from-pim-to-2d-to-3d-to-xr-explorations
	// or https://fabien.benetou.fr/pub/home/pimxr-experimentation/sources.json

// position should be configurable as rotation is handled by the OS

function parsePointer( x,y ){
		console.log(x,y)
	if (!sketchEl) {
		sketchEl = document.createElement("a-entity")
		// sketchEl.setAttribute("position", "0 1.4 -0.3") otherwise lines don't align
			// could counter that offset but might be problematic later on with translations/rotations
		targets.push( sketchEl )
		AFRAME.scenes[0].appendChild(sketchEl)
	}
	var el = document.createElement("a-sphere")
	var pos = x/1000 + " " + y/1000 + " 0"
		// should offset and flip properly
	el.setAttribute("position", pos)
	el.setAttribute("radius", 0.01)
	el.setAttribute("color", "green")
	sketchEl.appendChild( el )
	if (lastPointSketch){
		var oldpos = AFRAME.utils.coordinates.stringify( lastPointSketch.getAttribute("position") )
		sketchEl.setAttribute("line__"+ Date.now(), `start: ${oldpos}; end : ${pos};`)
	}
	lastPointSketch = el
	
}

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

AFRAME.registerComponent('hud', {
	init: function(){
		var feedbackHUDel= document.createElement("a-troika-text")
		feedbackHUDel.id = "feedbackhud"
		feedbackHUDel.setAttribute("value", "")
		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.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("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)
	return newnote
}

function interpretAny( code ){

	if (!code.match(/^dxr /)) return
	var newcode = code
	newcode = newcode.replace("dxr ", "")
	//newcode = newcode.replace(/bash ([^\s]+)/ ,`debian '$1'`) // syntax delegated server side
	fetch("/command?command="+newcode).then( d => d.json() ).then( d => {
		console.log( d.res )
		appendToHUD( d.res ) // consider shortcut like in jxr to modify the scene directly
		// res might return that said language isn't support
			// commandlistlanguages could return a list of supported languages
	})
}

function draw( position ){
	var drawingMoment = +Date.now()
	var pos = AFRAME.utils.coordinates.stringify( position )
	// add sphere per point
	var el = document.createElement("a-sphere")
	el.setAttribute("position", pos)
	el.setAttribute("radius", 0.001)
	el.setAttribute("color", "lightblue")
	el.setAttribute("dateadded", drawingMoment )
	// if previous point exist, draw line between both
	var pastPoints = Array.from( document.querySelectorAll("[dateadded]") )
		.sort( (a,b) => a.getAttribute("dateadded") - b.getAttribute("dateadded") )
	if (pastPoints.length) {
		var previousPoint = pastPoints[0]
		var drawing = previousPoint.parentElement
		var oldpos = AFRAME.utils.coordinates.stringify( previousPoint.getAttribute("position") )
		drawing.setAttribute("line__"+ drawingMoment, `start: ${oldpos}; end : ${pos};`)
	} else {
		var drawing = document.createElement("a-entity")
		drawing.className = "drawing"
		AFRAME.scenes[0].appendChild( drawing )
	}
	drawing.appendChild( el )
	// if sufficiently close to another sphere (the first) close the loop
	if (pastPoints.length>1) {
		var lastPoint = pastPoints[pastPoints.length-1]
		var oldpos = AFRAME.utils.coordinates.stringify( lastPoint.getAttribute("position") )
		if (lastPoint.getAttribute("position").distanceTo( position) < 0.1) // threshold
			drawing.setAttribute("line__"+ drawingMoment + "_closeloop", `start: ${oldpos}; end : ${pos};`)
			// then enter extrude mode (assume they are on 1 plane)
			// should also prevent from adding points to the current drawing
			// before investing too much effort in this, should consider how it would actually improve usage
			// especially as we can add AFrame primitives with the keyboard
				// intersection of kbd and hand tracked 6DoF being the primary usage
	}

	/* test values, must wait .1 second between otherwise there is no known position
		(most likely AFrame to threejs delay)
	draw( new THREE.Vector3(-0.04, 1.7, -1) );
	draw( new THREE.Vector3(0.04, 1.7, -1) );
	draw( new THREE.Vector3(0, 1.72, -1) );
	*/


}

// the goal is to associate objects as shape with volume to code snippet
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)

	// consider https://sketchfab.com/developers/download-api/downloading-models/javascript
}

function showhistory(){
	setFeedbackHUD("history :\n")
	commandhistory.map( i => appendToHUD(i.uninterpreted+"\n") )
}

function saveHistoryAsCompoundSnippet(){
	addNewNote( commandhistory.map( e => e.uninterpreted ).join("\n") )
}

function bindVariableValueToNewNote(variableName){
	// from observe jxr keyword
	const idName = "bindVariableValueToNewNote"+variableName
	addNewNote( variableName + ":" + eval(variableName), `-0.15 1.4 -0.1`,  "0.1 0.1 0.1", idName, "observers", "true" )
	// could add to the HUD instead and have a list of these
	return setInterval( _ => {
		const value = variableName+";"+eval(variableName)
		// not ideal for DOM elements, could have shortcuts for at least a-text with properties, e.g value or position
		document.getElementById(idName).setAttribute("value", value)
	}, 100 )
}

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]+) (.*)/,`.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) return
	if (code.length == 1) { // special case of being a single character, thus keyboard
		if (code == ">") { // Enter equivalent
			content =  hudTextEl.getAttribute("value") 
			if (Number.isFinite(Number(content))) {
				loadPageRange(Number(content));
			} else {
				addNewNote( content )
			}
			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('toolbox', { // ununsed
	init: function(){
		var el = this.el
		var e = document.createElement("a-sphere")
		e.setAttribute("scale", "0.1 0.1 0.1")
		e.setAttribute("color", "lightblue")
		e.setAttribute("pressable")
		e.id = "toolboxsphere"
		el.appendChild( e )
		var e = document.createElement("a-cylinder")
		e.setAttribute("scale", "0.1 0.1 0.1")
		e.setAttribute("color", "darkred")
		e.setAttribute("pressable")
		e.id = "toolboxcylinder"
		el.appendChild( e )
		var e = document.createElement("a-box")
		e.setAttribute("scale", "0.1 0.1 0.1")
		e.setAttribute("color", "pink")
		e.setAttribute("pressable")
		e.id = "toolbox"
		el.appendChild( e )
	},
	tick: function(){
		var toolbox = document.querySelector("#toolbox")
		var cam = document.querySelector("[camera]")
		toolbox.object3D.position.x = cam.getAttribute("position").x-0.5
		toolbox.object3D.position.z = cam.getAttribute("position").z+0.2
		//toolbox.object3D.rotation.y = cam.getAttribute("rotation").y
	}
})

// from https://aframe.io/aframe/examples/showcase/hand-tracking/pressable.js
AFRAME.registerComponent('pressable', {
	schema:{pressDistance:{default:0.06}},
	init:function(){this.worldPosition=new THREE.Vector3();this.handEls=document.querySelectorAll('[hand-tracking-controls]');this.pressed=false;},
	tick:function(){var handEls=this.handEls;var handEl;var distance;for(var i=0;i<handEls.length;i++){handEl=handEls[i];distance=this.calculateFingerDistance(handEl.components['hand-tracking-controls'].indexTipPosition);if(distance<this.data.pressDistance){if(!this.pressed){this.el.emit('pressedstarted');} this.pressed=true;return;}} if(this.pressed){this.el.emit('pressedended');} this.pressed=false;},
	calculateFingerDistance:function(fingerPosition){var el=this.el;var worldPosition=this.worldPosition;worldPosition.copy(el.object3D.position);el.object3D.parent.updateMatrixWorld();el.object3D.parent.localToWorld(worldPosition);return worldPosition.distanceTo(fingerPosition);}
});

AFRAME.registerComponent('selectionboxonpinches', {
	init:function(){
		AFRAME.scenes[0].object3D.add(selectionBox);
	}
})

AFRAME.registerComponent('keyboard', {
	init:function(){
		let generatorName = this.attrName
		const horizontaloffset = .7
		const horizontalratio = 1/20
		alphabet.map( (line,ln) => {
		 for (var i = 0; i < line.length; i++) {
			var pos = i * horizontalratio - horizontaloffset
                        addNewNote( line[i], pos+" "+(1.6-ln*.06)+" -.4", ".1 .1 .1", null, generatorName)
		 }
		})
	}
})
	
AFRAME.registerComponent('capturegesture', {
	init:function(){this.handEls=document.querySelectorAll('[hand-tracking-controls]');},
	tick:function(){
		document.querySelector("#rightHand").object3D.traverse( e => { if (e.name == "b_r_wrist") console.log("rw", e.rotation) })
		document.querySelector("#leftHand" ).object3D.traverse( e => { if (e.name == "b_l_wrist") console.log("rl", e.rotation) })
			// should look up thumb-metacarpal and index-finger-metacarpal if not sufficient
				// might trickle down iif wrist rotation itself is already good
			// https://immersive-web.github.io/webxr-hand-input/
	}
});

AFRAME.registerComponent('timeline', {
        init:function(){
		let generatorName = this.attrName
                fetch("../content/fot_timeline.json").then(res => res.json() ).then(res => {
                        res.fot_timeline.slice(0,maxItemsFromSources).map( (c,i) => addNewNote( c.year+"_"+c.event, "1 "+i/10+" -1", ".1 .1 .1", null, generatorName) ) 
                })
        },
});

AFRAME.registerComponent('glossary', {
	init:function(){
		let generatorName = this.attrName
		fetch("content/glossary.json").then(res => res.json() ).then(res => {
			Object.values(res.entries).slice(0,maxItemsFromSources).map( (c,i) => addNewNote( c.phrase + c.entry.slice(0,50)+"..." , "-1 "+i/10+" -1", ".1 .1 .1", null, generatorName) ) 
		})
	},
});

AFRAME.registerComponent('fot', {
	init:function(){
	},
	tick: function(){
		let generatorName = this.attrName
		fetch("https://fabien.benetou.fr/PIMVRdata/FoT?action=source#" + Date.now()).then(res => res.text() ).then(res => {
			res.split("\n").slice(0,maxItemsFromSources).map( (n,i) => {
				found = added.find((str) => str === n)
				if (typeof found === 'undefined'){
					added.push(n)
					addNewNote( n, "-1 "+(1+i/10)+" -2.5", ".1 .1 .1", null, generatorName ) 
				}
			})
		})
	}
});

AFRAME.registerComponent('issues', {
	init:function(){
		let generatorName = this.attrName
		// fetch("https://api.github.com/repos/Utopiah/relax-plus-think-space/issues").then(res => res.json() ).then(res => {
		fetch("https://git.benetou.fr/api/v1/repos/utopiah/text-code-xr-engine/issues").then(res => res.json() ).then(res => {
			res.slice(0,maxItemsFromSources).map( (n,i) => addNewNote( n.title, "0 "+(1+i/10)+" -1.8", ".1 .1 .1", null, generatorName ) )
		})
	},
});

AFRAME.registerComponent('dynamic-view', {
	init:function(){
		let generatorName = this.attrName
		fetch("content/DynamicView.json").then(res => res.json() ).then(res => {
			res.nodes.slice(0,maxItemsFromSources).map( n => addNewNote( n.title, "" + res.layout.nodePositions[n.identifier].x/100 + " " + res.layout.nodePositions[n.identifier].y/100 + " -1", ".1 .1 .1", null, generatorName ) )
		})
	},
});

function toggleVisibilityEntitiesFromClass(classname){
	let entities = Array.from( document.querySelectorAll("."+classname) )
	if (entities.length == 0) return
	let state = entities[0].getAttribute("visible") // assume they are all the same
	if (state)
		entities.map( e => e.setAttribute("visible", "false"))
	else
		entities.map( e => e.setAttribute("visible", "true"))
}

function pushUpClass(classname, value=.1){
	Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.y += value)
}

function pushDownClass(classname, value=.1){
// can be used for accessibiliy, either directly or sampling e.g 10s after entering VR to lower based on the estimated user height
	Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.y -= value)
}

function pushBackClass(classname, value=.1){
	Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.z -= value)
}

function pushFrontClass(classname, value=.1){
	Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.z += value)
}

function toggleVisibilityAllGenerators(){
	generators.split(" ").map( g => toggleVisibilityEntitiesFromClass(g) )
	// not hidableassets though
}

function toggleVisibilityAll(){
	toggleVisibilityAllGenerators()
	toggleVisibilityEntitiesFromClass("hidableassets")
}

function toggleVisibilityAllButClass(classname){
	generators.split(" ").filter( e => e != classname).map( g => toggleVisibilityEntitiesFromClass(g) )
	toggleVisibilityEntitiesFromClass("hidableassets")
}

AFRAME.registerComponent('adjust-height-in-vr', {
	init: function(){
		AFRAME.scenes[0].addEventListener("enter-vr", _ => {
			setTimeout( _ => { // getting the value right away returns 0, so short delay
				userHeight = document.querySelector("#player").object3D.position.y
				// assume the user does not change, some might prefer to use standing up first then sit down.
					// otherwise explicit controls
				heightAdjustableClasses.map( c => {
					max = Math.max.apply(null, Array.from( document.querySelectorAll("."+c) ).map( e => e.object3D.position.y) )	
					min = Math.min.apply(null, Array.from( document.querySelectorAll("."+c) ).map( e => e.object3D.position.y) )	
					pushDownClass(c, userHeight - (max-min)/2 )
					setFeedbackHUD( "adjusted height by:" + ( userHeight - (max-min)/2 ) )
				} )
			}, 100 )
		})
	}
})

AFRAME.registerComponent('commands-from-external-json', {
/*
// following discussion with Yanick

// for cabin.html to faciliate growth and flexibility

fetch('./commands.json') // then load like A-Frame elements
// e.g command //{name,defaultpose,autorun,description}
// could even be stored as wiki page

fetch('./templates.json')
// e.g [commands with optional pose]

// watch can be a template too
	<a-text side="double" networked="template:#text-template;attachTemplateToLocal:false;" id="instructionA" value="Interactive instructions:" position="0 1.5 -0.2" scale="0.2 0.2 0.2"></a-text>
	<a-text side="double" id="instructionB" target value="jxr lg Fox.glb 0.001" position="0 1.65 -0.2" scale="0.1 0.1 0.1">
		<a-entity gltf-model="Fox.glb" scale="0.005 0.005 0.005"></a-entity>
	</a-text>
	<a-text side="double" networked="template:#text-template;attachTemplateToLocal:false;" id="instructionC" target value="jxr obsv observablehq-numberOfPages-835aa7e9" position="0 1.55 -0.2" scale="0.1 0.1 0.1"></a-text>
	<a-text side="double" networked="template:#text-template;attachTemplateToLocal:false;" id="instructionD" target value="jxr 1s startSelectionVolume()" position="0 1.50 -0.2" scale="0.1 0.1 0.1"></a-text>
*/
	init:function(){
		var el = this.el
		let generatorName = this.attrName
		var links = [ // could be in the commands file instead
			"target:#instructionA; source:#instructionB",
			"target:#instructionA; source:#instructionC",
			"target:#instructionA; source:#instructionD",
		]
		links = []
		//fetch("commands.json").then(res => res.json() ).then(res => {
		var commandsURL = "https://fabien.benetou.fr/PIMVRdata/CabinCommands?action=source"
		commandsURL = "https://fabien.benetou.fr/PIMVRdata/EngineSequentialTutorialCommands?action=source" // new default
		var src = AFRAME.utils.getUrlParameter('commands-url')
		if (src && src != "") commandsURL = src
		fetch(commandsURL).then(res => res.json() ).then(res => {
			// to consider for remoteLoad/remoteSave instead, to distinguish from url though.
			// also potential security concern so might insure that only a specific user, with mandatory password access, added commands.
			var visible = true
			if (c.visible) visible = c.visible
			res.map( c => addNewNote( c.value, c.position, c.scale, c.id, generatorName, c.visible) )
				// missing name/title, autorun (true/false), description, 3D icon/visual, visiblity (useful for sequential tutorial)
			links.map( l => { var linkEl = document.createElement("a-entity"); 
					 linkEl.setAttribute("line-link-entities", l)
					 el.appendChild(linkEl) 
			} )
			var hideRest = AFRAME.utils.getUrlParameter('commands-hide-rest')
			if (hideRest && hideRest != "") setTimeout( _ => toggleVisibilityAllButClass('commands-from-external-json'), 5000) // waiting for everything to have loaded...
		})
	},
});

function save(){
	var data = targets.map( e => { return {
			localname: e.localName,
			src: e.getAttribute("src"),
			position: e.getAttribute("position"),
			rotation: e.getAttribute("rotation"),
			scale: e.getAttribute("scale"),
			value: e.getAttribute("value"),
		} } )
	cabin = data
	localStorage.setItem('cabin', JSON.stringify( data) )
	return data
	// could be called on page exit, unsure if reliable in VR
	// alternatively could be call after each content is moved or created
}

function load(){
	if (localStorage.getItem('cabin'))
		cabin = JSON.parse(localStorage.getItem('cabin'))
	cabin.map( e => {
		var newel = document.createElement(e.localname)
		savedProperties.map( p => {
			if (e[p] ) newel.setAttribute(p, e[p])
		})
		AFRAME.scenes[0].appendChild( newel )
	})
}

function remoteLoad(){
	fetch(url+'source') 
		.then( response => { return response.json() } )
		.then( data => { console.log("remote data:", data) })
	// does actually load back. Should consider what load() does instead.

	// for the reMarkable write back in source, OCR/HWR could be done on the WebXR device instead
		// alternatively "just" sending the .jpg thumbnail would be a good enough start
		// note that highlights are also JSON files
		// both might not be ideal directly in the original JSON but could be attachement as URLs
}

function remoteSave(){
	  fetch(url+'edit', {
		  method: 'POST',
		  headers: {'Content-Type':'application/x-www-form-urlencoded'},
		  body: "post=1&author=PIMVR&authpw=edit_password&text="+JSON.stringify( cabin )
	  }).then(res => res).then(res => console.log("saved remotely", res))
}

function switchSide(){
	// mostly works... but event listeners are not properly removed. Quickly creates a mess, low performance and unpredictable.
	document.querySelector("#"+sides[primarySide]+"Hand").removeAttribute("pinchprimary")
	document.querySelector("#"+sides[secondarySide]+"Hand").removeAttribute("pinchsecondary")
	document.querySelector("#"+sides[secondarySide]+"Hand").removeAttribute("wristattachsecondary")
	document.querySelector("#"+sides[secondarySide]+"Hand").setAttribute("pinchprimary", "")
	document.querySelector("#"+sides[primarySide]+"Hand").setAttribute("pinchsecondary", "")
	document.querySelector("#"+sides[primarySide]+"Hand").setAttribute("wristattachsecondary", "target: #box")
	if (primarySide == 0) {
		secondarySide = 0
		primarySide = 1
	} else {
		primarySide = 0
		secondarySide = 1
	}
}

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

function loadPageRange(start=1, end=-1, startPosition={x:0, y:1.3, z:-.7}, stepVector={x:.2, y:0, z:0}){
	const baseURL = "https://fabien.benetou.fr/pub/home/future_of_text_demo/content/book_as_png/gfg_d-"
	const extension = ".png"

	// assumes portrait A4-ish
	var rootEl = AFRAME.scenes[0]
	if (end<0) end = start
	
	let step = 0
	for (let i=start; i<=end; i++){
		step++
		let el = document.createElement("a-box")
		el.setAttribute("target", true)
		//el.setAttribute("position", ""+ step/5+ " 1.3 -.7") // could be based on selectedElements last position instead
		let pos = "" + startPosition.x+stepVector.x*step + " " + startPosition.y+stepVector.y*step + " " + startPosition.z+stepVector.z*step 
		el.setAttribute("position", pos)
		// layout system could be parametric, e.g over x or y or z or another system
		el.setAttribute("width", ".1")
		el.setAttribute("height", ".15")
		el.setAttribute("depth", ".01")
		pageNumber = i
		if (pageNumber<10) pageNumber = "0"+pageNumber
		if (pageNumber<100) pageNumber = "0"+pageNumber
		el.setAttribute("src", baseURL+pageNumber+extension)
		el.setAttribute("pagenumber", pageNumber)
		el.id = pageNumber + "_" + Date.now()
		rootEl.appendChild(el)
		let posInterface = "" + startPosition.x+stepVector.x*step + " " + startPosition.y+1+stepVector.y*step + " " + startPosition.z+stepVector.z*step 
		let UI = addNewNote("jxr nextPage('"+el.id+"')", posInterface, "0.1 0.1 0.1", el.id+"_interface")
		//el.setAttribute("attach","target:#"+el.id+"_interface")
	}
}

function writeWebDAV(){
	const webdavurl = "https://webdav.benetou.fr";
	const client = window.WebDAV.createClient(webdavurl)
	async function w(path = "/file.txt"){ return await client.putFileContents(path, "SpaSca test"); }
	w("/fot.txt") // need new permissions
}

function getPagesFromWebDAV(){
	const webdavurl = "https://webdav.benetou.fr";
	const client = window.WebDAV.createClient(webdavurl)
	async function getDirectory(path = "/"){ return await client.getDirectoryContents(path); }
	getDirectory("book_as_png").then( d => d.sort( (a,b) => (a.filename>b.filename)).slice(0,10).map( (c,i) => addPageFromURL(webdavurl+c.filename)))
}

function addPageFromURL(url){
	if (url.indexOf(".png")<0) return
	let el = document.createElement("a-box")
	el.setAttribute("position", -Math.random()+" "+Math.random()*3 + " -1")
	el.setAttribute("width", ".1")
	el.setAttribute("height", ".15")
	el.setAttribute("depth", ".01")
	el.setAttribute("src", url)
	AFRAME.scenes[0].appendChild(el)
	return el
}

// same principle to go from nextPage() to openingLinkedPages() from wiki URL
	// consider screenstack, could add a note to mode further

function loadWikiAsGraph(){
	fetch(wikiAsImages).then(response => response.json()).then(data => {
		Object.entries(data.Nodes).slice(0,maxItems).map( v => {
			let pageName = v[0]
			let targest = v[1].Targets
			let el = addPageFromURL(baseLiveURL+pageName.replace(".","_")+imageExtension)
			el.id = pageName
			el.classname = "wikipage"
			// should rely on tryCachedImageOtherwiseRenderLive(pages) instead
			setTimeout( _ => { 
				let pos = el.getAttribute("position")
				let UI = addNewNote("jxr openFromNode('"+el.id+"')", pos, "0.1 0.1 0.1", el.id+"_interface")
				console.log("should add: addNewNote('jxr openNewNode("+pageName+")')")
			}, 100 ) // wait for the entity to be actually added
			// to be coupled with loadCodeFromPage()
				// see also the idea that each wiki page wouldn't just be descriptive but also have code
					// related pages
					// https://fabien.benetou.fr/Fabien/Principle
					// https://fabien.benetou.fr/CognitiveEnvironments/CognitiveEnvironments
					// https://fabien.benetou.fr/Cookbook/Cognition
		})
	})
}

function nextPage(id){
console.log("nextpage()")
	// assuming only direct parent for now
	const baseURL = "https://fabien.benetou.fr/pub/home/future_of_text_demo/content/book_as_png/gfg_d-"
	const extension = ".png"
	let pageNumber = Number( id.split("_")[0] )
console.log(pageNumber+1)
	loadPageRange(pageNumber+1)
}

function loadCodeFromPage(url="https://fabien.benetou.fr/Analysis/BeyondTheCaseAgainstBooks?action=source"){
	// alternatively could load from a page number
	fetch(url)
	.then( r => r.text() )
	.then(data => { 
		let code = data.split("\n").filter( l => (l.slice(0,2) == "[@") )[0].slice(2).slice(0,-2);
		// example as PmWiki parsing
		eval(code)
	} )
}

function loadFromMastodon(statusesURL="https://mastodon.pirateparty.be/api/v1/accounts/56066/statuses"){
	fetch(statusesURL).then( r => r.json() ).then( t => t.filter( i => i.in_reply_to_id == null ).map( (i,n) => {
		let div = document.createElement("div")
		div.innerHTML = i.content
		addNewNote(div.innerText, "1 "+ (1.2+(n+1)/20) +" -0.4")
	} ) )
}

// could change model opacity based on hand position, fading out when within a (very small here) safe space
</script>
<div id="observablehq-key">
    <div id="observablehq-viewof-offsetExample-ab4c1560"></div>
    <div id="observablehq-result_as_html-ab4c1560"></div>
</div>
<a-scene cursor="rayOrigin: mouse" raycaster="objects: [html]; interval:100;" adjust-height-in-vr
	toolbox disable-components-via-url enable-components-via-url NOcommands-from-external-json keyboard >
	<!-- screenstack dynamic-view selectionboxonpinches glossary timeline issues fot
	networked-scene="serverURL: https://naf.benetou.fr/; adapter: easyrtc; audio: true;"
	-->
      <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-assets>

      <a-entity id="rig">
		<a-sound src="../content/summer-night-ambience.mp3" autoplay=true loop=true volume=0.5></a-sound><!-- warning skipped on Quest, does autoplay there -->
		<a-entity id="player" networked="template:#avatar-template;attachTemplateToLocal:false;" 
			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-entity hide-on-enter-ar="" id="environment" class="hidableenvironment" gltf-model="../content/virtual_reality_meta_room_2022.glb" scale="" position="-0.10754 0.2 6.25885" rotation="0 90 0"></a-entity>

      <a-entity light="type: ambient; color: #BBB; intensity: 0.6"></a-entity>
      <a-entity light="type: directional; color: #FFF; intensity: 0.6" position="-0.5 1 1"></a-entity>
      <a-sky hide-on-enter-ar src="../content/nebula.jpg"></a-sky>

      <a-text target value="instructions : pinch numbers then > to open a page " position="0 1.65 -0.2" scale="0.1 0.1 0.1"></a-text>
      <a-text target value="jxr loadPageRange(1,13)" position="0 1.35 -0.1" scale="0.1 0.1 0.1"></a-text>
      <a-text target value="jxr loadCodeFromPage()" position="0 1.45 -0.1" scale="0.1 0.1 0.1"></a-text>
      <a-text target value="jxr qs #rig sa position 0 0 10" position="0 1.55 .5" rotation="0 180 0" scale="0.1 0.1 0.1"></a-text>
      <!-- somehow disable hand interaction despite, according to the documentation, it should rely on world position -->

      <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-entity id="featureN">
	      <a-image rotation="0 180 0" position="-2 2 10.5" scale=".5 .5 .5" src="../content/features/containers.jpg"></a-image>
	      <a-text rotation="0 180 0" target value="containers" position="-2 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="-2 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="-2 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
      </a-entity>
	<a-image rotation="0 180 0" position="-1 2 10.5" scale=".5 .5 .5" src="../content/features/wireframe.jpg"></a-image>
	<a-image rotation="0 180 0" position="0 2 10.5" scale=".5 .5 .5" src="../content/features/remarkable_sketch.jpg"></a-image>
	<a-image rotation="0 180 0" position="1 2 10.5" scale=".5 .5 .5" src="../content/features/browsing_history.jpg"></a-image>
	<a-image rotation="0 180 0" position="2 2 10.5" scale=".5 .5 .5" src="../content/features/codeembedding.jpg"></a-image>
	<a-image rotation="0 180 0" position="3 2 10.5" scale=".5 .5 .5" src="../content/features/grouping.jpg"></a-image>
	<a-image rotation="0 180 0" position="4 2 10.5" scale=".5 .5 .5" src="../content/features/inspector.jpg"></a-image>
	<a-image rotation="0 180 0" position="5 2 10.5" scale=".5 .5 .5" src="../content/features/javascript_with_shortcuts.jpg"></a-image>
	<a-image rotation="0 180 0" position="6 2 10.5" scale=".5 .5 .5" src="../content/features/load_3D_models.jpg"></a-image>
	<a-image rotation="0 180 0" position="7 2 10.5" scale=".5 .5 .5" src="../content/features/math_plot.jpg"></a-image>
	<a-image rotation="0 180 0" position="8 2 10.5" scale=".5 .5 .5" src="../content/features/networked_input.jpg"></a-image>
	<a-image rotation="0 180 0" position="9 2 10.5" scale=".5 .5 .5" src="../content/features/observable_notebook.jpg"></a-image>
	<a-text rotation="0 180 0" target="" value="Features" position="4.70348 3.07329 10.2" scale="" text=""></a-text>
	      <a-text rotation="0 180 0" target value="containers" position="-1 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="-1 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="-1 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
	      <a-text rotation="0 180 0" target value="containers" position="0 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="0 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="0 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
	      <a-text rotation="0 180 0" target value="containers" position="1 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="1 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="1 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
	      <a-text rotation="0 180 0" target value="potato" position="2 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="2 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="2 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
	      <a-text rotation="0 180 0" target value="containers" position="3 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="3 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="3 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
	      <a-text rotation="0 180 0" target value="containers" position="4 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="4 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="4 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
	      <a-text rotation="0 180 0" target value="containers" position="5 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="5 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="5 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
	      <a-text rotation="0 180 0" target value="containers" position="6 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="6 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="6 1.35 10.5" scale="0.1 0.1 0.1"></a-text>
	      <a-text rotation="0 180 0" target value="containers" position="-2 2.25 10.5" scale=".5 .5 .5"></a-text>
	      <a-text rotation="0 180 0" target value="use the dxr prefix to send data to containers\n(requires backend)" position="-2 1.55 10.5" scale=".1 .1 .1"></a-text>
	      <a-text rotation="0 180 0" target value="dxr python print(7)" position="-2 1.35 10.5" scale="0.1 0.1 0.1"></a-text>

    </a-scene>
  </body>
</html>