Browse Source

added POC physics web worker implementation.

webworkersNetwork^2^2
Eric VanDever 9 years ago
parent
commit
23acabcb58
10 changed files with 6512 additions and 4445 deletions
  1. +4592
    -4404
      dist/vis.js
  2. +1
    -1
      dist/vis.min.css
  3. +1484
    -0
      dist/vis.physics.worker.js
  4. +7
    -1
      gulpfile.js
  5. +200
    -34
      lib/network/modules/PhysicsEngine.js
  6. +216
    -0
      lib/network/modules/PhysicsWorker.js
  7. +4
    -0
      lib/network/modules/PhysicsWorkerWrapper.js
  8. +5
    -4
      lib/network/modules/components/physics/SpringSolver.js
  9. +1
    -0
      lib/network/options.js
  10. +2
    -1
      package.json

+ 4592
- 4404
dist/vis.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/vis.min.css
File diff suppressed because it is too large
View File


+ 1484
- 0
dist/vis.physics.worker.js
File diff suppressed because it is too large
View File


+ 7
- 1
gulpfile.js View File

@ -52,9 +52,15 @@ var webpackConfig = {
wrappedContextRegExp: /$^/ wrappedContextRegExp: /$^/
}, },
plugins: [ bannerPlugin ], plugins: [ bannerPlugin ],
cache: true
cache: true,
//debug: true, //debug: true,
//bail: true //bail: true
worker: {
path: DIST,
output: {
filename: "vis.physics.worker.js"
}
}
}; };
var uglifyConfig = { var uglifyConfig = {

+ 200
- 34
lib/network/modules/PhysicsEngine.js View File

@ -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;

+ 216
- 0
lib/network/modules/PhysicsWorker.js View File

@ -0,0 +1,216 @@
import BarnesHutSolver from './components/physics/BarnesHutSolver';
import Repulsion from './components/physics/RepulsionSolver';
import HierarchicalRepulsion from './components/physics/HierarchicalRepulsionSolver';
import SpringSolver from './components/physics/SpringSolver';
import HierarchicalSpringSolver from './components/physics/HierarchicalSpringSolver';
import CentralGravitySolver from './components/physics/CentralGravitySolver';
import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedRepulsionSolver';
import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver';
class PhysicsWorker {
constructor(postMessage) {
this.body = {};
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
this.postMessage = postMessage;
this.options = {};
this.stabilized = false;
this.previousStates = {};
this.positions = {};
this.timestep = 0.5;
}
handleMessage(event) {
var msg = event.data;
switch (msg.type) {
case 'calculateForces':
this.calculateForces();
this.moveNodes();
this.postMessage({
type: 'positions',
data: {
positions: this.positions,
stabilized: this.stabilized
}
});
break;
case 'update':
let node = this.body.nodes[msg.data.id];
node.x = msg.data.x;
node.y = msg.data.y;
break;
case 'options':
this.options = msg.data;
this.timestep = this.options.timestep;
this.init();
break;
case 'physicsObjects':
this.body.nodes = msg.data.nodes;
this.body.edges = msg.data.edges;
this.updatePhysicsData();
break;
default:
console.warn('unknown message from PhysicsEngine', msg);
}
}
/**
* configure the engine.
*/
init() {
var options;
if (this.options.solver === 'forceAtlas2Based') {
options = this.options.forceAtlas2Based;
this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(this.body, this.physicsBody, options);
}
else if (this.options.solver === 'repulsion') {
options = this.options.repulsion;
this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
else if (this.options.solver === 'hierarchicalRepulsion') {
options = this.options.hierarchicalRepulsion;
this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
else { // barnesHut
options = this.options.barnesHut;
this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
this.modelOptions = options;
}
/**
* Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time.
*
* @private
*/
updatePhysicsData() {
this.physicsBody.forces = {};
this.physicsBody.physicsNodeIndices = [];
this.physicsBody.physicsEdgeIndices = [];
let nodes = this.body.nodes;
let edges = this.body.edges;
// get node indices for physics
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
this.physicsBody.physicsNodeIndices.push(nodeId);
this.positions[nodeId] = {
x: nodes[nodeId].x,
y: nodes[nodeId].y
}
}
}
// get edge indices for physics
for (let edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
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};
// 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];
}
}
// console.log(this.physicsBody);
}
/**
* move the nodes one timestap and check if they are stabilized
* @returns {boolean}
*/
moveNodes() {
var nodeIndices = this.physicsBody.physicsNodeIndices;
var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
var maxNodeVelocity = 0;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
let nodeVelocity = this._performStep(nodeId, maxVelocity);
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
}
// evaluating the stabilized and adaptiveTimestepEnabled conditions
this.stabilized = maxNodeVelocity < this.options.minVelocity;
}
/**
* Perform the actual step
*
* @param nodeId
* @param maxVelocity
* @returns {number}
* @private
*/
_performStep(nodeId,maxVelocity) {
let node = this.body.nodes[nodeId];
let timestep = this.timestep;
let forces = this.physicsBody.forces;
let velocities = this.physicsBody.velocities;
// store the state so we can revert
this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
if (node.options.fixed.x === false) {
let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
velocities[nodeId].x += ax * timestep; // velocity
velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
node.x += velocities[nodeId].x * timestep; // position
this.positions[nodeId].x = node.x;
}
else {
forces[nodeId].x = 0;
velocities[nodeId].x = 0;
}
if (node.options.fixed.y === false) {
let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
velocities[nodeId].y += ay * timestep; // velocity
velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
node.y += velocities[nodeId].y * timestep; // position
this.positions[nodeId].y = node.y;
}
else {
forces[nodeId].y = 0;
velocities[nodeId].y = 0;
}
let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
return totalVelocity;
}
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
}
export default PhysicsWorker;

+ 4
- 0
lib/network/modules/PhysicsWorkerWrapper.js View File

@ -0,0 +1,4 @@
import PhysicsWorker from './PhysicsWorker.js';
var physicsWorker = new PhysicsWorker((data) => postMessage(data));
self.addEventListener('message', (event) => physicsWorker.handleMessage(event), false);

+ 5
- 4
lib/network/modules/components/physics/SpringSolver.js View File

@ -18,6 +18,7 @@ class SpringSolver {
let edgeLength, edge; let edgeLength, edge;
let edgeIndices = this.physicsBody.physicsEdgeIndices; let edgeIndices = this.physicsBody.physicsEdgeIndices;
let edges = this.body.edges; let edges = this.body.edges;
let nodes = this.body.nodes;
let node1, node2, node3; let node1, node2, node3;
// forces caused by the edges, modelled as springs // forces caused by the edges, modelled as springs
@ -28,9 +29,9 @@ class SpringSolver {
if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
if (edge.edgeType.via !== undefined) { if (edge.edgeType.via !== undefined) {
edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length;
node1 = edge.to;
node2 = edge.edgeType.via;
node3 = edge.from;
node1 = nodes[edge.to.id];
node2 = nodes[edge.edgeType.via.id];
node3 = nodes[edge.from.id];
this._calculateSpringForce(node1, node2, 0.5 * edgeLength); this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength); this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
@ -39,7 +40,7 @@ class SpringSolver {
// the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use
// the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger.
edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5: edge.options.length; edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5: edge.options.length;
this._calculateSpringForce(edge.from, edge.to, edgeLength);
this._calculateSpringForce(nodes[edge.from.id], nodes[edge.to.id], edgeLength);
} }
} }
} }

+ 1
- 0
lib/network/options.js View File

@ -226,6 +226,7 @@ let allOptions = {
}, },
physics: { physics: {
enabled: { boolean }, enabled: { boolean },
useWorker: { boolean },
barnesHut: { barnesHut: {
gravitationalConstant: { number }, gravitationalConstant: { number },
centralGravity: { number }, centralGravity: { number },

+ 2
- 1
package.json View File

@ -34,7 +34,8 @@
"keycharm": "^0.2.0", "keycharm": "^0.2.0",
"moment": "^2.10.2", "moment": "^2.10.2",
"propagating-hammerjs": "^1.4.3", "propagating-hammerjs": "^1.4.3",
"uuid": "^2.0.1"
"uuid": "^2.0.1",
"worker-loader": "^0.6.0"
}, },
"devDependencies": { "devDependencies": {
"babel": "^5.1.11", "babel": "^5.1.11",

Loading…
Cancel
Save