Compare commits

...

24 Commits

Author SHA1 Message Date
Fabien Benetou 4c821e305d finetuning graph position 1 year ago
Fabien Benetou e7abb17c12 unfolding graph node via left pinch 1 year ago
Fabien Benetou e82e8db66c respositionning node (fixed) 1 year ago
Fabien Benetou d7884f0f0c respositionning node, once 1 year ago
Fabien Benetou 56f215cde1 display real data but with random layout 1 year ago
Fabien Benetou 146c41650b basic cytoscape loading, no display 1 year ago
Fabien Benetou d71c932e50 grid to snap as searchable datastructure 1 year ago
Fabien Benetou 3327a0e523 Tile generation and scaling 1 year ago
Fabien Benetou 108d178a7d added rotation 1 year ago
Fabien Benetou 3cf9065603 sharing between friends (but without rotation) 1 year ago
Fabien Benetou a92aa271a0 send image and models too 1 year ago
Fabien Benetou f2fbaa2b37 loadFromMastodon() 1 year ago
Fabien Benetou fab5a00f38 higher permission required to create own streams for custom collections 1 year ago
Fabien Benetou fe6383f1f6 event based loading 1 year ago
Fabien Benetou 86b4f3ed9c removed descriptive comments and used code instead with ims() shorthand 1 year ago
Fabien Benetou 1c73d4ecb0 Friends list and message displayed in 3D/XR 1 year ago
Fabien Benetou ac94b9ab3c immers deeper integration 1 year ago
Fabien Benetou 16e72b5a23 Immers setup goals 1 year ago
Fabien Benetou c212277b9a working jxr teleport example but creating bug on pinching (parent pos) 1 year ago
Fabien Benetou ce7eccd0b7 fixed sa shorthand. Example of in VR doc 1 year ago
Fabien Benetou 9b2dd9284c loadCodeFromPage() as example 1 year ago
Fabien Benetou cd56f69d58 next page working 1 year ago
Fabien Benetou 133f0b040e small cleanup 1 year ago
Fabien Benetou 6f6f764063 add page sequence 1 year ago
  1. 683
      index.html

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<title>SpaSca : Spatial Scaffolding</title>
<head>
<!-- Suggestions? https://git.benetou.fr/utopiah/text-code-xr-engine/issues/ -->
@ -7,7 +8,9 @@
<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" src='dependencies/immers-client.js'></script>
<script type="module" id=immersbundle src='dependencies/immers-client.js?save=true'></script>
<!--<script type="module" id=immersbundle src="https://cdn.jsdelivr.net/npm/immers-client/dist/destination.bundle.js?role=modFull"></script>-->
<script src="https://threejs.org/examples/js/exporters/GLTFExporter.js"></script>
<!-- for input sharing -->
<script src='dependencies/peerjs.min.js'></script>
@ -16,8 +19,11 @@
<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> -->
<!-- still experimenting, see webdav.html -->
<script src='dependencies/webdav.js'></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.8.2/cytoscape.min.js"></script>
<script src="https://cytoscape.org/cytoscape.js-euler/cytoscape-euler.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>
@ -53,6 +59,7 @@
</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";
@ -215,16 +222,90 @@ AFRAME.registerComponent('web-url', {
}
})
function sendGlbFromEl(el){
const gltfExporter = new THREE.GLTFExporter();
const mesh = el.object3D
const options = {
trs: true,
onlyVisible: true,
truncateDrawRange: false,
binary: true,
maxTextureSize: Infinity
};
gltfExporter.parse(
mesh,
function (result) {
if (immersClient) immersClient.sendModel("testing", new Blob([result]), "public")
console.log("sent blob")
// worked as https://immers.benetou.fr/s/639cb4171757b8382c120da1 of type model
// with glb as URL https://immers.benetou.fr/media/edf5641922e6371abb3118f56cd20b9b
},
function (error) {
console.log('An error happened during parsing', error);
},
options
);
}
var immersClient
setTimeout( _ => {
// 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').immersClient.sendModel = async function sendModel (name, glb, privacy, icon, to = []) {
return this.activities.model(name, glb, icon, to, privacy)
} // shim until API update
document.querySelector("immers-hud").setAttribute("access-role", "modFull")
document.querySelector("immers-hud").immersClient.addEventListener("immers-client-connected", _ => {
immersClient = document.querySelector("immers-hud").immersClient
console.log(immersClient.profile.displayName, "connected")
//immersClient.addEventListener("immers-client-new-message", e => addNewNote(e.detail.message.messageHTML) )
immersClient.addEventListener("immers-client-new-message", async e => {
if (e.detail.message.type == "chat"){
let msg = ( await immersClient.activities.getObject( e.detail.message.id ))
if (msg.object.context.location )
addNewNote( e.detail.message.messageHTML, msg.object.context.location.position ,
"0.1 0.1 0.1", null, "immerschat", "true", msg.object.context.location.rotation )
else
addNewNote( e.detail.message.messageHTML )
// could hook on pinchended
// immersClient.place.location = { position: "0 1.5 -2", rotation: "0 190 0" };
// immersClient.sendChatMessage(textvalue, "public");
}
if (e.detail.message.type == "media" && e.detail.message.mediaType == "image"){
console.log("src", e.detail.message.url)
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", e.detail.message.url.href)
AFRAME.scenes[0].appendChild(el)
}
if (e.detail.message.type == "other"){
let msg = ( await immersClient.activities.getObject( e.detail.message.id ))
console.log("maybe model, see object.type.model==model", msg )
}
})
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
// hidden for workshop
} )
} )
})
}, 1000)
});
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.
document.addEventListener("immers-client-connected", _ => console.log("connected"))
window.addEventListener("immers-client-connected", _ => console.log("connected"))
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?
*/
@ -672,7 +753,7 @@ function getClosestTargetElements( pos, threshold=0.05 ){
.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)
@ -775,6 +856,7 @@ function appendToFeedbackHUD(txt){
function setFeedbackHUD(txt){
document.querySelector("#feedbackhud").setAttribute("value",txt)
setTimeout( _ => document.querySelector("#feedbackhud").setAttribute("value","") , 2000)
}
function appendToHUD(txt){
@ -782,7 +864,7 @@ function appendToHUD(txt){
if ( textHUD == startingText)
setHUD( txt )
else
setHUD( textHUD + " " + txt )
setHUD( textHUD + txt )
}
function setHUD(txt){
@ -807,6 +889,28 @@ AFRAME.registerComponent('waistattach',{
},
});
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'},
@ -844,6 +948,7 @@ AFRAME.registerComponent('pinchsecondary', {
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") )
if (selectedElement) selectedElement.emit('lreleased')
selectedElement = null
if (setupMode) setupBBox["B"] = event.detail.position
if ( setupBBox["A"] && setupBBox["B"] ) {
@ -889,13 +994,24 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
// 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") )
//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")
setTimeout( _ => setFeedbackHUD(""), 2000)
}
if (selectedElement){
let content = selectedElement.getAttribute("value")
if (content && immersClient && immersClient.connected){
immersClient.place.location = {
position: AFRAME.utils.coordinates.stringify(event.detail.position),
rotation: AFRAME.utils.coordinates.stringify( selectedElement.getAttribute("rotation") )
};
immersClient.sendChatMessage(content, "public");
}
selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:true})
selectedElement.emit('released')
}
// unselect current target if any
selectedElement = null;
@ -921,6 +1037,7 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
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
@ -949,7 +1066,8 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
//selectedElement = clone
selectedElement = getClosestTargetElement( event.detail.position )
selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:true})
// 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
});
},
@ -1126,7 +1244,7 @@ AFRAME.registerComponent('hud', {
}
})
function addNewNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=null, classes=null, visible="true" ){
function addNewNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=null, classes=null, visible="true", rotation="0 0 0" ){
var newnote = document.createElement("a-troika-text")
newnote.setAttribute("anchor", "left" )
newnote.setAttribute("outline-width", "5%" )
@ -1146,9 +1264,11 @@ function addNewNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=nu
newnote.setAttribute("value", text )
//newnote.setAttribute("font", "sw-test/Roboto-msdf.json")
newnote.setAttribute("position", position)
newnote.setAttribute("rotation", rotation)
newnote.setAttribute("scale", scale)
AFRAME.scenes[0].appendChild( newnote )
targets.push(newnote)
return newnote
}
function interpretAny( code ){
@ -1212,14 +1332,15 @@ function draw( position ){
}
// the goal is to associate objects as shape with volume to code snippet
function addGltfFromURLAsTarget( url, scale=1 ){
function addGltfFromURLAsTarget( url, scale=1, position="0 1.7 -0.3" ){
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("position", position)
el.setAttribute("scale", scale + " " + scale + " " + scale)
targets.push(el)
return el
// consider https://sketchfab.com/developers/download-api/downloading-models/javascript
}
@ -1254,7 +1375,7 @@ function parseJXR( code ){
newcode = newcode.replace(/qs ([^\s]+)/ ,`document.querySelector('$1')`)
// sa X Y => .setAttribute("X", "Y")
newcode = newcode.replace(/ sa ([^\s]+) ([^\s]+)/,`.setAttribute('$1','$2')`)
newcode = newcode.replace(/ sa ([^\s]+) (.*)/,`.setAttribute('$1','$2')`)
// problematic for position as they include spaces
newcode = newcode.replace(/obsv ([^\s]+)/ ,`newNoteFromObservableCell('$1')`)
@ -1274,14 +1395,20 @@ function parseJXR( code ){
}
function interpretJXR( code ){
if (!code) return
if (code.length == 1) { // special case of being a single character, thus keyboard
if (code == ">") { // Enter equivalent
addNewNote( hudTextEl.getAttribute("value") )
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 )
appendToHUD( code )
}
}
if (!code.match(prefix)) return
@ -1350,12 +1477,12 @@ AFRAME.registerComponent('selectionboxonpinches', {
AFRAME.registerComponent('keyboard', {
init:function(){
let generatorName = this.attrName
const horizontaloffset = .5
const horizontalratio = 1/30
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*.03)+" -.4", ".1 .1 .1", null, generatorName)
addNewNote( line[i], pos+" "+(1.6-ln*.06)+" -.4", ".1 .1 .1", null, generatorName)
}
})
}
@ -1628,6 +1755,437 @@ function cloneAndDistribute(){
}
}
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
}
function getModelsFromWebDAV(){
const webdavurl = "https://webdav.benetou.fr";
const client = window.WebDAV.createClient(webdavurl)
async function getDirectory(path = "/"){ return await client.getDirectoryContents(path); }
getDirectory("models").then( d => d.sort( (a,b) => (a.filename>b.filename)).slice(0,10).map( (c,i) => addModelFromURL(webdavurl+c.filename)))
}
function addModelFromURL(url){
return addNewNote("jxr lg "+url+ " 0.001", -Math.random()+" "+Math.random()*3 + " -1")
// should try boxing it instead in 1m3
}
// 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")
} ) )
}
const tile_URL = "../content/KenneyHexTiles/"
const tile_extension = ".glb"
const tiles = ["building_cabin", "building_castle", "building_dock", "building_farm", "building_house", "building_market", "building_mill", "building_mine", "building_sheep", "building_smelter", "building_tower", "building_village", "building_wall", "building_water", "dirt", "dirt_lumber", "grass", "grass_forest", "grass_hill", "path_corner", "path_cornerSharp", "path_crossing", "path_end", "path_intersectionA", "path_intersectionB", "path_intersectionC", "path_intersectionD", "path_intersectionE", "path_intersectionF", "path_intersectionG", "path_intersectionH", "path_start", "path_straight", "river_corner", "river_cornerSharp", "river_crossing", "river_end", "river_intersectionA", "river_intersectionB", "river_intersectionC", "river_intersectionD", "river_intersectionE", "river_intersectionF", "river_intersectionG", "river_intersectionH", "river_start", "river_straight", "sand", "sand_rocks", "stone", "stone_hill", "stone_mountain", "stone_rocks", "unit_boat", "unit_house", "unit_houseLarge", "unit_mill", "unit_tower", "unit_tree", "unit_wallTower", "water", "water_island", "water_rocks",]
const tiles_types_full = [ "building_", "river_", "sand", "stone", "water"]
const tiles_types_parts = [ "unit_", "path_" ]
const tiles_types = [ ...tiles_types_full, ...tiles_types_parts]
const tiles_full = tiles.filter( t => { let present = false; tiles_types_full.map(m => { if (t.indexOf(m)>-1) present = true; }); return present} )
function displayAllTiles(){
const scale = 1/10
let last_type = null
tiles.map( (t) => {
let x = -1*scale
if (!last_type) n = 0
tiles_types.map( (tile_type,ttn) => {
if (t.indexOf(tile_type) > -1) {
x = ttn/10
if (tile_type != last_type) n = 0
last_type = tile_type
}
} )
let el = addGltfFromURLAsTarget( tile_URL+t+tile_extension, .09, ""+x+" 0.7 -"+n*scale )
// el.class = ...
n++
} )
// could consider a new spawner type so that picking a tile clones it first
// could do same behavior as on release or on picked, namely register listener then act on event
}
// try generating at scale, e.g 2, a landscape to explore based on type
// with scale adjusting as jxr line to be the Wondering pills/drinks/mushroom to change scale
// cf similar commands to move a class, consequently could add class after addGltfFromURLAsTarget
function randomTileFull(){
return tiles_full[Math.floor(Math.random()*tiles_full.length)]
}
var tiles_snapping_grid = []
function getClosestTilesSnappingPosition( t, threshold=0.05 ){
let point = null
let found = tiles_snapping_grid.map( i => { return { pos:i, dist: i.distanceTo(t) } } )
.filter( t => t.dist < threshold )
.sort( (a,b) => a.dist > b.dist)
if (found && found[0]) point = found[0].pos
return point
}
AFRAME.registerComponent('snap-on-pinchended', {
init: function(){
this.el.addEventListener('released', function (event) {
setFeedbackHUD("should snap to iso grid"); // TODO test
// if works, generalize and add to https://git.benetou.fr/utopiah/text-code-xr-engine/issues/66
// should come back from emit('released')
// could rely on getClosestTilesSnappingPosition()
// if it works, might check if position is not already used by a tile
})
}
})
function generateRandomPlace(max_i=10, max_j=10, scale=1/10, y=1.4){
// lifesize, y : -2, scale 1
// dollhouse, y : 1.4, scale 1/10
for (let i=0;i<max_i;i++){
for (let j=0;j<max_j;j++){
let pos = new THREE.Vector3( (i+(j%2?0:1/2))*scale, y, (j*8.5/10)*scale )
el = addGltfFromURLAsTarget( tile_URL+randomTileFull()+tile_extension, 1*scale,
AFRAME.utils.coordinates.stringify( pos ) )
tiles_snapping_grid.push( pos )
el.className += "tiles"
}
}
}
// could add behavior based on class or, maybe easier, add a snapping-after-release component
// it would register an event listener and the released element would trigger an event
function rescalePlace(scale = 10, yoffset=-1){
let places = Array.from( document.querySelectorAll(".tiles") )
tiles_snapping_grid = []
places.map( e => {
scl = e.getAttribute("scale"); e.setAttribute("scale", scl.x*scale+ " " + scl.y*scale + " " + scl.z*scale)
pos = e.getAttribute("position"); e.setAttribute("position", pos.x*scale+ " " + (pos.y+yoffset) + " " + pos.z*scale)
let pos3 = new THREE.Vector3( pos.x*scale, pos.y+yoffset, pos.z*scale )
tiles_snapping_grid.push( pos3 )
} )
}
// from https://glitch.com/edit/#!/zen-pim?path=graph-cyto-headless.html
// see also https://observablehq.com/@utopiah/d3-pim-graph
// motivated by https://these.arthurperret.fr/chapitre-4.html#cosmographe-et-cosmoscope
const cytoJson = "https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph_cyto.json"
var cy
var jsonLoaded
fetch(cytoJson)
.then(function (response) {
return response.json();
})
.then(function (json) {
// take nodes from wiki.Nodes then construct an array in elements for cytoscape format
// or directly from JSON http://js.cytoscape.org/#notation/elements-json
jsonLoaded = json
startCytoWithData(json)
})
.catch(function (err) {
console.log(err);
});
// should instead directly use https://vatelier.net/MyDemo/newtooling/wiki_graph_cyto.json
// generated from https://vatelier.net/MyDemo/newtooling/build_graph_cytograph.js
// cf instead http://js.cytoscape.org/demos/tokyo-railways/tokyo-railways.json more detail
// with details on how to load the JSON
function runCytoAnalysis(){
console.log('----------------- network analysis started --------------')
if (!cy.nodes('[id = "Analysis.Analysis"]').length) {
console.warn('Wrong dataset, cancelling')
return
}
// example of queries
var aStar = cy.elements().aStar({ root: '[id = "Analysis.Analysis"]', goal: '[id = "Analysis.CostsAndBenefitsOfSocietalMembership"]' })
if ( aStar.path){
console.log("Path from Analysis.Analysis to Analysis.CostsAndBenefitsOfSocietalMembership", aStar.path.edges() , aStar.path.nodes() )
aStar.path.select()
}
console.log( cy.nodes('[id = "Analysis.Analysis"]').neighborhood() )
before = Date.now()
var bc = cy.elements().bc()
after = Date.now()
console.log("took ", after-before, "ms to run.")
console.log( 'bc of j: ' + bc.betweenness('[id = "Analysis.Analysis"]') )
console.log( cy.nodes('[id = "Analysis.CostsAndBenefitsOfSocietalMembership"]').neighborhood() )
//no need to run cy.elements().bc() again. It's done once for the whole graph.
console.log( 'bc of j: ' + bc.betweenness('[id = "Analysis.CostsAndBenefitsOfSocietalMembership"]') )
console.log('----------------- network analysis done -----------------')
}
function startCytoWithData(json){
cy = cytoscape({
headless:true,
elements: json.elements
})
//runCytoAnalysis() //quite demanding, skipped for now.
let defaults = {
name: 'euler',
springLength: edge => 80,
springCoeff: edge => 0.0008,
mass: node => 4,
gravity: -1.2,
pull: 0.001,
theta: 0.666,
dragCoeff: 0.02,
movementThreshold: 1,
timeStep: 20,
refresh: 10,
animate: true,
animationDuration: undefined,
animationEasing: undefined,
maxIterations: 1000,
maxSimulationTime: 4000,
ungrabifyWhileSimulating: false,
fit: true,
padding: 30,
// Constrain layout bounds with one of
// - { x1, y1, x2, y2 }
// - { x1, y1, w, h }
// - undefined / null : Unconstrained
boundingBox: undefined,
// Layout event callbacks; equivalent to `layout.one('layoutready', callback)` for example
ready: function(){ console.log("graph ready", cy.json()) }, // on layoutready
stop: stableGraph(), // on layoutstop
randomize: false
};
// disabled for tests
//cy.layout( defaults ).run(); // too demanding for the entire graph, should limit to a subset
}
function stableGraph(){
var exportableJSON = cy.json()
console.log( 'exportable JSON', exportableJSON );
// not actually stable!
// still could be used as a form of caching BUT... would take into account new nodes added since
var node = "ReadingNotes.ApocalypticAI"
if (!cy.nodes('[id = "'+node+'"]').length) {
console.warn('Wrong dataset, cancelling')
return
}
console.log('should add/update AFrame nodes e.g', cy.elements('[id = "'+node+'"]').position())
console.log('could add all the nodes then their links with proper attributes in order to do select() after')
// using e.g. cy.nodes().forEach( n => console.log(n.data(), n.position() ) )
// cy.edges().forEach( e => console.log(e.data(), e.sourceEndpoint(), e.targetEndpoint() ))
// warning, very costly!
// run on entire wiki though whereas previous D3 instance limited to 10 pages and their targets
}
let edges_to_display = []
function displayLeafs(graph, graphEl, rootId, rootEl, depth=3){
console.log( graph[rootId].Id )
if (depth<1) return
graph[rootId].Targets.map( l => {
console.log( "-", graph[l].Id )
let x = addNodeFromGraph(graph[l].Id, "" + (Math.random()*2) + " " + (Math.random()*2) + " -" + (Math.random()*2) )
// layout could be done with Cytoscape but somehow seems I can't use it properly, in headless mode or not.
x.setAttribute('update-links-on-pinchended', true)
x.setAttribute('toggle-links-on-left-pinchended', true)
console.log("linking:", rootEl, x)
edges_to_display.push({graphel:graphEl, source:rootEl, target:x})
displayLeafs( graph, graphEl, graph[l].Id, x, --depth)
})
}
AFRAME.registerComponent('toggle-links-on-left-pinchended', {
init: function(){
let graphEl = document.querySelector("#graphroot")
let el = this.el
this.el.addEventListener('lreleased', function (event) {
// if it has children (...how knowing that we are not using the hierarchy?)
// delete them, including links (should be recursive too)
// if not, displayLeafs( mynodes, graphEl, root.Id, rootEl, 1)
// assumes not for now
displayLeafs( wikiStructure, graphEl, el.getAttribute("value"), el, 1)
setTimeout( _ => { edges_to_display.map( i => addEdgeBetweenNodesFromGraph( i.graphel, i.source, i.target ))}, 2000 )
})
}
})
AFRAME.registerComponent('update-links-on-pinchended', {
init: function(){
let rootEl = document.querySelector("#graphroot")
let el = this.el
this.el.addEventListener('released', function (event) {
Object.keys( rootEl.components ) // get all links
.filter( i => i.indexOf(el.id) > -1 ) // keeps links related to the moved node
.map( i => { // for each link
let newpos = AFRAME.utils.coordinates.stringify( el.getAttribute("position") )
let [src,tgt] = i.replace("line__","").split("__to__");
srcpos = AFRAME.utils.coordinates.stringify( rootEl.getAttribute(i).start )
tgtpos = AFRAME.utils.coordinates.stringify( rootEl.getAttribute(i).end )
if (src==el.id){
rootEl.setAttribute(i, { start: newpos, end: tgtpos })
} else {
rootEl.setAttribute(i, { start: srcpos, end: newpos })
}
})
})
}
})
let nodes = []
let edges = []
let wikiStructure = null
fetch('https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph.json').then( r => r.json() ).then( r => displayGraph(r.Nodes) );
// alternatively there is the issue component that could also display linked issues
function displayGraph(mynodes){
wikiStructure = mynodes
let startingNode = "Wiki.VirtualRealityInterface"
let root = mynodes[startingNode]
let graphEl = document.createElement("a-entity")
graphEl.id = "graphroot"
AFRAME.scenes[0].appendChild( graphEl )
let node_names = Object.keys( mynodes )
let rootEl = addNodeFromGraph(root.Id, "" + (Math.random()*2-1) + " " + (Math.random()+1) + " -" + (Math.random()*2-0.5) )
rootEl.setAttribute('update-links-on-pinchended', true)
displayLeafs( mynodes, graphEl, root.Id, rootEl, 2)
setTimeout( _ => { edges_to_display.map( i => addEdgeBetweenNodesFromGraph( i.graphel, i.source, i.target ))}, 2000 )
// quite unreliable
// should listen to an event instead to insure that nodes are all created before
}
function addNodeFromGraph(name, position="0 0 0"){
// add sphere, with its name, make it a target
// define what "it" is knowing we can't move a children with an offset
// consequently parenting should be done by the text
let el = addNewNote(name, position, ".1 .1 .1", "node_" +Date.now(), "node_from_graph")
let sphereEl = document.createElement("a-sphere")
sphereEl.setAttribute("radius", .1)
sphereEl.setAttribute("segments-height", 4)
sphereEl.setAttribute("segments-width", 4)
sphereEl.setAttribute("wireframe", true)
sphereEl.setAttribute("position", "0 -.1 0")
el.appendChild(sphereEl)
// position shouldn't have to be offset
return el
}
function addEdgeBetweenNodesFromGraph(graphEl, a, b){
// a.setAttribute( "line-link-entities", {source: a.id, target: b.id} ) doesn't seem work, back to basics for now
graphEl.setAttribute("line__"+a.id+"__to__"+b.id, {
start: AFRAME.utils.coordinates.stringify( a.getAttribute("position") ) ,
end: AFRAME.utils.coordinates.stringify( b.getAttribute("position") )
})
// not that this doesn't take into account the parent node moving
}
// could change model opacity based on hand position, fading out when within a (very small here) safe space
</script>
<div id="observablehq-key">
@ -1635,8 +2193,8 @@ function cloneAndDistribute(){
<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 >
<!-- screenstack dynamic-view selectionboxonpinches keyboard glossary timeline issues fot
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>
@ -1648,69 +2206,34 @@ function cloneAndDistribute(){
<a-entity networked-hand-controls="hand:right"></a-entity>
</template>
</a-assets>
<!--
<a-video src="https://video.benetou.fr/download/videos/318c8408-c34a-430c-846d-f875dc3c343e-480.mp4"></a-video>
<a-video position="0 2 -2" src="https://video.benetou.frstreaming-playlists/hls/91634fb7-116e-43a1-a4e7-144dd92da17c/1.m3u8"></a-video>
<a-video position="0 2 -2" src="https://video.benetou.fr/videos/embed/91634fb7-116e-43a1-a4e7-144dd92da17c"></a-video>
-->
<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>
<!-- remove for NAF equivalent
<a-entity id="my-tracked-left-hand" networked-hand-controls="hand:left" networked="template:#left-hand-default-template"
pinchsecondary wristattachsecondary="target: #box" ></a-entity>
<a-entity id="my-tracked-right-hand" networked-hand-controls="hand:right" networked="template:#right-hand-default-template"
pinchprimary ></a-entity>
<a-entity class=movebypinch >
<a-text target value="jxr document.getElementById('player').object3D.position.z++" position="0 1.15 0.1" rotation="-30 0 0" scale="0.1 0.1 0.1"></a-text>
<a-text target value="jxr player.object3D.position.z--" position="0 1.15 -0.1" rotation="-30 0 0" scale="0.1 0.1 0.1"></a-text>
<a-text target value="jxr player.object3D.position.x++" position="-0.3 1.15 0" rotation="-30 0 0" scale="0.1 0.1 0.1"></a-text>
<a-text target value="jxr player.object3D.position.x--" position="0.3 1.15 0" rotation="-30 0 0" scale="0.1 0.1 0.1"></a-text>
</a-entity>
-->
<!-- works on desktop via interpretJXR() but not in VR by trying to pinch... disabled for now for demo clarity-->
<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 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! -->
<a-entity id="scaledworld" class="hidableassets" scale=".05 .05 .05" position="0 1.45 -1"><!-- can't be used for interactions otherwise becomes indirect-->
<a-box position="-0.1 1.2 -0.3" scale="0.5 0.5 0.5" rotation="0 45 0" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
</a-entity>
<a-image id=background background-via-url visible=false position="0 1.5 -1.02" scale="2 1 1" src=""></a-image>
<a-image visible=false class=mural-instructions src="../content/future_of_text_symposium/mappinghypertext_typesofdiagrams2.png"
rotation="0 -45 0" position="1.5 1.7 -.7" scale=".4 .2 .2" ></a-image>
<a-image visible=false class=mural-instructions src="../content/future_of_text_symposium/mappinghypertext_typesofdiagrams1.png"
rotation="0 -45 0" position="1.5 1.4 -.7" scale=".4 .2 .2" ></a-image>
<a-image visible=false class=mural-instructions src="../content/future_of_text_symposium/mappinghypertext_semanticanalysisofdiagrams.png"
rotation="0 45 0" position="-1.5 1.7 -.7" scale=".4 .2 .2" ></a-image>
<a-image visible=false class=mural-instructions src="../content/future_of_text_symposium/mappinghypertext_mappingfusion.png"
rotation="0 45 0" position="-1.5 1.4 -.7" scale=".4 .2 .2" ></a-image>
<!-- visual reminders of shortcuts, a poster on the far left/right of keyboard shortcuts -->
<!-- assets CabanaAndCurtains.glb Pond.glb TempleOfLife.glb JapaneseRoom.glb -->
<a-entity hide-on-enter-ar id="environment" class="hidableenvironment" gltf-model="url(../content/Pond.glb)" scale="80 80 80" position="0 0.2 0.15" rotation="0 -90 0"></a-entity>
<a-entity hide-on-enter-ar class="hidableenvironment" gltf-model="url(../content/CabanaAndCurtains.glb)" scale=".010 .010 .010" position="0 0.2 0.15" rotation="0 0 0"></a-entity>
<a-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 color="#add8e6"></a-sky>
<a-entity light="type: directional; color: #FFF; intensity: 1.4" position="-0.5 1 1"></a-entity>
<a-sky hide-on-enter-ar src="../content/nebula.jpg"></a-sky>
<!-- permanent offline persistent e-ink based, rM2 size, reminder
<a-plane position="0 2 -2" scale="4 4 4" mirror></a-plane>
<a-plane position="0 1 -1" scale="0.21 0.15 1" rotation="-30 0 0" wireframe="true"></a-plane>
-->
<a-text target value="instructions : pinch displayGraph() to start navigating in it" position="0 1.65 -0.2" scale="0.1 0.1 0.1"></a-text>
<a-text target value="jxr displayGraph()" position="0 1.35 -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-text target value="instructions : pinch twice for distance then select element then execute cloneAndDistribute() " position="0 1.65 -0.2" scale="0.1 0.1 0.1"></a-text>
<a-text target value="jxr AFRAME.scenes[0].components.inspector.openInspector()" position="0 1.25 -0.2" scale="0.1 0.1 0.1"></a-text>
<a-text target value="jxr observe selectedElement" position="0 1.15 -0.2" scale="0.1 0.1 0.1"></a-text>
<a-text target value="jxr observe dl2p" position="0 1.35 -0.2" scale="0.1 0.1 0.1"></a-text>
<a-text target value="jxr cloneAndDistribute()" position="0 1.45 -0.2" scale="0.1 0.1 0.1"></a-text>
<a-box target position="-0.1 1.2 -0.3" scale=".1 1 0.01" rotation="0 45 0"
src="https://vatelier.benetou.fr/MyDemo/newtooling/textures/fabien.benetou.fr_Analysis_LibrarianMoveWalls.png"></a-box>
<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-scene>
</body>

Loading…
Cancel
Save