|
@ -6,6 +6,7 @@ import HierarchicalSpringSolver from './components/physics/Hierarchi |
|
|
import CentralGravitySolver from './components/physics/CentralGravitySolver'; |
|
|
import CentralGravitySolver from './components/physics/CentralGravitySolver'; |
|
|
import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedRepulsionSolver'; |
|
|
import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedRepulsionSolver'; |
|
|
import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver'; |
|
|
import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver'; |
|
|
|
|
|
import PhysicsWorker from 'worker!./PhysicsWorkerWrapper'; |
|
|
|
|
|
|
|
|
var util = require('../../util'); |
|
|
var util = require('../../util'); |
|
|
|
|
|
|
|
@ -38,6 +39,7 @@ class PhysicsEngine { |
|
|
this.options = {}; |
|
|
this.options = {}; |
|
|
this.defaultOptions = { |
|
|
this.defaultOptions = { |
|
|
enabled: true, |
|
|
enabled: true, |
|
|
|
|
|
useWorker: false, |
|
|
barnesHut: { |
|
|
barnesHut: { |
|
|
theta: 0.5, |
|
|
theta: 0.5, |
|
|
gravitationalConstant: -2000, |
|
|
gravitationalConstant: -2000, |
|
@ -87,6 +89,7 @@ class PhysicsEngine { |
|
|
util.extend(this.options, this.defaultOptions); |
|
|
util.extend(this.options, this.defaultOptions); |
|
|
this.timestep = 0.5; |
|
|
this.timestep = 0.5; |
|
|
this.layoutFailed = false; |
|
|
this.layoutFailed = false; |
|
|
|
|
|
this.draggingNodes = []; |
|
|
|
|
|
|
|
|
this.bindEventListeners(); |
|
|
this.bindEventListeners(); |
|
|
} |
|
|
} |
|
@ -111,6 +114,15 @@ class PhysicsEngine { |
|
|
this.body.emitter.on('destroy', () => { |
|
|
this.body.emitter.on('destroy', () => { |
|
|
this.stopSimulation(false); |
|
|
this.stopSimulation(false); |
|
|
this.body.emitter.off(); |
|
|
this.body.emitter.off(); |
|
|
|
|
|
}); |
|
|
|
|
|
// For identifying which nodes to send to worker thread
|
|
|
|
|
|
this.body.emitter.on('dragStart', (properties) => {this.draggingNodes = properties.nodes;}); |
|
|
|
|
|
this.body.emitter.on('dragEnd', () => {this.draggingNodes = [];}); |
|
|
|
|
|
this.body.emitter.on('destroy', () => { |
|
|
|
|
|
if (this.physicsWorker) { |
|
|
|
|
|
this.physicsWorker.terminate(); |
|
|
|
|
|
this.physicsWorker = undefined; |
|
|
|
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -144,14 +156,25 @@ class PhysicsEngine { |
|
|
this.timestep = this.options.timestep; |
|
|
this.timestep = this.options.timestep; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
this.init(); |
|
|
|
|
|
|
|
|
if (this.options.useWorker) { |
|
|
|
|
|
this.initPhysicsWorker(); |
|
|
|
|
|
this.physicsWorker.postMessage({type: 'options', data: this.options}); |
|
|
|
|
|
} else { |
|
|
|
|
|
this.initEmbeddedPhysics(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* configure the engine. |
|
|
* configure the engine. |
|
|
*/ |
|
|
*/ |
|
|
init() { |
|
|
|
|
|
|
|
|
initEmbeddedPhysics() { |
|
|
|
|
|
if (this.physicsWorker) { |
|
|
|
|
|
this.options.useWorker = false; |
|
|
|
|
|
this.physicsWorker.terminate(); |
|
|
|
|
|
this.physicsWorker = undefined; |
|
|
|
|
|
this.updatePhysicsData(); |
|
|
|
|
|
} |
|
|
var options; |
|
|
var options; |
|
|
if (this.options.solver === 'forceAtlas2Based') { |
|
|
if (this.options.solver === 'forceAtlas2Based') { |
|
|
options = this.options.forceAtlas2Based; |
|
|
options = this.options.forceAtlas2Based; |
|
@ -181,6 +204,65 @@ class PhysicsEngine { |
|
|
this.modelOptions = options; |
|
|
this.modelOptions = options; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
initPhysicsWorker() { |
|
|
|
|
|
if (!this.physicsWorker) { |
|
|
|
|
|
if (!__webpack_public_path__) { |
|
|
|
|
|
let parentScript = document.getElementById('visjs'); |
|
|
|
|
|
if (parentScript) { |
|
|
|
|
|
let src = parentScript.getAttribute('src') |
|
|
|
|
|
__webpack_public_path__ = src.substr(0, src.lastIndexOf('/') + 1); |
|
|
|
|
|
} else { |
|
|
|
|
|
let scripts = document.getElementsByTagName('script'); |
|
|
|
|
|
for (let i = 0; i < scripts.length; i++) { |
|
|
|
|
|
let src = scripts[i].getAttribute('src'); |
|
|
|
|
|
if (src && src.length >= 6) { |
|
|
|
|
|
let position = src.length - 6; |
|
|
|
|
|
let index = src.indexOf('vis.js', position); |
|
|
|
|
|
if (index === position) { |
|
|
|
|
|
__webpack_public_path__ = src.substr(0, src.lastIndexOf('/') + 1); |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
this.physicsWorker = new PhysicsWorker(); |
|
|
|
|
|
this.physicsWorker.addEventListener('message', (event) => { |
|
|
|
|
|
this.physicsWorkerMessageHandler(event); |
|
|
|
|
|
}); |
|
|
|
|
|
this.physicsWorker.onerror = (event) => { |
|
|
|
|
|
console.error('Falling back to embedded physics engine'); |
|
|
|
|
|
this.initEmbeddedPhysics(); |
|
|
|
|
|
// throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
|
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
physicsWorkerMessageHandler(event) { |
|
|
|
|
|
var msg = event.data; |
|
|
|
|
|
switch (msg.type) { |
|
|
|
|
|
case 'positions': |
|
|
|
|
|
this.stabilized = msg.data.stabilized; |
|
|
|
|
|
var positions = msg.data.positions; |
|
|
|
|
|
// console.log('received positions', positions);
|
|
|
|
|
|
for (let i = 0; i < this.draggingNodes; i++) { |
|
|
|
|
|
delete positions[this.draggingNodes[i]]; |
|
|
|
|
|
} |
|
|
|
|
|
let nodeIds = Object.keys(positions); |
|
|
|
|
|
for (let i = 0; i < nodeIds.length; i++) { |
|
|
|
|
|
let nodeId = nodeIds[i]; |
|
|
|
|
|
let node = this.body.nodes[nodeId]; |
|
|
|
|
|
// handle case where we get a positions from an old physicsObject
|
|
|
|
|
|
if (node) { |
|
|
|
|
|
node.x = positions[nodeId].x; |
|
|
|
|
|
node.y = positions[nodeId].y; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
break; |
|
|
|
|
|
default: |
|
|
|
|
|
console.warn('unhandled physics worker message:', msg); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* initialize the engine |
|
|
* initialize the engine |
|
@ -208,6 +290,20 @@ class PhysicsEngine { |
|
|
*/ |
|
|
*/ |
|
|
startSimulation() { |
|
|
startSimulation() { |
|
|
if (this.physicsEnabled === true && this.options.enabled === true) { |
|
|
if (this.physicsEnabled === true && this.options.enabled === true) { |
|
|
|
|
|
if (this.physicsWorker) { |
|
|
|
|
|
for(let i = 0; i < this.draggingNodes.length; i++) { |
|
|
|
|
|
let nodeId = this.draggingNodes[i]; |
|
|
|
|
|
let node = this.body.nodes[nodeId]; |
|
|
|
|
|
this.physicsWorker.postMessage({ |
|
|
|
|
|
type: 'update', |
|
|
|
|
|
data: { |
|
|
|
|
|
id: nodeId, |
|
|
|
|
|
x: node.x, |
|
|
|
|
|
y: node.y |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
this.stabilized = false; |
|
|
this.stabilized = false; |
|
|
|
|
|
|
|
|
// when visible, adaptivity is disabled.
|
|
|
// when visible, adaptivity is disabled.
|
|
@ -349,8 +445,13 @@ class PhysicsEngine { |
|
|
else { |
|
|
else { |
|
|
// case for the static timestep, we reset it to the one in options and take a normal step.
|
|
|
// case for the static timestep, we reset it to the one in options and take a normal step.
|
|
|
this.timestep = this.options.timestep; |
|
|
this.timestep = this.options.timestep; |
|
|
this.calculateForces(); |
|
|
|
|
|
this.moveNodes(); |
|
|
|
|
|
|
|
|
if (this.physicsWorker) { |
|
|
|
|
|
// console.log('asking working to do a physics iteration');
|
|
|
|
|
|
this.physicsWorker.postMessage({type: 'calculateForces'}); |
|
|
|
|
|
} else { |
|
|
|
|
|
this.calculateForces(); |
|
|
|
|
|
this.moveNodes(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// determine if the network has stabilzied
|
|
|
// determine if the network has stabilzied
|
|
@ -368,45 +469,110 @@ class PhysicsEngine { |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
updatePhysicsData() { |
|
|
updatePhysicsData() { |
|
|
this.physicsBody.forces = {}; |
|
|
|
|
|
this.physicsBody.physicsNodeIndices = []; |
|
|
|
|
|
this.physicsBody.physicsEdgeIndices = []; |
|
|
|
|
|
let nodes = this.body.nodes; |
|
|
let nodes = this.body.nodes; |
|
|
let edges = this.body.edges; |
|
|
let edges = this.body.edges; |
|
|
|
|
|
|
|
|
// get node indices for physics
|
|
|
|
|
|
for (let nodeId in nodes) { |
|
|
|
|
|
if (nodes.hasOwnProperty(nodeId)) { |
|
|
|
|
|
if (nodes[nodeId].options.physics === true) { |
|
|
|
|
|
this.physicsBody.physicsNodeIndices.push(nodeId); |
|
|
|
|
|
|
|
|
if (this.physicsWorker) { |
|
|
|
|
|
var physicsWorkerNodes = {}; |
|
|
|
|
|
var physicsWorkerEdges = {}; |
|
|
|
|
|
|
|
|
|
|
|
for (let nodeId in nodes) { |
|
|
|
|
|
if (nodes.hasOwnProperty(nodeId)) { |
|
|
|
|
|
let node = nodes[nodeId]; |
|
|
|
|
|
if (node.options.physics === true) { |
|
|
|
|
|
physicsWorkerNodes[nodeId] = { |
|
|
|
|
|
id: node.id, |
|
|
|
|
|
x: node.x, |
|
|
|
|
|
y: node.y, |
|
|
|
|
|
options: { |
|
|
|
|
|
fixed: { |
|
|
|
|
|
x: node.options.fixed.x, |
|
|
|
|
|
y: node.options.fixed.y |
|
|
|
|
|
}, |
|
|
|
|
|
mass: node.options.mass |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// get edge indices for physics
|
|
|
|
|
|
for (let edgeId in edges) { |
|
|
|
|
|
if (edges.hasOwnProperty(edgeId)) { |
|
|
|
|
|
if (edges[edgeId].options.physics === true) { |
|
|
|
|
|
this.physicsBody.physicsEdgeIndices.push(edgeId); |
|
|
|
|
|
|
|
|
for (let edgeId in edges) { |
|
|
|
|
|
if (edges.hasOwnProperty(edgeId)) { |
|
|
|
|
|
let edge = edges[edgeId]; |
|
|
|
|
|
if (edge.options.physics === true) { |
|
|
|
|
|
physicsWorkerEdges[edgeId] = { |
|
|
|
|
|
connected: edge.connected, |
|
|
|
|
|
id: edge.id, |
|
|
|
|
|
edgeType: {}, |
|
|
|
|
|
toId: edge.toId, |
|
|
|
|
|
fromId: edge.fromId, |
|
|
|
|
|
to: { |
|
|
|
|
|
id: edge.to.id |
|
|
|
|
|
}, |
|
|
|
|
|
from: { |
|
|
|
|
|
id: edge.from.id |
|
|
|
|
|
}, |
|
|
|
|
|
options: { |
|
|
|
|
|
length: edge.length |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
if (edge.edgeType.via) { |
|
|
|
|
|
physicsWorkerEdges[edgeId].edgeType = { |
|
|
|
|
|
via: { |
|
|
|
|
|
id: edge.edgeType.via.id |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.physicsWorker.postMessage({ |
|
|
|
|
|
type: 'physicsObjects', |
|
|
|
|
|
data: { |
|
|
|
|
|
nodes: physicsWorkerNodes, |
|
|
|
|
|
edges: physicsWorkerEdges |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} else { |
|
|
|
|
|
this.physicsBody.forces = {}; |
|
|
|
|
|
this.physicsBody.physicsNodeIndices = []; |
|
|
|
|
|
this.physicsBody.physicsEdgeIndices = []; |
|
|
|
|
|
|
|
|
|
|
|
// get node indices for physics
|
|
|
|
|
|
for (let nodeId in nodes) { |
|
|
|
|
|
if (nodes.hasOwnProperty(nodeId)) { |
|
|
|
|
|
if (nodes[nodeId].options.physics === true) { |
|
|
|
|
|
this.physicsBody.physicsNodeIndices.push(nodeId); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// get edge indices for physics
|
|
|
|
|
|
for (let edgeId in edges) { |
|
|
|
|
|
if (edges.hasOwnProperty(edgeId)) { |
|
|
|
|
|
if (edges[edgeId].options.physics === true) { |
|
|
|
|
|
this.physicsBody.physicsEdgeIndices.push(edgeId); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// get the velocity and the forces vector
|
|
|
|
|
|
for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { |
|
|
|
|
|
let nodeId = this.physicsBody.physicsNodeIndices[i]; |
|
|
|
|
|
this.physicsBody.forces[nodeId] = {x:0,y:0}; |
|
|
|
|
|
|
|
|
// get the velocity and the forces vector
|
|
|
|
|
|
for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { |
|
|
|
|
|
let nodeId = this.physicsBody.physicsNodeIndices[i]; |
|
|
|
|
|
this.physicsBody.forces[nodeId] = {x: 0, y: 0}; |
|
|
|
|
|
|
|
|
// forces can be reset because they are recalculated. Velocities have to persist.
|
|
|
|
|
|
if (this.physicsBody.velocities[nodeId] === undefined) { |
|
|
|
|
|
this.physicsBody.velocities[nodeId] = {x:0,y:0}; |
|
|
|
|
|
|
|
|
// forces can be reset because they are recalculated. Velocities have to persist.
|
|
|
|
|
|
if (this.physicsBody.velocities[nodeId] === undefined) { |
|
|
|
|
|
this.physicsBody.velocities[nodeId] = {x: 0, y: 0}; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// clean deleted nodes from the velocity vector
|
|
|
|
|
|
for (let nodeId in this.physicsBody.velocities) { |
|
|
|
|
|
if (nodes[nodeId] === undefined) { |
|
|
|
|
|
delete this.physicsBody.velocities[nodeId]; |
|
|
|
|
|
|
|
|
// clean deleted nodes from the velocity vector
|
|
|
|
|
|
for (let nodeId in this.physicsBody.velocities) { |
|
|
|
|
|
if (nodes[nodeId] === undefined) { |
|
|
|
|
|
delete this.physicsBody.velocities[nodeId]; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -621,7 +787,7 @@ class PhysicsEngine { |
|
|
this._freezeNodes(); |
|
|
this._freezeNodes(); |
|
|
} |
|
|
} |
|
|
this.stabilizationIterations = 0; |
|
|
this.stabilizationIterations = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => this._stabilizationBatch(),0); |
|
|
setTimeout(() => this._stabilizationBatch(),0); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -642,7 +808,7 @@ class PhysicsEngine { |
|
|
this.physicsTick(); |
|
|
this.physicsTick(); |
|
|
count++; |
|
|
count++; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) { |
|
|
if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) { |
|
|
this.body.emitter.emit('stabilizationProgress', {iterations: this.stabilizationIterations, total: this.targetIterations}); |
|
|
this.body.emitter.emit('stabilizationProgress', {iterations: this.stabilizationIterations, total: this.targetIterations}); |
|
|
setTimeout(this._stabilizationBatch.bind(this),0); |
|
|
setTimeout(this._stabilizationBatch.bind(this),0); |
|
@ -679,7 +845,7 @@ class PhysicsEngine { |
|
|
|
|
|
|
|
|
this.ready = true; |
|
|
this.ready = true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export default PhysicsEngine; |
|
|
export default PhysicsEngine; |