diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 3b0fc799..8c1c3c59 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -6,8 +6,8 @@ var HierarchicalSpringSolver = require('./components/physics/HierarchicalSpringS var CentralGravitySolver = require('./components/physics/CentralGravitySolver').default; var ForceAtlas2BasedRepulsionSolver = require('./components/physics/FA2BasedRepulsionSolver').default; var ForceAtlas2BasedCentralGravitySolver = require('./components/physics/FA2BasedCentralGravitySolver').default; - var util = require('../../util'); +var EndPoints = require('./components/edges/util/EndPoints').default; // for debugging with _drawForces() /** @@ -121,9 +121,8 @@ class PhysicsEngine { this.stopSimulation(false); this.body.emitter.off(); }); - // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed. this.body.emitter.on("_dataChanged", () => { - // update shortcut lists + // Nodes and/or edges have been added or removed, update shortcut lists. this.updatePhysicsData(); }); @@ -308,85 +307,95 @@ class PhysicsEngine { } } + /** - * A single simulation step (or 'tick') in the physics simulation + * Calculate the forces for one physics iteration and move the nodes. + * @private + */ + physicsStep() { + this.gravitySolver.solve(); + this.nodesSolver.solve(); + this.edgesSolver.solve(); + this.moveNodes(); + } + + + /** + * Make dynamic adjustments to the timestep, based on current state. * + * Helper function for physicsTick(). * @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(); - } + adjustTimeStep() { + const factor = 1.2; // Factor for increasing the timestep on success. - // increment the counter - this.adaptiveCounter += 1; + // 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 { - // 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(); + // 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); } + } + } + + + /** + * A single simulation step (or 'tick') in the physics simulation + * + * @private + */ + physicsTick() { + this._startStabilizing(); // this ensures that there is no start event when the network is already stable. + if (this.stabilized === true) return; + + // adaptivity means the timestep adapts to the situation, only applicable for stabilization + if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) { + // timestep remains stable for "interval" iterations. + let doAdaptive = (this.adaptiveCounter % this.adaptiveInterval === 0); - // determine if the network has stabilzied - if (this.stabilized === true) { - this.revert(); + if (doAdaptive) { + // first the big step and revert. + this.timestep = 2 * this.timestep; + this.physicsStep(); + this.revert(); // saves the reference state + + // 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.physicsStep(); + this.physicsStep(); + + this.adjustTimeStep(); + } + else { + this.physicsStep(); // normal step, keeping timestep constant } - this.stabilizationIterations++; + 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.physicsStep(); + } + + if (this.stabilized === true) this.revert(); + this.stabilizationIterations++; } + /** * 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. * @@ -497,7 +506,6 @@ class PhysicsEngine { */ moveNodes() { var nodeIndices = this.physicsBody.physicsNodeIndices; - var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9; var maxNodeVelocity = 0; var averageNodeVelocity = 0; @@ -506,9 +514,9 @@ class PhysicsEngine { for (let i = 0; i < nodeIndices.length; i++) { let nodeId = nodeIndices[i]; - let nodeVelocity = this._performStep(nodeId, maxVelocity); + let nodeVelocity = this._performStep(nodeId); // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized - maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity); + maxNodeVelocity = Math.max(maxNodeVelocity, nodeVelocity); averageNodeVelocity += nodeVelocity; } @@ -518,66 +526,72 @@ class PhysicsEngine { } + /** + * Calculate new velocity for a coordinate direction + * + * @param {number} v velocity for current coordinate + * @param {number} f regular force for current coordinate + * @param {number} m mass of current node + * @returns {number} new velocity for current coordinate + * @private + */ + calculateComponentVelocity(v,f, m) { + let df = this.modelOptions.damping * v; // damping force + let a = (f - df) / m; // acceleration + + v += a * this.timestep; + + // Put a limit on the velocities if it is really high + let maxV = this.options.maxVelocity || 1e9; + if (Math.abs(v) > maxV) { + v = ((v > 0) ? maxV: -maxV); + } + + return v; + } + + /** * Perform the actual step * * @param {Node.id} nodeId - * @param {number} maxVelocity - * @returns {number} + * @returns {number} the new velocity of given node * @private */ - _performStep(nodeId, maxVelocity) { + _performStep(nodeId) { let node = this.body.nodes[nodeId]; - let timestep = this.timestep; - let forces = this.physicsBody.forces; - let velocities = this.physicsBody.velocities; + let force = this.physicsBody.forces[nodeId]; + let velocity = this.physicsBody.velocities[nodeId]; // store the state so we can revert - this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y}; + this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocity.x, vy:velocity.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 + velocity.x = this.calculateComponentVelocity(velocity.x, force.x, node.options.mass); + node.x += velocity.x * this.timestep; } else { - forces[nodeId].x = 0; - velocities[nodeId].x = 0; + force.x = 0; + velocity.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 + velocity.y = this.calculateComponentVelocity(velocity.y, force.y, node.options.mass); + node.y += velocity.y * this.timestep; } else { - forces[nodeId].y = 0; - velocities[nodeId].y = 0; + force.y = 0; + velocity.y = 0; } - let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2)); + let totalVelocity = Math.sqrt(Math.pow(velocity.x,2) + Math.pow(velocity.y,2)); 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. + * 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. * * @private */ @@ -586,14 +600,16 @@ class PhysicsEngine { for (var id in nodes) { if (nodes.hasOwnProperty(id)) { if (nodes[id].x && nodes[id].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; + let fixed = nodes[id].options.fixed; + this.freezeCache[id] = {x:fixed.x, y:fixed.y}; + fixed.x = true; + fixed.y = true; } } } } + /** * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. * @@ -619,8 +635,8 @@ class PhysicsEngine { */ stabilize(iterations = this.options.stabilization.iterations) { if (typeof iterations !== 'number') { - console.log('The stabilize method needs a numeric amount of iterations. Switching to default: ', this.options.stabilization.iterations); iterations = this.options.stabilization.iterations; + console.log('The stabilize method needs a numeric amount of iterations. Switching to default: ', iterations); } if (this.physicsBody.physicsNodeIndices.length === 0) { @@ -634,10 +650,7 @@ class PhysicsEngine { // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); - // stop the render loop - this.stopSimulation(); - - // set stabilze to false + this.stopSimulation(); // stop the render loop this.stabilized = false; // block redraw requests @@ -654,25 +667,37 @@ class PhysicsEngine { } + /** + * If not already stabilizing, start it and emit a start event. + * + * @returns {boolean} true if stabilization started with this call + * @private + */ + _startStabilizing() { + if (this.startedStabilization === true) return false; + + this.body.emitter.emit('startStabilizing'); + this.startedStabilization = true; + return true; + } + + /** * One batch of stabilization * @private */ _stabilizationBatch() { - var self = this; - var running = () => (self.stabilized === false && self.stabilizationIterations < self.targetIterations); + var running = () => (this.stabilized === false && this.stabilizationIterations < this.targetIterations); + var sendProgress = () => { - self.body.emitter.emit('stabilizationProgress', { - iterations: self.stabilizationIterations, - total: self.targetIterations + this.body.emitter.emit('stabilizationProgress', { + iterations: this.stabilizationIterations, + total: this.targetIterations }); }; - // 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; - sendProgress(); + if (this._startStabilizing()) { + sendProgress(); // Ensure that there is at least one start event. } var count = 0; @@ -720,15 +745,22 @@ class PhysicsEngine { } + //--------------------------- DEBUGGING BELOW ---------------------------// + + /** - * TODO: Is this function used at all? If not, remove it! + * Debug function that display arrows for the forces currently active in the network. + * + * Use this when debugging only. + * * @param {CanvasRenderingContext2D} ctx * @private */ _drawForces(ctx) { for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { - let node = this.body.nodes[this.physicsBody.physicsNodeIndices[i]]; - let force = this.physicsBody.forces[this.physicsBody.physicsNodeIndices[i]]; + let index = this.physicsBody.physicsNodeIndices[i]; + let node = this.body.nodes[index]; + let force = this.physicsBody.forces[index]; let factor = 20; let colorFactor = 0.03; let forceSize = Math.sqrt(Math.pow(force.x,2) + Math.pow(force.x,2)); @@ -738,20 +770,25 @@ class PhysicsEngine { let color = util.HSVToHex((180 - Math.min(1,Math.max(0,colorFactor*forceSize))*180) / 360,1,1); + let point = { + x: node.x + factor*force.x, + y: node.y + factor*force.y + }; + ctx.lineWidth = size; ctx.strokeStyle = color; ctx.beginPath(); ctx.moveTo(node.x,node.y); - ctx.lineTo(node.x+factor*force.x, node.y+factor*force.y); + ctx.lineTo(point.x, point.y); ctx.stroke(); let angle = Math.atan2(force.y, force.x); ctx.fillStyle = color; - ctx.arrowEndpoint(node.x + factor*force.x + Math.cos(angle)*arrowSize, node.y + factor*force.y+Math.sin(angle)*arrowSize, angle, arrowSize); + EndPoints.draw(ctx, {type: 'arrow', point: point, angle: angle, length: arrowSize}); ctx.fill(); + } } - } export default PhysicsEngine; diff --git a/lib/network/modules/components/physics/BarnesHutSolver.js b/lib/network/modules/components/physics/BarnesHutSolver.js index 2f9be287..f1508fcd 100644 --- a/lib/network/modules/components/physics/BarnesHutSolver.js +++ b/lib/network/modules/components/physics/BarnesHutSolver.js @@ -15,7 +15,7 @@ class BarnesHutSolver { this.randomSeed = 5; // debug: show grid - //this.body.emitter.on("afterDrawing", (ctx) => {this._debug(ctx,'#ff0000')}) + // this.body.emitter.on("afterDrawing", (ctx) => {this._debug(ctx,'#ff0000')}) } /** @@ -25,7 +25,9 @@ class BarnesHutSolver { setOptions(options) { this.options = options; this.thetaInversed = 1 / this.options.theta; - this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1,this.options.avoidOverlap)); // if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius + + // if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius + this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1, this.options.avoidOverlap)); } /** @@ -62,16 +64,26 @@ class BarnesHutSolver { node = nodes[nodeIndices[i]]; if (node.options.mass > 0) { // starting with root is irrelevant, it never passes the BarnesHutSolver condition - this._getForceContribution(barnesHutTree.root.children.NW, node); - this._getForceContribution(barnesHutTree.root.children.NE, node); - this._getForceContribution(barnesHutTree.root.children.SW, node); - this._getForceContribution(barnesHutTree.root.children.SE, node); + this._getForceContributions(barnesHutTree.root, node); } } } } + /** + * @param {Object} parentBranch + * @param {Node} node + * @private + */ + _getForceContributions(parentBranch, node) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); + } + + /** * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. * If a region contains a single node, we check if it is not itself, then we apply the force. @@ -99,10 +111,7 @@ class BarnesHutSolver { else { // Did not pass the condition, go into children if available if (parentBranch.childrenCount === 4) { - this._getForceContribution(parentBranch.children.NW, node); - this._getForceContribution(parentBranch.children.NE, node); - this._getForceContribution(parentBranch.children.SW, node); - this._getForceContribution(parentBranch.children.SE, node); + this._getForceContributions(parentBranch, node); } else { // parentBranch must have only one node, if it was empty we wouldnt be here if (parentBranch.children.data.id != node.id) { // if it is not self @@ -164,9 +173,10 @@ class BarnesHutSolver { // get the range of the nodes for (let i = 1; i < nodeCount; i++) { - let x = nodes[nodeIndices[i]].x; - let y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { + let node = nodes[nodeIndices[i]]; + let x = node.x; + let y = node.y; + if (node.options.mass > 0) { if (x < minX) { minX = x; } @@ -238,14 +248,15 @@ class BarnesHutSolver { * @private */ _updateBranchMass(parentBranch, node) { + let centerOfMass = parentBranch.centerOfMass; let totalMass = parentBranch.mass + node.options.mass; let totalMassInv = 1 / totalMass; - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; + centerOfMass.x = centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + centerOfMass.x *= totalMassInv; - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; + centerOfMass.y = centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + centerOfMass.y *= totalMassInv; parentBranch.mass = totalMass; let biggestSize = Math.max(Math.max(node.height, node.radius), node.width); @@ -268,22 +279,26 @@ class BarnesHutSolver { this._updateBranchMass(parentBranch, node); } - if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { // in NW - this._placeInRegion(parentBranch, node, "NW"); + let range = parentBranch.children.NW.range; + let region; + if (range.maxX > node.x) { // in NW or SW + if (range.maxY > node.y) { + region = "NW"; } - else { // in SW - this._placeInRegion(parentBranch, node, "SW"); + else { + region = "SW"; } } else { // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { // in NE - this._placeInRegion(parentBranch, node, "NE"); + if (range.maxY > node.y) { + region = "NE"; } - else { // in SE - this._placeInRegion(parentBranch, node, "SE"); + else { + region = "SE"; } } + + this._placeInRegion(parentBranch, node, region); } @@ -296,27 +311,28 @@ class BarnesHutSolver { * @private */ _placeInRegion(parentBranch, node, region) { - switch (parentBranch.children[region].childrenCount) { + let children = parentBranch.children[region]; + + switch (children.childrenCount) { case 0: // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region], node); + children.children.data = node; + children.childrenCount = 1; + this._updateBranchMass(children, node); break; case 1: // convert into children // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) // we move one node a little bit and we do not put it in the tree. - if (parentBranch.children[region].children.data.x === node.x && - parentBranch.children[region].children.data.y === node.y) { + if (children.children.data.x === node.x && children.children.data.y === node.y) { node.x += this.seededRandom(); node.y += this.seededRandom(); } else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region], node); + this._splitBranch(children); + this._placeInTree(children, node); } break; case 4: // place in branch - this._placeInTree(parentBranch.children[region], node); + this._placeInTree(children, node); break; } } @@ -405,8 +421,6 @@ class BarnesHutSolver { } - - //--------------------------- DEBUGGING BELOW ---------------------------//