Compare commits

...

137 Commits

Author SHA1 Message Date
Fabien Benetou 0421cf3099 added onemptypinch 1 week ago
Fabien Benetou ffc8494062 splitting with some explorations onreleased/onpinched 2 weeks ago
Fabien Benetou a3042794db basis 1 month ago
Fabien Benetou cf71940aa1 zotero web API demo 1 month ago
Fabien Benetou c279afaa22 bump item up and down, clarified from ratio to step 1 month ago
Fabien Benetou 958acd82f1 moving from ondrop to onreleased, proper UUID on notes 1 month ago
Fabien Benetou 1caa31f9b7 ondrop timing fix 1 month ago
Fabien Benetou 898464c29b session Id to distinguish participants without player name 1 month ago
Fabien Benetou 20dca36947 JSON payload instead of string 2 months ago
Fabien Benetou ac4dc57975 controllers as example of Lynx initial support (via Wolvic own build) 2 months ago
Fabien Benetou 9c9dbb2527 user restart (in other language) and random targets 3 months ago
Fabien Benetou 04d78b7e47 share live event for supervisor 3 months ago
Fabien Benetou 5673d84b3d proper animations 5 months ago
Fabien Benetou fde44fc110 exercise component with animation examples 6 months ago
Fabien Benetou a758ed1b6c win condition 6 months ago
Fabien Benetou 1123a0b6ce working loader with animation and console feedback 6 months ago
Fabien Benetou 6e987e64dd example of backend code 6 months ago
Fabien Benetou 0843d858aa example of writing to glTF then loading back with interpretable code 6 months ago
Fabien Benetou 63d787dc08 animation example 6 months ago
Fabien Benetou 93c1d0ccb0 working example 6 months ago
Fabien Benetou 9dd35eb462 working example 6 months ago
Fabien Benetou 42ef7ff120 a-tube test and cleanup 6 months ago
Fabien Benetou 08084875c9 pull component with example 6 months ago
Fabien Benetou d68922c502 ice ring demo 6 months ago
Fabien Benetou d2cac98d64 bottomless examples 6 months ago
Fabien Benetou 87407c2552 cone change as core feedback mecanics 6 months ago
Fabien Benetou b741848ebb lower geomoetric complexity and faster refresh 7 months ago
Fabien Benetou 4b0026c4ec avoid cluttering 7 months ago
Fabien Benetou eeffe718f7 basic game mechanics 7 months ago
Fabien Benetou 07644a6cd1 example of state per distance 7 months ago
Fabien Benetou 4fc5891c71 playgrou 7 months ago
Fabien Benetou 8d0f5e2756 annotation as component proper 7 months ago
Fabien Benetou 63bc7a02c5 ice map and annotation 7 months ago
Fabien Benetou 3aacf514d5 example with start and end blocks 7 months ago
Fabien Benetou 7a05cb826c example 8 months ago
Fabien Benetou e3e804f30c comments on user facing and post-it drop 9 months ago
Fabien Benetou 08eb53957b add events and right angle example for post it generator 9 months ago
Fabien Benetou 23d7f935df working example of thumb/index pull 9 months ago
Fabien Benetou 6ae4f08e57 example of touching with index to change entity 11 months ago
Fabien Benetou c8b3cb5741 move hands to see the tension 11 months ago
Fabien Benetou ec3fcfe28a draw with index tip preview 12 months ago
Fabien Benetou c43df10bf5 execute graph 12 months ago
Fabien Benetou 6ad5b4c4f0 fixed on editors connections and generateGraphFromEditors() 12 months ago
Fabien Benetou 5a2794401a addConnectorsToCodeEditor() and checkConnectors() 12 months ago
Fabien Benetou 7dbeeed9fd visual basis to start 12 months ago
Fabien Benetou 4418866691 editor prevent reflow and optional width 12 months ago
Fabien Benetou 547423ce17 merging editors 12 months ago
Fabien Benetou 808a7aedc5 hide source editor, might cause pinching conflicts 12 months ago
Fabien Benetou 110282218c split on pinch 12 months ago
Fabien Benetou 4153bccc1e moved editors set 12 months ago
Fabien Benetou 4d83424052 fixed length of sections and added addCodeMultipleEditors() 12 months ago
Fabien Benetou d407058a18 horizontal split with new editors, split full lines and vertical positioning 12 months ago
Fabien Benetou bcd14747c3 bug fix on empty lines (trimmed) causing infinite loop 12 months ago
Fabien Benetou 90050fa547 fixing gutters for code, example with syntax highligting 12 months ago
Fabien Benetou 2ec6f31ac8 synax highlight example on split multiple editor 12 months ago
Fabien Benetou 5240ab9ff9 split 12 months ago
Fabien Benetou b8cdd97aac removed single line testj 12 months ago
Fabien Benetou 6f7eb6b656 caesure 12 months ago
Fabien Benetou b71562cca1 basis 12 months ago
Fabien Benetou a8c55d1e3c rough desc 12 months ago
Fabien Benetou 6b483775e8 pulling element to hand 1 year ago
Fabien Benetou ba8013a38e extending piggybacking on target 1 year ago
Fabien Benetou 0abe363450 basic principle piggybacking on wristattached component 1 year ago
Fabien Benetou 4abc5ea939 fixed wrist attachement with new hand API (1.3 to 1.4) 1 year ago
Fabien Benetou 699dbcd6b9 using the proper name from new hand tracking API for rotation 1 year ago
Fabien Benetou de5c6a41bd rotation fixed, again, somehow (does not seem stable) 1 year ago
Fabien Benetou 4cf9c0b148 optimized model, AFrame version bump with rotation fixed 1 year ago
Fabien Benetou a817d54242 modified for offline mode 1 year ago
Fabien Benetou 841980a10c bringing a-console and shiki locally to better support offline mode 1 year ago
Fabien Benetou b0dbc6579d working code editor but not yet adding text/code in place 1 year ago
Fabien Benetou ebd1fb19ea meshes with physics 1 year ago
Fabien Benetou 1b2fa784e3 directly manipulable meshes 1 year ago
Fabien Benetou f6fb9ae4d1 meshing that can be stopped, restarted and content moved 1 year ago
Fabien Benetou 3b99a58287 meshing 1 year ago
Fabien Benetou 660cacab27 broken 1 year ago
Fabien Benetou a71be51ac5 working principle of showing anchors 1 year ago
Fabien Benetou 9ea1d0a71c example of combining default behaviors 1 year ago
Fabien Benetou 59d3e0c64d scaling continuation and hotfix on refresh from wiki page (throttling) 1 year ago
Fabien Benetou f1c7815557 scaling via left pinch while holding somethinmg 1 year ago
Fabien Benetou 737f36b4e6 periodic refresh of preview image 1 year ago
Fabien Benetou d5bc01251e send and refresh perspective image 1 year ago
Fabien Benetou c9f3e9c3ec updated submit.html with iframe preview and shareable link 1 year ago
Fabien Benetou 65d15d3266 matching room as wiki pages to be able to write from submit.html and read from index.html 1 year ago
Fabien Benetou de42d9be55 rotation centered on user but resetting on new pinch 1 year ago
Fabien Benetou 704b60b293 rotation example, not ideal because not centered on user 1 year ago
Fabien Benetou 834ffb4091 coherence in directions 1 year ago
Fabien Benetou 68b8ee8136 example of faster speed 1 year ago
Fabien Benetou a0bbdcfcd6 forward and sideways 1 year ago
Fabien Benetou aae734a20c moving backward only 1 year ago
Fabien Benetou 20181ae2c1 visual grid 1 year ago
Fabien Benetou 1bb95fa69c from primitive to instance (without instancing proper) 1 year ago
Fabien Benetou 1ce813ebac moving away from text for troika-text 1 year ago
Fabien Benetou 4f72cc08be more examples of nextMovementToPoints() usage 1 year ago
Fabien Benetou 9b776462c6 fixed draw() and works with nextMovementToPoints() 1 year ago
Fabien Benetou c8e2ad2e70 fixed pointsFromMovement and added example 1 year ago
Fabien Benetou 1a7c5a8f11 a-console component for easier feedback in HMD 1 year ago
Fabien Benetou 21c74453f2 drop zone as a sphere 1 year ago
Fabien Benetou 1cb5a25bd3 points from movement 1 year ago
Fabien Benetou cdb241ac8f generalized addBlockCodeExample with position and return element 1 year ago
Fabien Benetou 5ce4daa61e generalized addBlockCodeExample with position and return element 1 year ago
Fabien Benetou 1030436f74 generalized addBlockCodeExample with position and return element 1 year ago
Fabien Benetou a28612d4b7 generalized addBlockCodeExample 1 year ago
Fabien Benetou 1f8026d89c snap ghost preview 1 year ago
Fabien Benetou 407b3b494d next pinch applied (lack of feedback is confusing) 1 year ago
Fabien Benetou b31e925156 apply modifier to class 1 year ago
Fabien Benetou 9b5058389f modifier based on ID selected from last pick 1 year ago
Fabien Benetou 0572fe7294 removeOutlineFromEntity 1 year ago
Fabien Benetou 17afdd7855 addBlockCodeExample (just text for now, no snap) 1 year ago
Fabien Benetou 0e1f297ec0 cloning primitives 1 year ago
Fabien Benetou 89ee270eec snapping sound and fixed sky 1 year ago
Fabien Benetou 3e3e6fa602 refactoring and compound example 1 year ago
Fabien Benetou e6d068922b primitives 1 year ago
Fabien Benetou e3604cc7de snapping suggestions 1 year ago
Fabien Benetou 321f47bca5 merged screenshot, snapping to 10cm (invisible) grid 1 year ago
Fabien Benetou 5dd554f8ee working 1 year ago
Fabien Benetou 73c9a94ec5 being able to dyanmically change asset kits and sequentially load and use multiples 1 year ago
Fabien Benetou a49d200684 higher and lower level asset set considerations 1 year ago
Fabien Benetou 328e6e69ec cleaned up and added shifts 1 year ago
Fabien Benetou 6f8a451b77 event working 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. 1922
      index.html
  2. 144
      index.js
  3. 875
      jxr-core.js
  4. 221
      jxr-extras.js
  5. 28
      submit.html

File diff suppressed because it is too large Load Diff

@ -0,0 +1,144 @@
/* nodejs server to grab the current URL as tridactyl command and serve back page with content */
// mkdir tabs polydata
// autocmd TabEnter .* js fetch("http://localhost:7777/?url="+window.location.href)
// might be interesting to keep track of document.referrer too
// consider instead Firefox Account
const fs = require('fs');
const express = require('express')
const cors = require('cors')
const https = require('https')
const path = require('path')
const {execSync} = require('child_process');
const fetch = require('node-fetch');
const app = express()
app.use(cors())
app.get('/getpoly', function(req, res){
const polypath = "polydata"
const url = "https://static.poly.pizza/"
const extensions = [".glb",".webp"]
const filepath = path.join(__dirname, polypath, req.query.id+extensions[0])
if (!fs.existsSync(filepath)){
execSync("wget "+url+req.query.id+extensions[0], {cwd:path.join(__dirname,polypath)})
execSync("wget "+url+req.query.id+extensions[1], {cwd:path.join(__dirname,polypath)})
}
res.send("received");
});
app.get('/voiceinput', function(req, res){
console.log(req.query.keyword)
sseSend(req.query.keyword)
res.send("received");
});
app.get('/search', async function(req, res){
const response = await fetch('https://api.poly.pizza/v1/search/'+req.query.keyword, {
headers: {'x-auth-token': 'e821ece91d1a43c1ac70299368a72b8a'}
});
const data = await response.json();
res.json(data);
});
app.get('/cabin', function(req, res){
res.sendFile(path.join(__dirname, 'cabin.html'))
});
app.get('/', function(req, res){
res.sendFile(path.join(__dirname, 'index.html'))
});
app.get('/tabs', function(req, res){
res.json({"files":fs.readdirSync("tabs")})
});
// resulting in possibly getting /static/screens/1652811988.png
app.get('/screens', function(req, res){
res.json({"files":fs.readdirSync("screens")})
});
const container = "docker run --rm -e UID=$(id -u) -e GID=$(id -g) "
// could try losing priviledges instead, if possible
fs.watch("cabin.html", (eventType, filename) => {
if (eventType == "change") sseSend(filename+" modified")
})
// SSE to force reload client-side
var connectedClients = []
function sseSend(data){
connectedClients.map( res => {
console.log("notifying client") // seems to be call very often (might try to send to closed clients?)
res.write(`data: ${JSON.stringify({status: data})}\n\n`);
})
}
app.get('/streaming', (req, res) => {
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Content-Type', 'text/event-stream');
//res.setHeader('Access-Control-Allow-Origin', '*');
// alread handled at the nginx level
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders(); // flush the headers to establish SSE with client
res.write(`data: ${JSON.stringify({event: "userconnect"})}\n\n`); // res.write() instead of res.send()
connectedClients.push(res)
// If client closes connection, stop sending events
res.on('close', () => {
console.log('client dropped me');
res.end();
});
});
// consider instead CloudInit so that it can be delegated to another machine
// keeping a pool of available machine would start with a single one
// namely that it is possible to run multiple containers on 1 instance, simultaneously or not.
containersSupportedByLanguage = {
bash: "debian ",
julia: "julia julia -E ",
python: "python python -c ",
// avoided file specific languages
// could be done by writing to a file in the container e.g /tmp/file.c then passing it as parameter
}
app.get('/invitereload', function(req, res){
sseSend("reload")
res.json({"res":"reload requested"})
})
app.get('/command', function(req, res){
var req = req.query.command
var foundContainer = containersSupportedByLanguage[req.split(" ")[0]]
var code = req.split(" ").slice(1).join(" ")
if (foundContainer)
res.json({"res":execSync(container+foundContainer+code).toString()})
else
res.json({"res":"language not supported"})
});
// could specify name, it then contiue instance
// ~/.openai-codex-test-xr
// potential alt backend to generate
app.get('/currenturl', function(req, res){
// could instead be the webxr page
let now = + new Date()
fs.appendFile('currentURL.txt', now+" "+req.query.url+" "+req.query.referrer+"\n", function (err) { if (err) throw err; });
// could be JSON instead
res.json({"status":"test"})
});
app.use('/static', express.static(path.join(__dirname, '.')))
// including currentURL.txt
const port = 7777
app.listen(port, () =>
console.log('listening on port', port)
);

@ -0,0 +1,875 @@
const prefix = /^jxr /
const codeFontColor = "lightgrey"
const fontColor= "white"
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.BoxHelper( bbox.object3D, 0x0000ff);
var groupHelpers = []
var primaryPinchStarted = false
var wristShortcut = "jxr switchToWireframe()"
var selectionPinchMode = false
var groupingMode = false
var hudTextEl // should instead rely on the #typinghud selector in most cases
const startingText = "[]"
var added = []
const maxItemsFromSources = 20
let alphabet = ['abcdefghijklmnopqrstuvwxyz', '0123456789', '<>'];
var commandhistory = []
var groupSelection = []
var primarySide = 0
const sides = ["right", "left"]
var pinches = [] // position, timestamp, primary vs secondary
var dl2p = null // from distanceLastTwoPinches
var selectedElements = [];
// ==================================== picking ======================================================
AFRAME.registerComponent('target', {
init: function () {
targets.push( this.el )
this.el.classList.add("collidable")
}
})
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 && t.dist > 0 )
.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
}
// ==================================== HUD ======================================================
var keyboardInputTarget = 'hud'
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) {
if (keyboardInputTarget != 'hud') return
parseKeys('keyup', event.key)
});
document.addEventListener('keydown', function(event) {
if (keyboardInputTarget != 'hud') return
parseKeys('keydown', event.key)
});
}
})
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)
}
function showhistory(){
setFeedbackHUD("history :\n")
commandhistory.map( i => appendToHUD(i.uninterpreted+"\n") )
}
function saveHistoryAsCompoundSnippet(){
addNewNote( commandhistory.map( e => e.uninterpreted ).join("\n") )
}
// ==================================== pinch primary and secondary ======================================================
AFRAME.registerComponent('pinchsecondary', {
init: function () {
this.el.addEventListener('pinchended', function (event) {
selectedElement = getClosestTargetElement( event.detail.position )
selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:false})
// if close enough to a target among a list of potential targets, unselect previous target then select new
if (selectedElement) interpretJXR( selectedElement.getAttribute("value") )
selectedElement = null
});
this.el.addEventListener('pinchmoved', function (event) {
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")
}
if (selectedElement){
let content = selectedElement.getAttribute("value")
selectedElement.emit('released', {element:selectedElement, timestamp:Date.now(), primary:true})
}
// unselect current target if any
selectedElement = null;
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 == "ring-finger-tip"){
selectedElement.object3D.rotation.copy( e.rotation )
}
})
// rotation isn't ideal with the wrist as tend not have wrist flat as we pinch
}
if (selectedElement) selectedElement.emit("moved")
});
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 )
if (selectedElement) {
selectedElements.push({element:selectedElement, timestamp:Date.now(), primary:true})
selectedElement.emit("picked")
} else {
AFRAME.scenes[0].emit('emptypinch', {position:event.detail.position, timestamp:Date.now() })
}
// 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.
}
});
// avoiding setOnDropFromAttribute() as it is not idiosyncratic and creates timing issues
AFRAME.registerComponent('onreleased', { // changed from ondrop to be coherent with event name
// could support multi
events: {
released: function (e) {
let code = this.el.getAttribute('onreleased')
// if multi, should also look for onreleased__ not just onreleased
try {
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
} catch (error) {
console.error(`Evaluation failed with ${error}`);
}
}
}
})
AFRAME.registerComponent('onpicked', {
// could support multi
events: {
picked: function (e) {
let code = this.el.getAttribute('onpicked')
// if multi, should also look for onreleased__ not just onreleased
try {
eval( code ) // should be jxr too e.g if (txt.match(prefix)) interpretJXR(txt)
} catch (error) {
console.error(`Evaluation failed with ${error}`);
}
}
}
})
function onNextPrimaryPinch(callback){
// could add an optional filter, e.g only on specific ID or class
// e.g function onNextPrimaryPinch(callback, filteringSelector){}
let lastPrimary = selectedElements.filter( e => e.primary ).length
let checkForNewPinches = setInterval( _ => {
if (selectedElements.filter( e => e.primary ).length > lastPrimary){
let latest = selectedElements[selectedElements.length-1].element
if (latest) callback(latest)
clearInterval(checkForNewPinches)
}
}, 50) // relatively cheap check, filtering on small array
}
function distanceLastTwoPinches(){
let dist = null
if (pinches.length>1){
dist = pinches[pinches.length-1].position.distanceTo( pinches[pinches.length-2].position )
}
return dist
}
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 )
}
// ==================================== keyboard ======================================================
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)
}
})
}
})
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 )
}
}
}
// ==================================== note as text and possibly executable snippet ======================================================
function addNewNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=null, classes="notes", visible="true", rotation="0 0 0" ){
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
else
newnote.id = "note_" + crypto.randomUUID() // not particularly descriptive but content might change later on
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("rotation", rotation)
newnote.setAttribute("scale", scale)
AFRAME.scenes[0].appendChild( newnote )
targets.push(newnote)
return newnote
}
AFRAME.registerComponent('annotation', {
// consider also multiple annotation but being mindful that it might clutter significantly
schema: {
content : {type: 'string'}
},
init: function () {
addAnnotation(this.el, this.data.content)
},
update: function () {
this.el.querySelector('.annotation').setAttribute('value', this.data.content )
// assuming single annotation
},
remove: function () {
this.el.querySelector('.annotation').removeFromParent()
//Array.from( this.el.querySelectorAll('.annotation') ).map( a => a.removeFromParent() )
}
})
function addAnnotation(el, content){
// could also appear only when in close proximity or while pinching
let annotation = document.createElement( 'a-troika-text' )
annotation.classList.add( 'annotation' )
annotation.setAttribute('value', content)
annotation.setAttribute('position', '0 .1 -.1')
annotation.setAttribute('rotation', '-90 0 0')
annotation.setAttribute("anchor", "left" )
annotation.setAttribute("outline-width", "5%" )
annotation.setAttribute("outline-color", "black" )
el.appendChild(annotation)
return el
}
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 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
}
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 )
}
AFRAME.registerComponent('gltf-jxr', {
events: {
"model-loaded": function (evt) {
this.el.object3D.traverse( n => { if (n.userData.jxr) {
console.log(n.userData)
// need to make gltf become a child of a note to be executable on pinch
// try reparenting first... otherwise var clone = this.el.cloneNode(true)
// might not be great, cf https://github.com/aframevr/aframe/issues/2425
let pos = this.el.object3D.position.clone()
let rot = this.el.object3D.rotation.clone()
this.el.remove()
let note = addNewNote( n.userData.jxr, pos, "0.1 0.1 0.1", null, "gltf-jxr-source")
let clone = this.el.cloneNode(true)
clone.setAttribute('position', '0 0 0')
clone.setAttribute('scale', '10 10 10') // assuming not scaled until now, surely wrong
// need rescaling to current scale by 1/0.1, clone.setAttribute(
clone.removeAttribute('gltf-jxr')
note.appendChild(clone)
}
})
},
},
/* example of backend code to annotate the glTF
import { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const document = await io.read('PopsicleChocolate.glb');
const node = document.getRoot() // doesn't seem to work.listNodes().find((node) => node.getName() === 'RootNode');
node.setExtras({jxr: "jxr addNewNote('hi')"});
await io.write('output.glb', document);
*/
});
// ==================================== interactions beyond pinch ======================================================
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 == "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.
}
});
function doublePinchToScale(){
let initialPositionSecondary
let initialScale
let elSecondary = document.querySelector('[pinchsecondary]')
elSecondary.addEventListener('pinchmoved', movedSecondary );
function movedSecondary(event){
if (!selectedElement) return
let scale = initialScale * initialPositionSecondary.distanceTo(event.detail.position) * 50
selectedElement.setAttribute("scale", ""+scale+" "+scale+" "+scale+" ")
}
elSecondary.addEventListener('pinchstarted', startedSecondary );
function startedSecondary(event){
initialPositionSecondary = event.detail.position.clone()
if (!selectedElement) return
initialScale = AFRAME.utils.coordinates.parse( selectedElement.getAttribute("scale") ).x
}
}
// 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('start-on-press', {
// should become a property of the component instead to be more flexible.
init: function(){
let el = this.el
this.el.addEventListener('pressedended', function (event) {
console.log(event)
// should ignore that if we entered XR recently
if (!primaryPinchStarted && wristShortcut.match(prefix)) interpretJXR(wristShortcut)
// seems to happen also when entering VR
// other action could possibly based on position relative to zones instead, i.e a list of bbox/functions pairs
})
}
})
function thumbToIndexPull(){
let p = document.querySelector('[pinchprimary]')
let tip = new THREE.Vector3(); // create once an reuse it
let proximal = new THREE.Vector3(); // create once an reuse it
let thumb = new THREE.Vector3(); // create once an reuse it
let touches = []
const threshold_thumb2tip = 0.01
const threshold_thumb2proximal = 0.05
let indexesTipTracking = setInterval( _ => {
// cpnsider getObjectByName() instead
p.object3D.traverse( e => { if (e.name == 'index-finger-tip' ) tip = e.position })
//index-finger-phalanx-distal
//index-finger-phalanx-intermediate
p.object3D.traverse( e => { if (e.name == 'index-finger-phalanx-proximal' ) proximal = e.position })
p.object3D.traverse( e => { if (e.name == 'thumb-tip' ) thumb = e.position })
let touch = {}
touch.date = Date.now()
touch.thumb2tip = thumb.distanceTo(tip)
if (!touch.thumb2tip) return
touch.thumb2proximal = thumb.distanceTo(proximal)
//console.log( touch.thumb2tip, touch.thumb2proximal )
// usually <1cm <4cm (!)
//if ((touch.thumb2tip && touch.thumb2tip < threshold_thumb2tip)
//|| (touch.thumb2proximal && touch.thumb2proximal < threshold_thumb2proximal))
if (touch.thumb2tip < threshold_thumb2tip
|| touch.thumb2proximal < threshold_thumb2proximal){
if (touches.length){
let previous = touches[touches.length-1]
if (touch.date - previous.date < 300){
if (touch.thumb2tip < threshold_thumb2tip &&
previous.thumb2proximal < threshold_thumb2proximal){
console.log('^')
p.emit('thumb2indexpull')
}
if (touch.thumb2proximal < threshold_thumb2proximal &&
previous.thumb2tip < threshold_thumb2tip){
console.log('v')
p.emit('thumb2indexpush')
}
}
}
touches.push(touch)
}
}, 50)
// TODO
// Bind thumb2indexpush/thumb2indexpull to zoom in/out "world" i.e all assets that aren't "special" e.g self, lights, UI
}
let changeovercheck
AFRAME.registerComponent('changeover', {
schema: { color : {type: 'string'} },
init: function () {
// (this.el, this.data.content)
if (changeovercheck) return
let player = document.getElementById('player') // assuming single player, non networked
console.log('adding timer')
changeovercheck = setInterval( _ => {
let pos = player.getAttribute('position').clone()
pos.y = 0.1 // hard coded but should be from component element
let hits = Array.from(document.querySelectorAll('[changeover]'))
.filter( e => e.getAttribute("visible") == true)
.map( t => { return { el: t, dist : pos.distanceTo(t.getAttribute("position") ) } })
.filter( t => t.dist < 0.02 )
.sort( (a,b) => a.dist > b.dist)
//console.log(hits.length)
if (hits.length>0) {
setFeedbackHUD('touching cone')
console.log('touching cone')
hits[hits.length-1].el.setAttribute('color', 'red')
}
}, 50)
}
})
// to add only on selectable elements, thus already with a target component attached
AFRAME.registerComponent('pull', {
events: {
picked: function (evt) {
this.startePos = this.el.getAttribute('position').clone()
this.starteRot = this.el.getAttribute('rotation')//.clone() not necessary as converted first
this.decimtersEl = document.createElement('a-troika-text')
AFRAME.scenes[0].appendChild(this.decimtersEl)
},
moved: function (evt) {
let pos = AFRAME.utils.coordinates.stringify( this.startePos )
let oldpos = AFRAME.utils.coordinates.stringify( this.el.getAttribute('position') )
AFRAME.scenes[0].setAttribute("line__pull", `start: ${oldpos}; end : ${pos};`)
let d = this.startePos.distanceTo( this.el.getAttribute('position') )
// could show a preview state before release, e.g
let decimeters = Math.round(d*10)
console.log('pulling '+decimeters+' pages')
// update visible value instead, ideally under line but still facing user
let textPos = new THREE.Vector3()
textPos.lerpVectors(this.startePos, this.el.getAttribute('position'), .7)
this.decimtersEl.setAttribute('position', textPos )
this.decimtersEl.setAttribute('rotation', this.el.getAttribute('rotation') )
this.decimtersEl.setAttribute('value', decimeters )
},
released: function (evt) {
let d = this.startePos.distanceTo( this.el.getAttribute('position') )
console.log('This entity was released '+ d + 'm away from picked pos')
this.el.setAttribute('position', AFRAME.utils.coordinates.stringify( this.startePos ))
this.el.setAttribute('rotation', AFRAME.utils.coordinates.stringify( this.starteRot ))
AFRAME.scenes[0].removeAttribute("line__pull")
this.decimtersEl.remove()
},
},
});
// ==================================== utils on entities and classes ======================================================
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 pushLeftClass(classname, value=.1){
Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.x -= value)
}
function pushRightClass(classname, value=.1){
Array.from( document.querySelectorAll("."+classname) ).map( e => e.object3D.position.x += value)
}
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")
}
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 getIdFromPick(){
let id = null
let pp = selectedElements.filter( e => e.primary )
if (pp && pp[pp.length-1] && pp[pp.length-1].element ){
if (!pp[pp.length-1].element.id) pp[pp.length-1].element.id= "missingid_"+Date.now()
id = pp[pp.length-1].element.id
setFeedbackHUD(id)
}
return id
}
function getClassFromPick(){ // should be classes, for now assuming one
let classFound = null
let pp = selectedElements.filter( e => e.primary )
if (pp && pp[pp.length-1] && pp[pp.length-1].element ){
//if (!pp[pp.length-1].element.className) pp[pp.length-1].element.className= "missingclass"
// arguable
classFound = pp[pp.length-1].element.className
setFeedbackHUD(classFound)
}
return classFound
}
function getArrayFromClass(classname){
return Array.from( document.querySelectorAll("."+classname) )
}
function applyToClass(classname, callback, value){
// example applyToClass("template_object", (e, val ) => e.setAttribute("scale", val), ".1 .1 .2")
getArrayFromClass(classname).map( e => callback(e, value))
// could instead become a jxr shortcut, namely apply a set attribute to a class of entities
}
function addDropZone(position="0 1.4 -0.6", callback=setFeedbackHUD, radius=0.11){
// consider how this behavior could be similar to the wrist watch shortcut
// namely binding it to a jxr function
let el = document.createElement("a-sphere")
el.setAttribute("wireframe", true)
el.setAttribute("radius", radius)
el.setAttribute("position", position)
el.id = "dropzone_"+Date.now()
AFRAME.scenes[0].appendChild( el )
let sphere = new THREE.Sphere( AFRAME.utils.coordinates.parse( position ), radius )
// could become movable but would then need to move the matching sphere too
// could be a child of that entity
let pincher = document.querySelector('[pinchprimary]')
pincher.addEventListener('pinchended', function (event) {
if (selectedElements.length){
let lastDrop = selectedElements[selectedElements.length-1]
if ((Date.now() - lastDrop.timestamp) < 1000){
if (sphere.containsPoint( lastDrop.element.getAttribute("position"))){
// should be a threejs sphere proper, not a mesh
console.log("called back" )
callback( lastDrop.selectedElement )
}
}
}
})
// never unregister
return el
}
// ==================================== facilitating debugging ======================================================
function makeAnchorsVisibleOnTargets(){
targets.map( t => {
let controlSphere = document.createElement("a-sphere")
controlSphere.setAttribute("radius", 0.05)
controlSphere.setAttribute("color", "blue")
controlSphere.setAttribute("wireframe", "true")
controlSphere.setAttribute("segments-width", 8)
controlSphere.setAttribute("segments-height", 8)
t.appendChild( controlSphere )
}) // could provide a proxy to be able to monitor efficiently
}
function switchToWireframe(){
let model = document.querySelector("#environment")?.object3D
if (model) model.traverse( o => { if (o.material) {
let visible = !o.material.wireframe
o.material.wireframe = visible;
o.material.opacity = visible ? 0.05 : 1;
o.material.transparent = visible;
} })
}

@ -0,0 +1,221 @@
console.log('jxr extras to gradually add, by default could refer to branches instead then add as components proper')
console.log('arguably utils and additional interactions beyond pinche could be extras rather than core')
console.log('extra could also be empty...')
// 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_" +crypto.randomUUID(), "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
}

@ -18,6 +18,12 @@ Examples, tap to add, optionally modify and send :
<li><a onclick=replaceWithThisText(this)>jxr toggleVisibilityEntitiesFromClass('fot')</a></li>
</ul>
Share this URL to the browser using e.g <a id="shareablelink" href="https://hmd.link">hmd.link</a> on the same WiFi network.
<hr>
<iframe width="100%" id="pastcommands"></iframe>
<hr>
Remote preview, if available :<br/>
<img height="400px" id="remotepreview" />
<hr>
<br>
Documentation as :
@ -29,13 +35,33 @@ Documentation as :
<li>open-source <a href=https://git.benetou.fr/utopiah/text-code-xr-engine/issues>code repository</a> of the code and to make your own suggestions via issues. </li>
<li><a href=qrcode.png>QRcode</a> of this page to share with others also on mobile.</li>
<li><a href=https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/?background=../content/future_of_text_symposium/HORN-2001-FutureOfHumanCognomeCL.png&fontcolor=lightgray>background</a> as URL parameter. Feel free to use your own content.</li>
<li>to create a room to work together in, add or change the ?roomname=<b>YourRoomName</b> in the browser URL
</ul>
<script>
function replaceWithThisText(element){
document.querySelector("textarea").value = element.innerText
}
const url = 'https://fabien.benetou.fr/PIMVRdata/FoT?action='
let pagename
const queryString = window.location.search
const urlParams = new URLSearchParams(queryString)
let = forcedPagename = urlParams.get('roomname')
forcedPagename?pagename=forcedPagename:pagename='FoT'
const baseURL = 'https://fabien.benetou.fr/PIMVRdata/'+pagename
const url = baseURL+'?action='
document.getElementById("pastcommands").src = baseURL+'?action=source'
document.getElementById("shareablelink").href = "https://hmd.link/?https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/?roomname="+pagename
function refreshPreview(){
document.getElementById("remotepreview").src = "https://fabien.benetou.fr/uploads/PIMVRdata/"+pagename+".jpg#"+Date.now()
}
// seems to get cached properly, i.e only transferring when update
setInterval( _ => refreshPreview(), 1000)
function sendtovr(cabin){
text = document.querySelector("textarea").value
document.querySelector("textarea").value = ''

Loading…
Cancel
Save