461 lines
No EOL
14 KiB
JavaScript
Executable file
461 lines
No EOL
14 KiB
JavaScript
Executable file
const createPatternBuilder = require('./createPatternBuilder');
|
|
const getVariableName = require('./getVariableName');
|
|
|
|
module.exports = generateQuadTreeFunction;
|
|
module.exports.generateQuadTreeFunctionBody = generateQuadTreeFunctionBody;
|
|
|
|
// These exports are for InlineTransform tool.
|
|
// InlineTransform: getInsertStackCode
|
|
module.exports.getInsertStackCode = getInsertStackCode;
|
|
// InlineTransform: getQuadNodeCode
|
|
module.exports.getQuadNodeCode = getQuadNodeCode;
|
|
// InlineTransform: isSamePosition
|
|
module.exports.isSamePosition = isSamePosition;
|
|
// InlineTransform: getChildBodyCode
|
|
module.exports.getChildBodyCode = getChildBodyCode;
|
|
// InlineTransform: setChildBodyCode
|
|
module.exports.setChildBodyCode = setChildBodyCode;
|
|
|
|
function generateQuadTreeFunction(dimension) {
|
|
let code = generateQuadTreeFunctionBody(dimension);
|
|
return (new Function(code))();
|
|
}
|
|
|
|
function generateQuadTreeFunctionBody(dimension) {
|
|
let pattern = createPatternBuilder(dimension);
|
|
let quadCount = Math.pow(2, dimension);
|
|
|
|
let code = `
|
|
${getInsertStackCode()}
|
|
${getQuadNodeCode(dimension)}
|
|
${isSamePosition(dimension)}
|
|
${getChildBodyCode(dimension)}
|
|
${setChildBodyCode(dimension)}
|
|
|
|
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) {
|
|
${assignQuads(' node.')}
|
|
node.body = null;
|
|
node.mass = ${pattern('node.mass_{var} = ', {join: ''})}0;
|
|
${pattern('node.min_{var} = node.max_{var} = ', {join: ''})}0;
|
|
} else {
|
|
node = new QuadNode();
|
|
nodesCache[currentInCache] = node;
|
|
}
|
|
|
|
++currentInCache;
|
|
return node;
|
|
}
|
|
|
|
function update(sourceBody) {
|
|
var queue = updateQueue;
|
|
var v;
|
|
${pattern('var d{var};', {indent: 4})}
|
|
var r;
|
|
${pattern('var f{var} = 0;', {indent: 4})}
|
|
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.
|
|
${pattern('d{var} = body.pos.{var} - sourceBody.pos.{var};', {indent: 8})}
|
|
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
|
|
|
|
if (r === 0) {
|
|
// Poor man's protection against zero distance.
|
|
${pattern('d{var} = (random.nextDouble() - 0.5) / 50;', {indent: 10})}
|
|
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
|
|
}
|
|
|
|
// 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);
|
|
${pattern('f{var} += v * d{var};', {indent: 8})}
|
|
} 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
|
|
${pattern('d{var} = node.mass_{var} / node.mass - sourceBody.pos.{var};', {indent: 8})}
|
|
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
|
|
|
|
if (r === 0) {
|
|
// Sorry about code duplication. I don't want to create many functions
|
|
// right away. Just want to see performance first.
|
|
${pattern('d{var} = (random.nextDouble() - 0.5) / 50;', {indent: 10})}
|
|
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
|
|
}
|
|
// 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_${getVariableName(0)} - node.min_${getVariableName(0)}) / 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);
|
|
${pattern('f{var} += v * d{var};', {indent: 10})}
|
|
} else {
|
|
// Otherwise, run the procedure recursively on each of the current node's children.
|
|
|
|
// I intentionally unfolded this loop, to save several CPU cycles.
|
|
${runRecursiveOnChildren()}
|
|
}
|
|
}
|
|
}
|
|
|
|
${pattern('sourceBody.force.{var} += f{var};', {indent: 4})}
|
|
}
|
|
|
|
function insertBodies(bodies) {
|
|
${pattern('var {var}min = Number.MAX_VALUE;', {indent: 4})}
|
|
${pattern('var {var}max = Number.MIN_VALUE;', {indent: 4})}
|
|
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;
|
|
${pattern('if (pos.{var} < {var}min) {var}min = pos.{var};', {indent: 6})}
|
|
${pattern('if (pos.{var} > {var}max) {var}max = pos.{var};', {indent: 6})}
|
|
}
|
|
|
|
// Makes the bounds square.
|
|
var maxSideLength = -Infinity;
|
|
${pattern('if ({var}max - {var}min > maxSideLength) maxSideLength = {var}max - {var}min ;', {indent: 4})}
|
|
|
|
currentInCache = 0;
|
|
root = newNode();
|
|
${pattern('root.min_{var} = {var}min;', {indent: 4})}
|
|
${pattern('root.max_{var} = {var}min + maxSideLength;', {indent: 4})}
|
|
|
|
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.
|
|
${pattern('var {var} = body.pos.{var};', {indent: 8})}
|
|
node.mass += body.mass;
|
|
${pattern('node.mass_{var} += body.mass * {var};', {indent: 8})}
|
|
|
|
// 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.
|
|
${pattern('var min_{var} = node.min_{var};', {indent: 8})}
|
|
${pattern('var max_{var} = (min_{var} + node.max_{var}) / 2;', {indent: 8})}
|
|
|
|
${assignInsertionQuadIndex(8)}
|
|
|
|
var child = getChild(node, quadIdx);
|
|
|
|
if (!child) {
|
|
// The node is internal but this quadrant is not taken. Add
|
|
// subnode to it.
|
|
child = newNode();
|
|
${pattern('child.min_{var} = min_{var};', {indent: 10})}
|
|
${pattern('child.max_{var} = max_{var};', {indent: 10})}
|
|
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();
|
|
${pattern('var d{var} = (node.max_{var} - node.min_{var}) * offset;', {indent: 12})}
|
|
|
|
${pattern('oldBody.pos.{var} = node.min_{var} + d{var};', {indent: 12})}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return createQuadTree;
|
|
|
|
`;
|
|
return code;
|
|
|
|
|
|
function assignInsertionQuadIndex(indentCount) {
|
|
let insertionCode = [];
|
|
let indent = Array(indentCount + 1).join(' ');
|
|
for (let i = 0; i < dimension; ++i) {
|
|
insertionCode.push(indent + `if (${getVariableName(i)} > max_${getVariableName(i)}) {`);
|
|
insertionCode.push(indent + ` quadIdx = quadIdx + ${Math.pow(2, i)};`);
|
|
insertionCode.push(indent + ` min_${getVariableName(i)} = max_${getVariableName(i)};`);
|
|
insertionCode.push(indent + ` max_${getVariableName(i)} = node.max_${getVariableName(i)};`);
|
|
insertionCode.push(indent + `}`);
|
|
}
|
|
return insertionCode.join('\n');
|
|
// if (x > max_x) { // somewhere in the eastern part.
|
|
// quadIdx = quadIdx + 1;
|
|
// left = right;
|
|
// right = node.right;
|
|
// }
|
|
}
|
|
|
|
function runRecursiveOnChildren() {
|
|
let indent = Array(11).join(' ');
|
|
let recursiveCode = [];
|
|
for (let i = 0; i < quadCount; ++i) {
|
|
recursiveCode.push(indent + `if (node.quad${i}) {`);
|
|
recursiveCode.push(indent + ` queue[pushIdx] = node.quad${i};`);
|
|
recursiveCode.push(indent + ` queueLength += 1;`);
|
|
recursiveCode.push(indent + ` pushIdx += 1;`);
|
|
recursiveCode.push(indent + `}`);
|
|
}
|
|
return recursiveCode.join('\n');
|
|
// if (node.quad0) {
|
|
// queue[pushIdx] = node.quad0;
|
|
// queueLength += 1;
|
|
// pushIdx += 1;
|
|
// }
|
|
}
|
|
|
|
function assignQuads(indent) {
|
|
// this.quad0 = null;
|
|
// this.quad1 = null;
|
|
// this.quad2 = null;
|
|
// this.quad3 = null;
|
|
let quads = [];
|
|
for (let i = 0; i < quadCount; ++i) {
|
|
quads.push(`${indent}quad${i} = null;`);
|
|
}
|
|
return quads.join('\n');
|
|
}
|
|
}
|
|
|
|
function isSamePosition(dimension) {
|
|
let pattern = createPatternBuilder(dimension);
|
|
return `
|
|
function isSamePosition(point1, point2) {
|
|
${pattern('var d{var} = Math.abs(point1.{var} - point2.{var});', {indent: 2})}
|
|
|
|
return ${pattern('d{var} < 1e-8', {join: ' && '})};
|
|
}
|
|
`;
|
|
}
|
|
|
|
function setChildBodyCode(dimension) {
|
|
var quadCount = Math.pow(2, dimension);
|
|
return `
|
|
function setChild(node, idx, child) {
|
|
${setChildBody()}
|
|
}`;
|
|
function setChildBody() {
|
|
let childBody = [];
|
|
for (let i = 0; i < quadCount; ++i) {
|
|
let prefix = (i === 0) ? ' ' : ' else ';
|
|
childBody.push(`${prefix}if (idx === ${i}) node.quad${i} = child;`);
|
|
}
|
|
|
|
return childBody.join('\n');
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
function getChildBodyCode(dimension) {
|
|
return `function getChild(node, idx) {
|
|
${getChildBody()}
|
|
return null;
|
|
}`;
|
|
|
|
function getChildBody() {
|
|
let childBody = [];
|
|
let quadCount = Math.pow(2, dimension);
|
|
for (let i = 0; i < quadCount; ++i) {
|
|
childBody.push(` if (idx === ${i}) return node.quad${i};`);
|
|
}
|
|
|
|
return childBody.join('\n');
|
|
// if (idx === 0) return node.quad0;
|
|
// if (idx === 1) return node.quad1;
|
|
// if (idx === 2) return node.quad2;
|
|
// if (idx === 3) return node.quad3;
|
|
}
|
|
}
|
|
|
|
function getQuadNodeCode(dimension) {
|
|
let pattern = createPatternBuilder(dimension);
|
|
let quadCount = Math.pow(2, dimension);
|
|
var quadNodeCode = `
|
|
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
|
|
${assignQuads(' this.')}
|
|
|
|
// Total mass of current node
|
|
this.mass = 0;
|
|
|
|
// Center of mass coordinates
|
|
${pattern('this.mass_{var} = 0;', {indent: 2})}
|
|
|
|
// bounding box coordinates
|
|
${pattern('this.min_{var} = 0;', {indent: 2})}
|
|
${pattern('this.max_{var} = 0;', {indent: 2})}
|
|
}
|
|
`;
|
|
return quadNodeCode;
|
|
|
|
function assignQuads(indent) {
|
|
// this.quad0 = null;
|
|
// this.quad1 = null;
|
|
// this.quad2 = null;
|
|
// this.quad3 = null;
|
|
let quads = [];
|
|
for (let i = 0; i < quadCount; ++i) {
|
|
quads.push(`${indent}quad${i} = null;`);
|
|
}
|
|
return quads.join('\n');
|
|
}
|
|
}
|
|
|
|
function getInsertStackCode() {
|
|
return `
|
|
/**
|
|
* 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
|
|
}
|
|
`;
|
|
} |