diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Tile.vue | 33 | ||||
| -rw-r--r-- | src/components/TileTree.vue | 110 | ||||
| -rw-r--r-- | src/layout.ts | 354 | ||||
| -rw-r--r-- | src/types.ts | 42 |
4 files changed, 281 insertions, 258 deletions
diff --git a/src/components/Tile.vue b/src/components/Tile.vue index 09d6128..3ca02ce 100644 --- a/src/components/Tile.vue +++ b/src/components/Tile.vue @@ -1,43 +1,44 @@ <script lang="ts"> import {defineComponent, PropType} from 'vue'; -import {LayoutNode} from '../types'; +import {LayoutNode} from '../layout'; -const TRANSITION_DURATION = 300; export default defineComponent({ name: 'tile', data(){ return { zIdx: 0, - transitionDuration: TRANSITION_DURATION, overFlow: 'visible', } }, props: { layoutNode: {type: Object as PropType<LayoutNode>, required: true}, + transitionDuration: {type: Number, required: true}, + headerSz: {type: Number, required: true}, + tileSpacing: {type: Number, required: true}, }, computed: { name(){return this.layoutNode.tolNode.name.replaceAll('\'', '\\\'')} }, methods: { onImgClick(){ - this.$emit('tile-clicked', [this.layoutNode]); + this.$emit('tile-clicked', this.layoutNode); //increase z-index and hide overflow during transition this.zIdx = 1; this.overFlow = 'hidden'; setTimeout(() => {this.zIdx = 0; this.overFlow = 'visible'}, this.transitionDuration); }, - onInnerTileClicked(nodeList: LayoutNode[]){ - this.$emit('tile-clicked', [...nodeList, this.layoutNode]); + onInnerTileClicked(node: LayoutNode){ + this.$emit('tile-clicked', node); }, onHeaderClick(){ - this.$emit('header-clicked', [this.layoutNode]); + this.$emit('header-clicked', this.layoutNode); //increase z-index and hide overflow during transition this.zIdx = 1; this.overFlow = 'hidden'; setTimeout(() => {this.zIdx = 0; this.overFlow = 'visible'}, this.transitionDuration); }, - onInnerHeaderClicked(nodeList: LayoutNode[]){ - this.$emit('header-clicked', [...nodeList, this.layoutNode]); + onInnerHeaderClicked(node: LayoutNode){ + this.$emit('header-clicked', node); } } }) @@ -57,32 +58,30 @@ export default defineComponent({ /> <div v-else> <div - v-if="(layoutNode.headerSz && !layoutNode.sepSweptArea) || + v-if="(layoutNode.showHeader && !layoutNode.sepSweptArea) || (layoutNode.sepSweptArea && layoutNode.sepSweptArea.sweptLeft)" - :style="{height: layoutNode.headerSz+'px'}" + :style="{height: headerSz+'px'}" class="text-center hover:cursor-pointer bg-stone-300" @click="onHeaderClick"> {{layoutNode.tolNode.name}} </div> <div v-if="layoutNode.sepSweptArea" :style="{position: 'absolute', left: layoutNode.sepSweptArea.pos[0]+'px', top: layoutNode.sepSweptArea.pos[1]+'px', - width: (layoutNode.sepSweptArea.dims[0] + - (layoutNode.sepSweptArea.sweptLeft ? layoutNode.sepSweptArea.tileSpacing+1 : 0))+'px', - height: (layoutNode.sepSweptArea.dims[1] + - (layoutNode.sepSweptArea.sweptLeft ? 0 : layoutNode.sepSweptArea.tileSpacing+1))+'px', + width: (layoutNode.sepSweptArea.dims[0]+(layoutNode.sepSweptArea.sweptLeft ? tileSpacing+1 : 0))+'px', + height: (layoutNode.sepSweptArea.dims[1]+(layoutNode.sepSweptArea.sweptLeft ? 0 : tileSpacing+1))+'px', borderRightColor: (layoutNode.sepSweptArea.sweptLeft ? 'white' : 'currentColor'), borderBottomColor: (layoutNode.sepSweptArea.sweptLeft ? 'currentColor' : 'white'), transitionDuration: transitionDuration+'ms'}" class="transition-[left,top,width,height] ease-out border border-stone-900 bg-white"> - <div v-if="!layoutNode.sepSweptArea.sweptLeft" :style="{height: layoutNode.headerSz+'px'}" + <div v-if="!layoutNode.sepSweptArea.sweptLeft" :style="{height: headerSz+'px'}" class="text-center hover:cursor-pointer bg-stone-300" @click="onHeaderClick"> {{layoutNode.tolNode.name}} </div> </div> <tile v-for="child in layoutNode.children" :key="child.tolNode.name" :layoutNode="child" + :headerSz="headerSz" :tileSpacing="tileSpacing" :transitionDuration="transitionDuration" @tile-clicked="onInnerTileClicked" @header-clicked="onInnerHeaderClicked" ></tile> </div> </div> </template> - diff --git a/src/components/TileTree.vue b/src/components/TileTree.vue index d662f50..0e5b793 100644 --- a/src/components/TileTree.vue +++ b/src/components/TileTree.vue @@ -2,12 +2,7 @@ import {defineComponent} from 'vue'; import Tile from './Tile.vue'; -import {TolNode, LayoutNode} from '../types'; -import {genLayout, layoutInfoHooks} from '../layout'; -//regarding importing a file f1.ts: - //using 'import f1.ts' makes vue-tsc complain, and 'import f1.js' makes vite complain - //using 'import f1' might cause problems with build systems other than vite - +import {TolNode} from '../types'; import tol from '../tol.json'; function preprocessTol(tree: any): void { if (!tree.children){ @@ -18,96 +13,67 @@ function preprocessTol(tree: any): void { } preprocessTol(tol); +import {LayoutTree, LayoutNode} from '../layout'; +import type {LayoutOptions} from '../layout'; +//regarding importing a file f1.ts: + //using 'import f1.ts' makes vue-tsc complain, and 'import f1.js' makes vite complain + //using 'import f1' might cause problems with build systems other than vite + +let defaultLayoutOptions: LayoutOptions = { + tileSpacing: 5, + headerSz: 20, + minTileSz: 50, + maxTileSz: 200, + layoutType: 'sweep', //'sqr' | 'rect' | 'sweep' + rectMode: 'auto', //'horz' | 'vert' | 'linear' | 'auto' + rectSpaceShifting: true, + sweepMode: 'left', //'left' | 'top' | 'shorter' | 'auto' + sweepingToParent: true, +}; +let defaultOtherOptions = { + transitionDuration: 300, +}; + export default defineComponent({ data(){ return { - layoutTree: this.initLayoutTree(tol as TolNode, 1), + layoutOptions: defaultLayoutOptions, + otherOptions: defaultOtherOptions, + layoutTree: new LayoutTree(tol as TolNode, 1, defaultLayoutOptions), width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, resizeThrottled: false, } }, methods: { - initLayoutTree(tol: TolNode, lvl: number): LayoutNode { - let node = new LayoutNode(tol, []); - function initRec(node: LayoutNode, lvl: number){ - if (lvl > 0) - node.children = node.tolNode.children.map( - (n: TolNode) => initRec(new LayoutNode(n, []), lvl-1)); - return node; - } - initRec(node, lvl); - layoutInfoHooks.initLayoutInfo(node) - return node; - }, onResize(){ if (!this.resizeThrottled){ this.width = document.documentElement.clientWidth; this.height = document.documentElement.clientHeight; - this.tryLayout(); + if (!this.layoutTree.tryLayout([0,0], [this.width,this.height])) + console.log('Unable to layout tree'); //prevent re-triggering until after a delay this.resizeThrottled = true; setTimeout(() => {this.resizeThrottled = false;}, 100); } }, - onInnerTileClicked(nodeList: LayoutNode[]){ - //nodeList is an array of layout-nodes, from the clicked-on-tile's node upward - let numNewTiles = nodeList[0].tolNode.children.length; - if (numNewTiles == 0){ + onInnerTileClicked(node: LayoutNode){ + if (node.tolNode.children.length == 0){ console.log('Tile-to-expand has no children'); return; } - //add children - nodeList[0].children = nodeList[0].tolNode.children.map((n: TolNode) => new LayoutNode(n, [])); - layoutInfoHooks.updateLayoutInfoOnExpand(nodeList); - //try to re-layout - if (!this.tryLayout()){ - nodeList[0].children = []; - layoutInfoHooks.updateLayoutInfoOnCollapse(nodeList); - } - }, - onInnerHeaderClicked(nodeList: LayoutNode[]){ - //nodeList is an array of layout-nodes, from the clicked-on-tile's node upward - let children = nodeList[0].children; - nodeList[0].children = []; - layoutInfoHooks.updateLayoutInfoOnCollapse(nodeList); - if (!this.tryLayout()){ - nodeList[0].children = children; - layoutInfoHooks.updateLayoutInfoOnExpand(nodeList); - } + if (!this.layoutTree.tryLayoutOnExpand([0,0], [this.width,this.height], node)) + console.log('Unable to layout tree'); }, - tryLayout(){ - let newLayout = genLayout(this.layoutTree, [0,0], [this.width,this.height], true); - if (newLayout == null){ + onInnerHeaderClicked(node: LayoutNode){ + if (!this.layoutTree.tryLayoutOnCollapse([0,0], [this.width,this.height], node)) console.log('Unable to layout tree'); - return false; - } else { - this.applyLayout(newLayout, this.layoutTree); - return true; - } }, - applyLayout(newLayout: LayoutNode, layoutTree: LayoutNode){ - layoutTree.pos = newLayout.pos; - layoutTree.dims = newLayout.dims; - layoutTree.headerSz = newLayout.headerSz; - newLayout.children.forEach((n,i) => this.applyLayout(n, layoutTree.children[i])); - //handle case where leaf nodes placed in leftover space from parent-sweep - if (newLayout.sepSweptArea != null){ - //add parent area coords - layoutTree.sepSweptArea = newLayout.sepSweptArea; - //move leaf node children to parent area - layoutTree.children.filter(n => n.children.length == 0).map(n => { - n.pos[0] += newLayout.sepSweptArea!.pos[0], - n.pos[1] += newLayout.sepSweptArea!.pos[1] - }); - } else { - layoutTree.sepSweptArea = null; - } - } }, created(){ window.addEventListener('resize', this.onResize); - this.tryLayout(); + if (!this.layoutTree.tryLayout([0,0], [this.width,this.height])) + console.log('Unable to layout tree'); }, unmounted(){ window.removeEventListener('resize', this.onResize); @@ -120,7 +86,9 @@ export default defineComponent({ <template> <div class="h-[100vh]"> - <tile :layoutNode="layoutTree" @tile-clicked="onInnerTileClicked" @header-clicked="onInnerHeaderClicked"></tile> + <tile :layoutNode="layoutTree.root" + :headerSz="layoutOptions.headerSz" :tileSpacing="layoutOptions.tileSpacing" + :transitionDuration="otherOptions.transitionDuration" + @tile-clicked="onInnerTileClicked" @header-clicked="onInnerHeaderClicked"></tile> </div> </template> - diff --git a/src/layout.ts b/src/layout.ts index 95e15d0..961083b 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -1,52 +1,141 @@ -import {TolNode, LayoutNode, SepSweptArea} from './types'; -export {genLayout, layoutInfoHooks}; +import {TolNode} from './types'; -type LayoutFn = (node: LayoutNode, pos: [number, number], dims: [number, number], hideHeader: boolean, - options?: {subLayoutFn?: LayoutFn, sepSweptArea?: SepSweptArea|null}) => LayoutNode | null; - -let TILE_SPACING = 5; -let HEADER_SZ = 20; -let MIN_TILE_SZ = 50; -let MAX_TILE_SZ = 200; -let RECT_MODE = 'auto'; //'horz', 'vert', 'linear', 'auto' -let SWEEP_MODE = 'left'; //'left', 'top', 'shorter', 'auto' -let ALLOW_SWEEP_TO_PARENT = true; -let RECT_SPC_SHIFTING = true; - -const layoutInfoHooks = { //made common-across-layout-types for layout inter-usability - initLayoutInfo(node: LayoutNode){ - if (node.children.length > 0){ - node.children.forEach((n: LayoutNode) => this.initLayoutInfo(n)); +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, []); } - this.updateLayoutInfo(node); - }, - updateLayoutInfoOnExpand(nodeList: LayoutNode[]){ - //given list of layout-nodes from expanded_child-to-parent, update layout-info - nodeList[0].children.forEach(this.updateLayoutInfo); - for (let node of nodeList){ - this.updateLayoutInfo(node); + } + tryLayout(pos: [number,number], dims: [number,number]){ + let newLayout: LayoutNode | null; + switch (this.options.layoutType){ + case 'sqr': newLayout = sqrLayoutFn(this.root, pos, dims, false, this.options); break; + case 'rect': newLayout = rectLayoutFn(this.root, pos, dims, false, this.options); break; + case 'sweep': newLayout = sweepLayoutFn(this.root, pos, dims, false, this.options); break; } - }, - updateLayoutInfoOnCollapse(nodeList: LayoutNode[]){ - //given list of layout-nodes from child_to_collapse-to-parent, update layout-info - for (let node of nodeList){ - this.updateLayoutInfo(node); + 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); } - }, - updateLayoutInfo(node: LayoutNode){ - if (node.children.length == 0){ - node.tileCount = 1; - } else { - node.tileCount = node.children.map(n => n.tileCount).reduce((x,y) => x+y); + 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; + //showHeader: 'all' | 'non-root' | 'expanded' | 'expanded non-root' | 'leaf' | 'none'? + 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, tileSpacing: number){ + 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, hideHeader){ +let sqrLayoutFn: LayoutFn = function (node, pos, dims, showHeader, opts){ //get number-of-columns with lowest leftover empty space - let headerSz = (hideHeader ? 0 : HEADER_SZ); - let availW = dims[0] - TILE_SPACING, availH = dims[1] - headerSz - TILE_SPACING; + 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; @@ -55,12 +144,12 @@ let sqrLayoutFn: LayoutFn = function (node, pos, dims, hideHeader){ 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) + let tileSz = ar > ar2 ? availH/nr-opts.tileSpacing : availW/nc-opts.tileSpacing; + if (tileSz < opts.minTileSz) continue; - else if (tileSz > MAX_TILE_SZ) - tileSz = MAX_TILE_SZ; - let empSpc = (1-frac)*availW*availH + (nc*nr-numChildren)*(tileSz - TILE_SPACING)**2; + 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; @@ -70,35 +159,37 @@ let sqrLayoutFn: LayoutFn = function (node, pos, dims, hideHeader){ } if (lowestEmp == Number.POSITIVE_INFINITY) return null; - let childLayouts = arrayOf(0, numChildren); + let childLayouts = arrayOf(null, numChildren); for (let i = 0; i < numChildren; i++){ let child = node.children[i]; - let childX = TILE_SPACING + (i % numCols)*(tileSize + TILE_SPACING); - let childY = TILE_SPACING + headerSz + Math.floor(i / numCols)*(tileSize + TILE_SPACING); + let childX = opts.tileSpacing + (i % numCols)*(tileSize + opts.tileSpacing); + let childY = opts.tileSpacing + headerSz + Math.floor(i / numCols)*(tileSize + opts.tileSpacing); if (child.children.length == 0){ childLayouts[i] = new LayoutNode(child.tolNode, [], [childX,childY], [tileSize,tileSize], - {headerSz: 0, usedDims: [tileSize,tileSize], empSpc: 0}); + {usedDims: [tileSize,tileSize], empSpc: 0}); } else { - childLayouts[i] = sqrLayoutFn(child, [childX,childY], [tileSize,tileSize], false); + childLayouts[i] = sqrLayoutFn(child, [childX,childY], [tileSize,tileSize], true, opts); if (childLayouts[i] == null) return null; lowestEmp += childLayouts[i].empSpc; } } - return new LayoutNode(node.tolNode, childLayouts, pos, dims, { - headerSz, - usedDims: [numCols * (tileSize + TILE_SPACING) + TILE_SPACING, - numRows * (tileSize + TILE_SPACING) + TILE_SPACING + headerSz], + 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, hideHeader, options={subLayoutFn: rectLayoutFn}){ +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, hideHeader); + return sqrLayoutFn(node, pos, dims, showHeader, opts); //find grid-arrangement with lowest leftover empty space - let headerSz = (hideHeader ? 0 : HEADER_SZ); - let availW = dims[0] - TILE_SPACING, availH = dims[1] - TILE_SPACING - headerSz; + 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; @@ -106,19 +197,20 @@ let rectLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, options={sub while (true){ //update rowBrks or exit loop if (rowBrks == null){ - if (RECT_MODE == 'vert'){ + if (opts.rectMode == 'vert'){ rowBrks = seq(numChildren); } else { rowBrks = [0]; } } else { - if (RECT_MODE == 'horz' || RECT_MODE == 'vert'){ + if (opts.rectMode == 'horz' || opts.rectMode == 'vert'){ break rowBrksLoop; - } else if (RECT_MODE == 'linear'){ - if (rowBrks.length == 1 && numChildren > 1) + } else if (opts.rectMode == 'linear'){ + if (rowBrks.length == 1 && numChildren > 1){ rowBrks = seq(numChildren); - else + } else { break rowBrksLoop; + } } else { let i = rowBrks.length-1; while (true){ @@ -138,15 +230,15 @@ let rectLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, options={sub } } } - //create list-of-lists representing each row's cells' tileCounts + //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].tileCount); + rowsOfCnts[r] = rowNodeIdxs.map(idx => node.children[idx].dCount); } //get cell dims - let totalTileCount = node.children.map(n => n.tileCount).reduce((x,y) => x+y); + 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++){ @@ -156,12 +248,12 @@ let rectLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, options={sub } } //impose min-tile-size - cellHs = limitVals(cellHs, MIN_TILE_SZ, Number.POSITIVE_INFINITY)!; + 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), - MIN_TILE_SZ, Number.POSITIVE_INFINITY); + opts.minTileSz, Number.POSITIVE_INFINITY); if (temp == null) continue rowBrksLoop; cellWs.splice(rowBrks[r], rowsOfCnts[r].length, ...temp); @@ -179,29 +271,29 @@ let rectLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, options={sub cellYs[r] = cellYs[r-1] + cellHs[r-1]; } //get child layouts and empty-space - let childLyts = arrayOf(0, numChildren); + 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] + TILE_SPACING, childY = cellYs[r] + TILE_SPACING + headerSz, - childW = cellWs[nodeIdx] - TILE_SPACING, childH = cellHs[r] - TILE_SPACING; + 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], - {headerSz: 0, usedDims: [contentSz,contentSz], empSpc: childW*childH - contentSz**2}); + {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], false); + childLyts[nodeIdx] = sqrLayoutFn(child, [childX,childY], [childW,childH], true, opts); } else { - let layoutFn = (options && options.subLayoutFn) || rectLayoutFn; - childLyts[nodeIdx] = layoutFn(child, [childX,childY], [childW,childH], false); + 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 (RECT_SPC_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]; @@ -214,7 +306,7 @@ let rectLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, options={sub } } //handle vertical empty-space-shifting - if (RECT_SPC_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); @@ -254,54 +346,60 @@ let rectLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, options={sub l.dims[1] = l.usedDims[1]; }); //determine layout - return new LayoutNode(node.tolNode, childLayouts, pos, dims, - {headerSz, usedDims: dims, empSpc: lowestEmp}); + 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 sweepLeavesLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, options={sepSweptArea: null}){ +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){ //if all leaves, use squares-layout - return sqrLayoutFn(node, pos, dims, hideHeader); + if (nonLeaves.length == 0){ + return sqrLayoutFn(node, pos, dims, showHeader, opts); } else if (leaves.length == 0){ - tempTree = new LayoutNode(new TolNode('SWEEP_REM_' + node.tolNode.name), nonLeaves); - return rectLayoutFn(tempTree, pos, dims, hideHeader, {subLayoutFn: sweepLeavesLayoutFn}); + return rectLayoutFn(node, pos, dims, showHeader, opts, {subLayoutFn:sweepLayoutFn}); } else { - let ratio = leaves.length / (leaves.length + nonLeaves.map(n => n.tileCount).reduce((x,y) => x+y)); - let headerSz = (hideHeader ? 0 : HEADER_SZ); + 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 = options && options.sepSweptArea, usingParentArea = false; - if (ALLOW_SWEEP_TO_PARENT && parentArea){ + 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); + sweptLayout = sqrLayoutFn(tempTree, [0,0], parentArea.dims, sweptLeft, opts); if (sweptLayout != null){ - let newDims: [number,number] = [dims[0], dims[1]-headerSz]; - if (!sweptLeft) //no remaining-area header if swept-upward - newDims[1] = dims[1]; + //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, true, {subLayoutFn: sweepLeavesLayoutFn}); + 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]-TILE_SPACING-headerSz], - [parentArea.dims[0], leftOverHeight-TILE_SPACING], sweptLeft, TILE_SPACING) : + [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]-TILE_SPACING, parentArea.pos[1] + headerSz], - [leftOverWidth-TILE_SPACING, parentArea.dims[1] - headerSz], sweptLeft, TILE_SPACING); - //call genLayout - nonLeavesLayout = rectLayoutFn(tempTree, [0,0], newDims, true, - {subLayoutFn: (n,p,d,h) => sweepLeavesLayoutFn(n,p,d,h,{sepSweptArea:leftoverArea})}); + [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)}); @@ -312,18 +410,19 @@ let sweepLeavesLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, optio 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]; + let xyChg: [number,number]; //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'){ + if (opts.sweepMode=='left' || (opts.sweepMode=='shorter' && documentAR >= 1) || opts.sweepMode=='auto'){ leftLayout = sqrLayoutFn(tempTree, [0,0], - [Math.max(newDims[0]*ratio, MIN_TILE_SZ+TILE_SPACING*2), newDims[1]], true); - } else if (SWEEP_MODE == 'top' || (SWEEP_MODE == 'shorter' && documentAR < 1) || SWEEP_MODE == 'auto'){ + [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, MIN_TILE_SZ+TILE_SPACING*2)], true); + [newDims[0], Math.max(newDims[1]*ratio, opts.minTileSz+opts.tileSpacing*2)], false, opts); } - if (SWEEP_MODE == 'auto'){ + if (opts.sweepMode == 'auto'){ sweptLayout = (leftLayout && topLayout && ((leftLayout.empSpc < topLayout.empSpc) ? leftLayout : topLayout)) || leftLayout || topLayout; @@ -336,37 +435,38 @@ let sweepLeavesLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, optio sweptLayout.children.forEach(layout => {layout.pos[1] += headerSz}); //get remaining-area layout if (sweptLeft){ - xyChg = [sweptLayout.usedDims[0] - TILE_SPACING, 0]; - newDims[0] += -sweptLayout.usedDims[0] + TILE_SPACING; + xyChg = [sweptLayout.usedDims[0] - opts.tileSpacing, 0]; + newDims[0] += -sweptLayout.usedDims[0] + opts.tileSpacing; } else { - xyChg = [0, sweptLayout.usedDims[1] - TILE_SPACING]; - newDims[1] += -sweptLayout.usedDims[1] + TILE_SPACING; + 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, true, {subLayoutFn: sweepLeavesLayoutFn}); + 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]+TILE_SPACING, sweptLayout.usedDims[1]-TILE_SPACING], - [sweptLayout.usedDims[0]-TILE_SPACING*2, newDims[1]-sweptLayout.usedDims[1]-TILE_SPACING], - sweptLeft, TILE_SPACING + [-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]-TILE_SPACING, -sweptLayout.usedDims[1]+TILE_SPACING], - [newDims[0]-sweptLayout.usedDims[0]-TILE_SPACING, sweptLayout.usedDims[1]-TILE_SPACING*2], - sweptLeft, TILE_SPACING + [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]); - //call genLayout - nonLeavesLayout = rectLayoutFn( - tempTree, [0,0], newDims, true, - {subLayoutFn: (n,p,d,h) => sweepLeavesLayoutFn(n,p,d,h,{sepSweptArea:leftoverArea})}); + //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; @@ -383,25 +483,23 @@ let sweepLeavesLayoutFn: LayoutFn = function (node, pos, dims, hideHeader, optio let layoutsInOldOrder = seq(node.children.length) .map(i => children.findIndex(n => n == node.children[i])) .map(i => layouts[i]); - return new LayoutNode(node.tolNode, layoutsInOldOrder, pos, dims, { - headerSz, + let newNode = new LayoutNode(node.tolNode, layoutsInOldOrder, pos, dims, { + showHeader, usedDims: [ usingParentArea ? nonLeavesLayout.dims[0] : (sweptLeft ? - sweptLayout.dims[0] + nonLeavesLayout.dims[0] - TILE_SPACING : + sweptLayout.dims[0] + nonLeavesLayout.dims[0] - opts.tileSpacing : Math.max(sweptLayout.dims[0], nonLeavesLayout.dims[0])), usingParentArea ? nonLeavesLayout.dims[1] + headerSz : (sweptLeft ? Math.max(sweptLayout.dims[1], nonLeavesLayout.dims[1]) + headerSz : - sweptLayout.dims[1] + nonLeavesLayout.dims[1] - TILE_SPACING + headerSz), + sweptLayout.dims[1] + nonLeavesLayout.dims[1] - opts.tileSpacing + headerSz), ], empSpc: sweptLayout.empSpc + nonLeavesLayout.empSpc, sepSweptArea: (usingParentArea && parentArea) ? parentArea : null, }); + layoutsInOldOrder.forEach(n => n.parent = newNode); + return newNode; } } -//default layout function -let genLayout: LayoutFn = function (node, pos, dims, hideHeader){ - return sweepLeavesLayoutFn(node, pos, dims, hideHeader); -} //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 { diff --git a/src/types.ts b/src/types.ts index cc5de25..87097a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,45 +6,3 @@ export class TolNode { this.children = children; } } -export class LayoutNode { - //set by TileTree and LayoutFn funcs, eventually used by Tile - tolNode: TolNode; - children: LayoutNode[]; - pos: [number, number]; - dims: [number, number]; - headerSz: number; - //set by layoutInfoHooks, used by LayoutFn funcs - tileCount: number; - //set_by/internal_to LayoutFn funcs - usedDims: [number, number]; - empSpc: number; - //set by LayoutFn funcs, eventually used by Tile - sepSweptArea: SepSweptArea | null; - // - constructor( - tolNode: TolNode, children: LayoutNode[], pos:[number,number]=[0,0], dims:[number,number]=[0,0], - {headerSz=0, tileCount=0, usedDims=[0,0] as [number,number], - empSpc=0, sepSweptArea=null as SepSweptArea|null} = {}){ - this.tolNode = tolNode; - this.children = children; - this.pos = pos; - this.dims = dims; - this.headerSz = headerSz; - this.tileCount = tileCount; - this.usedDims = usedDims; - this.empSpc = empSpc; - this.sepSweptArea = sepSweptArea; - } -} -export class SepSweptArea { - pos: [number, number]; - dims: [number, number]; - sweptLeft: boolean; - tileSpacing: number; - constructor(pos: [number, number], dims: [number, number], sweptLeft: boolean, tileSpacing: number){ - this.pos = pos; - this.dims = dims; - this.sweptLeft = sweptLeft; - this.tileSpacing = tileSpacing; - } -} |
