Browse Source

refactor common code into PhysicsBase

webworkersNetwork^2^2
Eric VanDever 9 years ago
parent
commit
86e5801420
3 changed files with 435 additions and 410 deletions
  1. +255
    -0
      lib/network/modules/PhysicsBase.js
  2. +96
    -305
      lib/network/modules/PhysicsEngine.js
  3. +84
    -105
      lib/network/modules/PhysicsWorker.js

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

@ -0,0 +1,255 @@
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;
}
/**
* 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;

+ 96
- 305
lib/network/modules/PhysicsEngine.js View File

@ -1,26 +1,17 @@
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'; 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;
@ -30,13 +21,9 @@ class PhysicsEngine {
this.adaptiveCounter = 0; this.adaptiveCounter = 0;
this.adaptiveInterval = 3; 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, useWorker: false,
@ -87,11 +74,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.draggingNodes = [];
this.positionUpdateHandler = () => {}; this.positionUpdateHandler = () => {};
this.physicsUpdateHandler = () => {}; this.physicsUpdateHandler = () => {};
this.emit = this.body.emitter.emit;
this.bindEventListeners(); this.bindEventListeners();
} }
@ -185,37 +172,12 @@ class PhysicsEngine {
this.physicsWorker = undefined; this.physicsWorker = undefined;
this.initPhysicsData(); this.initPhysicsData();
} }
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;
this.initPhysicsSolvers();
} }
initPhysicsWorker() { initPhysicsWorker() {
if (!this.physicsWorker) { if (!this.physicsWorker) {
// setup path to webworker javascript file
if (!__webpack_public_path__) { if (!__webpack_public_path__) {
let parentScript = document.getElementById('visjs'); let parentScript = document.getElementById('visjs');
if (parentScript) { if (parentScript) {
@ -236,6 +198,7 @@ class PhysicsEngine {
} }
} }
} }
// launch webworker
this.physicsWorker = new PhysicsWorker(); this.physicsWorker = new PhysicsWorker();
this.physicsWorker.addEventListener('message', (event) => { this.physicsWorker.addEventListener('message', (event) => {
this.physicsWorkerMessageHandler(event); this.physicsWorkerMessageHandler(event);
@ -249,41 +212,45 @@ class PhysicsEngine {
this.physicsWorker.postMessage({type: 'updatePositions', data: positions}); this.physicsWorker.postMessage({type: 'updatePositions', data: positions});
}; };
this.physicsUpdateHandler = (properties) => { this.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});
}
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 { } else {
this.physicsWorker.postMessage({type: 'updateProperties', data: properties});
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});
} }
} }
@ -292,26 +259,36 @@ class PhysicsEngine {
switch (msg.type) { switch (msg.type) {
case 'positions': case 'positions':
this.stabilized = msg.data.stabilized; this.stabilized = msg.data.stabilized;
var positions = msg.data.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);
}
}
this._receivedPositions(msg.data.positions);
break;
case 'finalizeStabilization':
this.stabilizationIterations = msg.data.stabilizationIterations;
this._finalizeStabilization();
break;
case 'emit':
this.emit(msg.data.event, msg.data.data);
break; break;
default: default:
console.warn('unhandled physics worker message:', msg); 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
*/ */
@ -380,17 +357,21 @@ class PhysicsEngine {
* *
*/ */
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) {
@ -398,7 +379,7 @@ class PhysicsEngine {
} }
} }
// TODO determine when startedStabilization needs to be propogated from the worker
/** /**
* trigger the stabilized event. * trigger the stabilized event.
* @private * @private
@ -413,90 +394,6 @@ class PhysicsEngine {
} }
} }
/**
* 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();
}
// 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;
if (this.physicsWorker) {
// console.log('asking working to do a physics iteration');
this.physicsWorker.postMessage({type: 'physicsTick'});
} else {
this.calculateForces();
this.moveNodes();
}
}
// determine if the network has stabilzied
if (this.stabilized === true) {
this.revert();
}
this.stabilizationIterations++;
}
}
createPhysicsNode(nodeId) { createPhysicsNode(nodeId) {
let node = this.body.nodes[nodeId]; let node = this.body.nodes[nodeId];
if (node) { if (node) {
@ -504,6 +401,10 @@ class PhysicsEngine {
id: node.id.toString(), id: node.id.toString(),
x: node.x, x: node.x,
y: node.y, y: node.y,
// TODO update on change
edges: {
length: node.edges.length
},
options: { options: {
fixed: { fixed: {
x: node.options.fixed.x, x: node.options.fixed.x,
@ -609,85 +510,6 @@ class PhysicsEngine {
} }
} }
/**
* 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;
}
}
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
* *
@ -733,18 +555,6 @@ class PhysicsEngine {
return totalVelocity; return totalVelocity;
} }
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
/** /**
* 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.
@ -804,7 +614,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
@ -817,37 +627,18 @@ class PhysicsEngine {
} }
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: 'stabilization',
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

+ 84
- 105
lib/network/modules/PhysicsWorker.js View File

@ -1,29 +1,21 @@
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';
class PhysicsWorker {
class PhysicsWorker extends PhysicsBase {
constructor(postMessage) { constructor(postMessage) {
super();
this.body = { this.body = {
nodes: {}, nodes: {},
edges: {} edges: {}
}; };
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
this.postMessage = postMessage; this.postMessage = postMessage;
this.options = {};
this.stabilized = false;
this.previousStates = {}; this.previousStates = {};
this.positions = {};
this.timestep = 0.5;
this.toRemove = { this.toRemove = {
nodeIds: [], nodeIds: [],
edgeIds: [] edgeIds: []
}; };
this.physicsTimeout = null;
this.isWorker = true;
this.emit = (event, data) => {this.postMessage({type: 'emit', data: {event: event, data: data}})};
} }
handleMessage(event) { handleMessage(event) {
@ -31,15 +23,10 @@ class PhysicsWorker {
switch (msg.type) { switch (msg.type) {
case 'physicsTick': case 'physicsTick':
this.physicsTick(); this.physicsTick();
this.sendPositions();
break; break;
case 'updatePositions': case 'updatePositions':
let updatedNode = this.body.nodes[msg.data.id];
if (updatedNode) {
updatedNode.x = msg.data.x;
updatedNode.y = msg.data.y;
this.physicsBody.forces[updatedNode.id] = {x: 0, y: 0};
this.physicsBody.velocities[updatedNode.id] = {x: 0, y: 0};
}
this.receivePositions(msg.data);
break; break;
case 'updateProperties': case 'updateProperties':
this.updateProperties(msg.data); this.updateProperties(msg.data);
@ -48,73 +35,78 @@ class PhysicsWorker {
this.addElements(msg.data); this.addElements(msg.data);
break; break;
case 'removeElements': case 'removeElements':
// 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, msg.data.nodeIds);
this.toRemove.edgeIds.push.apply(this.toRemove.edgeIds, msg.data.edgeIds);
this.removeElements(msg.data);
break;
case 'stabilization':
this.stabilize(msg.data);
break; break;
case 'initPhysicsData': case 'initPhysicsData':
console.debug('init physics data');
this.initPhysicsData(msg.data); this.initPhysicsData(msg.data);
break; break;
case 'options': case 'options':
this.options = msg.data; this.options = msg.data;
this.timestep = this.options.timestep; this.timestep = this.options.timestep;
this.init();
this.initPhysicsSolvers();
break; break;
default: default:
console.warn('unknown message from PhysicsEngine', msg); 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;
}
// physicsTick() {
// if(this.physicsTimeout) {
// // cancel any outstanding requests to prevent manipulation of data while iterating
// // and we're going to handle it next anyways.
// clearTimeout(this.physicsTimeout);
// }
// this.processRemovals();
// if (this.options.enabled) {
// this.calculateForces();
// this.moveNodes();
// // Handle the case where physics was enabled, data was removed
// // but this physics tick was longer than the timeout and during that delta
// // physics was disabled.
// this.processRemovals();
// }
// this.stabilizationIterations++;
// }
physicsTick() {
this.processRemovals();
this.calculateForces();
this.moveNodes();
for (let i = 0; i < this.toRemove.nodeIds.length; i++) {
delete this.positions[this.toRemove.nodeIds[i]];
sendPositions() {
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({ this.postMessage({
type: 'positions', type: 'positions',
data: { data: {
positions: this.positions,
positions: positions,
stabilized: this.stabilized stabilized: this.stabilized
} }
}); });
} }
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) { updateProperties(data) {
if (data.type === 'node') { if (data.type === 'node') {
let optionsNode = this.body.nodes[data.id]; let optionsNode = this.body.nodes[data.id];
@ -157,10 +149,6 @@ class PhysicsWorker {
if (replaceElements) { if (replaceElements) {
this.body.nodes[nodeId] = newNode; this.body.nodes[nodeId] = newNode;
} }
this.positions[nodeId] = {
x: newNode.x,
y: newNode.y
};
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) {
@ -182,6 +170,25 @@ class PhysicsWorker {
} }
} }
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.physicsTimeout) {
// don't schedule more than one physicsTick
clearTimeout(this.physicsTimeout);
}
this.physicsTimeout = setTimeout(()=> {
// if physics is still enabled, the next tick will handle removeElements
if (!this.options.enabled) {
this.physicsTimeout = null;
this.physicsTick();
}
}, 250);
}
processRemovals() { processRemovals() {
while (this.toRemove.nodeIds.length > 0) { while (this.toRemove.nodeIds.length > 0) {
let nodeId = this.toRemove.nodeIds.pop(); let nodeId = this.toRemove.nodeIds.pop();
@ -191,7 +198,6 @@ class PhysicsWorker {
} }
delete this.physicsBody.forces[nodeId]; delete this.physicsBody.forces[nodeId];
delete this.physicsBody.velocities[nodeId]; delete this.physicsBody.velocities[nodeId];
delete this.positions[nodeId];
delete this.body.nodes[nodeId]; delete this.body.nodes[nodeId];
} }
while (this.toRemove.edgeIds.length > 0) { while (this.toRemove.edgeIds.length > 0) {
@ -204,16 +210,10 @@ class PhysicsWorker {
} }
} }
/**
* 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
*/
initPhysicsData(data) { initPhysicsData(data) {
this.physicsBody.forces = {}; this.physicsBody.forces = {};
this.physicsBody.physicsNodeIndices = []; this.physicsBody.physicsNodeIndices = [];
this.physicsBody.physicsEdgeIndices = []; this.physicsBody.physicsEdgeIndices = [];
this.positions = {};
this.body.nodes = data.nodes; this.body.nodes = data.nodes;
this.body.edges = data.edges; this.body.edges = data.edges;
@ -227,26 +227,6 @@ class PhysicsWorker {
} }
} }
/**
* 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 * Perform the actual step
* *
@ -275,7 +255,6 @@ class PhysicsWorker {
forces[nodeId].x = 0; forces[nodeId].x = 0;
velocities[nodeId].x = 0; velocities[nodeId].x = 0;
} }
this.positions[nodeId].x = node.x;
if (node.options.fixed.y === false) { if (node.options.fixed.y === false) {
let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
@ -288,19 +267,19 @@ class PhysicsWorker {
forces[nodeId].y = 0; forces[nodeId].y = 0;
velocities[nodeId].y = 0; velocities[nodeId].y = 0;
} }
this.positions[nodeId].y = node.y;
let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2)); let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
return totalVelocity; return totalVelocity;
} }
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
_finalizeStabilization() {
this.sendPositions();
this.postMessage({
type: 'finalizeStabilization',
data: {
stabilizationIterations: this.stabilizationIterations
}
});
} }
} }

Loading…
Cancel
Save