diff options
Diffstat (limited to 'src/layout.js')
| -rw-r--r-- | src/layout.js | 468 |
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()]; -} |
