aframe-babia-components
Version:
A data visualization set of components for A-Frame.
865 lines (757 loc) • 28.6 kB
JavaScript
/**
*
*This component is based on vasturiano/aframe-forcegraph-component, that
is licensed under the The MIT License (MIT)
Copyright (c) 2017 Vasco Asturiano <vastur@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
let findProdComponent = require('../others/common').findProdComponent;
let parseJson = require('../others/common').parseJson;
const NotiBuffer = require("../../common/noti-buffer").NotiBuffer;
/* global AFRAME */
if (typeof AFRAME === 'undefined') {
throw new Error('Component attempted to register before AFRAME was available.');
}
let accessorFn = require('accessor-fn');
if ('default' in accessorFn) {
// unwrap default export
accessorFn = accessorFn.default;
}
let ThreeForceGraph = require('three-forcegraph');
if ('default' in ThreeForceGraph) {
// unwrap default export
ThreeForceGraph = ThreeForceGraph.default;
}
let parseFn = function (prop) {
if (typeof prop === 'function') return prop; // already a function
let geval = eval; // Avoid using eval directly https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval
try {
let evalled = geval('(' + prop + ')');
return evalled;
} catch (e) { } // Can't eval, not a function
return null;
};
let parseAccessor = function (prop) {
if (!isNaN(parseFloat(prop))) { return parseFloat(prop); } // parse numbers
if (parseFn(prop)) { return parseFn(prop); } // parse functions
return prop; // strings
};
/**
* 3D Force-Directed Graph component for A-Frame.
*/
let cursor = '';
AFRAME.registerComponent('babia-network', {
schema: {
nodeLegend: { type: 'boolean', default: false },
linkLegend: { type: 'boolean', default: false },
// Label lookat for following
legend_lookat: { type: 'string', default: "[camera]" },
// Scale for the label
legend_scale: { type: 'number', default: 1 },
from: { type: 'string' },
nodesFrom: { type: 'string' },
linksFrom: { type: 'string' },
data: { type: 'string', default: '' },
nodes: { type: 'string', default: '' },
links: { type: 'string', default: '' },
nodeId: { type: 'string', default: 'id' },
nodeLabel: { parse: parseAccessor, default: 'id' },
linkId: { type: 'string', default: '' },
linkSource: { type: 'string', default: 'source' },
linkTarget: { type: 'string', default: 'target' },
nodeVal: { type: 'string', default: '' },
nodeRelSize: { type: 'number', default: 4 },
nodeColor: { parse: parseAccessor, default: 'color' },
nodeAutoColorBy: { parse: parseAccessor, default: '' },
nodeResolution: { type: 'number', default: 8 },
linkColor: { parse: parseAccessor, default: 'color' },
linkAutoColorBy: { parse: parseAccessor, default: '' },
linkWidth: { parse: parseAccessor, default: 0 },
linkResolution: { type: 'number', default: 6 },
linkLabel: { parse: parseAccessor, default: '' },
nodeDesc: { parse: parseAccessor, default: 'desc' },
linkDesc: { parse: parseAccessor, default: 'desc' },
nodeVisibility: { parse: parseAccessor, default: true },
nodeOpacity: { type: 'number', default: 1 },
linkVisibility: { parse: parseAccessor, default: true },
linkOpacity: { type: 'number', default: 1 },
nodeThreeObject: { parse: parseAccessor, default: null },
nodeThreeObjectExtend: { parse: parseAccessor, default: false },
linkCurvature: { parse: parseAccessor, default: 0 },
linkCurveRotation: { parse: parseAccessor, default: 0 },
linkMaterial: { parse: parseAccessor, default: null },
linkThreeObject: { parse: parseAccessor, default: null },
linkThreeObjectExtend: { parse: parseAccessor, default: false },
linkPositionUpdate: { parse: parseFn, default: null },
linkDirectionalArrowLength: { parse: parseAccessor, default: 0 },
linkDirectionalArrowColor: { parse: parseAccessor, default: null },
linkDirectionalArrowRelPos: { parse: parseAccessor, default: 0.5 },
linkDirectionalArrowResolution: { type: 'number', default: 8 },
linkDirectionalParticles: { parse: parseAccessor, default: 0 },
linkDirectionalParticleSpeed: { parse: parseAccessor, default: 0.01 },
linkDirectionalParticleWidth: { parse: parseAccessor, default: 0.5 },
linkDirectionalParticleColor: { parse: parseAccessor, default: null },
linkDirectionalParticleResolution: { type: 'number', default: 4 },
linkDistance: { type: 'number', default: 30 },
linkHoverPrecision: { type: 'number', default: 2 },
onNodeCenterHover: { parse: parseFn, default: function () { } },
onLinkCenterHover: { parse: parseFn, default: function () { } },
numDimensions: { type: 'number', default: 3 },
dagMode: { type: 'string', default: '' },
dagLevelDistance: { type: 'number', default: 0 },
dagNodeFilter: { parse: parseFn, function() { return true; } },
onDagError: { parse: parseFn, default: undefined },
forceEngine: { type: 'string', default: 'd3' },
d3AlphaMin: { type: 'number', default: 0 },
d3AlphaDecay: { type: 'number', default: 0.0228 },
d3VelocityDecay: { type: 'number', default: 0.4 },
ngraphPhysics: { parse: parseJson, default: null },
warmupTicks: { type: 'int', default: 0 },
cooldownTicks: { type: 'int', default: 1e18 },
cooldownTime: { type: 'int', default: 15000 },
onEngineTick: { parse: parseFn, default: function () { } },
onEngineStop: { parse: parseFn, default: function () { } }
},
// Bind component methods
getGraphBbox: function () {
if (!this.forceGraph) {
// Got here before component init -> initialize forceGraph
this.forceGraph = new ThreeForceGraph();
}
return this.forceGraph.getGraphBbox();
},
emitParticle: function () {
if (!this.forceGraph) {
// Got here before component init -> initialize forceGraph
this.forceGraph = new ThreeForceGraph();
}
const forceGraph = this.forceGraph;
const returnVal = forceGraph.emitParticle.apply(forceGraph, arguments);
return returnVal === forceGraph
? this // return self, not the inner forcegraph component
: returnVal;
},
d3Force: function () {
if (!this.forceGraph) {
// Got here before component init -> initialize forceGraph
this.forceGraph = new ThreeForceGraph();
}
const forceGraph = this.forceGraph;
const returnVal = forceGraph.d3Force.apply(forceGraph, arguments);
return returnVal === forceGraph
? this // return self, not the inner forcegraph component
: returnVal;
},
d3ReheatSimulation: function () {
this.forceGraph && this.forceGraph.d3ReheatSimulation();
return this;
},
refresh: function () {
this.forceGraph && this.forceGraph.refresh();
return this;
},
/**
* List of visualization properties
*/
visProperties: {
'nodes': ['nodeId'],
'links0': ['linkId'],
'links1': ['linkSource', 'linkTarget']
},
init: function () {
const self = this;
this.isFirstTimeNodes = true;
this.isFirstTimeLinks = true;
this.notiBuffer = new NotiBuffer();
let state = this.state = {}; // Internal state
// Get camera dom element and attach fixed view elements to camera
let cameraEl = document.querySelector('a-entity[camera], a-camera');
// Keep reference to Three camera object
state.cameraObj = cameraEl.object3D.children
.filter(function (child) { return child.type === 'PerspectiveCamera'; })[0];
// On camera switch
this.el.sceneEl.addEventListener('camera-set-active', function (evt) {
// Switch camera reference
state.cameraObj = evt.detail.cameraEl.components.camera.camera;
});
// setup FG object
if (!this.forceGraph) this.forceGraph = new ThreeForceGraph(); // initialize forceGraph if it doesn't exist yet
this.el.object3D.add(this.forceGraph);
// Force distance if selected
if (this.data.linkDistance) {
this.forceGraph.d3Force('link').distance(function () { return self.data.linkDistance })
}
// setup cursor
setCursor();
},
update: function (oldData) {
let el = this.el;
let elData = this.data;
this.currentData = oldData;
/**
* Update or create chart component
*/
// Highest priority to data
if (elData.data && oldData.data !== elData.data) {
let _data = parseJson(elData.data);
this.processData(_data);
} else {
// Second highest priority to nodes and links
if (elData.nodes) {
if (oldData.nodes !== elData.nodes) {
let _nodes = parseJson(elData.nodes);
this.processNodes(_nodes);
}
if (oldData.links !== elData.links) {
let _links = parseJson(elData.links);
this.processLinks(_links)
}
// Data from querier
} else {
// If changed from, need to re-register to the new data component
if (elData.from !== oldData.from && !elData.nodesFrom) {
// Unregister for old producers
if (this.prodComponent) {
this.prodComponent.notiBuffer.unregister(this.notiBufferId)
};
if (this.nodesProdComponent) {
this.nodesProdComponent.notiBuffer.unregister(this.notiBufferId)
};
if (this.linksProdComponent) {
this.linksProdComponent.notiBuffer.unregister(this.notiBufferId)
};
this.prodComponent = findProdComponent(elData, el)
if (this.prodComponent.notiBuffer) {
this.notiBufferId = this.prodComponent.notiBuffer
.register(this.processData.bind(this))
}
} else if (elData.nodesFrom !== oldData.nodesFrom || elData.linksFrom !== oldData.linksFrom) {
// Now we have nodesFrom, should have linksFrom too
// Unregister for old producers
if (this.prodComponent) {
this.prodComponent.notiBuffer.unregister(this.notiBufferId)
};
if (this.nodesProdComponent) {
this.nodesProdComponent.notiBuffer.unregister(this.notiBufferId)
};
if (this.linksProdComponent) {
this.linksProdComponent.notiBuffer.unregister(this.notiBufferId)
};
let prodComponent = findProdComponent(elData, el)
// First, nodes
this.nodesProdComponent = prodComponent.nodes
if (this.nodesProdComponent.notiBuffer) {
this.notiBufferId = this.nodesProdComponent.notiBuffer
.register(this.processNodes.bind(this))
}
// Now, the same for links
this.linksProdComponent = prodComponent.links
if (this.linksProdComponent.notiBuffer) {
this.notiBufferId = this.linksProdComponent.notiBuffer
.register(this.processLinks.bind(this))
}
}
// If changed whatever, re-print with the current data
else if (elData !== oldData) {
if (this.newData) {
let _data = parseJson(this.newData);
this.processData(_data, true)
} else if (this.newNodes) {
if (elData.linkSource != oldData.linkSource || elData.linkTarget != oldData.linkTarget) {
let _links = parseJson(this.newLinks);
this.processLinks(_links, true)
} else {
let _nodes = parseJson(this.newNodes);
this.processNodes(_nodes, true)
}
}
}
}
}
},
/**
* Called on each scene tick.
*/
tick: function (t, td) {
// Cursor may not be initialized
if (!cursor) {
cursor = document.querySelector('[cursor]');
} else {
if (cursor.components.raycaster.raycaster) {
let intersects = cursor.components.raycaster.raycaster.intersectObjects(this.forceGraph.children)
.filter(function (o) { // Check only node/link objects
return ['node', 'link'].indexOf(o.object.__graphObjType) !== -1;
})
.sort(function (a, b) { // Prioritize nodes over links
return isNode(b) - isNode(a);
function isNode(o) { return o.object.__graphObjType === 'node'; }
});
let topObject = intersects.length ? intersects[0].object : null;
if (topObject !== this.state.hoverObj) {
const prevObjType = this.state.hoverObj ? this.state.hoverObj.__graphObjType : null;
const prevObjData = this.state.hoverObj ? this.state.hoverObj.__data : null;
const objType = topObject ? topObject.__graphObjType : null;
const objData = topObject ? topObject.__data : null;
if (prevObjType && prevObjType !== objType) {
// Hover out
this.data['on' + (prevObjType === 'node' ? 'Node' : 'Link') + 'CenterHover'](null, prevObjData);
}
if (this.data.nodeLegend && prevObjType === 'node') {
this.removeLegend(this)
} else if (this.data.linkLegend && this.data.linkLabel != "" && prevObjType === 'link') {
this.removeLegend(this)
}
if (objType) {
// Hover in
this.data['on' + (objType === 'node' ? 'Node' : 'Link') + 'CenterHover'](objData, prevObjType === objType ? prevObjData : null);
}
this.state.hoverObj = topObject;
if (topObject) {
if (this.data.nodeLegend && topObject.__graphObjType === 'node') {
this.showLegend(topObject, topObject.__data, this.data.nodeLabel, this.el.getAttribute("scale"))
} else if (this.data.linkLegend && topObject.__graphObjType === 'link') {
if (this.data.linkLabel != "") {
this.showLinkLegend(topObject, topObject.__data, this.data.linkLabel, this.el.getAttribute("scale"))
}
}
}
}
}
}
// Run force-graph ticker
this.forceGraph.tickFrame();
},
/**
* Querier component target
*/
prodComponent: undefined,
nodesProdComponent: undefined,
linksProdComponent: undefined,
/**
* NotiBuffer identifier
*/
notiBufferId: undefined,
/**
* Where the data is gonna be stored
*/
newData: undefined,
newNodes: undefined,
newLinks: undefined,
/**
* Boolean to know if it's first time for getting the ui fields
*/
isFirstTimeNodes: undefined,
isFirstTimeLinks: undefined,
/**
* Store the fields that we need to show in the ui and not the rest
*/
nodeFields: undefined,
linkFields: undefined,
/**
* Where the current data is stored
*/
currentData: undefined,
/**
* Where the metadata is gonna be stored
*/
babiaMetadata: {
id: 0
},
legend: '',
/*
* Update chart
*/
updateChart: function (nodes_links, self) {
if (self.currentData) {
let elData = [];
for (let attr in this.data) {
elData[attr] = this.data[attr]
}
elData.nodes = nodes_links.nodes;
elData.links = nodes_links.links;
let diff = AFRAME.utils.diff(elData, self.currentData);
let fgProps = [
'jsonUrl',
'numDimensions',
'dagMode',
'dagLevelDistance',
'dagNodeFilter',
'onDagError',
'nodeRelSize',
'nodeId',
'nodeVal',
'nodeResolution',
'nodeVisibility',
'nodeColor',
'nodeAutoColorBy',
'nodeOpacity',
'nodeThreeObject',
'nodeThreeObjectExtend',
'linkSource',
'linkTarget',
'linkVisibility',
'linkColor',
'linkAutoColorBy',
'linkOpacity',
'linkWidth',
'linkResolution',
'linkCurvature',
'linkCurveRotation',
'linkMaterial',
'linkThreeObject',
'linkThreeObjectExtend',
'linkPositionUpdate',
'linkDirectionalArrowLength',
'linkDirectionalArrowColor',
'linkDirectionalArrowRelPos',
'linkDirectionalArrowResolution',
'linkDirectionalParticles',
'linkDirectionalParticleSpeed',
'linkDirectionalParticleWidth',
'linkDirectionalParticleColor',
'linkDirectionalParticleResolution',
'forceEngine',
'd3AlphaMin',
'd3AphaDecay',
'd3VelocityDecay',
'ngraphPhysics',
'warmupTicks',
'cooldownTicks',
'cooldownTime',
'onEngineTick',
'onEngineStop'
];
fgProps
.filter(function (p) { return p in diff; })
.forEach(function (p) {
self.forceGraph[p](elData[p] !== '' ? elData[p] : null);
}); // Convert blank values into nulls
if ('nodes' in diff || 'links' in diff) {
self.forceGraph.graphData({
nodes: elData.nodes,
links: elData.links
});
}
}
},
/*
* Process data obtained from producer
*/
processData: function (_data, newData) {
console.log("processData", this);
if (this.newData != _data || newData) {
this.newData = _data;
let nodes_links = this.elDataFromData(_data);
this.babiaMetadata = { id: this.babiaMetadata.id++ };
console.log("Generating network...")
this.updateChart(nodes_links, this)
this.notiBuffer.set(_data)
}
},
processNodes: function (nodes, forceProcess) {
console.log("processNodes", this);
if (this.newNodes != nodes || forceProcess) {
this.newNodes = nodes;
let nodes_links = this.elDataFromNodesAndLinks(nodes, this.newLinks)
if (!nodes_links) {
if (this.data.links) {
let _links = parseJson(this.data.links);
this.processLinks(_links)
} else if (this.data.linksFrom) {
this.babiaMetadata = { id: this.babiaMetadata.id++ }
console.log("Generating network...")
}
} else {
this.babiaMetadata = { id: this.babiaMetadata.id++ }
console.log("Generating network...")
this.updateChart(nodes_links, this)
this.notiBuffer.set({ 'nodes': nodes_links.nodes, 'links': nodes_links.links })
}
}
},
processLinks: function (_links, forceProcess) {
console.log("processLinks", this);
let links = parseJson(_links);
if (this.newLinks != links || forceProcess) {
this.newLinks = links;
let nodes_links = this.elDataFromNodesAndLinks(this.newNodes, links)
if (!nodes_links) {
if (this.data.nodes) {
let _nodes = parseJson(this.data.nodes);
this.processNodes(_nodes)
} else if (this.data.nodesFrom) {
this.babiaMetadata = { id: this.babiaMetadata.id++ }
console.log("Generating network...")
}
} else {
this.babiaMetadata = { id: this.babiaMetadata.id++ }
console.log("Generating network...")
this.updateChart(nodes_links, this)
this.notiBuffer.set({ 'nodes': nodes_links.nodes, 'links': nodes_links.links })
}
}
},
// Format from data to nodes and links
elDataFromData: function (data) {
let nodes = [];
let links = [];
const nodeId = this.data.nodeId;
const linkId = this.data.linkId;
const nodeVal = this.data.nodeVal;
const source = this.data.linkSource;
const target = this.data.linkTarget;
const linkLabel = this.data.linkLabel;
data.forEach(element => {
let node = {};
Object.keys(element).forEach(function (k) {
if (k === nodeId) {
node[nodeId] = element[k];
}
if (k === linkId) {
node.linkId = element[k];
}
if (k === nodeVal) {
node[nodeVal] = element[k];
}
});
nodes.push(node);
});
nodes.forEach(firstNode => {
nodes.forEach(secondNode => {
if (firstNode[nodeId] !== secondNode[nodeId]) { // not same id
if (firstNode.linkId === secondNode.linkId) { // same linkId, make link
if (links.length > 0) {
let linkExists = false;
links.forEach(link => {
if ((link[source] === firstNode[nodeId] && link[target] === secondNode[nodeId]) || (link[source] === secondNode[nodeId] && link[target] === firstNode[nodeId])) { // the link does already exists, in any direction
linkExists = true;
}
})
if (!linkExists) {
let newLink = {}
//newLink[linkId] = firstNode[linkId]
newLink[linkLabel] = firstNode[linkLabel]
newLink[source] = firstNode[nodeId]
newLink[target] = secondNode[nodeId]
links.push(newLink);
}
} else {
let newLink = {}
//newLink[linkId] = firstNode[linkId]
newLink[linkLabel] = firstNode[linkLabel]
newLink[source] = firstNode[nodeId]
newLink[target] = secondNode[nodeId]
links.push(newLink);
}
}
}
})
});
return { nodes: nodes, links: links };
},
// Format nodes and links
elDataFromNodesAndLinks: function (nodes, links) {
if (!Array.isArray(nodes) || !Array.isArray(links)) {
// Either nodes or links are not ready yet
return false
}
if (this.isFirstTimeNodes) {
this.nodeFields = []
Object.keys(nodes[0]).forEach((k) => {
this.nodeFields.push(k)
})
this.isFirstTimeNodes = false;
}
if (this.isFirstTimeLinks) {
this.linkFields = []
Object.keys(links[0]).forEach((k) => {
this.linkFields.push(k)
})
this.isFirstTimeLinks = false;
}
if (typeof (links[0].source) === 'object') {
links.forEach((link) => {
link.source = link.source[this.data.nodeId];
link.target = link.target[this.data.nodeId];
})
}
let _nodes = [];
nodes.forEach((node) => {
let _node = {};
Object.keys(node).forEach((k) => {
for (let f in this.nodeFields) {
if (k == this.nodeFields[f]) {
_node[k] = node[k]
}
}
})
_nodes.push(_node)
})
let _links = [];
links.forEach((link) => {
let _link = {};
Object.keys(link).forEach((k) => {
for (let f in this.linkFields) {
if (k == this.linkFields[f] && k != this.data.linkId) {
_link[k] = link[k]
}
}
})
_links.push(_link)
})
return { nodes: _nodes, links: _links };
},
showLegend: function (nodeThree, node, nodeLabel, scale) {
let worldPosition = new THREE.Vector3();
nodeThree.getWorldPosition(worldPosition);
let radius = nodeThree.geometry.boundingSphere.radius
let sceneEl = document.querySelector('a-scene');
this.legend = generateLegend(this.data, node, nodeLabel, worldPosition, radius, scale);
if (this.legend) {
sceneEl.appendChild(this.legend);
}
},
showLinkLegend: function (linkThree, link, linkLabel, scale) {
let worldPosition = new THREE.Vector3();
let radius = linkThree.geometry.boundingSphere.radius
let sourcePos = new THREE.Vector3();
let targetPos = new THREE.Vector3();
let nodes = this.forceGraph.children.filter(element => element.__graphObjType == "node")
nodes.forEach(node => {
if (node.__data[this.data.nodeId] == link.source[this.data.nodeId]) {
node.getWorldPosition(sourcePos)
}
if (node.__data[this.data.nodeId] == link.target[this.data.nodeId]) {
node.getWorldPosition(targetPos)
}
})
worldPosition.x = (sourcePos.x + targetPos.x) / 2
worldPosition.y = (sourcePos.y + targetPos.y) / 2
worldPosition.z = (sourcePos.z + targetPos.z) / 2
let sceneEl = document.querySelector('a-scene');
this.legend = generateLinkLegend(this.data, link, linkLabel, worldPosition, radius, scale);
if (this.legend) {
sceneEl.appendChild(this.legend);
}
},
removeLegend: function () {
if (this.legend) {
let sceneEl = document.querySelector('a-scene');
sceneEl.removeChild(this.legend)
}
}
});
function generateLegend(data, node, nodeLabel, nodePosition, radius, scale) {
let name = node[nodeLabel];
if (name) {
let width = 2;
let height = 1;
if (name.length > 16)
width = name.length / 5;
if (data) {
let radiusText = "\n " + data.nodeVal + " (radius): " + Math.round(node[data.nodeVal] * 100) / 100
if (radiusText.length > 16)
width = radiusText.length / 5;
name += radiusText
if (data.nodeAutoColorBy) {
let autoColorText = "\n " + data.nodeAutoColorBy + " (color): " + node[data.nodeAutoColorBy]
if (autoColorText.length > 16 && autoColorText > radiusText)
width = autoColorText.length / 5;
name += autoColorText
} else {
let autoColorText = "\n " + data.nodeAutoColorBy + " (color): " + node[data.nodeAutoColorBy]
if (autoColorText.length > 16 && autoColorText > radiusText)
width = autoColorText.length / 5;
name += autoColorText
}
}
let x = 3;
let y = 3;
let z = 3;
if (name.length > 16)
width = name.length / 16;
if (scale) {
x = x * scale.x * data.legend_scale;
y = y * scale.y * data.legend_scale;
z = z * scale.z * data.legend_scale;
radius = (radius + 3) * scale.y * data.legend_scale;
}
let entity = document.createElement('a-plane');
entity.setAttribute('position', { x: nodePosition.x, y: nodePosition.y + radius, z: nodePosition.z })
entity.setAttribute('babia-lookat', data.legend_lookat);
entity.setAttribute('width', width);
entity.setAttribute('height', height);
entity.setAttribute('color', 'white');
entity.setAttribute('scale', { x: x * data.legend_scale, y: y * data.legend_scale, z: z * data.legend_scale })
entity.setAttribute('material', { 'side': 'double' });
entity.setAttribute('text', {
'value': name,
'align': 'center',
'width': 6,
'color': 'black',
'alphaTest': 6,
'opacity': 6,
'transparent': false
});
entity.classList.add("babiaxrLegend")
return entity;
}
}
function generateLinkLegend(data, link, linkLabel, linkPosition, radius, scale) {
let text = link[linkLabel];
if (text) {
let width = 2;
let x = 3;
let y = 3;
let z = 3;
if (text.length > 16)
width = text.length / 8;
if (scale) {
x = x * scale.x;
y = y * scale.y;
z = z * scale.z;
radius = (radius + 3) * scale.y;
}
let entity = document.createElement('a-plane');
entity.setAttribute('position', { x: linkPosition.x, y: linkPosition.y + radius, z: linkPosition.z })
entity.setAttribute('babia-lookat', data.legend_lookat);
entity.setAttribute('width', width);
entity.setAttribute('height', '1');
entity.setAttribute('color', 'white');
entity.setAttribute('scale', { x: x * data.legend_scale, y: y * data.legend_scale, z: z * data.legend_scale })
entity.setAttribute('material', { 'side': 'double' });
entity.setAttribute('text', {
'value': link[linkLabel],
'align': 'center',
'width': 6,
'color': 'black',
'alphaTest': 6,
'opacity': 6,
'transparent': false
});
entity.classList.add("babiaxrLegend")
return entity;
}
}
function setCursor() {
// When loading, cursor gets entity cursor
cursor = document.querySelector('[cursor]');
// When controllers are connected, change cursor to laser control
document.addEventListener('controllerconnected', (event) => {
cursor = document.querySelector('[laser-controls]');
});
}