createPhysicsSimulator.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /**
  2. * Manages a simulation of physical forces acting on bodies and springs.
  3. */
  4. module.exports = createPhysicsSimulator;
  5. var generateCreateBodyFunction = require('./codeGenerators/generateCreateBody');
  6. var generateQuadTreeFunction = require('./codeGenerators/generateQuadTree');
  7. var generateBoundsFunction = require('./codeGenerators/generateBounds');
  8. var generateCreateDragForceFunction = require('./codeGenerators/generateCreateDragForce');
  9. var generateCreateSpringForceFunction = require('./codeGenerators/generateCreateSpringForce');
  10. var generateIntegratorFunction = require('./codeGenerators/generateIntegrator');
  11. var dimensionalCache = {};
  12. function createPhysicsSimulator(settings) {
  13. var Spring = require('./spring');
  14. var merge = require('ngraph.merge');
  15. var eventify = require('ngraph.events');
  16. if (settings) {
  17. // Check for names from older versions of the layout
  18. if (settings.springCoeff !== undefined) throw new Error('springCoeff was renamed to springCoefficient');
  19. if (settings.dragCoeff !== undefined) throw new Error('dragCoeff was renamed to dragCoefficient');
  20. }
  21. settings = merge(settings, {
  22. /**
  23. * Ideal length for links (springs in physical model).
  24. */
  25. springLength: 10,
  26. /**
  27. * Hook's law coefficient. 1 - solid spring.
  28. */
  29. springCoefficient: 0.8,
  30. /**
  31. * Coulomb's law coefficient. It's used to repel nodes thus should be negative
  32. * if you make it positive nodes start attract each other :).
  33. */
  34. gravity: -12,
  35. /**
  36. * Theta coefficient from Barnes Hut simulation. Ranged between (0, 1).
  37. * The closer it's to 1 the more nodes algorithm will have to go through.
  38. * Setting it to one makes Barnes Hut simulation no different from
  39. * brute-force forces calculation (each node is considered).
  40. */
  41. theta: 0.8,
  42. /**
  43. * Drag force coefficient. Used to slow down system, thus should be less than 1.
  44. * The closer it is to 0 the less tight system will be.
  45. */
  46. dragCoefficient: 0.9, // TODO: Need to rename this to something better. E.g. `dragCoefficient`
  47. /**
  48. * Default time step (dt) for forces integration
  49. */
  50. timeStep : 0.5,
  51. /**
  52. * Adaptive time step uses average spring length to compute actual time step:
  53. * See: https://twitter.com/anvaka/status/1293067160755957760
  54. */
  55. adaptiveTimeStepWeight: 0,
  56. /**
  57. * This parameter defines number of dimensions of the space where simulation
  58. * is performed.
  59. */
  60. dimensions: 2,
  61. /**
  62. * In debug mode more checks are performed, this will help you catch errors
  63. * quickly, however for production build it is recommended to turn off this flag
  64. * to speed up computation.
  65. */
  66. debug: false
  67. });
  68. var factory = dimensionalCache[settings.dimensions];
  69. if (!factory) {
  70. var dimensions = settings.dimensions;
  71. factory = {
  72. Body: generateCreateBodyFunction(dimensions, settings.debug),
  73. createQuadTree: generateQuadTreeFunction(dimensions),
  74. createBounds: generateBoundsFunction(dimensions),
  75. createDragForce: generateCreateDragForceFunction(dimensions),
  76. createSpringForce: generateCreateSpringForceFunction(dimensions),
  77. integrate: generateIntegratorFunction(dimensions),
  78. };
  79. dimensionalCache[dimensions] = factory;
  80. }
  81. var Body = factory.Body;
  82. var createQuadTree = factory.createQuadTree;
  83. var createBounds = factory.createBounds;
  84. var createDragForce = factory.createDragForce;
  85. var createSpringForce = factory.createSpringForce;
  86. var integrate = factory.integrate;
  87. var createBody = pos => new Body(pos);
  88. var random = require('ngraph.random').random(42);
  89. var bodies = []; // Bodies in this simulation.
  90. var springs = []; // Springs in this simulation.
  91. var quadTree = createQuadTree(settings, random);
  92. var bounds = createBounds(bodies, settings, random);
  93. var springForce = createSpringForce(settings, random);
  94. var dragForce = createDragForce(settings);
  95. var totalMovement = 0; // how much movement we made on last step
  96. var forces = [];
  97. var forceMap = new Map();
  98. var iterationNumber = 0;
  99. addForce('nbody', nbodyForce);
  100. addForce('spring', updateSpringForce);
  101. var publicApi = {
  102. /**
  103. * Array of bodies, registered with current simulator
  104. *
  105. * Note: To add new body, use addBody() method. This property is only
  106. * exposed for testing/performance purposes.
  107. */
  108. bodies: bodies,
  109. quadTree: quadTree,
  110. /**
  111. * Array of springs, registered with current simulator
  112. *
  113. * Note: To add new spring, use addSpring() method. This property is only
  114. * exposed for testing/performance purposes.
  115. */
  116. springs: springs,
  117. /**
  118. * Returns settings with which current simulator was initialized
  119. */
  120. settings: settings,
  121. /**
  122. * Adds a new force to simulation
  123. */
  124. addForce: addForce,
  125. /**
  126. * Removes a force from the simulation.
  127. */
  128. removeForce: removeForce,
  129. /**
  130. * Returns a map of all registered forces.
  131. */
  132. getForces: getForces,
  133. /**
  134. * Performs one step of force simulation.
  135. *
  136. * @returns {boolean} true if system is considered stable; False otherwise.
  137. */
  138. step: function () {
  139. for (var i = 0; i < forces.length; ++i) {
  140. forces[i](iterationNumber);
  141. }
  142. var movement = integrate(bodies, settings.timeStep, settings.adaptiveTimeStepWeight);
  143. iterationNumber += 1;
  144. return movement;
  145. },
  146. /**
  147. * Adds body to the system
  148. *
  149. * @param {ngraph.physics.primitives.Body} body physical body
  150. *
  151. * @returns {ngraph.physics.primitives.Body} added body
  152. */
  153. addBody: function (body) {
  154. if (!body) {
  155. throw new Error('Body is required');
  156. }
  157. bodies.push(body);
  158. return body;
  159. },
  160. /**
  161. * Adds body to the system at given position
  162. *
  163. * @param {Object} pos position of a body
  164. *
  165. * @returns {ngraph.physics.primitives.Body} added body
  166. */
  167. addBodyAt: function (pos) {
  168. if (!pos) {
  169. throw new Error('Body position is required');
  170. }
  171. var body = createBody(pos);
  172. bodies.push(body);
  173. return body;
  174. },
  175. /**
  176. * Removes body from the system
  177. *
  178. * @param {ngraph.physics.primitives.Body} body to remove
  179. *
  180. * @returns {Boolean} true if body found and removed. falsy otherwise;
  181. */
  182. removeBody: function (body) {
  183. if (!body) { return; }
  184. var idx = bodies.indexOf(body);
  185. if (idx < 0) { return; }
  186. bodies.splice(idx, 1);
  187. if (bodies.length === 0) {
  188. bounds.reset();
  189. }
  190. return true;
  191. },
  192. /**
  193. * Adds a spring to this simulation.
  194. *
  195. * @returns {Object} - a handle for a spring. If you want to later remove
  196. * spring pass it to removeSpring() method.
  197. */
  198. addSpring: function (body1, body2, springLength, springCoefficient) {
  199. if (!body1 || !body2) {
  200. throw new Error('Cannot add null spring to force simulator');
  201. }
  202. if (typeof springLength !== 'number') {
  203. springLength = -1; // assume global configuration
  204. }
  205. var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1);
  206. springs.push(spring);
  207. // TODO: could mark simulator as dirty.
  208. return spring;
  209. },
  210. /**
  211. * Returns amount of movement performed on last step() call
  212. */
  213. getTotalMovement: function () {
  214. return totalMovement;
  215. },
  216. /**
  217. * Removes spring from the system
  218. *
  219. * @param {Object} spring to remove. Spring is an object returned by addSpring
  220. *
  221. * @returns {Boolean} true if spring found and removed. falsy otherwise;
  222. */
  223. removeSpring: function (spring) {
  224. if (!spring) { return; }
  225. var idx = springs.indexOf(spring);
  226. if (idx > -1) {
  227. springs.splice(idx, 1);
  228. return true;
  229. }
  230. },
  231. getBestNewBodyPosition: function (neighbors) {
  232. return bounds.getBestNewPosition(neighbors);
  233. },
  234. /**
  235. * Returns bounding box which covers all bodies
  236. */
  237. getBBox: getBoundingBox,
  238. getBoundingBox: getBoundingBox,
  239. invalidateBBox: function () {
  240. console.warn('invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call');
  241. },
  242. // TODO: Move the force specific stuff to force
  243. gravity: function (value) {
  244. if (value !== undefined) {
  245. settings.gravity = value;
  246. quadTree.options({gravity: value});
  247. return this;
  248. } else {
  249. return settings.gravity;
  250. }
  251. },
  252. theta: function (value) {
  253. if (value !== undefined) {
  254. settings.theta = value;
  255. quadTree.options({theta: value});
  256. return this;
  257. } else {
  258. return settings.theta;
  259. }
  260. },
  261. /**
  262. * Returns pseudo-random number generator instance.
  263. */
  264. random: random
  265. };
  266. // allow settings modification via public API:
  267. expose(settings, publicApi);
  268. eventify(publicApi);
  269. return publicApi;
  270. function getBoundingBox() {
  271. bounds.update();
  272. return bounds.box;
  273. }
  274. function addForce(forceName, forceFunction) {
  275. if (forceMap.has(forceName)) throw new Error('Force ' + forceName + ' is already added');
  276. forceMap.set(forceName, forceFunction);
  277. forces.push(forceFunction);
  278. }
  279. function removeForce(forceName) {
  280. var forceIndex = forces.indexOf(forceMap.get(forceName));
  281. if (forceIndex < 0) return;
  282. forces.splice(forceIndex, 1);
  283. forceMap.delete(forceName);
  284. }
  285. function getForces() {
  286. // TODO: Should I trust them or clone the forces?
  287. return forceMap;
  288. }
  289. function nbodyForce(/* iterationUmber */) {
  290. if (bodies.length === 0) return;
  291. quadTree.insertBodies(bodies);
  292. var i = bodies.length;
  293. while (i--) {
  294. var body = bodies[i];
  295. if (!body.isPinned) {
  296. body.reset();
  297. quadTree.updateBodyForce(body);
  298. dragForce.update(body);
  299. }
  300. }
  301. }
  302. function updateSpringForce() {
  303. var i = springs.length;
  304. while (i--) {
  305. springForce.update(springs[i]);
  306. }
  307. }
  308. }
  309. function expose(settings, target) {
  310. for (var key in settings) {
  311. augment(settings, target, key);
  312. }
  313. }
  314. function augment(source, target, key) {
  315. if (!source.hasOwnProperty(key)) return;
  316. if (typeof target[key] === 'function') {
  317. // this accessor is already defined. Ignore it
  318. return;
  319. }
  320. var sourceIsNumber = Number.isFinite(source[key]);
  321. if (sourceIsNumber) {
  322. target[key] = function (value) {
  323. if (value !== undefined) {
  324. if (!Number.isFinite(value)) throw new Error('Value of ' + key + ' should be a valid number.');
  325. source[key] = value;
  326. return target;
  327. }
  328. return source[key];
  329. };
  330. } else {
  331. target[key] = function (value) {
  332. if (value !== undefined) {
  333. source[key] = value;
  334. return target;
  335. }
  336. return source[key];
  337. };
  338. }
  339. }