From ea2a4951e58e44c3cd592331645b8654d2408ee9 Mon Sep 17 00:00:00 2001 From: Eric VanDever Date: Wed, 7 Oct 2015 13:39:05 -0400 Subject: [PATCH] switch node changes over to events --- lib/network/Network.js | 2 +- lib/network/modules/InteractionHandler.js | 6 +- lib/network/modules/LayoutEngine.js | 4 +- lib/network/modules/PhysicsEngine.js | 128 +++++++------------- lib/network/modules/PhysicsWorker.js | 138 ++++++++++++++++------ lib/network/modules/components/Node.js | 33 ++++-- 6 files changed, 175 insertions(+), 136 deletions(-) diff --git a/lib/network/Network.js b/lib/network/Network.js index 7b3ae0e1..536876b3 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -270,7 +270,7 @@ Network.prototype.bindEventListeners = function () { this.body.emitter.on("_dataChanged", () => { // update shortcut lists this._updateVisibleIndices(); - this.physics.updatePhysicsData(); + this.physics.initPhysicsData(); this.body.emitter.emit("_requestRedraw"); // call the dataUpdated event because the only difference between the two is the updating of the indices this.body.emitter.emit("_dataUpdated"); diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index 76cc15cc..884912cd 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -313,8 +313,7 @@ class InteractionHandler { yFixed: object.options.fixed.y }; - object.options.fixed.x = true; - object.options.fixed.y = true; + object.setFixed(true); this.drag.selection.push(s); } @@ -395,8 +394,7 @@ class InteractionHandler { if (selection && selection.length) { selection.forEach(function (s) { // restore original xFixed and yFixed - s.node.options.fixed.x = s.xFixed; - s.node.options.fixed.y = s.yFixed; + s.node.setFixed({x: s.xFixed, y: s.yFixed}); }); this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center)); this.body.emitter.emit('startSimulation'); diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index b38bc393..d3d5e443 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -384,11 +384,11 @@ class LayoutEngine { let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { node.y = this.options.hierarchical.levelSeparation * level; - node.options.fixed.y = true; + node.setFixed({y: true}); } else { node.x = this.options.hierarchical.levelSeparation * level; - node.options.fixed.x = true; + node.setFixed({x: true}); } if (distribution[level] === undefined) { distribution[level] = {amount: 0, nodes: {}, distance: 0}; diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 9f8fa20c..93065b8d 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -120,15 +120,13 @@ class PhysicsEngine { this.body.emitter.on('_positionUpdate', (properties) => this.positionUpdateHandler(properties)); this.body.emitter.on('_physicsUpdate', (properties) => this.physicsUpdateHandler(properties)); // For identifying which nodes to send to worker thread - this.body.emitter.on('dragStart', (properties) => {this.draggingNodes = properties.nodes;}); - this.body.emitter.on('dragEnd', () => { - // need one last update to handle the case where a drag happens - // and the user holds the node clicked at the final position - // for a time prior to releasing - this.updateWorkerPositions(); + this.body.emitter.on('dragStart', (properties) => { + this.draggingNodes = properties.nodes; + }); + this.body.emitter.on('dragEnd', () => { this.draggingNodes = []; }); - this.body.emitter.on('destroy', () => { + this.body.emitter.on('destroy', () => { if (this.physicsWorker) { this.physicsWorker.terminate(); this.physicsWorker = undefined; @@ -185,7 +183,7 @@ class PhysicsEngine { this.options.useWorker = false; this.physicsWorker.terminate(); this.physicsWorker = undefined; - this.updatePhysicsData(); + this.initPhysicsData(); } var options; if (this.options.solver === 'forceAtlas2Based') { @@ -253,12 +251,18 @@ class PhysicsEngine { this.physicsUpdateHandler = (properties) => { if (properties.options.physics !== undefined) { if (properties.options.physics) { - this.physicsWorker.postMessage({type: 'addElements', data: 'TODO: createNode'}); + this.physicsWorker.postMessage({ + type: 'addElements', + data: this.createPhysicsNode(properties.id) + }); } else { - this.physicsWorker.postMessage({type: 'removeElements', data: properties.id}); + this.physicsWorker.postMessage({type: 'removeElements', data: { + nodes: [properties.id.toString()], + edges: [] + }}); } } else { - this.physicsWorker.postMessage({type: 'updateProperty', data: properties}); + this.physicsWorker.postMessage({type: 'updateProperties', data: properties}); } }; } @@ -315,7 +319,6 @@ class PhysicsEngine { */ startSimulation() { if (this.physicsEnabled === true && this.options.enabled === true) { - this.updateWorkerPositions(); this.stabilized = false; // when visible, adaptivity is disabled. @@ -404,7 +407,6 @@ class PhysicsEngine { } if (this.stabilized === false) { - this.updateWorkerFixed(); // 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. @@ -460,7 +462,7 @@ class PhysicsEngine { this.timestep = this.options.timestep; if (this.physicsWorker) { // console.log('asking working to do a physics iteration'); - this.physicsWorker.postMessage({type: 'calculateForces'}); + this.physicsWorker.postMessage({type: 'physicsTick'}); } else { this.calculateForces(); this.moveNodes(); @@ -476,12 +478,32 @@ class PhysicsEngine { } } + createPhysicsNode(nodeId) { + let node = this.body.nodes[nodeId]; + if (node && node.options.physics === true) { + // for updating fixed later + this.physicsBody.physicsNodeIndices.push(nodeId); + return { + id: node.id.toString(), + x: node.x, + y: node.y, + options: { + fixed: { + x: node.options.fixed.x, + y: node.options.fixed.y + }, + mass: node.options.mass + } + } + } + } + /** * 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() { + initPhysicsData() { let nodes = this.body.nodes; let edges = this.body.edges; @@ -490,28 +512,12 @@ class PhysicsEngine { this.physicsBody.physicsEdgeIndices = []; if (this.physicsWorker) { - this.physicsWorkerNodes = {}; - var physicsWorkerEdges = {}; + let physicsWorkerNodes = {}; + let physicsWorkerEdges = {}; for (let nodeId in nodes) { if (nodes.hasOwnProperty(nodeId)) { - let node = nodes[nodeId]; - if (node.options.physics === true) { - // for updating fixed later - this.physicsBody.physicsNodeIndices.push(nodeId); - this.physicsWorkerNodes[nodeId] = { - id: node.id, - x: node.x, - y: node.y, - options: { - fixed: { - x: node.options.fixed.x, - y: node.options.fixed.y - }, - mass: node.options.mass - } - } - } + physicsWorkerNodes[nodeId] = this.createPhysicsNode(nodeId); } } @@ -549,7 +555,7 @@ class PhysicsEngine { this.physicsWorker.postMessage({ type: 'physicsObjects', data: { - nodes: this.physicsWorkerNodes, + nodes: physicsWorkerNodes, edges: physicsWorkerEdges } }); @@ -592,52 +598,6 @@ class PhysicsEngine { } } - updateWorkerPositions() { - 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: 'updatePositions', - data: { - id: nodeId, - x: node.x, - y: node.y - } - }); - } - } - } - - updateWorkerFixed() { - if (this.physicsWorker) { - for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { - let nodeId = this.physicsBody.physicsNodeIndices[i]; - let physicsNode = this.physicsWorkerNodes[nodeId]; - let node = this.body.nodes[nodeId]; - if (physicsNode.options.fixed.x !== node.options.fixed.x || - physicsNode.options.fixed.y !== node.options.fixed.y) - { - let fixed = { - x: node.options.fixed.x, - y: node.options.fixed.y - }; - physicsNode.options.fixed.x = fixed.x; - physicsNode.options.fixed.y = fixed.y; - this.physicsWorker.postMessage({ - type: 'updateFixed', - data: { - id: nodeId, - x: node.x, - y: node.y, - fixed: fixed - } - }); - } - } - } - } - /** * Revert the simulation one step. This is done so after stabilization, every new start of the simulation will also say stabilized. */ @@ -786,8 +746,7 @@ class PhysicsEngine { 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; + nodes[id].setFixed(true); } } } @@ -803,8 +762,7 @@ class PhysicsEngine { for (var id in nodes) { if (nodes.hasOwnProperty(id)) { if (this.freezeCache[id] !== undefined) { - nodes[id].options.fixed.x = this.freezeCache[id].x; - nodes[id].options.fixed.y = this.freezeCache[id].y; + nodes[id].setFixed({x: this.freezeCache[id].x, y: this.freezeCache[id].y}); } } } diff --git a/lib/network/modules/PhysicsWorker.js b/lib/network/modules/PhysicsWorker.js index 197a95ad..d379d920 100644 --- a/lib/network/modules/PhysicsWorker.js +++ b/lib/network/modules/PhysicsWorker.js @@ -20,21 +20,17 @@ class PhysicsWorker { this.previousStates = {}; this.positions = {}; this.timestep = 0.5; + this.toRemove = { + nodes: [], + edges: [] + }; } 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 - } - }); + case 'physicsTick': + this.physicsTick(); break; case 'updatePositions': let updatedNode = this.body.nodes[msg.data.id]; @@ -45,34 +41,22 @@ class PhysicsWorker { this.physicsBody.velocities[updatedNode.id] = {x: 0, y: 0}; } break; - case 'updateProperty': - let optionsNode = this.body.nodes[msg.data.id]; - if (optionsNode) { - let opts = msg.data.options; - // TODO see if we need a position with fixed - if (opts.fixed) { - if (opts.fixed.x !== undefined) { - optionsNode.options.fixed.x = opts.fixed.x; - } - if (opts.fixed.y !== undefined) { - optionsNode.options.fixed.y = opts.fixed.y; - } - } - if (opts.mass !== undefined) { - optionsNode.options.mass = opts.mass; - } - } + case 'updateProperties': + this.updateProperties(msg.data); break; case 'addElements': - console.log('add'); + this.addElements(msg.data); break; case 'removeElements': - console.log('remove'); + // schedule removal of elements on the next physicsTick + // avoids having to defensively check every node read in each physics implementation + this.toRemove.nodes.push.apply(this.toRemove.nodes, msg.data.nodes); + this.toRemove.edges.push.apply(this.toRemove.edges, msg.data.edges); break; case 'physicsObjects': this.body.nodes = msg.data.nodes; this.body.edges = msg.data.edges; - this.updatePhysicsData(); + this.initPhysicsData(); break; case 'options': this.options = msg.data; @@ -117,12 +101,92 @@ class PhysicsWorker { this.modelOptions = options; } + physicsTick() { + this.processRemovals(); + this.calculateForces(); + this.moveNodes(); + for (let i = 0; i < this.toRemove.nodes.length; i++) { + delete this.positions[this.toRemove.nodes[i]]; + } + this.postMessage({ + type: 'positions', + data: { + positions: this.positions, + stabilized: this.stabilized + } + }); + } + + updateProperties(data) { + let optionsNode = this.body.nodes[data.id]; + if (optionsNode) { + let opts = data.options; + if (opts.fixed) { + if (opts.fixed.x !== undefined) { + optionsNode.options.fixed.x = opts.fixed.x; + } + if (opts.fixed.y !== undefined) { + optionsNode.options.fixed.y = opts.fixed.y; + } + } + if (opts.mass !== undefined) { + optionsNode.options.mass = opts.mass; + } + } else { + console.log('sending property to unknown node'); + } + } + + addElements(data) { + // TODO expand to handle multiple and edges + let newNode = data; + let nodeId = newNode.id; + this.body.nodes[nodeId] = newNode; + this.positions[nodeId] = { + x: newNode.x, + y: newNode.y + }; + this.physicsBody.forces[nodeId] = {x: 0, y: 0}; + this.physicsBody.velocities[nodeId] = {x: 0, y: 0}; + if (this.physicsBody.physicsNodeIndices.indexOf(nodeId) === -1) { + this.physicsBody.physicsNodeIndices.push(nodeId); + } + console.log('added node', nodeId); + } + + processRemovals() { + while (this.toRemove.nodes.length > 0) { + let nodeId = this.toRemove.nodes.pop(); + // TODO any optimization here? + let index = this.physicsBody.physicsNodeIndices.indexOf(nodeId); + if (index === -1 && typeof nodeId === 'number') { + index = this.physicsBody.physicsNodeIndices.indexOf(nodeId.toString()); + } + if (index > -1) { + this.physicsBody.physicsNodeIndices.splice(index,1); + } + delete this.physicsBody.forces[nodeId]; + delete this.physicsBody.velocities[nodeId]; + delete this.positions[nodeId]; + delete this.body.nodes[nodeId]; + console.log('removed node', nodeId); + } + while (this.toRemove.edges.length > 0) { + let edgeId = this.toRemove.edges.pop(); + let index = this.physicsBody.physicsEdgeIndices.indexOf(edgeId); + if (index > -1) { + this.physicsBody.physicsEdgeIndices.splice(index,1); + } + delete this.body.edges[edgeId]; + } + } + /** * 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() { + initPhysicsData() { this.physicsBody.forces = {}; this.physicsBody.physicsNodeIndices = []; this.physicsBody.physicsEdgeIndices = []; @@ -132,18 +196,18 @@ class PhysicsWorker { // 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 - } + 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); + this.physicsBody.physicsEdgeIndices.push(edgeId); } } diff --git a/lib/network/modules/components/Node.js b/lib/network/modules/components/Node.js index 9e88e744..787520c9 100644 --- a/lib/network/modules/components/Node.js +++ b/lib/network/modules/components/Node.js @@ -97,6 +97,19 @@ class Node { this.body.emitter.emit('_positionUpdate', {id: this.id, x: this._x, y: this._y}); } + /** + * Emitting version + * + * @param newFixed + */ + setFixed(newFixed) { + // TODO split out fixed portion? + let physOpts = Node.parseOptions(this.options, {fixed: newFixed}); + if (Object.keys(physOpts).length > 0) { + this.body.emitter.emit('_physicsUpdate', {id: this.id, options: physOpts}); + } + } + /** * Non emitting version for use by physics engine so we don't create infinite loops. * @param newY @@ -169,7 +182,7 @@ class Node { } // this transforms all shorthands into fully defined options - this.parseOptions(this.options, options, true, this.globalOptions); + let physOpts = Node.parseOptions(this.options, options, true, this.globalOptions); // load the images if (this.options.image !== undefined) { @@ -186,11 +199,15 @@ class Node { if (options.mass !== undefined) { this.options.mass = options.mass; - this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {mass: options.mass}}); + physOpts.mass = options.mass; } if (options.physics !== undefined) { this.options.physics = options.physics; - this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {physics: options.physics}}); + physOpts.physics = options.physics; + } + + if (Object.keys(physOpts).length > 0) { + this.body.emitter.emit('_physicsUpdate', {id: this.id, options: physOpts}); } if (options.hidden !== undefined) { @@ -206,13 +223,14 @@ class Node { * @param parentOptions * @param newOptions */ - parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) { + static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) { var fields = [ 'color', 'font', 'fixed', 'shadow' ]; + var changedPhysicsOptions = {}; util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion); // merge the shadow options into the parent. @@ -233,7 +251,7 @@ class Node { if (parentOptions.fixed.x !== newOptions.fixed || parentOptions.fixed.y !== newOptions.fixed) { parentOptions.fixed.x = newOptions.fixed; parentOptions.fixed.y = newOptions.fixed; - this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {fixed: {x: newOptions.fixed, y: newOptions.fixed}}}); + changedPhysicsOptions.fixed = {x: newOptions.fixed, y: newOptions.fixed}; } } else { @@ -242,14 +260,14 @@ class Node { parentOptions.fixed.x !== newOptions.fixed.x) { parentOptions.fixed.x = newOptions.fixed.x; - this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {fixed: {x: newOptions.fixed.x}}}); + util.deepExtend(changedPhysicsOptions, {fixed: {x: newOptions.fixed.x}}); } if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean' && parentOptions.fixed.y !== newOptions.fixed.y) { parentOptions.fixed.y = newOptions.fixed.y; - this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {fixed: {y: newOptions.fixed.y}}}); + util.deepExtend(changedPhysicsOptions, {fixed: {y: newOptions.fixed.y}}); } } } @@ -266,6 +284,7 @@ class Node { if (newOptions.scaling !== undefined) { util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', allowDeletion, globalOptions.scaling); } + return changedPhysicsOptions; } updateLabelModule() {