From 86e58014204ce396b8438fd26287bae4ffd36a09 Mon Sep 17 00:00:00 2001 From: Eric VanDever Date: Fri, 23 Oct 2015 15:02:21 -0400 Subject: [PATCH] refactor common code into PhysicsBase --- lib/network/modules/PhysicsBase.js | 255 +++++++++++++++++ lib/network/modules/PhysicsEngine.js | 401 +++++++-------------------- lib/network/modules/PhysicsWorker.js | 189 ++++++------- 3 files changed, 435 insertions(+), 410 deletions(-) create mode 100644 lib/network/modules/PhysicsBase.js diff --git a/lib/network/modules/PhysicsBase.js b/lib/network/modules/PhysicsBase.js new file mode 100644 index 00000000..748997e6 --- /dev/null +++ b/lib/network/modules/PhysicsBase.js @@ -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; \ No newline at end of file diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index cc3fa775..9ddb7b75 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -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'; var util = require('../../util'); -class PhysicsEngine { +class PhysicsEngine extends PhysicsBase { constructor(body) { + super(); this.body = body; - this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}}; this.physicsEnabled = true; this.simulationInterval = 1000 / 60; this.requiresTimeout = true; - this.previousStates = {}; - this.referenceState = {}; this.freezeCache = {}; this.renderTimer = undefined; @@ -30,13 +21,9 @@ class PhysicsEngine { 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 // default options - this.options = {}; this.defaultOptions = { enabled: true, useWorker: false, @@ -87,11 +74,11 @@ class PhysicsEngine { adaptiveTimestep: true }; util.extend(this.options, this.defaultOptions); - this.timestep = 0.5; this.layoutFailed = false; this.draggingNodes = []; this.positionUpdateHandler = () => {}; this.physicsUpdateHandler = () => {}; + this.emit = this.body.emitter.emit; this.bindEventListeners(); } @@ -185,37 +172,12 @@ class PhysicsEngine { this.physicsWorker = undefined; 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() { if (!this.physicsWorker) { + // setup path to webworker javascript file if (!__webpack_public_path__) { let parentScript = document.getElementById('visjs'); if (parentScript) { @@ -236,6 +198,7 @@ class PhysicsEngine { } } } + // launch webworker this.physicsWorker = new PhysicsWorker(); this.physicsWorker.addEventListener('message', (event) => { this.physicsWorkerMessageHandler(event); @@ -249,41 +212,45 @@ class PhysicsEngine { this.physicsWorker.postMessage({type: 'updatePositions', data: positions}); }; 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 { - 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) { case 'positions': 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; 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 */ @@ -380,17 +357,21 @@ class PhysicsEngine { * */ 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(); + 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) { @@ -398,7 +379,7 @@ class PhysicsEngine { } } - + // TODO determine when startedStabilization needs to be propogated from the worker /** * trigger the stabilized event. * @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) { let node = this.body.nodes[nodeId]; if (node) { @@ -504,6 +401,10 @@ class PhysicsEngine { 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, @@ -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 * @@ -733,18 +555,6 @@ class PhysicsEngine { 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 * because only the supportnodes for the smoothCurves have to settle. @@ -804,7 +614,7 @@ class PhysicsEngine { // stop the render loop this.stopSimulation(); - // set stabilze to false + // set stabilize to false this.stabilized = false; // block redraw requests @@ -817,37 +627,18 @@ class PhysicsEngine { } 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. * @private diff --git a/lib/network/modules/PhysicsWorker.js b/lib/network/modules/PhysicsWorker.js index dc8fd72a..d21812ca 100644 --- a/lib/network/modules/PhysicsWorker.js +++ b/lib/network/modules/PhysicsWorker.js @@ -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) { + super(); this.body = { nodes: {}, edges: {} }; - this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}}; this.postMessage = postMessage; - this.options = {}; - this.stabilized = false; this.previousStates = {}; - this.positions = {}; - this.timestep = 0.5; this.toRemove = { nodeIds: [], edgeIds: [] }; + this.physicsTimeout = null; + this.isWorker = true; + this.emit = (event, data) => {this.postMessage({type: 'emit', data: {event: event, data: data}})}; } handleMessage(event) { @@ -31,15 +23,10 @@ class PhysicsWorker { switch (msg.type) { case 'physicsTick': this.physicsTick(); + this.sendPositions(); break; 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; case 'updateProperties': this.updateProperties(msg.data); @@ -48,73 +35,78 @@ class PhysicsWorker { this.addElements(msg.data); break; 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; case 'initPhysicsData': + console.debug('init physics data'); this.initPhysicsData(msg.data); break; case 'options': this.options = msg.data; this.timestep = this.options.timestep; - this.init(); + this.initPhysicsSolvers(); break; default: console.warn('unknown message from PhysicsEngine', msg); } } - /** - * configure the engine. - */ - init() { - var options; - if (this.options.solver === 'forceAtlas2Based') { - options = this.options.forceAtlas2Based; - this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); - this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(this.body, this.physicsBody, options); - } - else if (this.options.solver === 'repulsion') { - options = this.options.repulsion; - this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); - this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); - } - else if (this.options.solver === 'hierarchicalRepulsion') { - options = this.options.hierarchicalRepulsion; - this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); - this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); - this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); - } - else { // barnesHut - options = this.options.barnesHut; - this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); - this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); - } - - this.modelOptions = options; - } +// 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({ type: 'positions', data: { - positions: this.positions, + positions: positions, 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) { if (data.type === 'node') { let optionsNode = this.body.nodes[data.id]; @@ -157,10 +149,6 @@ class PhysicsWorker { if (replaceElements) { this.body.nodes[nodeId] = newNode; } - this.positions[nodeId] = { - x: newNode.x, - y: newNode.y - }; 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) { @@ -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() { while (this.toRemove.nodeIds.length > 0) { let nodeId = this.toRemove.nodeIds.pop(); @@ -191,7 +198,6 @@ class PhysicsWorker { } delete this.physicsBody.forces[nodeId]; delete this.physicsBody.velocities[nodeId]; - delete this.positions[nodeId]; delete this.body.nodes[nodeId]; } 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) { this.physicsBody.forces = {}; this.physicsBody.physicsNodeIndices = []; this.physicsBody.physicsEdgeIndices = []; - this.positions = {}; this.body.nodes = data.nodes; 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 * @@ -275,7 +255,6 @@ class PhysicsWorker { forces[nodeId].x = 0; velocities[nodeId].x = 0; } - this.positions[nodeId].x = node.x; if (node.options.fixed.y === false) { let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force @@ -288,19 +267,19 @@ class PhysicsWorker { forces[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)); 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 + } + }); } }