diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index ff13ccb0..9f8fa20c 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -90,6 +90,8 @@ class PhysicsEngine { this.timestep = 0.5; this.layoutFailed = false; this.draggingNodes = []; + this.positionUpdateHandler = () => {}; + this.physicsUpdateHandler = () => {}; this.bindEventListeners(); } @@ -115,6 +117,8 @@ class PhysicsEngine { this.stopSimulation(false); this.body.emitter.off(); }); + 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', () => { @@ -175,6 +179,8 @@ class PhysicsEngine { * configure the engine. */ initEmbeddedPhysics() { + this.positionUpdateHandler = () => {}; + this.physicsUpdateHandler = () => {}; if (this.physicsWorker) { this.options.useWorker = false; this.physicsWorker.terminate(); @@ -237,10 +243,24 @@ class PhysicsEngine { this.physicsWorkerMessageHandler(event); }); this.physicsWorker.onerror = (event) => { - console.error('Falling back to embedded physics engine'); + console.error('Falling back to embedded physics engine', event); this.initEmbeddedPhysics(); // throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")"); }; + this.positionUpdateHandler = (positions) => { + this.physicsWorker.postMessage({type: 'updatePositions', data: positions}); + }; + this.physicsUpdateHandler = (properties) => { + if (properties.options.physics !== undefined) { + if (properties.options.physics) { + this.physicsWorker.postMessage({type: 'addElements', data: 'TODO: createNode'}); + } else { + this.physicsWorker.postMessage({type: 'removeElements', data: properties.id}); + } + } else { + this.physicsWorker.postMessage({type: 'updateProperty', data: properties}); + } + }; } } @@ -259,8 +279,8 @@ class PhysicsEngine { let node = this.body.nodes[nodeId]; // handle case where we get a positions from an old physicsObject if (node) { - node.x = positions[nodeId].x; - node.y = positions[nodeId].y; + node.setX(positions[nodeId].x); + node.setY(positions[nodeId].y); } } break; @@ -719,7 +739,7 @@ class PhysicsEngine { 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 + node.setX(node.x + velocities[nodeId].x * timestep); // position } else { forces[nodeId].x = 0; @@ -731,7 +751,7 @@ class PhysicsEngine { 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 + node.setY(node.y + velocities[nodeId].y * timestep); // position } else { forces[nodeId].y = 0; diff --git a/lib/network/modules/PhysicsWorker.js b/lib/network/modules/PhysicsWorker.js index 70994847..197a95ad 100644 --- a/lib/network/modules/PhysicsWorker.js +++ b/lib/network/modules/PhysicsWorker.js @@ -9,7 +9,10 @@ import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedC class PhysicsWorker { constructor(postMessage) { - this.body = {}; + this.body = { + nodes: {}, + edges: {} + }; this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}}; this.postMessage = postMessage; this.options = {}; @@ -35,28 +38,47 @@ class PhysicsWorker { break; case 'updatePositions': let updatedNode = this.body.nodes[msg.data.id]; - updatedNode.x = msg.data.x; - updatedNode.y = msg.data.y; - this.physicsBody.forces[updatedNode.id] = {x: 0, y: 0}; - this.physicsBody.velocities[updatedNode.id] = {x: 0, y: 0}; + if (updatedNode) { + updatedNode.x = msg.data.x; + updatedNode.y = msg.data.y; + this.physicsBody.forces[updatedNode.id] = {x: 0, y: 0}; + this.physicsBody.velocities[updatedNode.id] = {x: 0, y: 0}; + } break; - case 'updateFixed': - let fixedNode = this.body.nodes[msg.data.id]; - fixedNode.x = msg.data.x; - fixedNode.y = msg.data.y; - fixedNode.options.fixed.x = msg.data.fixed.x; - fixedNode.options.fixed.y = msg.data.fixed.y; + 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; + } + } break; - case 'options': - this.options = msg.data; - this.timestep = this.options.timestep; - this.init(); + case 'addElements': + console.log('add'); + break; + case 'removeElements': + console.log('remove'); break; case 'physicsObjects': this.body.nodes = msg.data.nodes; this.body.edges = msg.data.edges; this.updatePhysicsData(); break; + case 'options': + this.options = msg.data; + this.timestep = this.options.timestep; + this.init(); + break; default: console.warn('unknown message from PhysicsEngine', msg); } diff --git a/lib/network/modules/components/Node.js b/lib/network/modules/components/Node.js index 89152195..9e88e744 100644 --- a/lib/network/modules/components/Node.js +++ b/lib/network/modules/components/Node.js @@ -59,8 +59,8 @@ class Node { this.grouplist = grouplist; // state options - this.x = undefined; - this.y = undefined; + this._x = undefined; + this._y = undefined; this.baseSize = this.options.size; this.baseFontSize = this.options.font.size; this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate @@ -71,6 +71,39 @@ class Node { this.setOptions(options); } + get x() { + return this._x; + } + + set x(newX) { + this._x = newX; + this.body.emitter.emit('_positionUpdate', {id: this.id, x: this._x, y: this._y}); + } + + /** + * Non emitting version for use by physics engine so we don't create infinite loops. + * @param newX + */ + setX(newX) { + this._x = newX; + } + + get y() { + return this._y; + } + + set y(newY) { + this._y = newY; + this.body.emitter.emit('_positionUpdate', {id: this.id, x: this._x, y: this._y}); + } + + /** + * Non emitting version for use by physics engine so we don't create infinite loops. + * @param newY + */ + setY(newY) { + this._y = newY; + } /** * Attach a edge to the node @@ -136,7 +169,7 @@ class Node { } // this transforms all shorthands into fully defined options - Node.parseOptions(this.options, options, true, this.globalOptions); + this.parseOptions(this.options, options, true, this.globalOptions); // load the images if (this.options.image !== undefined) { @@ -151,8 +184,16 @@ class Node { this.updateLabelModule(); this.updateShape(currentShape); + if (options.mass !== undefined) { + this.options.mass = options.mass; + this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {mass: options.mass}}); + } + if (options.physics !== undefined) { + this.options.physics = options.physics; + this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {physics: options.physics}}); + } - if (options.hidden !== undefined || options.physics !== undefined) { + if (options.hidden !== undefined) { return true; } return false; @@ -165,7 +206,7 @@ class Node { * @param parentOptions * @param newOptions */ - static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) { + parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) { var fields = [ 'color', 'font', @@ -189,15 +230,26 @@ class Node { // handle the fixed options if (newOptions.fixed !== undefined && newOptions.fixed !== null) { if (typeof newOptions.fixed === 'boolean') { - parentOptions.fixed.x = newOptions.fixed; - parentOptions.fixed.y = newOptions.fixed; + 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}}}); + } } else { - if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') { + if (newOptions.fixed.x !== undefined && + typeof newOptions.fixed.x === 'boolean' && + 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}}}); } - if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') { + 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}}}); } } }