From 6813bcd465c54e01352537e4d3757d66a93910d0 Mon Sep 17 00:00:00 2001 From: Eric VanDever Date: Tue, 6 Oct 2015 10:04:03 -0400 Subject: [PATCH] fixed whitespace and added initial documentation --- docs/network/physics.html | 11 +- lib/network/modules/PhysicsEngine.js | 96 +++---- lib/network/modules/PhysicsWorker.js | 406 +++++++++++++-------------- 3 files changed, 261 insertions(+), 252 deletions(-) diff --git a/docs/network/physics.html b/docs/network/physics.html index c8fe9920..f38fc3e9 100644 --- a/docs/network/physics.html +++ b/docs/network/physics.html @@ -139,7 +139,8 @@ var options = { fit: true }, timestep: 0.5, - adaptiveTimestep: true + adaptiveTimestep: true, + useWorker: false } } @@ -203,6 +204,14 @@ network.setOptions(options); stabilization.fit Boolean true Toggle whether or not you want the view to zoom to fit all nodes when the stabilization is finished. timestep Number 0.5 The physics simulation is discrete. This means we take a step in time, calculate the forces, move the nodes and take another step. If you increase this number the steps will be too large and the network can get unstable. If you see a lot of jittery movement in the network, you may want to reduce this value a little. adaptiveTimestep Boolean true If this is enabled, the timestep will intelligently be adapted (only during the stabilization stage if stabilization is enabled!) to greatly decrease stabilization times. The timestep configured above is taken as the minimum timestep. This can be further improved by using the improvedLayout algorithm. + useWorker Boolean false + If this is enabled, the physics calculation will be performed in a separate thread. The file vis.physics.worker.js must be available from the same webserver hosting vis.js at the same path. If + you are embedding vis into a javascript bundle, you can set the script id to "visjs" to enable + path resolution. If the worker fails for any reason, the system will fall back to standard + physics calculations. + WorkInProgress: This has only been tested with default physics selections and does not attempt to optimize + stabilization. + diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index d8b08dfa..c1ea64fe 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -89,7 +89,7 @@ class PhysicsEngine { util.extend(this.options, this.defaultOptions); this.timestep = 0.5; this.layoutFailed = false; - this.draggingNodes = []; + this.draggingNodes = []; this.bindEventListeners(); } @@ -115,9 +115,9 @@ class PhysicsEngine { this.stopSimulation(false); this.body.emitter.off(); }); - // For identifying which nodes to send to worker thread - this.body.emitter.on('dragStart', (properties) => {this.draggingNodes = properties.nodes;}); - this.body.emitter.on('dragEnd', () => {this.draggingNodes = [];}); + // For identifying which nodes to send to worker thread + this.body.emitter.on('dragStart', (properties) => {this.draggingNodes = properties.nodes;}); + this.body.emitter.on('dragEnd', () => {this.draggingNodes = [];}); this.body.emitter.on('destroy', () => { if (this.physicsWorker) { this.physicsWorker.terminate(); @@ -290,20 +290,20 @@ class PhysicsEngine { */ startSimulation() { if (this.physicsEnabled === true && this.options.enabled === true) { - if (this.physicsWorker) { - for(let i = 0; i < this.draggingNodes.length; i++) { - let nodeId = this.draggingNodes[i]; - let node = this.body.nodes[nodeId]; - this.physicsWorker.postMessage({ - type: 'update', - data: { - id: nodeId, - x: node.x, - y: node.y - } - }); - } - } + if (this.physicsWorker) { + for(let i = 0; i < this.draggingNodes.length; i++) { + let nodeId = this.draggingNodes[i]; + let node = this.body.nodes[nodeId]; + this.physicsWorker.postMessage({ + type: 'update', + data: { + id: nodeId, + x: node.x, + y: node.y + } + }); + } + } this.stabilized = false; // when visible, adaptivity is disabled. @@ -496,36 +496,36 @@ class PhysicsEngine { } } - for (let edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - let edge = edges[edgeId]; - if (edge.options.physics === true) { - physicsWorkerEdges[edgeId] = { - connected: edge.connected, - id: edge.id, - edgeType: {}, - toId: edge.toId, - fromId: edge.fromId, - to: { - id: edge.to.id - }, - from: { - id: edge.from.id - }, - options: { - length: edge.length - } - }; - if (edge.edgeType.via) { - physicsWorkerEdges[edgeId].edgeType = { - via: { - id: edge.edgeType.via.id - } - } - } - } - } - } + for (let edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + let edge = edges[edgeId]; + if (edge.options.physics === true) { + physicsWorkerEdges[edgeId] = { + connected: edge.connected, + id: edge.id, + edgeType: {}, + toId: edge.toId, + fromId: edge.fromId, + to: { + id: edge.to.id + }, + from: { + id: edge.from.id + }, + options: { + length: edge.length + } + }; + if (edge.edgeType.via) { + physicsWorkerEdges[edgeId].edgeType = { + via: { + id: edge.edgeType.via.id + } + } + } + } + } + } this.physicsWorker.postMessage({ type: 'physicsObjects', diff --git a/lib/network/modules/PhysicsWorker.js b/lib/network/modules/PhysicsWorker.js index b6b76335..f176a11c 100644 --- a/lib/network/modules/PhysicsWorker.js +++ b/lib/network/modules/PhysicsWorker.js @@ -8,209 +8,209 @@ import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedR import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver'; class PhysicsWorker { - constructor(postMessage) { - this.body = {}; - this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}}; - this.postMessage = postMessage; - this.options = {}; - this.stabilized = false; - this.previousStates = {}; - this.positions = {}; - this.timestep = 0.5; - } - - handleMessage(event) { - var msg = event.data; - switch (msg.type) { - case 'calculateForces': - this.calculateForces(); - this.moveNodes(); - this.postMessage({ - type: 'positions', - data: { - positions: this.positions, - stabilized: this.stabilized - } - }); - break; - case 'update': - let node = this.body.nodes[msg.data.id]; - node.x = msg.data.x; - node.y = msg.data.y; - break; - case 'options': - this.options = msg.data; - this.timestep = this.options.timestep; - this.init(); - break; - case 'physicsObjects': - this.body.nodes = msg.data.nodes; - this.body.edges = msg.data.edges; - this.updatePhysicsData(); - 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; - } - - /** - * 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 - */ - updatePhysicsData() { - this.physicsBody.forces = {}; - this.physicsBody.physicsNodeIndices = []; - this.physicsBody.physicsEdgeIndices = []; - let nodes = this.body.nodes; - let edges = this.body.edges; - - // get node indices for physics - for (let nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - this.physicsBody.physicsNodeIndices.push(nodeId); - this.positions[nodeId] = { - x: nodes[nodeId].x, - y: nodes[nodeId].y - } - } - } - - // get edge indices for physics - for (let edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - this.physicsBody.physicsEdgeIndices.push(edgeId); - } - } - - // get the velocity and the forces vector - for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { - let nodeId = this.physicsBody.physicsNodeIndices[i]; - 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) { - this.physicsBody.velocities[nodeId] = {x: 0, y: 0}; - } - } - - // clean deleted nodes from the velocity vector - for (let nodeId in this.physicsBody.velocities) { - if (nodes[nodeId] === undefined) { - delete this.physicsBody.velocities[nodeId]; - } - } - // console.log(this.physicsBody); - } - - /** - * 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 - * - * @param nodeId - * @param maxVelocity - * @returns {number} - * @private - */ - _performStep(nodeId,maxVelocity) { - let node = this.body.nodes[nodeId]; - let timestep = this.timestep; - let forces = this.physicsBody.forces; - let velocities = this.physicsBody.velocities; - - // store the state so we can revert - this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].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 - this.positions[nodeId].x = node.x; - } - else { - forces[nodeId].x = 0; - velocities[nodeId].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 - this.positions[nodeId].y = node.y; - } - else { - forces[nodeId].y = 0; - velocities[nodeId].y = 0; - } - - 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(); - } + constructor(postMessage) { + this.body = {}; + this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}}; + this.postMessage = postMessage; + this.options = {}; + this.stabilized = false; + this.previousStates = {}; + this.positions = {}; + this.timestep = 0.5; + } + + handleMessage(event) { + var msg = event.data; + switch (msg.type) { + case 'calculateForces': + this.calculateForces(); + this.moveNodes(); + this.postMessage({ + type: 'positions', + data: { + positions: this.positions, + stabilized: this.stabilized + } + }); + break; + case 'update': + let node = this.body.nodes[msg.data.id]; + node.x = msg.data.x; + node.y = msg.data.y; + break; + case 'options': + this.options = msg.data; + this.timestep = this.options.timestep; + this.init(); + break; + case 'physicsObjects': + this.body.nodes = msg.data.nodes; + this.body.edges = msg.data.edges; + this.updatePhysicsData(); + 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; + } + + /** + * 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 + */ + updatePhysicsData() { + this.physicsBody.forces = {}; + this.physicsBody.physicsNodeIndices = []; + this.physicsBody.physicsEdgeIndices = []; + let nodes = this.body.nodes; + let edges = this.body.edges; + + // get node indices for physics + for (let nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + this.physicsBody.physicsNodeIndices.push(nodeId); + this.positions[nodeId] = { + x: nodes[nodeId].x, + y: nodes[nodeId].y + } + } + } + + // get edge indices for physics + for (let edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + this.physicsBody.physicsEdgeIndices.push(edgeId); + } + } + + // get the velocity and the forces vector + for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { + let nodeId = this.physicsBody.physicsNodeIndices[i]; + 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) { + this.physicsBody.velocities[nodeId] = {x: 0, y: 0}; + } + } + + // clean deleted nodes from the velocity vector + for (let nodeId in this.physicsBody.velocities) { + if (nodes[nodeId] === undefined) { + delete this.physicsBody.velocities[nodeId]; + } + } + // console.log(this.physicsBody); + } + + /** + * 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 + * + * @param nodeId + * @param maxVelocity + * @returns {number} + * @private + */ + _performStep(nodeId,maxVelocity) { + let node = this.body.nodes[nodeId]; + let timestep = this.timestep; + let forces = this.physicsBody.forces; + let velocities = this.physicsBody.velocities; + + // store the state so we can revert + this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].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 + this.positions[nodeId].x = node.x; + } + else { + forces[nodeId].x = 0; + velocities[nodeId].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 + this.positions[nodeId].y = node.y; + } + else { + forces[nodeId].y = 0; + velocities[nodeId].y = 0; + } + + 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(); + } } export default PhysicsWorker;