Browse Source

Merge pull request #1440 from ericvandever/webworkersNetwork

Webworkers network
webworkersNetwork
Alex 8 years ago
parent
commit
8a787711a5
15 changed files with 935 additions and 286 deletions
  1. +11
    -1
      docs/network/physics.html
  2. +7
    -1
      gulpfile.js
  3. +1
    -1
      lib/network/Network.js
  4. +2
    -4
      lib/network/modules/InteractionHandler.js
  5. +2
    -2
      lib/network/modules/LayoutEngine.js
  6. +2
    -2
      lib/network/modules/NodesHandler.js
  7. +261
    -0
      lib/network/modules/PhysicsBase.js
  8. +272
    -263
      lib/network/modules/PhysicsEngine.js
  9. +268
    -0
      lib/network/modules/PhysicsWorker.js
  10. +4
    -0
      lib/network/modules/PhysicsWorkerWrapper.js
  11. +13
    -0
      lib/network/modules/components/Edge.js
  12. +83
    -8
      lib/network/modules/components/Node.js
  13. +5
    -4
      lib/network/modules/components/physics/SpringSolver.js
  14. +1
    -0
      lib/network/options.js
  15. +3
    -0
      package.json

+ 11
- 1
docs/network/physics.html View File

@ -139,7 +139,8 @@ var options = {
fit: true fit: true
}, },
timestep: 0.5, timestep: 0.5,
adaptiveTimestep: true
adaptiveTimestep: true,
useWorker: false
} }
} }
@ -203,6 +204,15 @@ network.setOptions(options);
<tr parent="stabilization" class="hidden"><td class="indent">stabilization.fit</td> <td>Boolean</td> <td><code>true</code></td> <td>Toggle whether or not you want the view to zoom to fit all nodes when the stabilization is finished.</td></tr> <tr parent="stabilization" class="hidden"><td class="indent">stabilization.fit</td> <td>Boolean</td> <td><code>true</code></td> <td>Toggle whether or not you want the view to zoom to fit all nodes when the stabilization is finished.</td></tr>
<tr><td>timestep</td> <td>Number</td> <td><code>0.5</code></td> <td>The physics simulation is discrete. This means we take a step in time, calculate the forces, move the nodes and take another step. If you increase this number the steps will be too large and the network can get unstable. If you see a lot of jittery movement in the network, you may want to reduce this value a little.</td></tr> <tr><td>timestep</td> <td>Number</td> <td><code>0.5</code></td> <td>The physics simulation is discrete. This means we take a step in time, calculate the forces, move the nodes and take another step. If you increase this number the steps will be too large and the network can get unstable. If you see a lot of jittery movement in the network, you may want to reduce this value a little.</td></tr>
<tr><td>adaptiveTimestep</td> <td>Boolean</td> <td><code>true</code></td> <td>If this is enabled, the timestep will intelligently be adapted <b>(only during the stabilization stage if stabilization is enabled!)</b> to greatly decrease stabilization times. The timestep configured above is taken as the minimum timestep. <a href="layout.html#layout" target="_blank">This can be further improved by using the improvedLayout algorithm</a>.</td></tr> <tr><td>adaptiveTimestep</td> <td>Boolean</td> <td><code>true</code></td> <td>If this is enabled, the timestep will intelligently be adapted <b>(only during the stabilization stage if stabilization is enabled!)</b> to greatly decrease stabilization times. The timestep configured above is taken as the minimum timestep. <a href="layout.html#layout" target="_blank">This can be further improved by using the improvedLayout algorithm</a>.</td></tr>
<tr><td>useWorker</td> <td>Boolean</td> <td><code>false</code></td>
<td>If this is enabled, the physics calculation will be performed in a separate thread. The file vis.physics.worker.js must be available from the same webserver hosting vis.js at the same path. If
you are embedding vis into a javascript bundle, you can set the script id to "visjs" to enable
path resolution. If the worker fails for any reason, the system will fall back to standard
physics calculations.
WorkInProgress: This has only been tested with default physics selections and does not attempt to optimize
stabilization. Has not been optimized for small changes to underlying dataset, so each change will
cause all data to be recopied to the worker thread.
</td></tr>
</table> </table>
</div> </div>

+ 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 = {

+ 1
- 1
lib/network/Network.js View File

@ -270,7 +270,7 @@ Network.prototype.bindEventListeners = function () {
this.body.emitter.on("_dataChanged", () => { this.body.emitter.on("_dataChanged", () => {
// update shortcut lists // update shortcut lists
this._updateVisibleIndices(); this._updateVisibleIndices();
this.physics.updatePhysicsData();
this.physics.initPhysicsData();
this.body.emitter.emit("_requestRedraw"); this.body.emitter.emit("_requestRedraw");
// call the dataUpdated event because the only difference between the two is the updating of the indices // call the dataUpdated event because the only difference between the two is the updating of the indices
this.body.emitter.emit("_dataUpdated"); this.body.emitter.emit("_dataUpdated");

+ 2
- 4
lib/network/modules/InteractionHandler.js View File

@ -313,8 +313,7 @@ class InteractionHandler {
yFixed: object.options.fixed.y yFixed: object.options.fixed.y
}; };
object.options.fixed.x = true;
object.options.fixed.y = true;
object.setFixed(true);
this.drag.selection.push(s); this.drag.selection.push(s);
} }
@ -395,8 +394,7 @@ class InteractionHandler {
if (selection && selection.length) { if (selection && selection.length) {
selection.forEach(function (s) { selection.forEach(function (s) {
// restore original xFixed and yFixed // restore original xFixed and yFixed
s.node.options.fixed.x = s.xFixed;
s.node.options.fixed.y = s.yFixed;
s.node.setFixed({x: s.xFixed, y: s.yFixed});
}); });
this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center)); this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center));
this.body.emitter.emit('startSimulation'); this.body.emitter.emit('startSimulation');

+ 2
- 2
lib/network/modules/LayoutEngine.js View File

@ -384,11 +384,11 @@ class LayoutEngine {
let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId];
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
node.y = this.options.hierarchical.levelSeparation * level; node.y = this.options.hierarchical.levelSeparation * level;
node.options.fixed.y = true;
node.setFixed({y: true});
} }
else { else {
node.x = this.options.hierarchical.levelSeparation * level; node.x = this.options.hierarchical.levelSeparation * level;
node.options.fixed.x = true;
node.setFixed({x: true});
} }
if (distribution[level] === undefined) { if (distribution[level] === undefined) {
distribution[level] = {amount: 0, nodes: {}, distance: 0}; distribution[level] = {amount: 0, nodes: {}, distance: 0};

+ 2
- 2
lib/network/modules/NodesHandler.js View File

@ -353,10 +353,10 @@ class NodesHandler {
var dataset = this.body.data.nodes.getDataSet(); var dataset = this.body.data.nodes.getDataSet();
for (let nodeId in dataset._data) { for (let nodeId in dataset._data) {
if (dataset._data.hasOwnProperty(nodeId)) {
if (dataset._data.hasOwnProperty(nodeId) && this.body.nodes.hasOwnProperty(nodeId)) {
let node = this.body.nodes[nodeId]; let node = this.body.nodes[nodeId];
if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) { if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) {
dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) });
dataArray.push({id: nodeId, x: Math.round(node.x), y: Math.round(node.y)});
} }
} }
} }

+ 261
- 0
lib/network/modules/PhysicsBase.js View File

@ -0,0 +1,261 @@
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 PhysicsBase {
constructor() {
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
this.options = {};
this.referenceState = {};
this.previousStates = {};
this.startedStabilization = false;
this.stabilized = false;
this.stabilizationIterations = 0;
this.timestep = 0.5;
// parameters for the adaptive timestep
this.adaptiveTimestep = false;
this.adaptiveTimestepEnabled = false;
this.adaptiveCounter = 0;
this.adaptiveInterval = 3;
}
/**
* configure the engine.
*/
initPhysicsSolvers() {
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;
}
/**
* A single simulation step (or 'tick') in the physics simulation
*
* @private
*/
physicsTick() {
// this is here to ensure that there is no start event when the network is already stable.
if (this.startedStabilization === false) {
this.emit('startStabilizing');
this.startedStabilization = true;
}
if (this.stabilized === false) {
// adaptivity means the timestep adapts to the situation, only applicable for stabilization
if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) {
// this is the factor for increasing the timestep on success.
let factor = 1.2;
// we assume the adaptive interval is
if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations.
// first the big step and revert. Revert saves the reference state.
this.timestep = 2 * this.timestep;
this.calculateForces();
this.moveNodes();
this.revert();
// now the normal step. Since this is the last step, it is the more stable one and we will take this.
this.timestep = 0.5 * this.timestep;
// since it's half the step, we do it twice.
this.calculateForces();
this.moveNodes();
this.calculateForces();
this.moveNodes();
// we compare the two steps. if it is acceptable we double the step.
if (this._evaluateStepQuality() === true) {
this.timestep = factor * this.timestep;
}
else {
// if not, we decrease the step to a minimum of the options timestep.
// if the decreased timestep is smaller than the options step, we do not reset the counter
// we assume that the options timestep is stable enough.
if (this.timestep/factor < this.options.timestep) {
this.timestep = this.options.timestep;
}
else {
// if the timestep was larger than 2 times the option one we check the adaptivity again to ensure
// that large instabilities do not form.
this.adaptiveCounter = -1; // check again next iteration
this.timestep = Math.max(this.options.timestep, this.timestep/factor);
}
}
}
else {
// normal step, keeping timestep constant
this.calculateForces();
this.moveNodes();
}
// increment the counter
this.adaptiveCounter += 1;
}
else {
// case for the static timestep, we reset it to the one in options and take a normal step.
this.timestep = this.options.timestep;
this.calculateForces();
this.moveNodes();
}
// determine if the network has stabilzied
if (this.stabilized === true) {
this.revert();
}
this.stabilizationIterations++;
}
}
/**
* Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized.
*/
revert() {
var nodeIds = Object.keys(this.previousStates);
var nodes = this.body.nodes;
var velocities = this.physicsBody.velocities;
this.referenceState = {};
for (let i = 0; i < nodeIds.length; i++) {
let nodeId = nodeIds[i];
if (nodes[nodeId] !== undefined) {
if (this.isWorker || nodes[nodeId].options.physics === true) {
this.referenceState[nodeId] = {
positions: {x:nodes[nodeId].x, y:nodes[nodeId].y}
};
velocities[nodeId].x = this.previousStates[nodeId].vx;
velocities[nodeId].y = this.previousStates[nodeId].vy;
nodes[nodeId].x = this.previousStates[nodeId].x;
nodes[nodeId].y = this.previousStates[nodeId].y;
}
}
else {
delete this.previousStates[nodeId];
}
}
}
/**
* This compares the reference state to the current state
*/
_evaluateStepQuality() {
let dx, dy, dpos;
let nodes = this.body.nodes;
let reference = this.referenceState;
let posThreshold = 0.3;
for (let nodeId in this.referenceState) {
if (this.referenceState.hasOwnProperty(nodeId) && nodes[nodeId] !== undefined) {
dx = nodes[nodeId].x - reference[nodeId].positions.x;
dy = nodes[nodeId].y - reference[nodeId].positions.y;
dpos = Math.sqrt(Math.pow(dx,2) + Math.pow(dy,2))
if (dpos > posThreshold) {
return false;
}
}
}
return true;
}
/**
* 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;
var averageNodeVelocity = 0;
// the velocity threshold (energy in the system) for the adaptivity toggle
var velocityAdaptiveThreshold = 5;
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);
averageNodeVelocity += nodeVelocity;
}
// evaluating the stabilized and adaptiveTimestepEnabled conditions
this.adaptiveTimestepEnabled = (averageNodeVelocity/nodeIndices.length) < velocityAdaptiveThreshold;
this.stabilized = maxNodeVelocity < this.options.minVelocity;
}
// TODO consider moving _performStep in here
// right now Physics nodes don't have setX setY functions
// - maybe switch logic of setX and set x?
// - add functions to physics nodes - seems not desirable
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
/**
* One batch of stabilization
* @private
*/
_stabilizationBatch() {
// this is here to ensure that there is at least one start event.
if (this.startedStabilization === false) {
this.emit('startStabilizing');
this.startedStabilization = true;
}
var count = 0;
while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) {
this.physicsTick();
count++;
}
if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) {
this.emit('stabilizationProgress', {iterations: this.stabilizationIterations, total: this.targetIterations});
setTimeout(this._stabilizationBatch.bind(this),0);
}
else {
this._finalizeStabilization();
}
}
}
export default PhysicsBase;

+ 272
- 263
lib/network/modules/PhysicsEngine.js View File

@ -1,43 +1,26 @@
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';
import PhysicsBase from './PhysicsBase';
import PhysicsWorker from 'worker!./PhysicsWorkerWrapper';
var util = require('../../util'); var util = require('../../util');
class PhysicsEngine {
class PhysicsEngine extends PhysicsBase {
constructor(body) { constructor(body) {
super();
this.body = body; this.body = body;
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
this.physicsEnabled = true; this.physicsEnabled = true;
this.simulationInterval = 1000 / 60; this.simulationInterval = 1000 / 60;
this.requiresTimeout = true; this.requiresTimeout = true;
this.previousStates = {};
this.referenceState = {};
this.freezeCache = {}; this.freezeCache = {};
this.renderTimer = undefined; this.renderTimer = undefined;
// parameters for the adaptive timestep
this.adaptiveTimestep = false;
this.adaptiveTimestepEnabled = false;
this.adaptiveCounter = 0;
this.adaptiveInterval = 3;
this.stabilized = false;
this.startedStabilization = false;
this.stabilizationIterations = 0;
this.ready = false; // will be set to true if the stabilize this.ready = false; // will be set to true if the stabilize
// default options // default 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,
@ -85,8 +68,11 @@ class PhysicsEngine {
adaptiveTimestep: true adaptiveTimestep: true
}; };
util.extend(this.options, this.defaultOptions); util.extend(this.options, this.defaultOptions);
this.timestep = 0.5;
this.layoutFailed = false; this.layoutFailed = false;
this.draggingNodes = [];
this.positionUpdateHandler = () => {};
this.physicsUpdateHandler = () => {};
this.emit = this.body.emitter.emit;
this.bindEventListeners(); this.bindEventListeners();
} }
@ -112,6 +98,20 @@ class PhysicsEngine {
this.stopSimulation(false); this.stopSimulation(false);
this.body.emitter.off(); this.body.emitter.off();
}); });
this.body.emitter.on('_positionUpdate', (properties) => this.positionUpdateHandler(properties));
this.body.emitter.on('_physicsUpdate', (properties) => this.physicsUpdateHandler(properties));
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,43 +144,153 @@ 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() {
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;
initEmbeddedPhysics() {
this.positionUpdateHandler = () => {};
this.physicsUpdateHandler = (properties) => {
if (properties.options.physics !== undefined) {
// we've received a node that has changed physics state
// so rebuild physicsBody
this.initPhysicsData();
}
// else we're accessing the information directly out of the node
// so no need to do anything.
};
if (this.physicsWorker) {
this.options.useWorker = false;
this.physicsWorker.terminate();
this.physicsWorker = undefined;
this.initPhysicsData();
}
this.initPhysicsSolvers();
}
initPhysicsWorker() {
if (!this.physicsWorker) {
// setup path to webworker javascript file
if (!__webpack_public_path__) {
// search for element with id of 'visjs'
let parentScript = document.getElementById('visjs');
if (parentScript) {
let src = parentScript.getAttribute('src')
__webpack_public_path__ = src.substr(0, src.lastIndexOf('/') + 1);
} else {
// search all scripts for 'vis.js'
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;
}
}
}
}
}
// launch webworker
this.physicsWorker = new PhysicsWorker();
this.physicsWorker.addEventListener('message', (event) => {
this.physicsWorkerMessageHandler(event);
});
this.physicsWorker.onerror = (event) => {
console.error('Falling back to embedded physics engine', event);
this.initEmbeddedPhysics();
// throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
};
this.positionUpdateHandler = (positions) => {
this.physicsWorker.postMessage({type: 'updatePositions', data: positions});
};
this.physicsUpdateHandler = (properties) => {
this._physicsUpdateHandler(properties);
};
}
} }
_physicsUpdateHandler(properties) {
if (properties.options.physics !== undefined) {
if (properties.options.physics) {
let data = {
nodes: {},
edges: {}
};
if (properties.type === 'node') {
data.nodes[properties.id] = this.createPhysicsNode(properties.id);
} else if (properties.type === 'edge') {
data.edges[properties.id] = this.createPhysicsEdge(properties.id);
} else {
console.warn('invalid element type');
}
this.physicsWorker.postMessage({
type: 'addElements',
data: data
});
} else {
let data = {
nodeIds: [],
edgeIds: []
};
if (properties.type === 'node') {
data.nodeIds = [properties.id.toString()];
} else if (properties.type === 'edge') {
data.edgeIds = [properties.id.toString()];
} else {
console.warn('invalid element type');
}
this.physicsWorker.postMessage({type: 'removeElements', data: data});
}
} else {
this.physicsWorker.postMessage({type: 'updateProperties', data: properties});
}
}
physicsWorkerMessageHandler(event) {
var msg = event.data;
switch (msg.type) {
case 'tickResults':
this.stabilized = msg.data.stabilized;
this.stabilizationIterations = msg.data.stabilizationIterations;
this._receivedPositions(msg.data.positions);
break;
case 'finalizeStabilization':
this._finalizeStabilization();
break;
case 'emit':
this.emit(msg.data.event, msg.data.data);
break;
default:
console.warn('unhandled physics worker message:', msg);
}
}
_receivedPositions(positions) {
for (let i = 0; i < this.draggingNodes.length; 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.setX(positions[nodeId].x);
node.setY(positions[nodeId].y);
}
}
}
/** /**
* initialize the engine * initialize the engine
@ -209,6 +319,7 @@ class PhysicsEngine {
startSimulation() { startSimulation() {
if (this.physicsEnabled === true && this.options.enabled === true) { if (this.physicsEnabled === true && this.options.enabled === true) {
this.stabilized = false; this.stabilized = false;
this._updateWorkerStabilized();
// when visible, adaptivity is disabled. // when visible, adaptivity is disabled.
this.adaptiveTimestep = false; this.adaptiveTimestep = false;
@ -232,6 +343,8 @@ class PhysicsEngine {
*/ */
stopSimulation(emit = true) { stopSimulation(emit = true) {
this.stabilized = true; this.stabilized = true;
this._updateWorkerStabilized();
if (emit === true) { if (emit === true) {
this._emitStabilized(); this._emitStabilized();
} }
@ -244,23 +357,35 @@ class PhysicsEngine {
} }
} }
_updateWorkerStabilized() {
if (this.physicsWorker) {
this.physicsWorker.postMessage({
type: 'setStabilized',
data: this.stabilized
});
}
}
/** /**
* The viewFunction inserts this step into each renderloop. It calls the physics tick and handles the cleanup at stabilized. * The viewFunction inserts this step into each renderloop. It calls the physics tick and handles the cleanup at stabilized.
* *
*/ */
simulationStep() { simulationStep() {
// check if the physics have settled
var startTime = Date.now();
this.physicsTick();
var physicsTime = Date.now() - startTime;
// run double speed if it is a little graph
if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed === true) && this.stabilized === false) {
if (this.physicsWorker) {
this.physicsWorker.postMessage({type: 'physicsTick'});
} else {
// check if the physics have settled
var startTime = Date.now();
this.physicsTick(); this.physicsTick();
var physicsTime = Date.now() - startTime;
// run double speed if it is a little graph
if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed === true) && this.stabilized === false) {
this.physicsTick();
// this makes sure there is no jitter. The decision is taken once to run it at double speed.
this.runDoubleSpeed = true;
// this makes sure there is no jitter. The decision is taken once to run it at double speed.
this.runDoubleSpeed = true;
}
} }
if (this.stabilized === true) { if (this.stabilized === true) {
@ -268,6 +393,13 @@ class PhysicsEngine {
} }
} }
_sendWorkerStabilized() {
if (this.physicsWorker) {
this.physicsWorker.postMessage({
type: 'stabilized'
});
}
}
/** /**
* trigger the stabilized event. * trigger the stabilized event.
@ -279,86 +411,55 @@ class PhysicsEngine {
this.body.emitter.emit('stabilized', {iterations: amountOfIterations}); this.body.emitter.emit('stabilized', {iterations: amountOfIterations});
this.startedStabilization = false; this.startedStabilization = false;
this.stabilizationIterations = 0; this.stabilizationIterations = 0;
this._sendWorkerStabilized();
}, 0); }, 0);
} }
} }
/**
* A single simulation step (or 'tick') in the physics simulation
*
* @private
*/
physicsTick() {
// this is here to ensure that there is no start event when the network is already stable.
if (this.startedStabilization === false) {
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
}
if (this.stabilized === false) {
// adaptivity means the timestep adapts to the situation, only applicable for stabilization
if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) {
// this is the factor for increasing the timestep on success.
let factor = 1.2;
// we assume the adaptive interval is
if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations.
// first the big step and revert. Revert saves the reference state.
this.timestep = 2 * this.timestep;
this.calculateForces();
this.moveNodes();
this.revert();
// now the normal step. Since this is the last step, it is the more stable one and we will take this.
this.timestep = 0.5 * this.timestep;
// since it's half the step, we do it twice.
this.calculateForces();
this.moveNodes();
this.calculateForces();
this.moveNodes();
// we compare the two steps. if it is acceptable we double the step.
if (this._evaluateStepQuality() === true) {
this.timestep = factor * this.timestep;
}
else {
// if not, we decrease the step to a minimum of the options timestep.
// if the decreased timestep is smaller than the options step, we do not reset the counter
// we assume that the options timestep is stable enough.
if (this.timestep/factor < this.options.timestep) {
this.timestep = this.options.timestep;
}
else {
// if the timestep was larger than 2 times the option one we check the adaptivity again to ensure
// that large instabilities do not form.
this.adaptiveCounter = -1; // check again next iteration
this.timestep = Math.max(this.options.timestep, this.timestep/factor);
}
}
}
else {
// normal step, keeping timestep constant
this.calculateForces();
this.moveNodes();
createPhysicsNode(nodeId) {
let node = this.body.nodes[nodeId];
if (node) {
return {
id: node.id.toString(),
x: node.x,
y: node.y,
// TODO update on change
edges: {
length: node.edges.length
},
options: {
fixed: {
x: node.options.fixed.x,
y: node.options.fixed.y
},
mass: node.options.mass
} }
// increment the counter
this.adaptiveCounter += 1;
}
else {
// case for the static timestep, we reset it to the one in options and take a normal step.
this.timestep = this.options.timestep;
this.calculateForces();
this.moveNodes();
} }
}
}
// determine if the network has stabilzied
if (this.stabilized === true) {
this.revert();
createPhysicsEdge(edgeId) {
let edge = this.body.edges[edgeId];
if (edge && edge.options.physics === true) {
let physicsEdge = {
id: edge.id,
connected: edge.connected,
edgeType: {},
toId: edge.toId,
fromId: edge.fromId,
options: {
length: edge.length
}
};
// TODO test/implment dynamic
if (edge.edgeType.via) {
physicsEdge.edgeType = {
via: {
id: edge.edgeType.via.id
}
}
} }
this.stabilizationIterations++;
return physicsEdge;
} }
} }
@ -367,18 +468,25 @@ class PhysicsEngine {
* *
* @private * @private
*/ */
updatePhysicsData() {
initPhysicsData() {
let nodes = this.body.nodes;
let edges = this.body.edges;
this.physicsBody.forces = {}; this.physicsBody.forces = {};
this.physicsBody.physicsNodeIndices = []; this.physicsBody.physicsNodeIndices = [];
this.physicsBody.physicsEdgeIndices = []; this.physicsBody.physicsEdgeIndices = [];
let nodes = this.body.nodes;
let edges = this.body.edges;
let physicsWorkerNodes = {};
let physicsWorkerEdges = {};
// get node indices for physics // get node indices for physics
for (let nodeId in nodes) { for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) { if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].options.physics === true) { if (nodes[nodeId].options.physics === true) {
this.physicsBody.physicsNodeIndices.push(nodeId); this.physicsBody.physicsNodeIndices.push(nodeId);
if (this.physicsWorker) {
physicsWorkerNodes[nodeId] = this.createPhysicsNode(nodeId);
}
} }
} }
} }
@ -388,6 +496,9 @@ class PhysicsEngine {
if (edges.hasOwnProperty(edgeId)) { if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].options.physics === true) { if (edges[edgeId].options.physics === true) {
this.physicsBody.physicsEdgeIndices.push(edgeId); this.physicsBody.physicsEdgeIndices.push(edgeId);
if (this.physicsWorker) {
physicsWorkerEdges[edgeId] = this.createPhysicsEdge(edgeId);
}
} }
} }
} }
@ -395,11 +506,11 @@ class PhysicsEngine {
// get the velocity and the forces vector // get the velocity and the forces vector
for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
let nodeId = this.physicsBody.physicsNodeIndices[i]; let nodeId = this.physicsBody.physicsNodeIndices[i];
this.physicsBody.forces[nodeId] = {x:0,y:0};
this.physicsBody.forces[nodeId] = {x: 0, y: 0};
// forces can be reset because they are recalculated. Velocities have to persist. // forces can be reset because they are recalculated. Velocities have to persist.
if (this.physicsBody.velocities[nodeId] === undefined) { if (this.physicsBody.velocities[nodeId] === undefined) {
this.physicsBody.velocities[nodeId] = {x:0,y:0};
this.physicsBody.velocities[nodeId] = {x: 0, y: 0};
} }
} }
@ -409,88 +520,18 @@ class PhysicsEngine {
delete this.physicsBody.velocities[nodeId]; delete this.physicsBody.velocities[nodeId];
} }
} }
}
/**
* Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized.
*/
revert() {
var nodeIds = Object.keys(this.previousStates);
var nodes = this.body.nodes;
var velocities = this.physicsBody.velocities;
this.referenceState = {};
for (let i = 0; i < nodeIds.length; i++) {
let nodeId = nodeIds[i];
if (nodes[nodeId] !== undefined) {
if (nodes[nodeId].options.physics === true) {
this.referenceState[nodeId] = {
positions: {x:nodes[nodeId].x, y:nodes[nodeId].y}
};
velocities[nodeId].x = this.previousStates[nodeId].vx;
velocities[nodeId].y = this.previousStates[nodeId].vy;
nodes[nodeId].x = this.previousStates[nodeId].x;
nodes[nodeId].y = this.previousStates[nodeId].y;
if (this.physicsWorker) {
this.physicsWorker.postMessage({
type: 'initPhysicsData',
data: {
nodes: physicsWorkerNodes,
edges: physicsWorkerEdges
} }
}
else {
delete this.previousStates[nodeId];
}
});
} }
} }
/**
* This compares the reference state to the current state
*/
_evaluateStepQuality() {
let dx, dy, dpos;
let nodes = this.body.nodes;
let reference = this.referenceState;
let posThreshold = 0.3;
for (let nodeId in this.referenceState) {
if (this.referenceState.hasOwnProperty(nodeId) && nodes[nodeId] !== undefined) {
dx = nodes[nodeId].x - reference[nodeId].positions.x;
dy = nodes[nodeId].y - reference[nodeId].positions.y;
dpos = Math.sqrt(Math.pow(dx,2) + Math.pow(dy,2))
if (dpos > posThreshold) {
return false;
}
}
}
return true;
}
/**
* 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;
var averageNodeVelocity = 0;
// the velocity threshold (energy in the system) for the adaptivity toggle
var velocityAdaptiveThreshold = 5;
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);
averageNodeVelocity += nodeVelocity;
}
// evaluating the stabilized and adaptiveTimestepEnabled conditions
this.adaptiveTimestepEnabled = (averageNodeVelocity/nodeIndices.length) < velocityAdaptiveThreshold;
this.stabilized = maxNodeVelocity < this.options.minVelocity;
}
/** /**
* Perform the actual step * Perform the actual step
* *
@ -513,7 +554,7 @@ class PhysicsEngine {
let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
velocities[nodeId].x += ax * timestep; // velocity velocities[nodeId].x += ax * timestep; // velocity
velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x; 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
node.setX(node.x + velocities[nodeId].x * timestep); // position
} }
else { else {
forces[nodeId].x = 0; forces[nodeId].x = 0;
@ -525,7 +566,7 @@ class PhysicsEngine {
let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
velocities[nodeId].y += ay * timestep; // velocity velocities[nodeId].y += ay * timestep; // velocity
velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y; 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
node.setY(node.y + velocities[nodeId].y * timestep); // position
} }
else { else {
forces[nodeId].y = 0; forces[nodeId].y = 0;
@ -536,18 +577,7 @@ class PhysicsEngine {
return totalVelocity; return totalVelocity;
} }
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
// TODO probably want to move freeze/restore to PhysicsBase and do in worker if running
/** /**
* When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
* because only the supportnodes for the smoothCurves have to settle. * because only the supportnodes for the smoothCurves have to settle.
@ -560,8 +590,7 @@ class PhysicsEngine {
if (nodes.hasOwnProperty(id)) { if (nodes.hasOwnProperty(id)) {
if (nodes[id].x && nodes[id].y) { if (nodes[id].x && nodes[id].y) {
this.freezeCache[id] = {x:nodes[id].options.fixed.x,y:nodes[id].options.fixed.y}; this.freezeCache[id] = {x:nodes[id].options.fixed.x,y:nodes[id].options.fixed.y};
nodes[id].options.fixed.x = true;
nodes[id].options.fixed.y = true;
nodes[id].setFixed(true);
} }
} }
} }
@ -577,8 +606,7 @@ class PhysicsEngine {
for (var id in nodes) { for (var id in nodes) {
if (nodes.hasOwnProperty(id)) { if (nodes.hasOwnProperty(id)) {
if (this.freezeCache[id] !== undefined) { if (this.freezeCache[id] !== undefined) {
nodes[id].options.fixed.x = this.freezeCache[id].x;
nodes[id].options.fixed.y = this.freezeCache[id].y;
nodes[id].setFixed({x: this.freezeCache[id].x, y: this.freezeCache[id].y});
} }
} }
} }
@ -609,7 +637,7 @@ class PhysicsEngine {
// stop the render loop // stop the render loop
this.stopSimulation(); this.stopSimulation();
// set stabilze to false
// set stabilize to false
this.stabilized = false; this.stabilized = false;
// block redraw requests // block redraw requests
@ -621,38 +649,19 @@ class PhysicsEngine {
this._freezeNodes(); this._freezeNodes();
} }
this.stabilizationIterations = 0; this.stabilizationIterations = 0;
setTimeout(() => this._stabilizationBatch(),0);
}
/**
* One batch of stabilization
* @private
*/
_stabilizationBatch() {
// this is here to ensure that there is at least one start event.
if (this.startedStabilization === false) {
this.body.emitter.emit('startStabilizing');
this.startedStabilization = true;
}
var count = 0;
while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) {
this.physicsTick();
count++;
}
if (this.stabilized === false && this.stabilizationIterations < this.targetIterations) {
this.body.emitter.emit('stabilizationProgress', {iterations: this.stabilizationIterations, total: this.targetIterations});
setTimeout(this._stabilizationBatch.bind(this),0);
}
else {
this._finalizeStabilization();
if (this.physicsWorker) {
this.physicsWorker.postMessage({
type: 'stabilize',
data: {
targetIterations: iterations
}
});
} else {
setTimeout(() => this._stabilizationBatch(), 0);
} }
} }
/** /**
* Wrap up the stabilization, fit and emit the events. * Wrap up the stabilization, fit and emit the events.
* @private * @private
@ -679,7 +688,7 @@ class PhysicsEngine {
this.ready = true; this.ready = true;
} }
} }
export default PhysicsEngine; export default PhysicsEngine;

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

@ -0,0 +1,268 @@
import PhysicsBase from './PhysicsBase';
class PhysicsWorker extends PhysicsBase {
constructor(postMessage) {
super();
this.body = {
nodes: {},
edges: {}
};
this.postMessage = postMessage;
this.previousStates = {};
this.toRemove = {
nodeIds: [],
edgeIds: []
};
this.isWorker = true;
this.emit = (event, data) => {this.postMessage({type: 'emit', data: {event: event, data: data}})};
}
handleMessage(event) {
var msg = event.data;
switch (msg.type) {
case 'physicsTick':
this.processRemovals();
this.physicsTick();
this.sendTickResults();
break;
case 'updatePositions':
this.receivePositions(msg.data);
break;
case 'updateProperties':
this.updateProperties(msg.data);
break;
case 'addElements':
this.addElements(msg.data);
break;
case 'removeElements':
this.removeElements(msg.data);
break;
case 'stabilize':
this.stabilize(msg.data);
break;
case 'setStabilized':
this.stabilized = msg.data;
break;
case 'stabilized':
this.startedStabilization = false;
this.stabilizationIterations = 0;
break;
case 'initPhysicsData':
console.debug('init physics data');
this.initPhysicsData(msg.data);
break;
case 'options':
this.options = msg.data;
this.timestep = this.options.timestep;
this.initPhysicsSolvers();
break;
default:
console.warn('unknown message from PhysicsEngine', msg);
}
}
sendTickResults() {
let nodeIndices = this.physicsBody.physicsNodeIndices;
let positions = {};
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
let node = this.body.nodes[nodeId];
positions[nodeId] = {x:node.x, y:node.y};
}
this.postMessage({
type: 'tickResults',
data: {
positions: positions,
stabilized: this.stabilized,
stabilizationIterations: this.stabilizationIterations
}
});
}
receivePositions(data) {
let updatedNode = this.body.nodes[data.id];
if (updatedNode) {
updatedNode.x = data.x;
updatedNode.y = data.y;
this.physicsBody.forces[updatedNode.id] = {x: 0, y: 0};
this.physicsBody.velocities[updatedNode.id] = {x: 0, y: 0};
}
}
stabilize(data) {
this.stabilized = false;
this.targetIterations = data.targetIterations;
this.stabilizationIterations = 0;
setTimeout(() => this._stabilizationBatch(), 0);
}
updateProperties(data) {
if (data.type === 'node') {
let optionsNode = this.body.nodes[data.id];
if (optionsNode) {
let opts = data.options;
if (opts.fixed) {
if (opts.fixed.x !== undefined) {
optionsNode.options.fixed.x = opts.fixed.x;
}
if (opts.fixed.y !== undefined) {
optionsNode.options.fixed.y = opts.fixed.y;
}
}
if (opts.mass !== undefined) {
optionsNode.options.mass = opts.mass;
}
if (opts.edges && opts.edges.length) {
optionsNode.edges.length = opts.edges.length;
}
} else {
console.warn('sending properties to unknown node', data.id, data.options);
}
} else if (data.type === 'edge') {
let edge = this.body.edges[data.id];
if (edge) {
let opts = data.options;
if (opts.connected) {
edge.connected = opts.connected;
}
} else {
console.warn('sending properties to unknown edge', data.id, data.options);
}
} else {
console.warn('sending properties to unknown element', data.id, data.options);
}
}
addElements(data, replaceElements = true) {
let nodeIds = Object.keys(data.nodes);
for (let i = 0; i < nodeIds.length; i++) {
let nodeId = nodeIds[i];
let newNode = data.nodes[nodeId];
if (replaceElements) {
this.body.nodes[nodeId] = newNode;
}
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};
}
if (this.physicsBody.physicsNodeIndices.indexOf(nodeId) === -1) {
this.physicsBody.physicsNodeIndices.push(nodeId);
}
}
let edgeIds = Object.keys(data.edges);
for (let i = 0; i < edgeIds.length; i++) {
let edgeId = edgeIds[i];
if (replaceElements) {
this.body.edges[edgeId] = data.edges[edgeId];
}
if (this.physicsBody.physicsEdgeIndices.indexOf(edgeId) === -1) {
this.physicsBody.physicsEdgeIndices.push(edgeId);
}
}
}
removeElements(data) {
// schedule removal of elements on the next physicsTick
// avoids having to defensively check every node read in each physics implementation
this.toRemove.nodeIds.push.apply(this.toRemove.nodeIds, data.nodeIds);
this.toRemove.edgeIds.push.apply(this.toRemove.edgeIds, data.edgeIds);
// Handle case where physics is disabled.
if (!this.options.enabled) {
this.processRemovals();
}
}
processRemovals() {
while (this.toRemove.nodeIds.length > 0) {
let nodeId = this.toRemove.nodeIds.pop();
let index = this.physicsBody.physicsNodeIndices.indexOf(nodeId);
if (index > -1) {
this.physicsBody.physicsNodeIndices.splice(index,1);
}
delete this.physicsBody.forces[nodeId];
delete this.physicsBody.velocities[nodeId];
delete this.body.nodes[nodeId];
}
while (this.toRemove.edgeIds.length > 0) {
let edgeId = this.toRemove.edgeIds.pop();
let index = this.physicsBody.physicsEdgeIndices.indexOf(edgeId);
if (index > -1) {
this.physicsBody.physicsEdgeIndices.splice(index,1);
}
delete this.body.edges[edgeId];
}
}
initPhysicsData(data) {
this.physicsBody.forces = {};
this.physicsBody.physicsNodeIndices = [];
this.physicsBody.physicsEdgeIndices = [];
this.body.nodes = data.nodes;
this.body.edges = data.edges;
this.addElements(data, false);
// clean deleted nodes from the velocity vector
for (let nodeId in this.physicsBody.velocities) {
if (this.body.nodes[nodeId] === undefined) {
delete this.physicsBody.velocities[nodeId];
}
}
}
/**
* 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
}
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
}
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;
}
_finalizeStabilization() {
this.sendTickResults();
this.postMessage({
type: 'finalizeStabilization'
});
}
}
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);

+ 13
- 0
lib/network/modules/components/Edge.js View File

@ -51,7 +51,10 @@ class Edge {
this.labelModule = new Label(this.body, this.options); this.labelModule = new Label(this.body, this.options);
// prevents sending connected messages on initial creation as it should be handled by added element
this.sendPhysicsUpdates = false;
this.setOptions(options); this.setOptions(options);
this.sendPhysicsUpdates = true;
} }
@ -86,10 +89,14 @@ class Edge {
// A node is connected when it has a from and to node that both exist in the network.body.nodes. // A node is connected when it has a from and to node that both exist in the network.body.nodes.
this.connect(); this.connect();
// TODO make changing physics of 1 edge not trigger a complete rebuild of physics processing.
if (options.hidden !== undefined || options.physics !== undefined) { if (options.hidden !== undefined || options.physics !== undefined) {
dataChanged = true; dataChanged = true;
} }
// TODO if edgeType.via.id, toId, fromId, to.id, or from.id changed
// emit _physicsUpdate
return dataChanged; return dataChanged;
} }
@ -252,6 +259,8 @@ class Edge {
* Connect an edge to its nodes * Connect an edge to its nodes
*/ */
connect() { connect() {
let previousConnected = this.connected;
this.disconnect(); this.disconnect();
this.from = this.body.nodes[this.fromId] || undefined; this.from = this.body.nodes[this.fromId] || undefined;
@ -272,6 +281,10 @@ class Edge {
} }
this.edgeType.connect(); this.edgeType.connect();
if (this.sendPhysicsUpdates && this.connected !== previousConnected) {
this.body.emitter.emit('_physicsUpdate', {type: 'edge', id: this.id, options: {connected: this.connected}});
}
} }

+ 83
- 8
lib/network/modules/components/Node.js View File

@ -59,8 +59,8 @@ class Node {
this.grouplist = grouplist; this.grouplist = grouplist;
// state options // state options
this.x = undefined;
this.y = undefined;
this._x = undefined;
this._y = undefined;
this.baseSize = this.options.size; this.baseSize = this.options.size;
this.baseFontSize = this.options.font.size; this.baseFontSize = this.options.font.size;
this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate
@ -68,9 +68,58 @@ class Node {
this.hover = false; this.hover = false;
this.labelModule = new Label(this.body, this.options); this.labelModule = new Label(this.body, this.options);
// prevents sending connected messages on initial creation as it should be handled by added element
this.sendPhysicsUpdates = false;
this.setOptions(options); this.setOptions(options);
this.sendPhysicsUpdates = true;
} }
get x() {
return this._x;
}
set x(newX) {
this._x = newX;
this.body.emitter.emit('_positionUpdate', {id: this.id, x: this._x, y: this._y});
}
/**
* Non emitting version for use by physics engine so we don't create infinite loops.
* @param newX
*/
setX(newX) {
this._x = newX;
}
get y() {
return this._y;
}
set y(newY) {
this._y = newY;
this.body.emitter.emit('_positionUpdate', {id: this.id, x: this._x, y: this._y});
}
/**
* Emitting version
*
* @param newFixed
*/
setFixed(newFixed) {
// TODO split out fixed portion?
let physOpts = Node.parseOptions(this.options, {fixed: newFixed});
if (Object.keys(physOpts).length > 0) {
this.body.emitter.emit('_physicsUpdate', {type: 'node', id: this.id, options: physOpts});
}
}
/**
* Non emitting version for use by physics engine so we don't create infinite loops.
* @param newY
*/
setY(newY) {
this._y = newY;
}
/** /**
* Attach a edge to the node * Attach a edge to the node
@ -136,7 +185,7 @@ class Node {
} }
// this transforms all shorthands into fully defined options // this transforms all shorthands into fully defined options
Node.parseOptions(this.options, options, true, this.globalOptions);
let physOpts = Node.parseOptions(this.options, options, true, this.globalOptions);
// load the images // load the images
if (this.options.image !== undefined) { if (this.options.image !== undefined) {
@ -151,8 +200,21 @@ class Node {
this.updateLabelModule(); this.updateLabelModule();
this.updateShape(currentShape); this.updateShape(currentShape);
if (options.mass !== undefined) {
this.options.mass = options.mass;
physOpts.mass = options.mass;
}
if (options.physics !== undefined) {
this.options.physics = options.physics;
physOpts.physics = options.physics;
}
if (this.sendPhysicsUpdates && Object.keys(physOpts).length > 0) {
this.body.emitter.emit('_physicsUpdate', {type: 'node', id: this.id, options: physOpts});
}
if (options.hidden !== undefined || options.physics !== undefined) {
// TODO make embedded physics trigger this or handle _physicsUpdate messages
if (options.hidden !== undefined) {
return true; return true;
} }
return false; return false;
@ -172,6 +234,7 @@ class Node {
'fixed', 'fixed',
'shadow' 'shadow'
]; ];
var changedPhysicsOptions = {};
util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion); util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion);
// merge the shadow options into the parent. // merge the shadow options into the parent.
@ -189,15 +252,26 @@ class Node {
// handle the fixed options // handle the fixed options
if (newOptions.fixed !== undefined && newOptions.fixed !== null) { if (newOptions.fixed !== undefined && newOptions.fixed !== null) {
if (typeof newOptions.fixed === 'boolean') { if (typeof newOptions.fixed === 'boolean') {
parentOptions.fixed.x = newOptions.fixed;
parentOptions.fixed.y = newOptions.fixed;
if (parentOptions.fixed.x !== newOptions.fixed || parentOptions.fixed.y !== newOptions.fixed) {
parentOptions.fixed.x = newOptions.fixed;
parentOptions.fixed.y = newOptions.fixed;
changedPhysicsOptions.fixed = {x: newOptions.fixed, y: newOptions.fixed};
}
} }
else { else {
if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') {
if (newOptions.fixed.x !== undefined &&
typeof newOptions.fixed.x === 'boolean' &&
parentOptions.fixed.x !== newOptions.fixed.x)
{
parentOptions.fixed.x = newOptions.fixed.x; parentOptions.fixed.x = newOptions.fixed.x;
util.deepExtend(changedPhysicsOptions, {fixed: {x: newOptions.fixed.x}});
} }
if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') {
if (newOptions.fixed.y !== undefined &&
typeof newOptions.fixed.y === 'boolean' &&
parentOptions.fixed.y !== newOptions.fixed.y)
{
parentOptions.fixed.y = newOptions.fixed.y; parentOptions.fixed.y = newOptions.fixed.y;
util.deepExtend(changedPhysicsOptions, {fixed: {y: newOptions.fixed.y}});
} }
} }
} }
@ -214,6 +288,7 @@ class Node {
if (newOptions.scaling !== undefined) { if (newOptions.scaling !== undefined) {
util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', allowDeletion, globalOptions.scaling); util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', allowDeletion, globalOptions.scaling);
} }
return changedPhysicsOptions;
} }
updateLabelModule() { updateLabelModule() {

+ 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.toId];
node2 = nodes[edge.edgeType.via.id];
node3 = nodes[edge.fromId];
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.fromId], nodes[edge.toId], 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 },

+ 3
- 0
package.json View File

@ -36,6 +36,9 @@
"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": {
"babel": "^5.1.11", "babel": "^5.1.11",
"babel-loader": "^5.0.0", "babel-loader": "^5.0.0",
"babelify": "^6.0.2", "babelify": "^6.0.2",

Loading…
Cancel
Save