@ -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 : 1 e9 ;
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 || 1 e9 ;
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 ;