|
@ -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 |
|
|