aboutsummaryrefslogtreecommitdiff
path: root/src/layout.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout.js')
-rw-r--r--src/layout.js468
1 files changed, 0 insertions, 468 deletions
diff --git a/src/layout.js b/src/layout.js
deleted file mode 100644
index 4a714fa..0000000
--- a/src/layout.js
+++ /dev/null
@@ -1,468 +0,0 @@
-export {staticSqrLayout, staticRectLayout, sweepToSideLayout, layoutInfoHooks};
-
-const TILE_SPACING = 5;
-const HEADER_SZ = 20;
-const MIN_TILE_SZ = 50;
-const MAX_TILE_SZ = 200;
-const RECT_MODE = 'auto'; //'horz', 'vert', 'linear', 'auto'
-const SWEEP_MODE = 'left'; //'left', 'top', 'shorter', 'auto'
-const ALLOW_SWEEP_TO_PARENT = true;
-const RECT_SPC_SHIFTING = true;
-
-class LayoutNode {
- constructor({name, x, y, w, h, headerSz, children, contentW, contentH, empSpc, sideArea, postProcessData}){
- this.name = name;
- this.x = x;
- this.y = y;
- this.w = w;
- this.h = h;
- this.headerSz = headerSz;
- this.children = children;
- this.contentW = contentW;
- this.contentH = contentH;
- this.empSpc = empSpc;
- this.sideArea = sideArea;
- this.postProcessData = postProcessData;
- }
-}
-
-const layoutInfoHooks = { //made common-across-layout-types for layout inter-usability
- initLayoutInfo(tree){
- if (tree.children.length > 0){
- tree.children.forEach(e => this.initLayoutInfo(e));
- }
- this.updateLayoutInfo(tree);
- },
- updateLayoutInfoOnExpand(nodeList){ //given list of tree-nodes from expanded_child-to-parent, update layout-info
- nodeList[0].children.forEach(this.updateLayoutInfo);
- for (let node of nodeList){
- this.updateLayoutInfo(node);
- }
- },
- updateLayoutInfoOnCollapse(nodeList){ //given list of tree-nodes from child_to_collapse-to-parent, update layout-info
- for (let node of nodeList){
- this.updateLayoutInfo(node);
- }
- },
- updateLayoutInfo(tree){
- if (tree.children.length == 0){
- tree.tileCount = 1;
- } else {
- tree.tileCount = tree.children.map(e => e.tileCount).reduce((x,y) => x+y);
- }
- }
-}
-
-//lays out nodes as squares in a rectangle, with spacing
-function staticSqrLayout(node, x, y, w, h, hideHeader){
- //get number-of-columns with lowest leftover empty space
- let headerSz = (hideHeader ? 0 : HEADER_SZ);
- let availW = w - TILE_SPACING, availH = h - headerSz - TILE_SPACING;
- if (availW*availH <= 0)
- return null;
- let numChildren = node.children.length, ar = availW/availH;
- let lowestEmp = Number.POSITIVE_INFINITY, numCols, numRows, tileSize;
- 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-TILE_SPACING : availW/nc-TILE_SPACING;
- if (tileSz < MIN_TILE_SZ)
- continue;
- else if (tileSz > MAX_TILE_SZ)
- tileSz = MAX_TILE_SZ;
- let empSpc = (1-frac)*availW*availH + (nc*nr-numChildren)*(tileSz - TILE_SPACING)**2;
- if (empSpc < lowestEmp){
- lowestEmp = empSpc;
- numCols = nc;
- numRows = nr;
- tileSize = tileSz;
- }
- }
- if (lowestEmp == Number.POSITIVE_INFINITY)
- return null;
- let childLayouts = arrayOf(0, numChildren);
- for (let i = 0; i < numChildren; i++){
- let childX = TILE_SPACING + (i % numCols)*(tileSize + TILE_SPACING);
- let childY = TILE_SPACING + headerSz + Math.floor(i / numCols)*(tileSize + TILE_SPACING);
- if (node.children[i].children.length == 0){
- childLayouts[i] = {
- x: childX, y: childY, w: tileSize, h: tileSize, headerSz: 0,
- children: [],
- contentW: tileSize, contentH: tileSize, empSpc: 0,
- }
- } else {
- childLayouts[i] = staticSqrLayout(node.children[i], childX, childY, tileSize, tileSize, false);
- if (childLayouts[i] == null)
- return null;
- lowestEmp += childLayouts[i].empSpc;
- }
- }
- return new LayoutNode({
- name: node.tolNode.name,
- x: x, y: y, w: w, h: h, headerSz: headerSz,
- children: childLayouts,
- contentW: numCols * (tileSize + TILE_SPACING) + TILE_SPACING,
- contentH: numRows * (tileSize + TILE_SPACING) + TILE_SPACING + headerSz,
- empSpc: lowestEmp,
- postProcessData: {type: 'staticSqr'},
- });
-}
-//lays out nodes as rectangles organised into rows, partially using other layouts for children
-function staticRectLayout(node, x, y, w, h, hideHeader, subLayoutGen = staticRectLayout){
- if (node.children.every(n => n.children.length == 0))
- return staticSqrLayout(node, x, y, w, h, hideHeader);
- //find grid-arrangement with lowest leftover empty space
- let headerSz = (hideHeader ? 0 : HEADER_SZ);
- let availW = w - TILE_SPACING, availH = h - TILE_SPACING - headerSz;
- let numChildren = node.children.length;
- let rowBrks = null; //will holds node indices at which each row starts
- let lowestEmp = Number.POSITIVE_INFINITY, rowBreaks, rowsOfCounts, childLayouts;
- rowBrksLoop:
- while (true){
- //update rowBrks or exit loop
- if (rowBrks == null){
- if (RECT_MODE == 'vert'){
- rowBrks = seq(numChildren);
- } else {
- rowBrks = [0];
- }
- } else {
- if (RECT_MODE == 'horz' || RECT_MODE == 'vert'){
- break rowBrksLoop;
- } else if (RECT_MODE == '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' tileCounts
- let rowsOfCnts = arrayOf(0, 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].tileCount);
- }
- //get cell dims
- let totalTileCount = node.children.map(n => n.tileCount).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, MIN_TILE_SZ, 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),
- MIN_TILE_SZ, 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(0, 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] + TILE_SPACING, childY = cellYs[r] + TILE_SPACING + headerSz,
- childW = cellWs[nodeIdx] - TILE_SPACING, childH = cellHs[r] - TILE_SPACING;
- if (child.children.length == 0){
- let contentSz = Math.min(childW, childH);
- childLyts[nodeIdx] = {
- x: childX, y: childY, w: childW, h: childH, headerSz: 0,
- children: [],
- contentW: contentSz, contentH: contentSz, empSpc: childW*childH - contentSz**2,
- };
- } else if (child.children.every(n => n.children.length == 0)){
- childLyts[nodeIdx] = staticSqrLayout(child, childX, childY, childW, childH, false);
- } else {
- childLyts[nodeIdx] = subLayoutGen(child, childX, childY, childW, childH, false);
- }
- if (childLyts[nodeIdx] == null)
- continue rowBrksLoop;
- //handle horizontal empty-space-shifting
- if (RECT_SPC_SHIFTING){
- let empHorz = childLyts[nodeIdx].w - childLyts[nodeIdx].contentW;
- childLyts[nodeIdx].w -= empHorz;
- childLyts[nodeIdx].empSpc -= empHorz * childLyts[nodeIdx].h;
- if (c < rowsOfCnts[r].length-1){
- cellXs[nodeIdx+1] -= empHorz;
- cellWs[nodeIdx+1] += empHorz;
- } else {
- empHorzTotal = empHorz;
- }
- }
- }
- //handle vertical empty-space-shifting
- if (RECT_SPC_SHIFTING){
- let nodeIdxs = seq(rowsOfCnts[r].length).map(i => rowBrks[r]+i);
- let empVerts = nodeIdxs.map(idx => childLyts[idx].h - childLyts[idx].contentH);
- let minEmpVert = Math.min(...empVerts);
- nodeIdxs.forEach(idx => {
- childLyts[idx].h -= minEmpVert;
- childLyts[idx].empSpc -= minEmpVert * childLyts[idx].w;
- });
- if (r < rowBrks.length-1){
- cellYs[r+1] -= minEmpVert;
- cellHs[r+1] += minEmpVert;
- } else {
- empVTotal = minEmpVert;
- }
- }
- //other updates
- empSpc += empHorzTotal * childLyts[rowBrks[r]].h;
- }
- //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];
- rowsOfCounts = rowsOfCnts;
- childLayouts = childLyts;
- }
- }
- if (rowBreaks == null)
- return null;
- //make no-child tiles have width/height fitting their content
- childLayouts.filter(l => l.children.length == 0).forEach(l => {l.w = l.contentW; l.h = l.contentH;});
- //determine layout
- return new LayoutNode({
- name: node.tolNode.name,
- x: x, y: y, w: w, h: h, headerSz: headerSz,
- children: childLayouts,
- contentW: w, //trying to shrink this causes problems with swept-to-parent-area div-alignment
- contentH: h,
- empSpc: lowestEmp,
- postProcessData: {type: 'staticRect', rowBreaks, rowsOfCounts, childLayouts},
- });
-}
-//lays out nodes by pushing leaves to one side, partially using other layouts for children
-function sweepToSideLayout(node, x, y, w, h, hideHeader, parentArea = null){
- //separate leaf and non-leaf nodes
- let leaves = [], nonLeaves = [];
- node.children.forEach(n => (n.children.length == 0 ? leaves : nonLeaves).push(n));
- //determine layout
- let tempTree;
- if (nonLeaves.length == 0){ //if all leaves, use squares-layout
- return staticSqrLayout(node, x, y, w, h, hideHeader);
- } else if (leaves.length == 0){
- tempTree = {tolNode: {name: 'SWEEP_REM_' + node.tolNode.name}, children: nonLeaves};
- return staticRectLayout(tempTree, x, y, w, h, hideHeader, sweepToSideLayout);
- } else {
- let ratio = leaves.length / (leaves.length + nonLeaves.map(e => e.tileCount).reduce((x,y) => x+y));
- let headerSz = (hideHeader ? 0 : HEADER_SZ);
- let sweptLayout = null, nonLeavesLayout, sweptLeft = false, leftOverArea;
- //get swept-area layout
- let usingParentArea = false;
- if (ALLOW_SWEEP_TO_PARENT && parentArea != null){
- tempTree = {tolNode: {name: 'SWEEP_' + node.tolNode.name}, children: leaves};
- sweptLeft = parentArea.sweptLeft;
- sweptLayout = staticSqrLayout(tempTree, 0, 0, parentArea.w, parentArea.h, sweptLeft);
- if (sweptLayout != null){
- let area = {x: x, y: y+headerSz, w: w, h: h-headerSz};
- if (!sweptLeft){ //no remaining-area header if swept-upward
- area.y = y; area.h = h;
- }
- //get remaining-area layout
- tempTree = {tolNode: {name: 'SWEEP_REM_' + node.tolNode.name}, children: nonLeaves};
- if (nonLeaves.length > 1){
- nonLeavesLayout = staticRectLayout(tempTree, 0, 0, area.w, area.h, true, sweepToSideLayout);
- } else {
- //get leftover swept-layout-area to propagate
- let leftOverWidth = parentArea.w - sweptLayout.contentW;
- let leftOverHeight = parentArea.h - sweptLayout.contentH;
- leftOverArea = sweptLeft ?
- {...parentArea, parentY:parentArea.parentY+sweptLayout.contentH-TILE_SPACING-headerSz,
- h:leftOverHeight-TILE_SPACING} :
- {...parentArea,
- parentX:parentArea.parentX+sweptLayout.contentW-TILE_SPACING,
- parentY:parentArea.parentY + headerSz,
- w:leftOverWidth-TILE_SPACING, h:parentArea.h - headerSz};
- //call genLayout
- nonLeavesLayout = staticRectLayout(
- tempTree, 0, 0, area.w, area.h, true,
- (n,x,y,w,h,hh) => sweepToSideLayout(n,x,y,w,h,hh,leftOverArea));
- }
- if (nonLeavesLayout != null){
- nonLeavesLayout.children.forEach(layout => {layout.y += (sweptLeft ? headerSz : 0)});
- usingParentArea = true;
- }
- }
- }
- if (!usingParentArea){
- let area = {x: x, y: y+headerSz, w: w, h: h-headerSz};
- tempTree = {tolNode: {name: 'SWEEP_' + node.tolNode.name}, children: leaves};
- let xyChg;
- //get swept-area layout
- let leftLayout = null, topLayout = null;
- let documentAR = document.documentElement.clientWidth / document.documentElement.clientHeight;
- if (SWEEP_MODE == 'left' || (SWEEP_MODE == 'shorter' && documentAR >= 1) || SWEEP_MODE == 'auto'){
- leftLayout = staticSqrLayout(tempTree, 0, 0,
- Math.max(area.w*ratio, MIN_TILE_SZ+TILE_SPACING*2), area.h, true);
- } else if (SWEEP_MODE == 'top' || (SWEEP_MODE == 'shorter' && documentAR < 1) || SWEEP_MODE == 'auto'){
- topLayout = staticSqrLayout(tempTree, 0, 0,
- area.w, Math.max(area.h*ratio, MIN_TILE_SZ+TILE_SPACING*2), true);
- }
- if (SWEEP_MODE == '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.y += headerSz});
- //get remaining-area layout
- if (sweptLeft){
- xyChg = [sweptLayout.contentW - TILE_SPACING, 0];
- area.w += -sweptLayout.contentW + TILE_SPACING;
- } else {
- xyChg = [0, sweptLayout.contentH - TILE_SPACING];
- area.h += -sweptLayout.contentH + TILE_SPACING;
- }
- tempTree = {tolNode: {name: 'SWEEP_REM_' + node.tolNode.name}, children: nonLeaves};
- if (nonLeaves.length > 1){
- nonLeavesLayout = staticRectLayout(tempTree, 0, 0, area.w, area.h, true, sweepToSideLayout);
- } else {
- //get leftover swept-layout-area to propagate
- if (sweptLeft){
- leftOverArea = { //parentX and parentY are relative to the non-leaves-area
- parentX: -sweptLayout.contentW + TILE_SPACING, parentY: sweptLayout.contentH - TILE_SPACING,
- w: sweptLayout.contentW - TILE_SPACING*2, h: area.h-sweptLayout.contentH - TILE_SPACING,
- };
- } else {
- leftOverArea = {
- parentX: sweptLayout.contentW - TILE_SPACING, parentY: -sweptLayout.contentH + TILE_SPACING,
- w: area.w-sweptLayout.contentW - TILE_SPACING, h: sweptLayout.contentH - TILE_SPACING*2
- };
- }
- leftOverArea.w = Math.max(0, leftOverArea.w);
- leftOverArea.h = Math.max(0, leftOverArea.h);
- leftOverArea.sweptLeft = sweptLeft;
- //call genLayout
- nonLeavesLayout = staticRectLayout(
- tempTree, 0, 0, area.w, area.h, true,
- (n,x,y,w,h,hh) => sweepToSideLayout(n,x,y,w,h,hh,leftOverArea));
- }
- if (nonLeavesLayout == null)
- return null;
- nonLeavesLayout.children.forEach(layout => {layout.x += xyChg[0]; layout.y += xyChg[1] + headerSz;});
- }
- //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]);
- return new LayoutNode({
- name: node.tolNode.name,
- x: x, y: y, w: w, h: h, headerSz: headerSz,
- children: layoutsInOldOrder,
- contentW: usingParentArea ? nonLeavesLayout.contentW : (sweptLeft ?
- sweptLayout.contentW + nonLeavesLayout.contentW - TILE_SPACING :
- Math.max(sweptLayout.contentW, nonLeavesLayout.contentW)),
- contentH: usingParentArea ? nonLeavesLayout.contentH + headerSz : (sweptLeft ?
- Math.max(sweptLayout.contentH, nonLeavesLayout.contentH) + headerSz :
- sweptLayout.contentH + nonLeavesLayout.contentH - TILE_SPACING + headerSz),
- empSpc: sweptLayout.empSpc + nonLeavesLayout.empSpc,
- sideArea: !usingParentArea ? null : {
- x: parentArea.parentX, y: parentArea.parentY,
- w: parentArea.w, h: parentArea.h,
- sweptLeft: sweptLeft, extraSz: TILE_SPACING+1,
- },
- postProcessData: {type: 'sweepToSide', nonLeavesData: nonLeavesLayout.postProcessData},
- });
- }
-}
-
-//clips values in array to within [min,max], and redistributes to compensate, returning null if unable
-function limitVals(arr, min, max){
- 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,n,i) => {if (n < max) arr.push(i); return arr;},
- []);
- } else {
- indicesToUpdate = vals.reduce(
- (arr,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, len){ //returns an array of 'len' 'val's
- return Array(len).fill(val);
-}
-function seq(len){ //returns [0, ..., len]
- return [...Array(len).keys()];
-}