mgraph.forcelayout
Version:
Force directed graph drawing layout
270 lines (233 loc) • 6.85 kB
JavaScript
// lib/createPhysicsSimulator.js
import merge from 'mgraph.merge';
import eventify from 'mgraph.events';
import random from 'mgraph.random';
import {
generateCreateBodyFunction,
generateQuadTreeFunction,
generateBoundsFunction,
generateCreateDragForceFunction,
generateCreateSpringForceFunction,
generateIntegratorFunction
} from './code-generators.js';
const dimensionalCache = {};
export default function createPhysicsSimulator(settings) {
if (settings) {
if (settings.springCoeff !== undefined) throw new Error('springCoeff was renamed to springCoefficient');
if (settings.dragCoeff !== undefined) throw new Error('dragCoeff was renamed to dragCoefficient');
}
settings = merge(settings, {
springLength: 10,
springCoefficient: 0.8,
gravity: -12,
theta: 0.8,
dragCoefficient: 0.9,
timeStep: 0.5,
adaptiveTimeStepWeight: 0,
dimensions: 2,
debug: false
});
let factory = dimensionalCache[settings.dimensions];
if (!factory) {
const dimensions = settings.dimensions;
factory = {
Body: generateCreateBodyFunction(dimensions, settings.debug),
createQuadTree: generateQuadTreeFunction(dimensions),
createBounds: generateBoundsFunction(dimensions),
createDragForce: generateCreateDragForceFunction(dimensions),
createSpringForce: generateCreateSpringForceFunction(dimensions),
integrate: generateIntegratorFunction(dimensions),
};
dimensionalCache[dimensions] = factory;
}
const Body = factory.Body;
const createQuadTree = factory.createQuadTree;
const createBounds = factory.createBounds;
const createDragForce = factory.createDragForce;
const createSpringForce = factory.createSpringForce;
const integrate = factory.integrate;
const rng = random.random(42);
const bodies = [];
const springs = [];
// Fix: Call the factory function to get the actual QuadTree constructor
const QuadTreeConstructor = createQuadTree();
const quadTree = QuadTreeConstructor(settings, rng);
const bounds = createBounds(bodies, settings, rng);
const springForce = createSpringForce(settings, rng);
const dragForce = createDragForce(settings);
const forces = [];
const forceMap = new Map();
let iterationNumber = 0;
addForce('nbody', nbodyForce);
addForce('spring', updateSpringForce);
const publicApi = {
bodies,
quadTree,
springs,
settings,
addForce,
removeForce,
getForces,
step() {
for (let i = 0; i < forces.length; ++i) {
forces[i](iterationNumber);
}
const movement = integrate(bodies, settings.timeStep, settings.adaptiveTimeStepWeight);
iterationNumber += 1;
return movement;
},
addBody(body) {
if (!body) throw new Error('Body is required');
bodies.push(body);
return body;
},
addBodyAt(pos) {
if (!pos) throw new Error('Body position is required');
const body = createBody(pos);
bodies.push(body);
return body;
},
removeBody(body) {
if (!body) return;
const idx = bodies.indexOf(body);
if (idx < 0) return;
bodies.splice(idx, 1);
if (bodies.length === 0) {
bounds.reset();
}
return true;
},
addSpring(body1, body2, springLength, springCoefficient) {
if (!body1 || !body2) {
throw new Error('Cannot add null spring to force simulator');
}
if (typeof springLength !== 'number') {
springLength = -1;
}
const spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1);
springs.push(spring);
return spring;
},
getTotalMovement() {
return integrate.totalMovement || 0;
},
removeSpring(spring) {
if (!spring) return;
const idx = springs.indexOf(spring);
if (idx > -1) {
springs.splice(idx, 1);
return true;
}
},
getBestNewBodyPosition(neighbors) {
return bounds.getBestNewPosition(neighbors);
},
getBBox: getBoundingBox,
getBoundingBox: getBoundingBox,
invalidateBBox() {
console.warn('invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call');
},
gravity(value) {
if (value !== undefined) {
settings.gravity = value;
quadTree.options({gravity: value});
return this;
} else {
return settings.gravity;
}
},
theta(value) {
if (value !== undefined) {
settings.theta = value;
quadTree.options({theta: value});
return this;
} else {
return settings.theta;
}
},
random: rng
};
// Helper function to create bodies
function createBody(pos) {
return new Body(pos);
}
expose(settings, publicApi);
eventify(publicApi);
return publicApi;
function getBoundingBox() {
bounds.update();
return bounds.box;
}
function addForce(forceName, forceFunction) {
if (forceMap.has(forceName)) throw new Error('Force ' + forceName + ' is already added');
forceMap.set(forceName, forceFunction);
forces.push(forceFunction);
}
function removeForce(forceName) {
const forceIndex = forces.indexOf(forceMap.get(forceName));
if (forceIndex < 0) return;
forces.splice(forceIndex, 1);
forceMap.delete(forceName);
}
function getForces() {
return forceMap;
}
function nbodyForce() {
if (bodies.length === 0) return;
quadTree.insertBodies(bodies);
let i = bodies.length;
while (i--) {
const body = bodies[i];
if (!body.isPinned) {
body.reset();
quadTree.updateBodyForce(body);
dragForce.update(body);
}
}
}
function updateSpringForce() {
let i = springs.length;
while (i--) {
springForce.update(springs[i]);
}
}
}
// Simple Spring class
class Spring {
constructor(fromBody, toBody, length, springCoefficient) {
this.from = fromBody;
this.to = toBody;
this.length = length;
this.coefficient = springCoefficient;
}
}
function expose(settings, target) {
for (const key in settings) {
augment(settings, target, key);
}
}
function augment(source, target, key) {
if (!source.hasOwnProperty(key)) return;
if (typeof target[key] === 'function') {
return;
}
const sourceIsNumber = Number.isFinite(source[key]);
if (sourceIsNumber) {
target[key] = function (value) {
if (value !== undefined) {
if (!Number.isFinite(value)) throw new Error('Value of ' + key + ' should be a valid number.');
source[key] = value;
return target;
}
return source[key];
};
} else {
target[key] = function (value) {
if (value !== undefined) {
source[key] = value;
return target;
}
return source[key];
};
}
}