src/lib/vpsc.js (439 lines of code) (raw):

/* This file is compiled from https://github.com/tgdwyer/WebCola/blob/master/WebCola/src/vpsc.ts and modified slightly. The MIT License (MIT) Copyright (c) 2013 Tim Dwyer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var vpsc = {}; var PositionStats = (function () { function PositionStats(scale) { this.scale = scale; this.AB = 0; this.AD = 0; this.A2 = 0; } PositionStats.prototype.addVariable = function (v) { var ai = this.scale / v.scale; var bi = v.offset / v.scale; var wi = v.weight; this.AB += wi * ai * bi; this.AD += wi * ai * v.desiredPosition; this.A2 += wi * ai * ai; }; PositionStats.prototype.getPosn = function () { return (this.AD - this.AB) / this.A2; }; return PositionStats; })(); vpsc.PositionStats = PositionStats; var Constraint = (function () { function Constraint(left, right, gap, equality) { if (equality === void 0) { equality = false; } this.left = left; this.right = right; this.gap = gap; this.equality = equality; this.active = false; this.unsatisfiable = false; this.left = left; this.right = right; this.gap = gap; this.equality = equality; } Constraint.prototype.slack = function () { return this.unsatisfiable ? Number.MAX_VALUE : this.right.scale * this.right.position() - this.gap - this.left.scale * this.left.position(); }; return Constraint; })(); vpsc.Constraint = Constraint; var Variable = (function () { function Variable(desiredPosition, weight, scale) { if (weight === void 0) { weight = 1; } if (scale === void 0) { scale = 1; } this.desiredPosition = desiredPosition; this.weight = weight; this.scale = scale; this.offset = 0; } Variable.prototype.dfdv = function () { return 2.0 * this.weight * (this.position() - this.desiredPosition); }; Variable.prototype.position = function () { return (this.block.ps.scale * this.block.posn + this.offset) / this.scale; }; // visit neighbours by active constraints within the same block Variable.prototype.visitNeighbours = function (prev, f) { var ff = function (c, next) { return c.active && prev !== next && f(c, next); }; this.cOut.forEach(function (c) { return ff(c, c.right); }); this.cIn.forEach(function (c) { return ff(c, c.left); }); }; return Variable; })(); vpsc.Variable = Variable; var Block = (function () { function Block(v) { this.vars = []; v.offset = 0; this.ps = new PositionStats(v.scale); this.addVariable(v); } Block.prototype.addVariable = function (v) { v.block = this; this.vars.push(v); this.ps.addVariable(v); this.posn = this.ps.getPosn(); }; // move the block where it needs to be to minimize cost Block.prototype.updateWeightedPosition = function () { this.ps.AB = this.ps.AD = this.ps.A2 = 0; for (var i = 0, n = this.vars.length; i < n; ++i) this.ps.addVariable(this.vars[i]); this.posn = this.ps.getPosn(); }; Block.prototype.compute_lm = function (v, u, postAction) { var _this = this; var dfdv = v.dfdv(); v.visitNeighbours(u, function (c, next) { var _dfdv = _this.compute_lm(next, v, postAction); if (next === c.right) { dfdv += _dfdv * c.left.scale; c.lm = _dfdv; } else { dfdv += _dfdv * c.right.scale; c.lm = -_dfdv; } postAction(c); }); return dfdv / v.scale; }; Block.prototype.populateSplitBlock = function (v, prev) { var _this = this; v.visitNeighbours(prev, function (c, next) { next.offset = v.offset + (next === c.right ? c.gap : -c.gap); _this.addVariable(next); _this.populateSplitBlock(next, v); }); }; // traverse the active constraint tree applying visit to each active constraint Block.prototype.traverse = function (visit, acc, v, prev) { var _this = this; if (v === void 0) { v = this.vars[0]; } if (prev === void 0) { prev = null; } v.visitNeighbours(prev, function (c, next) { acc.push(visit(c)); _this.traverse(visit, acc, next, v); }); }; // calculate lagrangian multipliers on constraints and // find the active constraint in this block with the smallest lagrangian. // if the lagrangian is negative, then the constraint is a split candidate. Block.prototype.findMinLM = function () { var m = null; this.compute_lm(this.vars[0], null, function (c) { if (!c.equality && (m === null || c.lm < m.lm)) m = c; }); return m; }; Block.prototype.findMinLMBetween = function (lv, rv) { this.compute_lm(lv, null, function () { }); var m = null; this.findPath(lv, null, rv, function (c, next) { if (!c.equality && c.right === next && (m === null || c.lm < m.lm)) m = c; }); return m; }; Block.prototype.findPath = function (v, prev, to, visit) { var _this = this; var endFound = false; v.visitNeighbours(prev, function (c, next) { if (!endFound && (next === to || _this.findPath(next, v, to, visit))) { endFound = true; visit(c, next); } }); return endFound; }; // Search active constraint tree from u to see if there is a directed path to v. // Returns true if path is found. Block.prototype.isActiveDirectedPathBetween = function (u, v) { if (u === v) return true; var i = u.cOut.length; while (i--) { var c = u.cOut[i]; if (c.active && this.isActiveDirectedPathBetween(c.right, v)) return true; } return false; }; // split the block into two by deactivating the specified constraint Block.split = function (c) { /* DEBUG console.log("split on " + c); console.assert(c.active, "attempt to split on inactive constraint"); DEBUG */ c.active = false; return [Block.createSplitBlock(c.left), Block.createSplitBlock(c.right)]; }; Block.createSplitBlock = function (startVar) { var b = new Block(startVar); b.populateSplitBlock(startVar, null); return b; }; // find a split point somewhere between the specified variables Block.prototype.splitBetween = function (vl, vr) { /* DEBUG console.assert(vl.block === this); console.assert(vr.block === this); DEBUG */ var c = this.findMinLMBetween(vl, vr); if (c !== null) { var bs = Block.split(c); return { constraint: c, lb: bs[0], rb: bs[1] }; } // couldn't find a split point - for example the active path is all equality constraints return null; }; Block.prototype.mergeAcross = function (b, c, dist) { c.active = true; for (var i = 0, n = b.vars.length; i < n; ++i) { var v = b.vars[i]; v.offset += dist; this.addVariable(v); } this.posn = this.ps.getPosn(); }; Block.prototype.cost = function () { var sum = 0, i = this.vars.length; while (i--) { var v = this.vars[i], d = v.position() - v.desiredPosition; sum += d * d * v.weight; } return sum; }; return Block; })(); vpsc.Block = Block; var Blocks = (function () { function Blocks(vs) { this.vs = vs; var n = vs.length; this.list = new Array(n); while (n--) { var b = new Block(vs[n]); this.list[n] = b; b.blockInd = n; } } Blocks.prototype.cost = function () { var sum = 0, i = this.list.length; while (i--) sum += this.list[i].cost(); return sum; }; Blocks.prototype.insert = function (b) { /* DEBUG console.assert(!this.contains(b), "blocks error: tried to reinsert block " + b.blockInd) DEBUG */ b.blockInd = this.list.length; this.list.push(b); /* DEBUG console.log("insert block: " + b.blockInd); this.contains(b); DEBUG */ }; Blocks.prototype.remove = function (b) { /* DEBUG console.log("remove block: " + b.blockInd); console.assert(this.contains(b)); DEBUG */ var last = this.list.length - 1; var swapBlock = this.list[last]; this.list.length = last; if (b !== swapBlock) { this.list[b.blockInd] = swapBlock; swapBlock.blockInd = b.blockInd; } }; // merge the blocks on either side of the specified constraint, by copying the smaller block into the larger // and deleting the smaller. Blocks.prototype.merge = function (c) { var l = c.left.block, r = c.right.block; /* DEBUG console.assert(l!==r, "attempt to merge within the same block"); DEBUG */ var dist = c.right.offset - c.left.offset - c.gap; if (l.vars.length < r.vars.length) { r.mergeAcross(l, c, dist); this.remove(l); } else { l.mergeAcross(r, c, -dist); this.remove(r); } /* DEBUG console.assert(Math.abs(c.slack()) < 1e-6, "Error: Constraint should be at equality after merge!"); console.log("merged on " + c); DEBUG */ }; Blocks.prototype.forEach = function (f) { this.list.forEach(f); }; // useful, for example, after variable desired positions change. Blocks.prototype.updateBlockPositions = function () { this.list.forEach(function (b) { return b.updateWeightedPosition(); }); }; // split each block across its constraint with the minimum lagrangian Blocks.prototype.split = function (inactive) { var _this = this; this.updateBlockPositions(); this.list.forEach(function (b) { var v = b.findMinLM(); if (v !== null && v.lm < Solver.LAGRANGIAN_TOLERANCE) { b = v.left.block; Block.split(v).forEach(function (nb) { return _this.insert(nb); }); _this.remove(b); inactive.push(v); } }); }; return Blocks; })(); vpsc.Blocks = Blocks; var Solver = (function () { function Solver(vs, cs) { this.vs = vs; this.cs = cs; this.vs = vs; vs.forEach(function (v) { v.cIn = [], v.cOut = []; /* DEBUG v.toString = () => "v" + vs.indexOf(v); DEBUG */ }); this.cs = cs; cs.forEach(function (c) { c.left.cOut.push(c); c.right.cIn.push(c); /* DEBUG c.toString = () => c.left + "+" + c.gap + "<=" + c.right + " slack=" + c.slack() + " active=" + c.active; DEBUG */ }); this.inactive = cs.map(function (c) { c.active = false; return c; }); this.bs = null; } Solver.prototype.cost = function () { return this.bs.cost(); }; // set starting positions without changing desired positions. // Note: it throws away any previous block structure. Solver.prototype.setStartingPositions = function (ps) { this.inactive = this.cs.map(function (c) { c.active = false; return c; }); this.bs = new Blocks(this.vs); this.bs.forEach(function (b, i) { return b.posn = ps[i]; }); }; Solver.prototype.setDesiredPositions = function (ps) { this.vs.forEach(function (v, i) { return v.desiredPosition = ps[i]; }); }; /* DEBUG private getId(v: Variable): number { return this.vs.indexOf(v); } // sanity check of the index integrity of the inactive list checkInactive(): void { var inactiveCount = 0; this.cs.forEach(c=> { var i = this.inactive.indexOf(c); console.assert(!c.active && i >= 0 || c.active && i < 0, "constraint should be in the inactive list if it is not active: " + c); if (i >= 0) { inactiveCount++; } else { console.assert(c.active, "inactive constraint not found in inactive list: " + c); } }); console.assert(inactiveCount === this.inactive.length, inactiveCount + " inactive constraints found, " + this.inactive.length + "in inactive list"); } // after every call to satisfy the following should check should pass checkSatisfied(): void { this.cs.forEach(c=>console.assert(c.slack() >= vpsc.Solver.ZERO_UPPERBOUND, "Error: Unsatisfied constraint! "+c)); } DEBUG */ Solver.prototype.mostViolated = function () { var minSlack = Number.MAX_VALUE, v = null, l = this.inactive, n = l.length, deletePoint = n; for (var i = 0; i < n; ++i) { var c = l[i]; if (c.unsatisfiable) continue; var slack = c.slack(); if (c.equality || slack < minSlack) { minSlack = slack; v = c; deletePoint = i; if (c.equality) break; } } if (deletePoint !== n && (minSlack < Solver.ZERO_UPPERBOUND && !v.active || v.equality)) { l[deletePoint] = l[n - 1]; l.length = n - 1; } return v; }; // satisfy constraints by building block structure over violated constraints // and moving the blocks to their desired positions Solver.prototype.satisfy = function () { if (this.bs == null) { this.bs = new Blocks(this.vs); } /* DEBUG console.log("satisfy: " + this.bs); DEBUG */ this.bs.split(this.inactive); var v = null; while ((v = this.mostViolated()) && (v.equality || v.slack() < Solver.ZERO_UPPERBOUND && !v.active)) { var lb = v.left.block, rb = v.right.block; /* DEBUG console.log("most violated is: " + v); this.bs.contains(lb); this.bs.contains(rb); DEBUG */ if (lb !== rb) { this.bs.merge(v); } else { if (lb.isActiveDirectedPathBetween(v.right, v.left)) { // cycle found! v.unsatisfiable = true; continue; } // constraint is within block, need to split first var split = lb.splitBetween(v.left, v.right); if (split !== null) { this.bs.insert(split.lb); this.bs.insert(split.rb); this.bs.remove(lb); this.inactive.push(split.constraint); } else { /* DEBUG console.log("unsatisfiable constraint found"); DEBUG */ v.unsatisfiable = true; continue; } if (v.slack() >= 0) { /* DEBUG console.log("violated constraint indirectly satisfied: " + v); DEBUG */ // v was satisfied by the above split! this.inactive.push(v); } else { /* DEBUG console.log("merge after split:"); DEBUG */ this.bs.merge(v); } } } /* DEBUG this.checkSatisfied(); DEBUG */ }; // repeatedly build and split block structure until we converge to an optimal solution Solver.prototype.solve = function () { this.satisfy(); var lastcost = Number.MAX_VALUE, cost = this.bs.cost(); while (Math.abs(lastcost - cost) > 0.0001) { this.satisfy(); lastcost = cost; cost = this.bs.cost(); } return cost; }; Solver.LAGRANGIAN_TOLERANCE = -1e-4; Solver.ZERO_UPPERBOUND = -1e-10; return Solver; })(); vpsc.Solver = Solver; module.exports = vpsc;