12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652 |
- (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ngraphCreate2dLayout = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
- module.exports = createLayout;
- module.exports.simulator = require('./lib/createPhysicsSimulator');
- var eventify = require('ngraph.events');
- /**
- * Creates force based layout for a given graph.
- *
- * @param {ngraph.graph} graph which needs to be laid out
- * @param {object} physicsSettings if you need custom settings
- * for physics simulator you can pass your own settings here. If it's not passed
- * a default one will be created.
- */
- function createLayout(graph, physicsSettings) {
- if (!graph) {
- throw new Error('Graph structure cannot be undefined');
- }
- var createSimulator = (physicsSettings && physicsSettings.createSimulator) || require('./lib/createPhysicsSimulator');
- var physicsSimulator = createSimulator(physicsSettings);
- if (Array.isArray(physicsSettings)) throw new Error('Physics settings is expected to be an object');
- var nodeMass = defaultNodeMass;
- if (physicsSettings && typeof physicsSettings.nodeMass === 'function') {
- nodeMass = physicsSettings.nodeMass;
- }
- var nodeBodies = new Map();
- var springs = {};
- var bodiesCount = 0;
- var springTransform = physicsSimulator.settings.springTransform || noop;
- // Initialize physics with what we have in the graph:
- initPhysics();
- listenToEvents();
- var wasStable = false;
- var api = {
- /**
- * Performs one step of iterative layout algorithm
- *
- * @returns {boolean} true if the system should be considered stable; False otherwise.
- * The system is stable if no further call to `step()` can improve the layout.
- */
- step: function() {
- if (bodiesCount === 0) {
- updateStableStatus(true);
- return true;
- }
- var lastMove = physicsSimulator.step();
- // Save the movement in case if someone wants to query it in the step
- // callback.
- api.lastMove = lastMove;
- // Allow listeners to perform low-level actions after nodes are updated.
- api.fire('step');
- var ratio = lastMove/bodiesCount;
- var isStableNow = ratio <= 0.01; // TODO: The number is somewhat arbitrary...
- updateStableStatus(isStableNow);
- return isStableNow;
- },
- /**
- * For a given `nodeId` returns position
- */
- getNodePosition: function (nodeId) {
- return getInitializedBody(nodeId).pos;
- },
- /**
- * Sets position of a node to a given coordinates
- * @param {string} nodeId node identifier
- * @param {number} x position of a node
- * @param {number} y position of a node
- * @param {number=} z position of node (only if applicable to body)
- */
- setNodePosition: function (nodeId) {
- var body = getInitializedBody(nodeId);
- body.setPosition.apply(body, Array.prototype.slice.call(arguments, 1));
- },
- /**
- * @returns {Object} Link position by link id
- * @returns {Object.from} {x, y} coordinates of link start
- * @returns {Object.to} {x, y} coordinates of link end
- */
- getLinkPosition: function (linkId) {
- var spring = springs[linkId];
- if (spring) {
- return {
- from: spring.from.pos,
- to: spring.to.pos
- };
- }
- },
- /**
- * @returns {Object} area required to fit in the graph. Object contains
- * `x1`, `y1` - top left coordinates
- * `x2`, `y2` - bottom right coordinates
- */
- getGraphRect: function () {
- return physicsSimulator.getBBox();
- },
- /**
- * Iterates over each body in the layout simulator and performs a callback(body, nodeId)
- */
- forEachBody: forEachBody,
- /*
- * Requests layout algorithm to pin/unpin node to its current position
- * Pinned nodes should not be affected by layout algorithm and always
- * remain at their position
- */
- pinNode: function (node, isPinned) {
- var body = getInitializedBody(node.id);
- body.isPinned = !!isPinned;
- },
- /**
- * Checks whether given graph's node is currently pinned
- */
- isNodePinned: function (node) {
- return getInitializedBody(node.id).isPinned;
- },
- /**
- * Request to release all resources
- */
- dispose: function() {
- graph.off('changed', onGraphChanged);
- api.fire('disposed');
- },
- /**
- * Gets physical body for a given node id. If node is not found undefined
- * value is returned.
- */
- getBody: getBody,
- /**
- * Gets spring for a given edge.
- *
- * @param {string} linkId link identifer. If two arguments are passed then
- * this argument is treated as formNodeId
- * @param {string=} toId when defined this parameter denotes head of the link
- * and first argument is treated as tail of the link (fromId)
- */
- getSpring: getSpring,
- /**
- * Returns length of cumulative force vector. The closer this to zero - the more stable the system is
- */
- getForceVectorLength: getForceVectorLength,
- /**
- * [Read only] Gets current physics simulator
- */
- simulator: physicsSimulator,
- /**
- * Gets the graph that was used for layout
- */
- graph: graph,
- /**
- * Gets amount of movement performed during last step operation
- */
- lastMove: 0
- };
- eventify(api);
- return api;
- function updateStableStatus(isStableNow) {
- if (wasStable !== isStableNow) {
- wasStable = isStableNow;
- onStableChanged(isStableNow);
- }
- }
- function forEachBody(cb) {
- nodeBodies.forEach(cb);
- }
- function getForceVectorLength() {
- var fx = 0, fy = 0;
- forEachBody(function(body) {
- fx += Math.abs(body.force.x);
- fy += Math.abs(body.force.y);
- });
- return Math.sqrt(fx * fx + fy * fy);
- }
- function getSpring(fromId, toId) {
- var linkId;
- if (toId === undefined) {
- if (typeof fromId !== 'object') {
- // assume fromId as a linkId:
- linkId = fromId;
- } else {
- // assume fromId to be a link object:
- linkId = fromId.id;
- }
- } else {
- // toId is defined, should grab link:
- var link = graph.hasLink(fromId, toId);
- if (!link) return;
- linkId = link.id;
- }
- return springs[linkId];
- }
- function getBody(nodeId) {
- return nodeBodies.get(nodeId);
- }
- function listenToEvents() {
- graph.on('changed', onGraphChanged);
- }
- function onStableChanged(isStable) {
- api.fire('stable', isStable);
- }
- function onGraphChanged(changes) {
- for (var i = 0; i < changes.length; ++i) {
- var change = changes[i];
- if (change.changeType === 'add') {
- if (change.node) {
- initBody(change.node.id);
- }
- if (change.link) {
- initLink(change.link);
- }
- } else if (change.changeType === 'remove') {
- if (change.node) {
- releaseNode(change.node);
- }
- if (change.link) {
- releaseLink(change.link);
- }
- }
- }
- bodiesCount = graph.getNodesCount();
- }
- function initPhysics() {
- bodiesCount = 0;
- graph.forEachNode(function (node) {
- initBody(node.id);
- bodiesCount += 1;
- });
- graph.forEachLink(initLink);
- }
- function initBody(nodeId) {
- var body = nodeBodies.get(nodeId);
- if (!body) {
- var node = graph.getNode(nodeId);
- if (!node) {
- throw new Error('initBody() was called with unknown node id');
- }
- var pos = node.position;
- if (!pos) {
- var neighbors = getNeighborBodies(node);
- pos = physicsSimulator.getBestNewBodyPosition(neighbors);
- }
- body = physicsSimulator.addBodyAt(pos);
- body.id = nodeId;
- nodeBodies.set(nodeId, body);
- updateBodyMass(nodeId);
- if (isNodeOriginallyPinned(node)) {
- body.isPinned = true;
- }
- }
- }
- function releaseNode(node) {
- var nodeId = node.id;
- var body = nodeBodies.get(nodeId);
- if (body) {
- nodeBodies.delete(nodeId);
- physicsSimulator.removeBody(body);
- }
- }
- function initLink(link) {
- updateBodyMass(link.fromId);
- updateBodyMass(link.toId);
- var fromBody = nodeBodies.get(link.fromId),
- toBody = nodeBodies.get(link.toId),
- spring = physicsSimulator.addSpring(fromBody, toBody, link.length);
- springTransform(link, spring);
- springs[link.id] = spring;
- }
- function releaseLink(link) {
- var spring = springs[link.id];
- if (spring) {
- var from = graph.getNode(link.fromId),
- to = graph.getNode(link.toId);
- if (from) updateBodyMass(from.id);
- if (to) updateBodyMass(to.id);
- delete springs[link.id];
- physicsSimulator.removeSpring(spring);
- }
- }
- function getNeighborBodies(node) {
- // TODO: Could probably be done better on memory
- var neighbors = [];
- if (!node.links) {
- return neighbors;
- }
- var maxNeighbors = Math.min(node.links.length, 2);
- for (var i = 0; i < maxNeighbors; ++i) {
- var link = node.links[i];
- var otherBody = link.fromId !== node.id ? nodeBodies.get(link.fromId) : nodeBodies.get(link.toId);
- if (otherBody && otherBody.pos) {
- neighbors.push(otherBody);
- }
- }
- return neighbors;
- }
- function updateBodyMass(nodeId) {
- var body = nodeBodies.get(nodeId);
- body.mass = nodeMass(nodeId);
- if (Number.isNaN(body.mass)) {
- throw new Error('Node mass should be a number');
- }
- }
- /**
- * Checks whether graph node has in its settings pinned attribute,
- * which means layout algorithm cannot move it. Node can be marked
- * as pinned, if it has "isPinned" attribute, or when node.data has it.
- *
- * @param {Object} node a graph node to check
- * @return {Boolean} true if node should be treated as pinned; false otherwise.
- */
- function isNodeOriginallyPinned(node) {
- return (node && (node.isPinned || (node.data && node.data.isPinned)));
- }
- function getInitializedBody(nodeId) {
- var body = nodeBodies.get(nodeId);
- if (!body) {
- initBody(nodeId);
- body = nodeBodies.get(nodeId);
- }
- return body;
- }
- /**
- * Calculates mass of a body, which corresponds to node with given id.
- *
- * @param {String|Number} nodeId identifier of a node, for which body mass needs to be calculated
- * @returns {Number} recommended mass of the body;
- */
- function defaultNodeMass(nodeId) {
- var links = graph.getLinks(nodeId);
- if (!links) return 1;
- return 1 + links.length / 3.0;
- }
- }
- function noop() { }
- },{"./lib/createPhysicsSimulator":8,"ngraph.events":10}],2:[function(require,module,exports){
- module.exports = function() { return function anonymous(bodies,settings,random
- ) {
- var boundingBox = {
- min_x: 0, max_x: 0,
- min_y: 0, max_y: 0,
- };
- return {
- box: boundingBox,
- update: updateBoundingBox,
- reset: resetBoundingBox,
- getBestNewPosition: function (neighbors) {
- var base_x = 0, base_y = 0;
- if (neighbors.length) {
- for (var i = 0; i < neighbors.length; ++i) {
- let neighborPos = neighbors[i].pos;
- base_x += neighborPos.x;
- base_y += neighborPos.y;
- }
- base_x /= neighbors.length;
- base_y /= neighbors.length;
- } else {
- base_x = (boundingBox.min_x + boundingBox.max_x) / 2;
- base_y = (boundingBox.min_y + boundingBox.max_y) / 2;
- }
- var springLength = settings.springLength;
- return {
- x: base_x + (random.nextDouble() - 0.5) * springLength,
- y: base_y + (random.nextDouble() - 0.5) * springLength,
- };
- }
- };
- function updateBoundingBox() {
- var i = bodies.length;
- if (i === 0) return; // No bodies - no borders.
- var max_x = -Infinity;
- var max_y = -Infinity;
- var min_x = Infinity;
- var min_y = Infinity;
- while(i--) {
- // this is O(n), it could be done faster with quadtree, if we check the root node bounds
- var bodyPos = bodies[i].pos;
- if (bodyPos.x < min_x) min_x = bodyPos.x;
- if (bodyPos.y < min_y) min_y = bodyPos.y;
- if (bodyPos.x > max_x) max_x = bodyPos.x;
- if (bodyPos.y > max_y) max_y = bodyPos.y;
- }
- boundingBox.min_x = min_x;
- boundingBox.min_y = min_y;
- boundingBox.max_x = max_x;
- boundingBox.max_y = max_y;
- }
- function resetBoundingBox() {
- boundingBox.min_x = boundingBox.max_x = 0;
- boundingBox.min_y = boundingBox.max_y = 0;
- }
- } }
- },{}],3:[function(require,module,exports){
- function Vector(x, y) {
-
- if (typeof arguments[0] === 'object') {
- // could be another vector
- let v = arguments[0];
- if (!Number.isFinite(v.x)) throw new Error("Expected value is not a finite number at Vector constructor (x)");
- if (!Number.isFinite(v.y)) throw new Error("Expected value is not a finite number at Vector constructor (y)");
- this.x = v.x;
- this.y = v.y;
- } else {
- this.x = typeof x === "number" ? x : 0;
- this.y = typeof y === "number" ? y : 0;
- }
- }
-
- Vector.prototype.reset = function () {
- this.x = this.y = 0;
- };
- function Body(x, y) {
- this.isPinned = false;
- this.pos = new Vector(x, y);
- this.force = new Vector();
- this.velocity = new Vector();
- this.mass = 1;
- this.springCount = 0;
- this.springLength = 0;
- }
- Body.prototype.reset = function() {
- this.force.reset();
- this.springCount = 0;
- this.springLength = 0;
- }
- Body.prototype.setPosition = function (x, y) {
- this.pos.x = x || 0;
- this.pos.y = y || 0;
- };
- module.exports = function() { return Body; }
- },{}],4:[function(require,module,exports){
- module.exports = function() { return function anonymous(options
- ) {
- if (!Number.isFinite(options.dragCoefficient)) throw new Error('dragCoefficient is not a finite number');
- return {
- update: function(body) {
- body.force.x -= options.dragCoefficient * body.velocity.x;
- body.force.y -= options.dragCoefficient * body.velocity.y;
- }
- };
- } }
- },{}],5:[function(require,module,exports){
- module.exports = function() { return function anonymous(options,random
- ) {
- if (!Number.isFinite(options.springCoefficient)) throw new Error('Spring coefficient is not a number');
- if (!Number.isFinite(options.springLength)) throw new Error('Spring length is not a number');
- return {
- /**
- * Updates forces acting on a spring
- */
- update: function (spring) {
- var body1 = spring.from;
- var body2 = spring.to;
- var length = spring.length < 0 ? options.springLength : spring.length;
- var dx = body2.pos.x - body1.pos.x;
- var dy = body2.pos.y - body1.pos.y;
- var r = Math.sqrt(dx * dx + dy * dy);
- if (r === 0) {
- dx = (random.nextDouble() - 0.5) / 50;
- dy = (random.nextDouble() - 0.5) / 50;
- r = Math.sqrt(dx * dx + dy * dy);
- }
- var d = r - length;
- var coefficient = ((spring.coefficient > 0) ? spring.coefficient : options.springCoefficient) * d / r;
- body1.force.x += coefficient * dx
- body1.force.y += coefficient * dy;
- body1.springCount += 1;
- body1.springLength += r;
- body2.force.x -= coefficient * dx
- body2.force.y -= coefficient * dy;
- body2.springCount += 1;
- body2.springLength += r;
- }
- };
- } }
- },{}],6:[function(require,module,exports){
- module.exports = function() { return function anonymous(bodies,timeStep,adaptiveTimeStepWeight
- ) {
- var length = bodies.length;
- if (length === 0) return 0;
- var dx = 0, tx = 0;
- var dy = 0, ty = 0;
- for (var i = 0; i < length; ++i) {
- var body = bodies[i];
- if (body.isPinned) continue;
- if (adaptiveTimeStepWeight && body.springCount) {
- timeStep = (adaptiveTimeStepWeight * body.springLength/body.springCount);
- }
- var coeff = timeStep / body.mass;
- body.velocity.x += coeff * body.force.x;
- body.velocity.y += coeff * body.force.y;
- var vx = body.velocity.x;
- var vy = body.velocity.y;
- var v = Math.sqrt(vx * vx + vy * vy);
- if (v > 1) {
- // We normalize it so that we move within timeStep range.
- // for the case when v <= 1 - we let velocity to fade out.
- body.velocity.x = vx / v;
- body.velocity.y = vy / v;
- }
- dx = timeStep * body.velocity.x;
- dy = timeStep * body.velocity.y;
- body.pos.x += dx;
- body.pos.y += dy;
- tx += Math.abs(dx);
- ty += Math.abs(dy);
- }
- return (tx * tx + ty * ty)/length;
- } }
- },{}],7:[function(require,module,exports){
- /**
- * Our implementation of QuadTree is non-recursive to avoid GC hit
- * This data structure represent stack of elements
- * which we are trying to insert into quad tree.
- */
- function InsertStack () {
- this.stack = [];
- this.popIdx = 0;
- }
- InsertStack.prototype = {
- isEmpty: function() {
- return this.popIdx === 0;
- },
- push: function (node, body) {
- var item = this.stack[this.popIdx];
- if (!item) {
- // we are trying to avoid memory pressure: create new element
- // only when absolutely necessary
- this.stack[this.popIdx] = new InsertStackElement(node, body);
- } else {
- item.node = node;
- item.body = body;
- }
- ++this.popIdx;
- },
- pop: function () {
- if (this.popIdx > 0) {
- return this.stack[--this.popIdx];
- }
- },
- reset: function () {
- this.popIdx = 0;
- }
- };
- function InsertStackElement(node, body) {
- this.node = node; // QuadTree node
- this.body = body; // physical body which needs to be inserted to node
- }
- function QuadNode() {
- // body stored inside this node. In quad tree only leaf nodes (by construction)
- // contain bodies:
- this.body = null;
- // Child nodes are stored in quads. Each quad is presented by number:
- // 0 | 1
- // -----
- // 2 | 3
- this.quad0 = null;
- this.quad1 = null;
- this.quad2 = null;
- this.quad3 = null;
- // Total mass of current node
- this.mass = 0;
- // Center of mass coordinates
- this.mass_x = 0;
- this.mass_y = 0;
- // bounding box coordinates
- this.min_x = 0;
- this.min_y = 0;
- this.max_x = 0;
- this.max_y = 0;
- }
- function isSamePosition(point1, point2) {
- var dx = Math.abs(point1.x - point2.x);
- var dy = Math.abs(point1.y - point2.y);
-
- return dx < 1e-8 && dy < 1e-8;
- }
- function getChild(node, idx) {
- if (idx === 0) return node.quad0;
- if (idx === 1) return node.quad1;
- if (idx === 2) return node.quad2;
- if (idx === 3) return node.quad3;
- return null;
- }
- function setChild(node, idx, child) {
- if (idx === 0) node.quad0 = child;
- else if (idx === 1) node.quad1 = child;
- else if (idx === 2) node.quad2 = child;
- else if (idx === 3) node.quad3 = child;
- }
- module.exports = function() { return function createQuadTree(options, random) {
- options = options || {};
- options.gravity = typeof options.gravity === 'number' ? options.gravity : -1;
- options.theta = typeof options.theta === 'number' ? options.theta : 0.8;
- var gravity = options.gravity;
- var updateQueue = [];
- var insertStack = new InsertStack();
- var theta = options.theta;
- var nodesCache = [];
- var currentInCache = 0;
- var root = newNode();
- return {
- insertBodies: insertBodies,
- /**
- * Gets root node if it is present
- */
- getRoot: function() {
- return root;
- },
- updateBodyForce: update,
- options: function(newOptions) {
- if (newOptions) {
- if (typeof newOptions.gravity === 'number') {
- gravity = newOptions.gravity;
- }
- if (typeof newOptions.theta === 'number') {
- theta = newOptions.theta;
- }
- return this;
- }
- return {
- gravity: gravity,
- theta: theta
- };
- }
- };
- function newNode() {
- // To avoid pressure on GC we reuse nodes.
- var node = nodesCache[currentInCache];
- if (node) {
- node.quad0 = null;
- node.quad1 = null;
- node.quad2 = null;
- node.quad3 = null;
- node.body = null;
- node.mass = node.mass_x = node.mass_y = 0;
- node.min_x = node.max_x = node.min_y = node.max_y = 0;
- } else {
- node = new QuadNode();
- nodesCache[currentInCache] = node;
- }
- ++currentInCache;
- return node;
- }
- function update(sourceBody) {
- var queue = updateQueue;
- var v;
- var dx;
- var dy;
- var r;
- var fx = 0;
- var fy = 0;
- var queueLength = 1;
- var shiftIdx = 0;
- var pushIdx = 1;
- queue[0] = root;
- while (queueLength) {
- var node = queue[shiftIdx];
- var body = node.body;
- queueLength -= 1;
- shiftIdx += 1;
- var differentBody = (body !== sourceBody);
- if (body && differentBody) {
- // If the current node is a leaf node (and it is not source body),
- // calculate the force exerted by the current node on body, and add this
- // amount to body's net force.
- dx = body.pos.x - sourceBody.pos.x;
- dy = body.pos.y - sourceBody.pos.y;
- r = Math.sqrt(dx * dx + dy * dy);
- if (r === 0) {
- // Poor man's protection against zero distance.
- dx = (random.nextDouble() - 0.5) / 50;
- dy = (random.nextDouble() - 0.5) / 50;
- r = Math.sqrt(dx * dx + dy * dy);
- }
- // This is standard gravitation force calculation but we divide
- // by r^3 to save two operations when normalizing force vector.
- v = gravity * body.mass * sourceBody.mass / (r * r * r);
- fx += v * dx;
- fy += v * dy;
- } else if (differentBody) {
- // Otherwise, calculate the ratio s / r, where s is the width of the region
- // represented by the internal node, and r is the distance between the body
- // and the node's center-of-mass
- dx = node.mass_x / node.mass - sourceBody.pos.x;
- dy = node.mass_y / node.mass - sourceBody.pos.y;
- r = Math.sqrt(dx * dx + dy * dy);
- if (r === 0) {
- // Sorry about code duplication. I don't want to create many functions
- // right away. Just want to see performance first.
- dx = (random.nextDouble() - 0.5) / 50;
- dy = (random.nextDouble() - 0.5) / 50;
- r = Math.sqrt(dx * dx + dy * dy);
- }
- // If s / r < θ, treat this internal node as a single body, and calculate the
- // force it exerts on sourceBody, and add this amount to sourceBody's net force.
- if ((node.max_x - node.min_x) / r < theta) {
- // in the if statement above we consider node's width only
- // because the region was made into square during tree creation.
- // Thus there is no difference between using width or height.
- v = gravity * node.mass * sourceBody.mass / (r * r * r);
- fx += v * dx;
- fy += v * dy;
- } else {
- // Otherwise, run the procedure recursively on each of the current node's children.
- // I intentionally unfolded this loop, to save several CPU cycles.
- if (node.quad0) {
- queue[pushIdx] = node.quad0;
- queueLength += 1;
- pushIdx += 1;
- }
- if (node.quad1) {
- queue[pushIdx] = node.quad1;
- queueLength += 1;
- pushIdx += 1;
- }
- if (node.quad2) {
- queue[pushIdx] = node.quad2;
- queueLength += 1;
- pushIdx += 1;
- }
- if (node.quad3) {
- queue[pushIdx] = node.quad3;
- queueLength += 1;
- pushIdx += 1;
- }
- }
- }
- }
- sourceBody.force.x += fx;
- sourceBody.force.y += fy;
- }
- function insertBodies(bodies) {
- var xmin = Number.MAX_VALUE;
- var ymin = Number.MAX_VALUE;
- var xmax = Number.MIN_VALUE;
- var ymax = Number.MIN_VALUE;
- var i = bodies.length;
- // To reduce quad tree depth we are looking for exact bounding box of all particles.
- while (i--) {
- var pos = bodies[i].pos;
- if (pos.x < xmin) xmin = pos.x;
- if (pos.y < ymin) ymin = pos.y;
- if (pos.x > xmax) xmax = pos.x;
- if (pos.y > ymax) ymax = pos.y;
- }
- // Makes the bounds square.
- var maxSideLength = -Infinity;
- if (xmax - xmin > maxSideLength) maxSideLength = xmax - xmin ;
- if (ymax - ymin > maxSideLength) maxSideLength = ymax - ymin ;
- currentInCache = 0;
- root = newNode();
- root.min_x = xmin;
- root.min_y = ymin;
- root.max_x = xmin + maxSideLength;
- root.max_y = ymin + maxSideLength;
- i = bodies.length - 1;
- if (i >= 0) {
- root.body = bodies[i];
- }
- while (i--) {
- insert(bodies[i], root);
- }
- }
- function insert(newBody) {
- insertStack.reset();
- insertStack.push(root, newBody);
- while (!insertStack.isEmpty()) {
- var stackItem = insertStack.pop();
- var node = stackItem.node;
- var body = stackItem.body;
- if (!node.body) {
- // This is internal node. Update the total mass of the node and center-of-mass.
- var x = body.pos.x;
- var y = body.pos.y;
- node.mass += body.mass;
- node.mass_x += body.mass * x;
- node.mass_y += body.mass * y;
- // Recursively insert the body in the appropriate quadrant.
- // But first find the appropriate quadrant.
- var quadIdx = 0; // Assume we are in the 0's quad.
- var min_x = node.min_x;
- var min_y = node.min_y;
- var max_x = (min_x + node.max_x) / 2;
- var max_y = (min_y + node.max_y) / 2;
- if (x > max_x) {
- quadIdx = quadIdx + 1;
- min_x = max_x;
- max_x = node.max_x;
- }
- if (y > max_y) {
- quadIdx = quadIdx + 2;
- min_y = max_y;
- max_y = node.max_y;
- }
- var child = getChild(node, quadIdx);
- if (!child) {
- // The node is internal but this quadrant is not taken. Add
- // subnode to it.
- child = newNode();
- child.min_x = min_x;
- child.min_y = min_y;
- child.max_x = max_x;
- child.max_y = max_y;
- child.body = body;
- setChild(node, quadIdx, child);
- } else {
- // continue searching in this quadrant.
- insertStack.push(child, body);
- }
- } else {
- // We are trying to add to the leaf node.
- // We have to convert current leaf into internal node
- // and continue adding two nodes.
- var oldBody = node.body;
- node.body = null; // internal nodes do not cary bodies
- if (isSamePosition(oldBody.pos, body.pos)) {
- // Prevent infinite subdivision by bumping one node
- // anywhere in this quadrant
- var retriesCount = 3;
- do {
- var offset = random.nextDouble();
- var dx = (node.max_x - node.min_x) * offset;
- var dy = (node.max_y - node.min_y) * offset;
- oldBody.pos.x = node.min_x + dx;
- oldBody.pos.y = node.min_y + dy;
- retriesCount -= 1;
- // Make sure we don't bump it out of the box. If we do, next iteration should fix it
- } while (retriesCount > 0 && isSamePosition(oldBody.pos, body.pos));
- if (retriesCount === 0 && isSamePosition(oldBody.pos, body.pos)) {
- // This is very bad, we ran out of precision.
- // if we do not return from the method we'll get into
- // infinite loop here. So we sacrifice correctness of layout, and keep the app running
- // Next layout iteration should get larger bounding box in the first step and fix this
- return;
- }
- }
- // Next iteration should subdivide node further.
- insertStack.push(node, oldBody);
- insertStack.push(node, body);
- }
- }
- }
- } }
- },{}],8:[function(require,module,exports){
- /**
- * Manages a simulation of physical forces acting on bodies and springs.
- */
- module.exports = createPhysicsSimulator;
- var generateCreateBodyFunction = require('./codeGenerators/generateCreateBody');
- var generateQuadTreeFunction = require('./codeGenerators/generateQuadTree');
- var generateBoundsFunction = require('./codeGenerators/generateBounds');
- var generateCreateDragForceFunction = require('./codeGenerators/generateCreateDragForce');
- var generateCreateSpringForceFunction = require('./codeGenerators/generateCreateSpringForce');
- var generateIntegratorFunction = require('./codeGenerators/generateIntegrator');
- var dimensionalCache = {};
- function createPhysicsSimulator(settings) {
- var Spring = require('./spring');
- var merge = require('ngraph.merge');
- var eventify = require('ngraph.events');
- if (settings) {
- // Check for names from older versions of the layout
- 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, {
- /**
- * Ideal length for links (springs in physical model).
- */
- springLength: 10,
- /**
- * Hook's law coefficient. 1 - solid spring.
- */
- springCoefficient: 0.8,
- /**
- * Coulomb's law coefficient. It's used to repel nodes thus should be negative
- * if you make it positive nodes start attract each other :).
- */
- gravity: -12,
- /**
- * Theta coefficient from Barnes Hut simulation. Ranged between (0, 1).
- * The closer it's to 1 the more nodes algorithm will have to go through.
- * Setting it to one makes Barnes Hut simulation no different from
- * brute-force forces calculation (each node is considered).
- */
- theta: 0.8,
- /**
- * Drag force coefficient. Used to slow down system, thus should be less than 1.
- * The closer it is to 0 the less tight system will be.
- */
- dragCoefficient: 0.9, // TODO: Need to rename this to something better. E.g. `dragCoefficient`
- /**
- * Default time step (dt) for forces integration
- */
- timeStep : 0.5,
- /**
- * Adaptive time step uses average spring length to compute actual time step:
- * See: https://twitter.com/anvaka/status/1293067160755957760
- */
- adaptiveTimeStepWeight: 0,
- /**
- * This parameter defines number of dimensions of the space where simulation
- * is performed.
- */
- dimensions: 2,
- /**
- * In debug mode more checks are performed, this will help you catch errors
- * quickly, however for production build it is recommended to turn off this flag
- * to speed up computation.
- */
- debug: false
- });
- var factory = dimensionalCache[settings.dimensions];
- if (!factory) {
- var 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;
- }
- var Body = factory.Body;
- var createQuadTree = factory.createQuadTree;
- var createBounds = factory.createBounds;
- var createDragForce = factory.createDragForce;
- var createSpringForce = factory.createSpringForce;
- var integrate = factory.integrate;
- var createBody = pos => new Body(pos);
- var random = require('ngraph.random').random(42);
- var bodies = []; // Bodies in this simulation.
- var springs = []; // Springs in this simulation.
- var quadTree = createQuadTree(settings, random);
- var bounds = createBounds(bodies, settings, random);
- var springForce = createSpringForce(settings, random);
- var dragForce = createDragForce(settings);
- var totalMovement = 0; // how much movement we made on last step
- var forces = [];
- var forceMap = new Map();
- var iterationNumber = 0;
-
- addForce('nbody', nbodyForce);
- addForce('spring', updateSpringForce);
- var publicApi = {
- /**
- * Array of bodies, registered with current simulator
- *
- * Note: To add new body, use addBody() method. This property is only
- * exposed for testing/performance purposes.
- */
- bodies: bodies,
-
- quadTree: quadTree,
- /**
- * Array of springs, registered with current simulator
- *
- * Note: To add new spring, use addSpring() method. This property is only
- * exposed for testing/performance purposes.
- */
- springs: springs,
- /**
- * Returns settings with which current simulator was initialized
- */
- settings: settings,
- /**
- * Adds a new force to simulation
- */
- addForce: addForce,
-
- /**
- * Removes a force from the simulation.
- */
- removeForce: removeForce,
- /**
- * Returns a map of all registered forces.
- */
- getForces: getForces,
- /**
- * Performs one step of force simulation.
- *
- * @returns {boolean} true if system is considered stable; False otherwise.
- */
- step: function () {
- for (var i = 0; i < forces.length; ++i) {
- forces[i](iterationNumber);
- }
- var movement = integrate(bodies, settings.timeStep, settings.adaptiveTimeStepWeight);
- iterationNumber += 1;
- return movement;
- },
- /**
- * Adds body to the system
- *
- * @param {ngraph.physics.primitives.Body} body physical body
- *
- * @returns {ngraph.physics.primitives.Body} added body
- */
- addBody: function (body) {
- if (!body) {
- throw new Error('Body is required');
- }
- bodies.push(body);
- return body;
- },
- /**
- * Adds body to the system at given position
- *
- * @param {Object} pos position of a body
- *
- * @returns {ngraph.physics.primitives.Body} added body
- */
- addBodyAt: function (pos) {
- if (!pos) {
- throw new Error('Body position is required');
- }
- var body = createBody(pos);
- bodies.push(body);
- return body;
- },
- /**
- * Removes body from the system
- *
- * @param {ngraph.physics.primitives.Body} body to remove
- *
- * @returns {Boolean} true if body found and removed. falsy otherwise;
- */
- removeBody: function (body) {
- if (!body) { return; }
- var idx = bodies.indexOf(body);
- if (idx < 0) { return; }
- bodies.splice(idx, 1);
- if (bodies.length === 0) {
- bounds.reset();
- }
- return true;
- },
- /**
- * Adds a spring to this simulation.
- *
- * @returns {Object} - a handle for a spring. If you want to later remove
- * spring pass it to removeSpring() method.
- */
- addSpring: function (body1, body2, springLength, springCoefficient) {
- if (!body1 || !body2) {
- throw new Error('Cannot add null spring to force simulator');
- }
- if (typeof springLength !== 'number') {
- springLength = -1; // assume global configuration
- }
- var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1);
- springs.push(spring);
- // TODO: could mark simulator as dirty.
- return spring;
- },
- /**
- * Returns amount of movement performed on last step() call
- */
- getTotalMovement: function () {
- return totalMovement;
- },
- /**
- * Removes spring from the system
- *
- * @param {Object} spring to remove. Spring is an object returned by addSpring
- *
- * @returns {Boolean} true if spring found and removed. falsy otherwise;
- */
- removeSpring: function (spring) {
- if (!spring) { return; }
- var idx = springs.indexOf(spring);
- if (idx > -1) {
- springs.splice(idx, 1);
- return true;
- }
- },
- getBestNewBodyPosition: function (neighbors) {
- return bounds.getBestNewPosition(neighbors);
- },
- /**
- * Returns bounding box which covers all bodies
- */
- getBBox: getBoundingBox,
- getBoundingBox: getBoundingBox,
- invalidateBBox: function () {
- console.warn('invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call');
- },
- // TODO: Move the force specific stuff to force
- gravity: function (value) {
- if (value !== undefined) {
- settings.gravity = value;
- quadTree.options({gravity: value});
- return this;
- } else {
- return settings.gravity;
- }
- },
- theta: function (value) {
- if (value !== undefined) {
- settings.theta = value;
- quadTree.options({theta: value});
- return this;
- } else {
- return settings.theta;
- }
- },
- /**
- * Returns pseudo-random number generator instance.
- */
- random: random
- };
- // allow settings modification via public API:
- 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) {
- var forceIndex = forces.indexOf(forceMap.get(forceName));
- if (forceIndex < 0) return;
- forces.splice(forceIndex, 1);
- forceMap.delete(forceName);
- }
- function getForces() {
- // TODO: Should I trust them or clone the forces?
- return forceMap;
- }
- function nbodyForce(/* iterationUmber */) {
- if (bodies.length === 0) return;
- quadTree.insertBodies(bodies);
- var i = bodies.length;
- while (i--) {
- var body = bodies[i];
- if (!body.isPinned) {
- body.reset();
- quadTree.updateBodyForce(body);
- dragForce.update(body);
- }
- }
- }
- function updateSpringForce() {
- var i = springs.length;
- while (i--) {
- springForce.update(springs[i]);
- }
- }
- }
- function expose(settings, target) {
- for (var key in settings) {
- augment(settings, target, key);
- }
- }
- function augment(source, target, key) {
- if (!source.hasOwnProperty(key)) return;
- if (typeof target[key] === 'function') {
- // this accessor is already defined. Ignore it
- return;
- }
- var 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];
- };
- }
- }
- },{"./codeGenerators/generateBounds":2,"./codeGenerators/generateCreateBody":3,"./codeGenerators/generateCreateDragForce":4,"./codeGenerators/generateCreateSpringForce":5,"./codeGenerators/generateIntegrator":6,"./codeGenerators/generateQuadTree":7,"./spring":9,"ngraph.events":10,"ngraph.merge":11,"ngraph.random":12}],9:[function(require,module,exports){
- module.exports = Spring;
- /**
- * Represents a physical spring. Spring connects two bodies, has rest length
- * stiffness coefficient and optional weight
- */
- function Spring(fromBody, toBody, length, springCoefficient) {
- this.from = fromBody;
- this.to = toBody;
- this.length = length;
- this.coefficient = springCoefficient;
- }
- },{}],10:[function(require,module,exports){
- module.exports = function eventify(subject) {
- validateSubject(subject);
- var eventsStorage = createEventsStorage(subject);
- subject.on = eventsStorage.on;
- subject.off = eventsStorage.off;
- subject.fire = eventsStorage.fire;
- return subject;
- };
- function createEventsStorage(subject) {
- // Store all event listeners to this hash. Key is event name, value is array
- // of callback records.
- //
- // A callback record consists of callback function and its optional context:
- // { 'eventName' => [{callback: function, ctx: object}] }
- var registeredEvents = Object.create(null);
- return {
- on: function (eventName, callback, ctx) {
- if (typeof callback !== 'function') {
- throw new Error('callback is expected to be a function');
- }
- var handlers = registeredEvents[eventName];
- if (!handlers) {
- handlers = registeredEvents[eventName] = [];
- }
- handlers.push({callback: callback, ctx: ctx});
- return subject;
- },
- off: function (eventName, callback) {
- var wantToRemoveAll = (typeof eventName === 'undefined');
- if (wantToRemoveAll) {
- // Killing old events storage should be enough in this case:
- registeredEvents = Object.create(null);
- return subject;
- }
- if (registeredEvents[eventName]) {
- var deleteAllCallbacksForEvent = (typeof callback !== 'function');
- if (deleteAllCallbacksForEvent) {
- delete registeredEvents[eventName];
- } else {
- var callbacks = registeredEvents[eventName];
- for (var i = 0; i < callbacks.length; ++i) {
- if (callbacks[i].callback === callback) {
- callbacks.splice(i, 1);
- }
- }
- }
- }
- return subject;
- },
- fire: function (eventName) {
- var callbacks = registeredEvents[eventName];
- if (!callbacks) {
- return subject;
- }
- var fireArguments;
- if (arguments.length > 1) {
- fireArguments = Array.prototype.splice.call(arguments, 1);
- }
- for(var i = 0; i < callbacks.length; ++i) {
- var callbackInfo = callbacks[i];
- callbackInfo.callback.apply(callbackInfo.ctx, fireArguments);
- }
- return subject;
- }
- };
- }
- function validateSubject(subject) {
- if (!subject) {
- throw new Error('Eventify cannot use falsy object as events subject');
- }
- var reservedWords = ['on', 'fire', 'off'];
- for (var i = 0; i < reservedWords.length; ++i) {
- if (subject.hasOwnProperty(reservedWords[i])) {
- throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'");
- }
- }
- }
- },{}],11:[function(require,module,exports){
- module.exports = merge;
- /**
- * Augments `target` with properties in `options`. Does not override
- * target's properties if they are defined and matches expected type in
- * options
- *
- * @returns {Object} merged object
- */
- function merge(target, options) {
- var key;
- if (!target) { target = {}; }
- if (options) {
- for (key in options) {
- if (options.hasOwnProperty(key)) {
- var targetHasIt = target.hasOwnProperty(key),
- optionsValueType = typeof options[key],
- shouldReplace = !targetHasIt || (typeof target[key] !== optionsValueType);
- if (shouldReplace) {
- target[key] = options[key];
- } else if (optionsValueType === 'object') {
- // go deep, don't care about loops here, we are simple API!:
- target[key] = merge(target[key], options[key]);
- }
- }
- }
- }
- return target;
- }
- },{}],12:[function(require,module,exports){
- module.exports = random;
- // TODO: Deprecate?
- module.exports.random = random,
- module.exports.randomIterator = randomIterator
- /**
- * Creates seeded PRNG with two methods:
- * next() and nextDouble()
- */
- function random(inputSeed) {
- var seed = typeof inputSeed === 'number' ? inputSeed : (+new Date());
- return new Generator(seed)
- }
- function Generator(seed) {
- this.seed = seed;
- }
- /**
- * Generates random integer number in the range from 0 (inclusive) to maxValue (exclusive)
- *
- * @param maxValue Number REQUIRED. Omitting this number will result in NaN values from PRNG.
- */
- Generator.prototype.next = next;
- /**
- * Generates random double number in the range from 0 (inclusive) to 1 (exclusive)
- * This function is the same as Math.random() (except that it could be seeded)
- */
- Generator.prototype.nextDouble = nextDouble;
- /**
- * Returns a random real number uniformly in [0, 1)
- */
- Generator.prototype.uniform = nextDouble;
- Generator.prototype.gaussian = gaussian;
- function gaussian() {
- // use the polar form of the Box-Muller transform
- // based on https://introcs.cs.princeton.edu/java/23recursion/StdRandom.java
- var r, x, y;
- do {
- x = this.nextDouble() * 2 - 1;
- y = this.nextDouble() * 2 - 1;
- r = x * x + y * y;
- } while (r >= 1 || r === 0);
- return x * Math.sqrt(-2 * Math.log(r)/r);
- }
- function nextDouble() {
- var seed = this.seed;
- // Robert Jenkins' 32 bit integer hash function.
- seed = ((seed + 0x7ed55d16) + (seed << 12)) & 0xffffffff;
- seed = ((seed ^ 0xc761c23c) ^ (seed >>> 19)) & 0xffffffff;
- seed = ((seed + 0x165667b1) + (seed << 5)) & 0xffffffff;
- seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff;
- seed = ((seed + 0xfd7046c5) + (seed << 3)) & 0xffffffff;
- seed = ((seed ^ 0xb55a4f09) ^ (seed >>> 16)) & 0xffffffff;
- this.seed = seed;
- return (seed & 0xfffffff) / 0x10000000;
- }
- function next(maxValue) {
- return Math.floor(this.nextDouble() * maxValue);
- }
- /*
- * Creates iterator over array, which returns items of array in random order
- * Time complexity is guaranteed to be O(n);
- */
- function randomIterator(array, customRandom) {
- var localRandom = customRandom || random();
- if (typeof localRandom.next !== 'function') {
- throw new Error('customRandom does not match expected API: next() function is missing');
- }
- return {
- forEach: forEach,
- /**
- * Shuffles array randomly, in place.
- */
- shuffle: shuffle
- };
- function shuffle() {
- var i, j, t;
- for (i = array.length - 1; i > 0; --i) {
- j = localRandom.next(i + 1); // i inclusive
- t = array[j];
- array[j] = array[i];
- array[i] = t;
- }
- return array;
- }
- function forEach(callback) {
- var i, j, t;
- for (i = array.length - 1; i > 0; --i) {
- j = localRandom.next(i + 1); // i inclusive
- t = array[j];
- array[j] = array[i];
- array[i] = t;
- callback(t);
- }
- if (array.length) {
- callback(array[0]);
- }
- }
- }
- },{}]},{},[1])(1)
- });
|