aboutsummaryrefslogtreecommitdiff
path: root/src/lib.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.ts')
-rw-r--r--src/lib.ts554
1 files changed, 554 insertions, 0 deletions
diff --git a/src/lib.ts b/src/lib.ts
new file mode 100644
index 0000000..e5bd980
--- /dev/null
+++ b/src/lib.ts
@@ -0,0 +1,554 @@
+export class TolNode {
+ name: string;
+ children: TolNode[];
+ constructor(name: string, children: TolNode[] = []){
+ this.name = name;
+ this.children = children;
+ }
+}
+export class LayoutTree {
+ root: LayoutNode;
+ options: LayoutOptions;
+ constructor(tol: TolNode, depth: number, options: LayoutOptions){
+ this.root = this.initHelper(tol, depth);
+ this.options = options;
+ }
+ initHelper(tolNode: TolNode, depth: number): LayoutNode {
+ if (depth > 0){
+ let children = tolNode.children.map(
+ (n: TolNode) => this.initHelper(n, depth-1));
+ let node = new LayoutNode(tolNode, children);
+ children.forEach(n => n.parent = node);
+ return node;
+ } else {
+ return new LayoutNode(tolNode, []);
+ }
+ }
+ tryLayout(pos: [number,number], dims: [number,number]){
+ let newLayout: LayoutNode | null;
+ switch (this.options.layoutType){
+ case 'sqr': newLayout = sqrLayoutFn(this.root, pos, dims, true, this.options); break;
+ case 'rect': newLayout = rectLayoutFn(this.root, pos, dims, true, this.options); break;
+ case 'sweep': newLayout = sweepLayoutFn(this.root, pos, dims, true, this.options); break;
+ }
+ if (newLayout == null)
+ return false;
+ this.copyTreeForRender(newLayout, this.root);
+ return true;
+ }
+ tryLayoutOnExpand(pos: [number,number], dims: [number,number], node: LayoutNode){
+ //add children
+ node.children = node.tolNode.children.map((n: TolNode) => new LayoutNode(n, []));
+ node.children.forEach(n => n.parent = node);
+ this.updateDCounts(node, node.children.length-1);
+ //try layout
+ let success = this.tryLayout(pos, dims);
+ if (!success){ //remove children
+ node.children = [];
+ this.updateDCounts(node, -node.tolNode.children.length+1);
+ }
+ return success;
+ }
+ tryLayoutOnCollapse(pos: [number,number], dims: [number,number], node: LayoutNode){
+ //remove children
+ let children = node.children;
+ node.children = [];
+ this.updateDCounts(node, -children.length+1);
+ //try layout
+ let success = this.tryLayout(pos, dims);
+ if (!success){ //add children
+ node.children = children;
+ this.updateDCounts(node, node.children.length-1);
+ }
+ return success;
+ }
+ copyTreeForRender(node: LayoutNode, target: LayoutNode): void {
+ target.pos = node.pos;
+ target.dims = node.dims;
+ target.showHeader = node.showHeader;
+ target.sepSweptArea = node.sepSweptArea;
+ //these are arguably redundant
+ target.dCount = node.dCount;
+ target.usedDims = node.usedDims;
+ target.empSpc = node.empSpc;
+ //recurse on children
+ node.children.forEach((n,i) => this.copyTreeForRender(n, target.children[i]));
+ }
+ updateDCounts(node: LayoutNode | null, diff: number): void{
+ while (node != null){
+ node.dCount += diff;
+ node = node.parent;
+ }
+ }
+}
+export type LayoutOptions = {
+ tileSpacing: number;
+ headerSz: number;
+ minTileSz: number;
+ maxTileSz: number;
+ layoutType: 'sqr' | 'rect' | 'sweep';
+ rectMode: 'horz' | 'vert' | 'linear' | 'auto';
+ rectSpaceShifting: boolean;
+ sweepMode: 'left' | 'top' | 'shorter' | 'auto';
+ sweepingToParent: boolean;
+};
+export class LayoutNode {
+ //structure-related
+ tolNode: TolNode;
+ children: LayoutNode[];
+ parent: LayoutNode | null;
+ //used for rendering
+ pos: [number, number];
+ dims: [number, number];
+ showHeader: boolean;
+ sepSweptArea: SepSweptArea | null;
+ //used for layout heuristics
+ dCount: number; //number of descendant leaf nodes
+ usedDims: [number, number];
+ empSpc: number;
+ //
+ constructor(
+ tolNode: TolNode, children: LayoutNode[], pos=[0,0] as [number,number], dims=[0,0] as [number,number],
+ {showHeader=false, sepSweptArea=null as SepSweptArea|null, usedDims=[0,0] as [number,number], empSpc=0} = {}){
+ this.tolNode = tolNode;
+ this.children = children;
+ this.parent = null;
+ this.pos = pos;
+ this.dims = dims;
+ this.showHeader = showHeader;
+ this.sepSweptArea = sepSweptArea;
+ this.dCount = children.length == 0 ? 1 : children.map(n => n.dCount).reduce((x,y) => x+y);
+ this.usedDims = usedDims;
+ this.empSpc = empSpc;
+ }
+}
+export class SepSweptArea {
+ pos: [number, number];
+ dims: [number, number];
+ sweptLeft: boolean;
+ constructor(pos: [number, number], dims: [number, number], sweptLeft: boolean){
+ this.pos = pos;
+ this.dims = dims;
+ this.sweptLeft = sweptLeft;
+ }
+}
+
+type LayoutFn = (node: LayoutNode, pos: [number, number], dims: [number, number], showHeader: boolean,
+ opts: LayoutOptions, ownOpts?: {subLayoutFn?: LayoutFn, sepSweptArea?: SepSweptArea|null}) => LayoutNode | null;
+
+//lays out nodes as squares in a rectangle, with spacing
+let sqrLayoutFn: LayoutFn = function (node, pos, dims, showHeader, opts){
+ if (node.children.length == 0){
+ let tileSz = Math.min(dims[0], dims[1], opts.maxTileSz);
+ if (tileSz < opts.minTileSz)
+ return null;
+ return new LayoutNode(node.tolNode, [], pos, [tileSz,tileSz], {usedDims: [tileSz,tileSz], empSpc: 0});
+ }
+ //get number-of-columns with lowest leftover empty space
+ let headerSz = showHeader ? opts.headerSz : 0;
+ let availW = dims[0] - opts.tileSpacing, availH = dims[1] - headerSz - opts.tileSpacing;
+ if (availW*availH <= 0)
+ return null;
+ let numChildren = node.children.length, ar = availW/availH;
+ let lowestEmp = Number.POSITIVE_INFINITY, numCols = 0, numRows = 0, tileSize = 0;
+ for (let nc = 1; nc <= numChildren; nc++){
+ let nr = Math.ceil(numChildren/nc);
+ let ar2 = nc/nr;
+ let frac = ar > ar2 ? ar2/ar : ar/ar2;
+ let tileSz = ar > ar2 ? availH/nr-opts.tileSpacing : availW/nc-opts.tileSpacing;
+ if (tileSz < opts.minTileSz)
+ continue;
+ else if (tileSz > opts.maxTileSz)
+ tileSz = opts.maxTileSz;
+ let empSpc = (1-frac)*availW*availH + (nc*nr-numChildren)*(tileSz - opts.tileSpacing)**2;
+ if (empSpc < lowestEmp){
+ lowestEmp = empSpc;
+ numCols = nc;
+ numRows = nr;
+ tileSize = tileSz;
+ }
+ }
+ if (lowestEmp == Number.POSITIVE_INFINITY)
+ return null;
+ let childLayouts = arrayOf(null, numChildren);
+ for (let i = 0; i < numChildren; i++){
+ let child = node.children[i];
+ let childX = opts.tileSpacing + (i % numCols)*(tileSize + opts.tileSpacing);
+ let childY = opts.tileSpacing + headerSz + Math.floor(i / numCols)*(tileSize + opts.tileSpacing);
+ childLayouts[i] = sqrLayoutFn(child, [childX,childY], [tileSize,tileSize], true, opts);
+ if (childLayouts[i] == null)
+ return null;
+ lowestEmp += childLayouts[i].empSpc;
+ }
+ let newNode = new LayoutNode(node.tolNode, childLayouts, pos, dims, {
+ showHeader,
+ usedDims: [numCols * (tileSize + opts.tileSpacing) + opts.tileSpacing,
+ numRows * (tileSize + opts.tileSpacing) + opts.tileSpacing + headerSz],
+ empSpc: lowestEmp,
+ });
+ childLayouts.forEach(n => n.parent = newNode);
+ return newNode;
+}
+//lays out nodes as rectangles organised into rows, partially using other layouts for children
+let rectLayoutFn: LayoutFn = function (node, pos, dims, showHeader, opts, ownOpts={subLayoutFn: rectLayoutFn}){
+ if (node.children.every(n => n.children.length == 0))
+ return sqrLayoutFn(node, pos, dims, showHeader, opts);
+ //find grid-arrangement with lowest leftover empty space
+ let headerSz = showHeader ? opts.headerSz : 0;
+ let availW = dims[0] - opts.tileSpacing, availH = dims[1] - opts.tileSpacing - headerSz;
+ let numChildren = node.children.length;
+ let rowBrks: number[]|null = null; //will holds node indices at which each row starts
+ let lowestEmp = Number.POSITIVE_INFINITY, rowBreaks = null, childLayouts = null;
+ rowBrksLoop:
+ while (true){
+ //update rowBrks or exit loop
+ if (rowBrks == null){
+ if (opts.rectMode == 'vert'){
+ rowBrks = seq(numChildren);
+ } else {
+ rowBrks = [0];
+ }
+ } else {
+ if (opts.rectMode == 'horz' || opts.rectMode == 'vert'){
+ break rowBrksLoop;
+ } else if (opts.rectMode == 'linear'){
+ if (rowBrks.length == 1 && numChildren > 1){
+ rowBrks = seq(numChildren);
+ } else {
+ break rowBrksLoop;
+ }
+ } else {
+ let i = rowBrks.length-1;
+ while (true){
+ if (i > 0 && rowBrks[i] < numChildren-1 - (rowBrks.length-1 - i)){
+ rowBrks[i]++;
+ break;
+ } else if (i > 0){
+ i--;
+ } else {
+ if (rowBrks.length < numChildren){
+ rowBrks = seq(rowBrks.length+1);
+ } else {
+ break rowBrksLoop;
+ }
+ break;
+ }
+ }
+ }
+ }
+ //create list-of-lists representing each row's cells' dCounts
+ let rowsOfCnts: number[][] = arrayOf([], rowBrks.length);
+ for (let r = 0; r < rowBrks.length; r++){
+ let numNodes = (r == rowBrks.length-1) ? numChildren-rowBrks[r] : rowBrks[r+1]-rowBrks[r];
+ let rowNodeIdxs = seq(numNodes).map(i => i+rowBrks![r]);
+ rowsOfCnts[r] = rowNodeIdxs.map(idx => node.children[idx].dCount);
+ }
+ //get cell dims
+ let totalTileCount = node.children.map(n => n.dCount).reduce((x,y) => x+y);
+ let cellHs = rowsOfCnts.map(row => row.reduce((x,y) => x+y) / totalTileCount * availH);
+ let cellWs = arrayOf(0, numChildren);
+ for (let r = 0; r < rowsOfCnts.length; r++){
+ let rowCount = rowsOfCnts[r].reduce((x,y) => x+y);
+ for (let c = 0; c < rowsOfCnts[r].length; c++){
+ cellWs[rowBrks[r]+c] = rowsOfCnts[r][c] / rowCount * availW;
+ }
+ }
+ //impose min-tile-size
+ cellHs = limitVals(cellHs, opts.minTileSz, Number.POSITIVE_INFINITY)!;
+ if (cellHs == null)
+ continue rowBrksLoop;
+ for (let r = 0; r < rowsOfCnts.length; r++){
+ let temp = limitVals(cellWs.slice(rowBrks[r], rowBrks[r] + rowsOfCnts[r].length),
+ opts.minTileSz, Number.POSITIVE_INFINITY);
+ if (temp == null)
+ continue rowBrksLoop;
+ cellWs.splice(rowBrks[r], rowsOfCnts[r].length, ...temp);
+ }
+ //get cell x/y coords
+ let cellXs = arrayOf(0, cellWs.length);
+ for (let r = 0; r < rowBrks.length; r++){
+ for (let c = 1; c < rowsOfCnts[r].length; c++){
+ let nodeIdx = rowBrks[r]+c;
+ cellXs[nodeIdx] = cellXs[nodeIdx-1] + cellWs[nodeIdx-1];
+ }
+ }
+ let cellYs = arrayOf(0, cellHs.length);
+ for (let r = 1; r < rowBrks.length; r++){
+ cellYs[r] = cellYs[r-1] + cellHs[r-1];
+ }
+ //get child layouts and empty-space
+ let childLyts = arrayOf(null, numChildren);
+ let empVTotal = 0, empSpc = 0;
+ for (let r = 0; r < rowBrks.length; r++){
+ let empHorzTotal = 0;
+ for (let c = 0; c < rowsOfCnts[r].length; c++){
+ let nodeIdx = rowBrks[r]+c;
+ let child = node.children[nodeIdx];
+ let childX = cellXs[nodeIdx] + opts.tileSpacing, childY = cellYs[r] + opts.tileSpacing + headerSz,
+ childW = cellWs[nodeIdx] - opts.tileSpacing, childH = cellHs[r] - opts.tileSpacing;
+ if (child.children.length == 0){
+ let contentSz = Math.min(childW, childH);
+ childLyts[nodeIdx] = new LayoutNode(child.tolNode, [], [childX,childY], [childW,childH],
+ {usedDims: [contentSz,contentSz], empSpc: childW*childH - contentSz**2})
+ } else if (child.children.every(n => n.children.length == 0)){
+ childLyts[nodeIdx] = sqrLayoutFn(child, [childX,childY], [childW,childH], true, opts);
+ } else {
+ let layoutFn = (ownOpts && ownOpts.subLayoutFn) || rectLayoutFn;
+ childLyts[nodeIdx] = layoutFn(child, [childX,childY], [childW,childH], true, opts);
+ }
+ if (childLyts[nodeIdx] == null)
+ continue rowBrksLoop;
+ //handle horizontal empty-space-shifting
+ if (opts.rectSpaceShifting){
+ let empHorz = childLyts[nodeIdx].dims[0] - childLyts[nodeIdx].usedDims[0];
+ childLyts[nodeIdx].dims[0] -= empHorz;
+ childLyts[nodeIdx].empSpc -= empHorz * childLyts[nodeIdx].dims[1];
+ if (c < rowsOfCnts[r].length-1){
+ cellXs[nodeIdx+1] -= empHorz;
+ cellWs[nodeIdx+1] += empHorz;
+ } else {
+ empHorzTotal = empHorz;
+ }
+ }
+ }
+ //handle vertical empty-space-shifting
+ if (opts.rectSpaceShifting){
+ let nodeIdxs = seq(rowsOfCnts[r].length).map(i => rowBrks![r]+i);
+ let empVerts = nodeIdxs.map(idx => childLyts[idx].dims[1] - childLyts[idx].usedDims[1]);
+ let minEmpVert = Math.min(...empVerts);
+ nodeIdxs.forEach(idx => {
+ childLyts[idx].dims[1] -= minEmpVert;
+ childLyts[idx].empSpc -= minEmpVert * childLyts[idx].dims[0];
+ });
+ if (r < rowBrks.length-1){
+ cellYs[r+1] -= minEmpVert;
+ cellHs[r+1] += minEmpVert;
+ } else {
+ empVTotal = minEmpVert;
+ }
+ }
+ //other updates
+ empSpc += empHorzTotal * childLyts[rowBrks[r]].dims[1];
+ }
+ //get empty-space
+ for (let r = 0; r < rowBrks.length; r++){
+ for (let c = 0; c < rowsOfCnts[r].length; c++){
+ empSpc += childLyts[rowBrks[r]+c].empSpc;
+ }
+ }
+ empSpc += empVTotal * availW;
+ //check with best-so-far
+ if (empSpc < lowestEmp){
+ lowestEmp = empSpc;
+ rowBreaks = [...rowBrks];
+ childLayouts = childLyts;
+ }
+ }
+ if (rowBreaks == null || childLayouts == null) //redundant hinting for tsc
+ return null;
+ //make no-child tiles have width/height fitting their content
+ childLayouts.filter(l => l.children.length == 0).forEach(l => {
+ l.dims[0] = l.usedDims[0];
+ l.dims[1] = l.usedDims[1];
+ });
+ //determine layout
+ let newNode = new LayoutNode(node.tolNode, childLayouts, pos, dims,
+ {showHeader, usedDims: dims, empSpc: lowestEmp});
+ //trying to shrink usedDims causes problems with swept-to-parent-area div-alignment
+ childLayouts.forEach(n => n.parent = newNode);
+ return newNode;
+}
+//lays out nodes by pushing leaves to one side, partially using other layouts for children
+let sweepLayoutFn: LayoutFn = function (node, pos, dims, showHeader, opts, ownOpts={sepSweptArea: null}){
+ //separate leaf and non-leaf nodes
+ let leaves: LayoutNode[] = [], nonLeaves: LayoutNode[] = [];
+ node.children.forEach(n => (n.children.length == 0 ? leaves : nonLeaves).push(n));
+ //determine layout
+ let tempTree: LayoutNode;
+ if (nonLeaves.length == 0){
+ return sqrLayoutFn(node, pos, dims, showHeader, opts);
+ } else if (leaves.length == 0){
+ return rectLayoutFn(node, pos, dims, showHeader, opts, {subLayoutFn:sweepLayoutFn});
+ } else {
+ let ratio = leaves.length / (leaves.length + nonLeaves.map(n => n.dCount).reduce((x,y) => x+y));
+ let headerSz = showHeader ? opts.headerSz : 0;
+ let sweptLayout = null, nonLeavesLayout = null, sweptLeft = false;
+ //get swept-area layout
+ let parentArea = ownOpts && ownOpts.sepSweptArea, usingParentArea = false;
+ if (opts.sweepingToParent && parentArea){
+ tempTree = new LayoutNode(new TolNode('SWEEP_' + node.tolNode.name), leaves);
+ //not updating the children to point to tempTree as a parent seems acceptable here
+ sweptLeft = parentArea.sweptLeft;
+ sweptLayout = sqrLayoutFn(tempTree, [0,0], parentArea.dims, !sweptLeft, opts);
+ if (sweptLayout != null){
+ //move leaves to parent area
+ sweptLayout.children.map(n => {
+ n.pos[0] += parentArea!.pos[0];
+ n.pos[1] += parentArea!.pos[1];
+ });
+ //get remaining-area layout
+ let newDims: [number,number] = [dims[0], dims[1] - (sweptLeft ? headerSz : 0)];
+ tempTree = new LayoutNode(new TolNode('SWEEP_REM_' + node.tolNode.name), nonLeaves);
+ if (nonLeaves.length > 1){
+ nonLeavesLayout = rectLayoutFn(tempTree, [0,0], newDims, false, opts,
+ {subLayoutFn: sweepLayoutFn});
+ } else {
+ //get leftover swept-layout-area to propagate
+ let leftOverWidth = parentArea.dims[0] - sweptLayout.usedDims[0];
+ let leftOverHeight = parentArea.dims[1] - sweptLayout.usedDims[1];
+ let leftoverArea = sweptLeft ?
+ new SepSweptArea(
+ [parentArea.pos[0], parentArea.pos[1]+sweptLayout.usedDims[1]-opts.tileSpacing-headerSz],
+ [parentArea.dims[0], leftOverHeight-opts.tileSpacing], sweptLeft) :
+ new SepSweptArea(
+ [parentArea.pos[0]+sweptLayout.usedDims[0]-opts.tileSpacing, parentArea.pos[1]+headerSz],
+ [leftOverWidth-opts.tileSpacing, parentArea.dims[1]-headerSz], sweptLeft);
+ //generate layout
+ nonLeavesLayout = rectLayoutFn(tempTree, [0,0], newDims, false, opts,
+ {subLayoutFn: (n,p,d,h,o) => sweepLayoutFn(n,p,d,h,o,{sepSweptArea:leftoverArea})});
+ }
+ if (nonLeavesLayout != null){
+ nonLeavesLayout.children.forEach(layout => {layout.pos[1] += (sweptLeft ? headerSz : 0)});
+ usingParentArea = true;
+ }
+ }
+ }
+ if (!usingParentArea){
+ let newDims: [number,number] = [dims[0], dims[1]-headerSz];
+ tempTree = new LayoutNode(new TolNode('SWEEP_' + node.tolNode.name), leaves);
+ let xyChg: [number,number];
+ //get swept-area layout
+ let leftLayout = null, topLayout = null;
+ let documentAR = document.documentElement.clientWidth / document.documentElement.clientHeight;
+ if (opts.sweepMode=='left' || (opts.sweepMode=='shorter' && documentAR >= 1) || opts.sweepMode=='auto'){
+ leftLayout = sqrLayoutFn(tempTree, [0,0],
+ [Math.max(newDims[0]*ratio, opts.minTileSz+opts.tileSpacing*2), newDims[1]], false, opts);
+ }
+ if (opts.sweepMode=='top' || (opts.sweepMode=='shorter' && documentAR < 1) || opts.sweepMode=='auto'){
+ topLayout = sqrLayoutFn(tempTree, [0,0],
+ [newDims[0], Math.max(newDims[1]*ratio, opts.minTileSz+opts.tileSpacing*2)], false, opts);
+ }
+ if (opts.sweepMode == 'auto'){
+ sweptLayout =
+ (leftLayout && topLayout && ((leftLayout.empSpc < topLayout.empSpc) ? leftLayout : topLayout)) ||
+ leftLayout || topLayout;
+ } else {
+ sweptLayout = leftLayout || topLayout;
+ }
+ sweptLeft = (sweptLayout == leftLayout);
+ if (sweptLayout == null)
+ return null;
+ sweptLayout.children.forEach(layout => {layout.pos[1] += headerSz});
+ //get remaining-area layout
+ if (sweptLeft){
+ xyChg = [sweptLayout.usedDims[0] - opts.tileSpacing, 0];
+ newDims[0] += -sweptLayout.usedDims[0] + opts.tileSpacing;
+ } else {
+ xyChg = [0, sweptLayout.usedDims[1] - opts.tileSpacing];
+ newDims[1] += -sweptLayout.usedDims[1] + opts.tileSpacing;
+ }
+ tempTree = new LayoutNode(new TolNode('SWEEP_REM_' + node.tolNode.name), nonLeaves);
+ if (nonLeaves.length > 1){
+ nonLeavesLayout = rectLayoutFn(tempTree, [0,0], newDims, false, opts, {subLayoutFn:sweepLayoutFn});
+ } else {
+ //get leftover swept-layout-area to propagate
+ let leftoverArea : SepSweptArea;
+ if (sweptLeft){
+ leftoverArea = new SepSweptArea( //pos is relative to the non-leaves-area
+ [-sweptLayout.usedDims[0]+opts.tileSpacing, sweptLayout.usedDims[1]-opts.tileSpacing],
+ [sweptLayout.usedDims[0]-opts.tileSpacing*2,
+ newDims[1]-sweptLayout.usedDims[1]-opts.tileSpacing],
+ sweptLeft
+ );
+ } else {
+ leftoverArea = new SepSweptArea(
+ [sweptLayout.usedDims[0]-opts.tileSpacing, -sweptLayout.usedDims[1]+opts.tileSpacing],
+ [newDims[0]-sweptLayout.usedDims[0]-opts.tileSpacing,
+ sweptLayout.usedDims[1]-opts.tileSpacing*2],
+ sweptLeft
+ );
+ }
+ leftoverArea.dims[0] = Math.max(0, leftoverArea.dims[0]);
+ leftoverArea.dims[1] = Math.max(0, leftoverArea.dims[1]);
+ //generate layout
+ nonLeavesLayout = rectLayoutFn(tempTree, [0,0], newDims, false, opts,
+ {subLayoutFn: (n,p,d,h,o) => sweepLayoutFn(n,p,d,h,o,{sepSweptArea:leftoverArea})});
+ }
+ if (nonLeavesLayout == null)
+ return null;
+ nonLeavesLayout.children.forEach(layout => {
+ layout.pos[0] += xyChg[0];
+ layout.pos[1] += xyChg[1] + headerSz;
+ });
+ }
+ if (sweptLayout == null || nonLeavesLayout == null) //hint for tsc
+ return null;
+ //return combined layouts
+ let children = leaves.concat(nonLeaves);
+ let layouts = sweptLayout.children.concat(nonLeavesLayout.children);
+ let layoutsInOldOrder = seq(node.children.length)
+ .map(i => children.findIndex(n => n == node.children[i]))
+ .map(i => layouts[i]);
+ let newNode = new LayoutNode(node.tolNode, layoutsInOldOrder, pos, dims, {
+ showHeader,
+ usedDims: [
+ usingParentArea ? nonLeavesLayout.usedDims[0] : (sweptLeft ?
+ sweptLayout.usedDims[0] + nonLeavesLayout.usedDims[0] - opts.tileSpacing :
+ Math.max(sweptLayout.usedDims[0], nonLeavesLayout.usedDims[0])),
+ usingParentArea ? nonLeavesLayout.usedDims[1] + headerSz : (sweptLeft ?
+ Math.max(sweptLayout.usedDims[1], nonLeavesLayout.usedDims[1]) + headerSz :
+ sweptLayout.usedDims[1] + nonLeavesLayout.usedDims[1] - opts.tileSpacing + headerSz),
+ ],
+ empSpc: sweptLayout.empSpc + nonLeavesLayout.empSpc,
+ sepSweptArea: (usingParentArea && parentArea) ? parentArea : null,
+ });
+ layoutsInOldOrder.forEach(n => n.parent = newNode);
+ return newNode;
+ }
+}
+
+//clips values in array to within [min,max], and redistributes to compensate, returning null if unable
+function limitVals(arr: number[], min: number, max: number): number[]|null {
+ let vals = [...arr];
+ let clipped = arrayOf(false, vals.length);
+ let owedChg = 0;
+ while (true){
+ for (let i = 0; i < vals.length; i++){
+ if (clipped[i])
+ continue;
+ if (vals[i] < min){
+ owedChg += vals[i] - min;
+ vals[i] = min;
+ clipped[i] = true;
+ } else if (vals[i] > max){
+ owedChg += vals[i] - max;
+ vals[i] = max;
+ clipped[i] = true;
+ }
+ }
+ if (Math.abs(owedChg) < Number.EPSILON)
+ return vals;
+ let indicesToUpdate;
+ if (owedChg > 0){
+ indicesToUpdate = vals.reduce(
+ (arr: number[], n, i) => {if (n < max) arr.push(i); return arr;},
+ []);
+ } else {
+ indicesToUpdate = vals.reduce(
+ (arr: number[], n, i) => {if (n > min) arr.push(i); return arr;},
+ []);
+ }
+ if (indicesToUpdate.length == 0)
+ return null;
+ for (let i of indicesToUpdate){
+ vals[i] += owedChg / indicesToUpdate.length;
+ }
+ owedChg = 0;
+ }
+}
+function arrayOf(val: any, len: number){ //returns an array of 'len' 'val's
+ return Array(len).fill(val);
+}
+function seq(len: number){ //returns [0, ..., len]
+ return [...Array(len).keys()];
+}