diff options
Diffstat (limited to 'src/layout.js')
| -rw-r--r-- | src/layout.js | 368 |
1 files changed, 167 insertions, 201 deletions
diff --git a/src/layout.js b/src/layout.js index b713e29..1fa1f2d 100644 --- a/src/layout.js +++ b/src/layout.js @@ -1,45 +1,50 @@ -export {defaultLayout, initTree}; +export {staticSqrLayout, staticRectLayout, sweepToSideLayout}; -const DEFAULT_TILE_SPACING = 5; -const DEFAULT_HEADER_SZ = 20; +const TILE_SPACING = 5; +const HEADER_SZ = 20; -const staticSqrLayout = { //determines layout for squares in a specified rectangle, with spacing - TILE_SPACING: DEFAULT_TILE_SPACING, - HEADER_SZ: DEFAULT_HEADER_SZ, - genLayout(nodes, x0, y0, w, h, hideHeader){ - //get number-of-columns with highest occupied-fraction of rectangles with aspect-ratio w/h - //account for tile-spacing?, account for parent-box-border?, cache results?, - let hOffset = (hideHeader ? 0 : this.HEADER_SZ); - let availW = w - this.TILE_SPACING, availH = h - hOffset - this.TILE_SPACING; - let numTiles = nodes.length, ar = availW/availH; - let numCols, numRows, bestFrac = 0, tileSz, empSpc; - for (let nc = 1; nc <= numTiles; nc++){ - let nr = Math.ceil(numTiles/nc); +const staticSqrLayout = { //lays out nodes as squares in a rectangle, with spacing + genLayout(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; + 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; - if (frac > bestFrac){ - bestFrac = frac; + let tileSz = ar > ar2 ? availH/nr-TILE_SPACING : availW/nc-TILE_SPACING; + let empSpc = (1-frac)*availW*availH + (nc*nr-numChildren)*(tileSz - TILE_SPACING)**2; + if (empSpc < lowestEmp){ + lowestEmp = empSpc; numCols = nc; numRows = nr; - tileSz = ar > ar2 ? availH/numRows-this.TILE_SPACING : availW/numCols-this.TILE_SPACING; - empSpc = (1 - frac) * availW * availH; - empSpc += (nc*nr - numTiles) * tileSz**2; + tileSize = tileSz; + } + } + let childLayouts = Array(numChildren).fill(); + 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] = this.genLayout(node.children[i], childX, childY, tileSize, tileSize, false); + lowestEmp += childLayouts[i].empSpc; } } - //determine layout return { - coords: Object.fromEntries( - nodes.map((el, idx) => [el.tolNode.name, { - x: x0 + this.TILE_SPACING + (idx % numCols)*(tileSz + this.TILE_SPACING), - y: y0 + this.TILE_SPACING + hOffset + Math.floor(idx / numCols)*(tileSz + this.TILE_SPACING), - w: tileSz, - h: tileSz - }]) - ), - contentW: numCols * (tileSz + this.TILE_SPACING) + this.TILE_SPACING, - contentH: numRows * (tileSz + this.TILE_SPACING) + this.TILE_SPACING + hOffset, - empSpc: empSpc - }; + 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, + } }, initLayoutInfo(tree){ return; @@ -52,28 +57,47 @@ const staticSqrLayout = { //determines layout for squares in a specified rectang } }; const staticRectLayout = { - TILE_SPACING: DEFAULT_TILE_SPACING, - HEADER_SZ: DEFAULT_HEADER_SZ, - genLayout(nodes, x0, y0, w, h, hideHeader){ - if (nodes.every(e => e.children.length == 0)){ - return staticSqrLayout.genLayout(nodes, x0, y0, w, h, hideHeader); - } - //if a node has children, find 'best' grid-arrangement - let hOffset = (hideHeader ? 0 : this.HEADER_SZ); - let availW = w - this.TILE_SPACING, availH = h - this.TILE_SPACING - hOffset; - let numChildren = nodes.length; - let rowBrks = [0]; //holds node indices at which each row starts - let rowBreaks, lowestEmp = Number.POSITIVE_INFINITY, rowsOfCounts, cellWidths, cellHeights, nodeDims; + genLayout(node, x, y, w, h, hideHeader, subLayout = staticRectLayout){ + if (node.children.every(n => n.children.length == 0)) + return staticSqrLayout.genLayout(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){ + rowBrks = [0]; + } 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 = Array.from({length: rowBrks.length+1}, (x,i) => i); + } else { + break rowBrksLoop; + } + break; + } + } + } //create list-of-lists representing each row's cells' tileCounts let rowsOfCnts = Array(rowBrks.length).fill(); for (let r = 0; r < rowBrks.length; r++){ let numNodes = (r == rowBrks.length-1) ? numChildren-rowBrks[r] : rowBrks[r+1]-rowBrks[r]; let rowNodeIdxs = Array.from({length: numNodes}, (x,i) => i+rowBrks[r]); - rowsOfCnts[r] = rowNodeIdxs.map(idx => nodes[idx].tileCount); + rowsOfCnts[r] = rowNodeIdxs.map(idx => node.children[idx].tileCount); } //get cell dims - let totalTileCount = nodes.map(e => e.tileCount).reduce((x,y) => x+y); + 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 = Array(numChildren).fill(); for (let r = 0; r < rowsOfCnts.length; r++){ @@ -82,56 +106,61 @@ const staticRectLayout = { cellWs[rowBrks[r]+c] = rowsOfCnts[r][c] / rowCount * availW; } } - //get node dims and empty-space - let emptySpace = 0; - let nodeDs = Array(numChildren).fill(); + //get cell x/y coords + let cellXs = Array(cellWs.length).fill(0); for (let r = 0; r < rowBrks.length; r++){ - for (let c = 0; c < rowsOfCnts[r].length; c++){ + for (let c = 1; c < rowsOfCnts[r].length; c++){ let nodeIdx = rowBrks[r]+c; - let res = nodes[nodeIdx].layoutRes(cellWs[nodeIdx], cellHs[r]); - emptySpace += res.empSpc; - nodeDs[nodeIdx] = [res.contentW, res.contentH]; + cellXs[nodeIdx] = cellXs[nodeIdx-1] + cellWs[nodeIdx-1]; } } - if (emptySpace < lowestEmp){ - lowestEmp = emptySpace; - rowBreaks = [...rowBrks]; - rowsOfCounts = rowsOfCnts; - cellWidths = cellWs; - cellHeights = cellHs; - nodeDims = nodeDs; + let cellYs = Array(cellHs.length).fill(0); + for (let r = 1; r < rowBrks.length; r++){ + cellYs[r] = cellYs[r-1] + cellHs[r-1]; } - //update rowBrks or exit loop - let i = rowBrks.length-1, exitLoop = false; - 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 = Array.from({length: rowBrks.length+1}, (x,i) => i); + //get child layouts and empty-space + let childLyts = Array(numChildren).fill(), empSpc = 0; + for (let r = 0; r < rowBrks.length; r++){ + 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: contentSz, h: contentSz, 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.genLayout(child, childX, childY, childW, childH, false); } else { - exitLoop = true; + childLyts[nodeIdx] = subLayout.genLayout(child, childX, childY, childW, childH, false); } - break; + empSpc += childLyts[nodeIdx].empSpc; } } - if (exitLoop) - break; + //check with best-so-far + if (empSpc < lowestEmp){ + lowestEmp = empSpc; + rowBreaks = [...rowBrks]; + rowsOfCounts = rowsOfCnts; + childLayouts = childLyts; + } } //for each row, shift empty right-space to rightmost cell let minEmpHorzTotal = Number.POSITIVE_INFINITY; for (let r = 0; r < rowBreaks.length; r++){ - let empHorzTotal = 0; + let empHorzTotal = 0, leftShiftTotal = 0; for (let c = 0; c < rowsOfCounts[r].length - 1; c++){ let nodeIdx = rowBreaks[r] + c; - let empHorz = cellWidths[nodeIdx] - nodeDims[nodeIdx][0]; - cellWidths[nodeIdx] -= empHorz; + let empHorz = childLayouts[nodeIdx].w - childLayouts[nodeIdx].contentW; + childLayouts[nodeIdx].w -= empHorz; empHorzTotal += empHorz; + childLayouts[nodeIdx+1].x -= empHorzTotal; } - cellWidths[rowBreaks[r] + rowsOfCounts[r].length - 1] += empHorzTotal; + childLayouts[rowBreaks[r] + rowsOfCounts[r].length - 1].w += empHorzTotal; if (empHorzTotal < minEmpHorzTotal) minEmpHorzTotal = empHorzTotal; } @@ -139,43 +168,24 @@ const staticRectLayout = { let empVertTotal = 0; for (let r = 0; r < rowBreaks.length - 1; r++){ let nodeIdxs = Array.from({length: rowsOfCounts[r].length}, (x,i) => rowBreaks[r] + i); - let empVerts = nodeIdxs.map(idx => cellHeights[r] - nodeDims[idx][1]); + nodeIdxs.forEach(idx => childLayouts[idx].y -= empVertTotal); + let empVerts = nodeIdxs.map(idx => childLayouts[idx].h - childLayouts[idx].contentH); let minEmpVert = Math.min(...empVerts); - cellHeights[r] -= minEmpVert; + nodeIdxs.forEach(idx => childLayouts[idx].h -= minEmpVert); empVertTotal += minEmpVert; } - cellHeights[rowBreaks.length - 1] += empVertTotal; + let lastRowIdx = rowBreaks.length-1; + let lastNodeIdxs = Array.from({length: rowsOfCounts[lastRowIdx].length}, (x,i) => rowBreaks[lastRowIdx] + i); + lastNodeIdxs.forEach(idx => childLayouts[idx].y -= empVertTotal); + lastNodeIdxs.map(idx => childLayouts[idx].h += empVertTotal); //determine layout - let cellHorzPoints = Array(cellWidths.length).fill(0); - for (let r = 0; r < rowBreaks.length; r++){ - for (let c = 1; c < rowsOfCounts[r].length; c++){ - let nodeIdx = rowBreaks[r]+c; - cellHorzPoints[nodeIdx] = cellHorzPoints[nodeIdx-1] + cellWidths[nodeIdx-1]; - } - } - let cellVertPoints = Array(cellHeights.length).fill(0); - for (let r = 1; r < rowBreaks.length; r++){ - cellVertPoints[r] = cellVertPoints[r-1] + cellHeights[r-1]; - } return { - coords: Object.fromEntries( - nodes.map((el, idx) => { - let cellW = cellWidths[idx]; - let rowIdx = rowBreaks.findIndex((e,i) => i==rowBreaks.length-1 || rowBreaks[i+1] > idx); - let cellH = cellHeights[rowIdx]; - let cellAR = cellW / cellH; - return [el.tolNode.name, { - x: x0 + this.TILE_SPACING + cellHorzPoints[idx], - y: y0 + this.TILE_SPACING + cellVertPoints[rowIdx] + hOffset, - w: (el.children.length == 0 ? (cellAR>1 ? cellW * 1/cellAR : cellW) : cellW) - this.TILE_SPACING, - h: (el.children.length == 0 ? (cellAR>1 ? cellH : cellH * cellAR) : cellH) - this.TILE_SPACING - }]; - }) - ), + x: x, y: y, w: w, h: h, headerSz: headerSz, + children: childLayouts, contentW: w - minEmpHorzTotal, contentH: h - empVertTotal, - empSpc: lowestEmp - }; + empSpc: lowestEmp, + } }, initLayoutInfo(tree){ if (tree.children.length > 0){ @@ -197,75 +207,65 @@ const staticRectLayout = { updateLayoutInfo(tree){ if (tree.children.length == 0){ tree.tileCount = 1; - tree.layoutRes = (w, h) => { - let ar = w / h; - let ar2 = 1; - let frac = ar > ar2 ? ar2/ar : ar/ar2; - return { - empSpc: (1 - frac) * w * h, - contentW: ar > ar2 ? frac * w : w, - contentH: ar > ar2 ? h : frac * h - }; - }; } else { tree.tileCount = tree.children.map(e => e.tileCount).reduce((x,y) => x+y); - if (tree.children.every(e => e.children.length == 0)){ - tree.layoutRes = (w, h) => { - let layout = staticSqrLayout.genLayout(tree.children, 0, 0, w, h, tree.hideHeader); - return {empSpc: layout.empSpc, contentW: layout.contentW, contentH: layout.contentH}; - }; - } else { - tree.layoutRes = (w, h) => { - let layout = staticRectLayout.genLayout(tree.children, 0, 0, w, h, tree.hideHeader); - return {empSpc: layout.empSpc, contentW: layout.contentW, contentH: layout.contentH}; - }; - } } } }; const sweepToSideLayout = { - TILE_SPACING: DEFAULT_TILE_SPACING, - HEADER_SZ: DEFAULT_HEADER_SZ, - genLayout(nodes, x0, y0, w, h, hideHeader){ + genLayout(node, x, y, w, h, hideHeader){ //separate leaf and non-leaf nodes let leaves = [], nonLeaves = []; - nodes.forEach(e => (e.children.length == 0 ? leaves : nonLeaves).push(e)); + 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.genLayout(nodes, x0, y0, w, h, hideHeader); - } else { //if some non-leaves, use rect-layout - if (leaves.length == 0){ - return staticRectLayout.genLayout(nonLeaves, x0, y0, w, h, hideHeader); + return staticSqrLayout.genLayout(node, x, y, w, h, hideHeader); + } else if (leaves.length == 0){ + tempTree = {tolNode: null, children: nonLeaves}; + return staticRectLayout.genLayout(tempTree, x, y, w, h, hideHeader); + } else { + let ratio = leaves.length / (leaves.length + nonLeaves.map(e => e.tileCount).reduce((x,y) => x+y)); + let headerSz = (hideHeader ? 0 : HEADER_SZ); + //get swept-area layout + let area = {x: x, y: y+headerSz, w: w, h: h-headerSz}; + tempTree = {tolNode: null, children: leaves}; + let leftLayout = staticSqrLayout.genLayout(tempTree, area.x, area.y, area.w*ratio, area.h, true); + let topLayout = staticSqrLayout.genLayout(tempTree, area.x, area.y, area.w, area.h*ratio, true); + //let sweptLayout = leftLayout; + let sweptLayout = (leftLayout.empSpc < topLayout.empSpc) ? leftLayout : topLayout; + sweptLayout.children.forEach(layout => {layout.y += headerSz}); + //get remaining-area layout + let xyChg; + if (sweptLayout == leftLayout){ + xyChg = [sweptLayout.contentW - TILE_SPACING, 0]; + area.w += -sweptLayout.contentW + TILE_SPACING; } else { - let ratio = leaves.length / (leaves.length + nonLeaves.map(e => e.tileCount).reduce((x,y) => x+y)); - let hOffset = (hideHeader ? 0 : this.HEADER_SZ); - //get swept-area layout - let area = {x: x0, y: y0+hOffset, w: w, h: h-hOffset}; - let leftLayout = staticSqrLayout.genLayout(leaves, area.x, area.y, area.w*ratio, area.h, true); - let topLayout = staticSqrLayout.genLayout(leaves, area.x, area.y, area.w, area.h*ratio, true); - //let sweptLayout = leftLayout; - let sweptLayout = (leftLayout.empSpc < topLayout.empSpc) ? leftLayout : topLayout; - //get remaining-area layout - if (sweptLayout == leftLayout){ - area.x += sweptLayout.contentW - this.TILE_SPACING; - area.w += -sweptLayout.contentW + this.TILE_SPACING; - } else { - area.y += sweptLayout.contentH - this.TILE_SPACING; - area.h += -sweptLayout.contentH + this.TILE_SPACING; - } - let nonLeavesLayout = staticRectLayout.genLayout(nonLeaves, area.x, area.y, area.w, area.h, true); - //return combined layout - return { - coords: {...sweptLayout.coords, ...nonLeavesLayout.coords}, - contentW: (sweptLayout == leftLayout) ? - sweptLayout.contentW + nonLeavesLayout.contentW - this.TILE_SPACING : - Math.max(sweptLayout.contentW, nonLeavesLayout.contentW), - contentH: (sweptLayout == leftLayout) ? - Math.max(sweptLayout.contentH, nonLeavesLayout.contentH) : - sweptLayout.contentH + nonLeavesLayout.contentH - this.TILE_SPACING, - empSpc: sweptLayout.empSpc + nonLeavesLayout.empSpc - }; + xyChg = [0, sweptLayout.contentH - TILE_SPACING]; + area.h += -sweptLayout.contentH + TILE_SPACING; } + tempTree = {tolNode: null, children: nonLeaves} + let nonLeavesLayout = staticRectLayout.genLayout( + tempTree, area.x, area.y, area.w, area.h, true, sweepToSideLayout); + 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 = [...Array(node.children.length).keys()] + .map(i => children.findIndex(n => n == node.children[i])) + .map(i => layouts[i]); + return { + x: x, y: y, w: w, h: h, headerSz: headerSz, + //children: [...sweptLayout.children, ...nonLeavesLayout.children], + children: layoutsInOldOrder, + contentW: (sweptLayout == leftLayout) ? + sweptLayout.contentW + nonLeavesLayout.contentW - TILE_SPACING : + Math.max(sweptLayout.contentW, nonLeavesLayout.contentW), + contentH: (sweptLayout == leftLayout) ? + Math.max(sweptLayout.contentH, nonLeavesLayout.contentH) : + sweptLayout.contentH + nonLeavesLayout.contentH - TILE_SPACING, + empSpc: sweptLayout.empSpc + nonLeavesLayout.empSpc + }; } }, initLayoutInfo(tree){ @@ -288,43 +288,9 @@ const sweepToSideLayout = { updateLayoutInfo(tree){ if (tree.children.length == 0){ tree.tileCount = 1; - tree.layoutRes = (w, h) => { - let ar = w / h; - let ar2 = 1; - let frac = ar > ar2 ? ar2/ar : ar/ar2; - return { - empSpc: (1 - frac) * w * h, - contentW: ar > ar2 ? frac * w : w, - contentH: ar > ar2 ? h : frac * h - }; - }; } else { tree.tileCount = tree.children.map(e => e.tileCount).reduce((x,y) => x+y); - if (tree.children.every(e => e.children.length == 0)){ - tree.layoutRes = (w, h) => { - let layout = staticSqrLayout.genLayout(tree.children, 0, 0, w, h, tree.hideHeader); - return {empSpc: layout.empSpc, contentW: layout.contentW, contentH: layout.contentH}; - } - } else { - tree.layoutRes = (w, h) => { - let layout = sweepToSideLayout.genLayout(tree.children, 0, 0, w, h, tree.hideHeader); - return {empSpc: layout.empSpc, contentW: layout.contentW, contentH: layout.contentH}; - } - } } } }; -let defaultLayout = sweepToSideLayout; -function initTree(tol, lvl, layout = defaultLayout){ - let tree = {tolNode: tol, children: []}; - initTreeRec(tree, lvl); - layout.initLayoutInfo(tree) - return tree; -} -function initTreeRec(tree, lvl){ - if (lvl > 0){ - tree.children = tree.tolNode.children.map(e => initTreeRec({tolNode: e, children: []}, lvl-1)); - } - return tree; -} |
