diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.vue | 2 | ||||
| -rw-r--r-- | src/components/SettingsModal.vue | 17 | ||||
| -rw-r--r-- | src/layout.ts | 358 |
3 files changed, 192 insertions, 185 deletions
diff --git a/src/App.vue b/src/App.vue index 50c2ae4..5cf2e95 100644 --- a/src/App.vue +++ b/src/App.vue @@ -55,7 +55,7 @@ const defaultLytOpts: LayoutOptions = { rectMode: 'auto first-row', //'horz' | 'vert' | 'linear' | 'auto' | 'auto first-row' sweepMode: 'left', //'left' | 'top' | 'shorter' | 'auto' sweptNodesPrio: 'linear', //'linear' | 'sqrt' | 'pow-2/3' - sweepToParent: true, + sweepToParent: 'auto', //'never' | 'always' | 'auto' }; const defaultUiOpts = { // For tiles diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index 1f56cfb..20864fd 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -107,8 +107,21 @@ export default defineComponent({ </div> <hr class="border-stone-400"/> <div> - <label> <input type="checkbox" v-model="lytOpts.sweepToParent" - @change="onLytOptChg"/> Sweep to parent</label> + Sweep to parent + <ul> + <li> + <label> <input type="radio" v-model="lytOpts.sweepToParent" value="never" + @change="onLytOptChg"/> Never </label> + </li> + <li> + <label> <input type="radio" v-model="lytOpts.sweepToParent" value="always" + @change="onLytOptChg"/> Always </label> + </li> + <li> + <label> <input type="radio" v-model="lytOpts.sweepToParent" value="auto" + @change="onLytOptChg"/> Auto </label> + </li> + </ul> </div> <hr class="border-stone-400"/> <div> diff --git a/src/layout.ts b/src/layout.ts index e76a943..1eba33b 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -174,7 +174,7 @@ export type LayoutOptions = { // Rect-layout in 1 row, 1 column, 1 row or column, or multiple rows (optionally with first-row-heuristic) sweepMode: 'left' | 'top' | 'shorter' | 'auto'; // Sweep to left, top, shorter-side, or to minimise empty space sweptNodesPrio: 'linear' | 'sqrt' | 'pow-2/3'; // Specifies allocation of space to swept-vs-remaining nodes - sweepToParent: boolean; // Allow swept nodes to occupy empty space in a parent's swept-leaves area + sweepToParent: 'never' | 'always' | 'auto'; // Allow placing swept nodes in a parent swept-leaves area }; // Represents a change to a LayoutNode tree export type LayoutTreeChg = { @@ -399,9 +399,9 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, let lowestEmpSpc = Number.POSITIVE_INFINITY; let usedTree: LayoutNode | null = null; // Best-so-far layout let usedEmpRight = 0, usedEmpBottom = 0; // usedTree's empty-space at-right-of-all-rows and below-last-row - const minCellDims = [ // Can situationally assume non-leaf children + const minCellDims = [ opts.minTileSz + opts.tileSpacing + - (opts.layoutType == 'sweep' ? opts.tileSpacing*2 : 0), + (opts.layoutType == 'sweep' ? opts.tileSpacing*2 : 0), // Can situationally assume non-leaf children opts.minTileSz + opts.tileSpacing + (opts.layoutType == 'sweep' ? opts.tileSpacing*2 + opts.headerSz : 0) ]; @@ -606,8 +606,125 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse let headerSz = showHeader ? opts.headerSz : 0; let leavesLyt: LayoutNode | null = null, nonLeavesLyt: LayoutNode | null = null, sweptLeft = false; let sepArea: SepSweptArea | null = null, sepAreaUsed = false; // Represents leaf-section area provided for a child + // Try laying-out normally + if (ownOpts == null || ownOpts.sepArea == null || opts.sweepToParent != 'always'){ + // Choose proportion of area to use for leaves + let ratio: number; // area-for-leaves / area-for-non-leaves + let nonLeavesTiles = arraySum(nonLeaves.map(n => n.dCount)); + switch (opts.sweptNodesPrio){ + case 'linear': + ratio = leaves.length / (leaves.length + nonLeavesTiles); + break; + case 'sqrt': + ratio = Math.sqrt(leaves.length) / (Math.sqrt(leaves.length) + Math.sqrt(nonLeavesTiles)); + break; + case 'pow-2/3': + ratio = Math.pow(leaves.length, 2/3) / + (Math.pow(leaves.length, 2/3) + Math.pow(nonLeavesTiles, 2/3)); + break; + } + // Attempt leaves layout + let newPos = [0, headerSz]; + let newDims: [number,number] = [dims[0], dims[1] - headerSz]; + leavesLyt = new LayoutNode('SWEEP_' + node.name, leaves); + let minSz = opts.minTileSz + opts.tileSpacing*2; + let sweptW = Math.max(minSz, newDims[0] * ratio), sweptH = Math.max(minSz, newDims[1] * ratio); + let leavesSuccess: boolean; + switch (opts.sweepMode){ + case 'left': + leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); + sweptLeft = true; + break; + case 'top': + leavesSuccess = sqrLayout(leavesLyt, [0,0], [newDims[0], sweptH], false, false, opts); + sweptLeft = false; + break; + case 'shorter': + let documentAR = document.documentElement.clientWidth / document.documentElement.clientHeight; + if (documentAR >= 1){ + leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); + sweptLeft = true; + } else { + leavesSuccess = sqrLayout(leavesLyt, [0,0], [newDims[0], sweptH], false, false, opts); + sweptLeft = false; + } + break; + case 'auto': + // Attempt left-sweep, then top-sweep on a copy, and copy over if better + leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); + sweptLeft = true; + let tempTree = leavesLyt.cloneNodeTree(); + let sweptTopSuccess = sqrLayout(tempTree, [0,0], [newDims[0], sweptH], false, false, opts);; + if (sweptTopSuccess && (!leavesSuccess || tempTree.empSpc < leavesLyt.empSpc)){ + tempTree.copyTreeForRender(leavesLyt); + sweptLeft = false; + leavesSuccess = true; + } + break; + } + if (leavesSuccess){ + leavesLyt.children.forEach(lyt => {lyt.pos[1] += headerSz}); + // Attempt non-leaves layout + if (sweptLeft){ + newPos[0] += leavesLyt.dims[0] - opts.tileSpacing; + newDims[0] += -leavesLyt.dims[0] + opts.tileSpacing; + } else { + newPos[1] += leavesLyt.dims[1] - opts.tileSpacing; + newDims[1] += -leavesLyt.dims[1] + opts.tileSpacing + } + nonLeavesLyt = new LayoutNode('SWEEP_REM_' + node.name, nonLeaves); + let nonLeavesSuccess: boolean; + if (nonLeaves.length > 1){ + nonLeavesSuccess = rectLayout(nonLeavesLyt, [0,0], newDims, false, false, opts, {subLayoutFn: + ((n,p,d,h,a,o) => sweepLayout(n,p,d,h,allowCollapse,o,{sepArea:sepArea})) as LayoutFn}); + } else { + if (opts.sweepToParent){ + // Get leftover area usable by non-leaf child + if (sweptLeft){ + sepArea = new SepSweptArea( + [-leavesLyt.dims[0] + opts.tileSpacing, leavesLyt.dims[1] - opts.tileSpacing], // Relative to child + [leavesLyt.dims[0], newDims[1] - leavesLyt.dims[1] - opts.tileSpacing], + sweptLeft, false + ); + } else { + sepArea = new SepSweptArea( + [leavesLyt.dims[0] - opts.tileSpacing, -leavesLyt.dims[1] + opts.tileSpacing], + [newDims[0] - leavesLyt.dims[0] - opts.tileSpacing, leavesLyt.dims[1]], + sweptLeft, false + ); + } + } + // Attempt layout + nonLeavesSuccess = rectLayout(nonLeavesLyt, [0,0], newDims, false, false, opts, {subLayoutFn: + ((n,p,d,h,a,o) => sweepLayout(n,p,d,h,allowCollapse,o,{sepArea:sepArea})) as LayoutFn}); + } + if (nonLeavesSuccess){ + nonLeavesLyt.children.forEach(lyt => { + lyt.pos[0] += newPos[0]; + lyt.pos[1] += newPos[1]; + }); + // Create combined layout + let usedDims: [number, number]; + if (sweptLeft){ + usedDims = [ + leavesLyt.dims[0] + nonLeavesLyt.dims[0] - opts.tileSpacing, + Math.max(leavesLyt.dims[1] + (sepArea != null && sepArea.used ? sepArea.dims[1] : 0), nonLeavesLyt.dims[1]) + + headerSz + ]; + } else { + usedDims = [ + Math.max(leavesLyt.dims[0] + (sepArea != null && sepArea.used ? sepArea.dims[0] : 0), nonLeavesLyt.dims[0]), + leavesLyt.dims[1] + nonLeavesLyt.dims[1] - opts.tileSpacing + headerSz + ]; + } + let empSpc = leavesLyt.empSpc + nonLeavesLyt.empSpc; + node.assignLayoutData(pos, usedDims, {showHeader, empSpc, sepSweptArea: null}); + return true; + } + } + } // Try using parent-provided area - if (opts.sweepToParent && ownOpts != null && ownOpts.sepArea != null){ + if (ownOpts != null && ownOpts.sepArea != null && opts.sweepToParent != 'never'){ let parentArea = ownOpts.sepArea; // Attempt leaves layout sweptLeft = parentArea.sweptLeft; @@ -642,195 +759,72 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse nonLeavesSuccess = rectLayout(nonLeavesLyt, [0,0], newDims, false, false, opts, {subLayoutFn: ((n,p,d,h,a,o) => sweepLayout(n,p,d,h,allowCollapse,o,{sepArea:sepArea})) as LayoutFn}); } - if (!nonLeavesSuccess){ - if (allowCollapse){ - node.children = []; - LayoutNode.updateDCounts(node, 1 - node.dCount); - return oneSqrLayout(node, pos, dims, false, false, opts); + if (nonLeavesSuccess){ + // Adjust non-leaf child positions + if (sweptLeft){ + nonLeavesLyt.children.forEach(lyt => {lyt.pos[1] += headerSz}); } - return false; - } - // Adjust non-leaf child positions - if (sweptLeft){ - nonLeavesLyt.children.forEach(lyt => {lyt.pos[1] += headerSz}); - } - // Update parentArea to represent space used - parentArea.used = true; - if (sweptLeft){ - parentArea.dims[1] = leavesLyt.dims[1]; - let newX = parentArea.pos[0] + (parentArea.dims[0] - leavesLyt.dims[0]); - let newW = leavesLyt.dims[0]; - if (sepArea != null && sepArea.used){ - parentArea.dims[1] += sepArea.dims[1] + opts.tileSpacing; - if (sepArea.dims[0] + opts.tileSpacing > leavesLyt.dims[0]){ - newX = parentArea.pos[0] + (parentArea.dims[0] - sepArea.dims[0] - opts.tileSpacing); - newW = sepArea.dims[0] + opts.tileSpacing; + // Update parentArea to represent space used + parentArea.used = true; + if (sweptLeft){ + parentArea.dims[1] = leavesLyt.dims[1]; + let newX = parentArea.pos[0] + (parentArea.dims[0] - leavesLyt.dims[0]); + let newW = leavesLyt.dims[0]; + if (sepArea != null && sepArea.used){ + parentArea.dims[1] += sepArea.dims[1] + opts.tileSpacing; + if (sepArea.dims[0] + opts.tileSpacing > leavesLyt.dims[0]){ + newX = parentArea.pos[0] + (parentArea.dims[0] - sepArea.dims[0] - opts.tileSpacing); + newW = sepArea.dims[0] + opts.tileSpacing; + } + } + // Shrink to avoid excess space between leaves and non-leaves + parentArea.pos[0] = newX; + parentArea.dims[0] = newW; + } else { + parentArea.dims[0] = leavesLyt.dims[0]; + if (sepArea != null && sepArea.used){ + parentArea.dims[0] += sepArea.dims[0] + opts.tileSpacing; } } - // Shrink to avoid excess space between leaves and non-leaves - parentArea.pos[0] = newX; - parentArea.dims[0] = newW; - } else { - parentArea.dims[0] = leavesLyt.dims[0]; - if (sepArea != null && sepArea.used){ - parentArea.dims[0] += sepArea.dims[0] + opts.tileSpacing; - } - } - // Align parentArea size with non-leaves area - if (sweptLeft){ - if (parentArea.pos[1] + parentArea.dims[1] > nonLeavesLyt.dims[1] + headerSz){ - nonLeavesLyt.dims[1] = parentArea.pos[1] + parentArea.dims[1] - headerSz; + // Align parentArea size with non-leaves area + if (sweptLeft){ + if (parentArea.pos[1] + parentArea.dims[1] > nonLeavesLyt.dims[1] + headerSz){ + nonLeavesLyt.dims[1] = parentArea.pos[1] + parentArea.dims[1] - headerSz; + } else { + parentArea.dims[1] = nonLeavesLyt.dims[1] + headerSz - parentArea.pos[1]; + } } else { - parentArea.dims[1] = nonLeavesLyt.dims[1] + headerSz - parentArea.pos[1]; + if (parentArea.pos[0] + parentArea.dims[0] > nonLeavesLyt.dims[0]){ + nonLeavesLyt.dims[0] = parentArea.pos[0] + parentArea.dims[0]; + } else { + parentArea.dims[0] = nonLeavesLyt.dims[0] - parentArea.pos[0]; + } } - } else { - if (parentArea.pos[0] + parentArea.dims[0] > nonLeavesLyt.dims[0]){ - nonLeavesLyt.dims[0] = parentArea.pos[0] + parentArea.dims[0]; + // Adjust area to avoid overlap with non-leaves + if (sweptLeft){ + parentArea.dims[0] -= opts.tileSpacing; } else { - parentArea.dims[0] = nonLeavesLyt.dims[0] - parentArea.pos[0]; + parentArea.dims[1] -= opts.tileSpacing; } + // Move leaves to parent area + leavesLyt.children.map(lyt => { + lyt.pos[0] += parentArea!.pos[0]; + lyt.pos[1] += parentArea!.pos[1]; + }); + // Return with updated layout + let usedDims: [number,number] = [nonLeavesLyt.dims[0], nonLeavesLyt.dims[1] + (sweptLeft ? headerSz : 0)]; + node.assignLayoutData(pos, usedDims, {showHeader, empSpc: nonLeavesLyt.empSpc, sepSweptArea: parentArea}); + return true; } - // Adjust area to avoid overlap with non-leaves - if (sweptLeft){ - parentArea.dims[0] -= opts.tileSpacing; - } else { - parentArea.dims[1] -= opts.tileSpacing; - } - // Move leaves to parent area - leavesLyt.children.map(lyt => { - lyt.pos[0] += parentArea!.pos[0]; - lyt.pos[1] += parentArea!.pos[1]; - }); - // Return with updated layout - let usedDims: [number,number] = [nonLeavesLyt.dims[0], nonLeavesLyt.dims[1] + (sweptLeft ? headerSz : 0)]; - node.assignLayoutData(pos, usedDims, {showHeader, empSpc: nonLeavesLyt.empSpc, sepSweptArea: parentArea}); - return true; } } - // Choose proportion of area to use for leaves - let ratio: number; // area-for-leaves / area-for-non-leaves - let nonLeavesTiles = arraySum(nonLeaves.map(n => n.dCount)); - switch (opts.sweptNodesPrio){ - case 'linear': - ratio = leaves.length / (leaves.length + nonLeavesTiles); - break; - case 'sqrt': - ratio = Math.sqrt(leaves.length) / (Math.sqrt(leaves.length) + Math.sqrt(nonLeavesTiles)); - break; - case 'pow-2/3': - ratio = Math.pow(leaves.length, 2/3) / - (Math.pow(leaves.length, 2/3) + Math.pow(nonLeavesTiles, 2/3)); - break; - } - // Attempt leaves layout - let newPos = [0, headerSz]; - let newDims: [number,number] = [dims[0], dims[1] - headerSz]; - leavesLyt = new LayoutNode('SWEEP_' + node.name, leaves); - let minSz = opts.minTileSz + opts.tileSpacing*2; - let sweptW = Math.max(minSz, newDims[0] * ratio), sweptH = Math.max(minSz, newDims[1] * ratio); - let leavesSuccess: boolean; - switch (opts.sweepMode){ - case 'left': - leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); - sweptLeft = true; - break; - case 'top': - leavesSuccess = sqrLayout(leavesLyt, [0,0], [newDims[0], sweptH], false, false, opts); - sweptLeft = false; - break; - case 'shorter': - let documentAR = document.documentElement.clientWidth / document.documentElement.clientHeight; - if (documentAR >= 1){ - leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); - sweptLeft = true; - } else { - leavesSuccess = sqrLayout(leavesLyt, [0,0], [newDims[0], sweptH], false, false, opts); - sweptLeft = false; - } - break; - case 'auto': - // Attempt left-sweep, then top-sweep on a copy, and copy over if better - leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); - sweptLeft = true; - let tempTree = leavesLyt.cloneNodeTree(); - let sweptTopSuccess = sqrLayout(tempTree, [0,0], [newDims[0], sweptH], false, false, opts);; - if (sweptTopSuccess && (!leavesSuccess || tempTree.empSpc < leavesLyt.empSpc)){ - tempTree.copyTreeForRender(leavesLyt); - sweptLeft = false; - leavesSuccess = true; - } - break; - } - if (!leavesSuccess){ - if (allowCollapse){ - node.children = []; - LayoutNode.updateDCounts(node, 1 - node.dCount); - return oneSqrLayout(node, pos, dims, false, false, opts); - } - return false; - } - leavesLyt.children.forEach(lyt => {lyt.pos[1] += headerSz}); - // Attempt non-leaves layout - if (sweptLeft){ - newPos[0] += leavesLyt.dims[0] - opts.tileSpacing; - newDims[0] += -leavesLyt.dims[0] + opts.tileSpacing; - } else { - newPos[1] += leavesLyt.dims[1] - opts.tileSpacing; - newDims[1] += -leavesLyt.dims[1] + opts.tileSpacing - } - nonLeavesLyt = new LayoutNode('SWEEP_REM_' + node.name, nonLeaves); - let nonLeavesSuccess: boolean; - if (nonLeaves.length > 1){ - nonLeavesSuccess = rectLayout(nonLeavesLyt, [0,0], newDims, false, false, opts, {subLayoutFn: - ((n,p,d,h,a,o) => sweepLayout(n,p,d,h,allowCollapse,o,{sepArea:sepArea})) as LayoutFn}); - } else { - // Get leftover area usable by non-leaf child - if (sweptLeft){ - sepArea = new SepSweptArea( - [-leavesLyt.dims[0] + opts.tileSpacing, leavesLyt.dims[1] - opts.tileSpacing], // Relative to child - [leavesLyt.dims[0], newDims[1] - leavesLyt.dims[1] - opts.tileSpacing], - sweptLeft, false - ); - } else { - sepArea = new SepSweptArea( - [leavesLyt.dims[0] - opts.tileSpacing, -leavesLyt.dims[1] + opts.tileSpacing], - [newDims[0] - leavesLyt.dims[0] - opts.tileSpacing, leavesLyt.dims[1]], - sweptLeft, false - ); - } - // Attempt layout - nonLeavesSuccess = rectLayout(nonLeavesLyt, [0,0], newDims, false, false, opts, {subLayoutFn: - ((n,p,d,h,a,o) => sweepLayout(n,p,d,h,allowCollapse,o,{sepArea:sepArea})) as LayoutFn}); - } - if (!nonLeavesSuccess){ - if (allowCollapse){ - node.children = []; - LayoutNode.updateDCounts(node, 1 - node.dCount); - return oneSqrLayout(node, pos, dims, false, false, opts); - } - return false; + // Handle layout-failure + if (allowCollapse){ + node.children = []; + LayoutNode.updateDCounts(node, 1 - node.dCount); + return oneSqrLayout(node, pos, dims, false, false, opts); } - nonLeavesLyt.children.forEach(lyt => { - lyt.pos[0] += newPos[0]; - lyt.pos[1] += newPos[1]; - }); - // Create combined layout - let usedDims: [number, number]; - if (sweptLeft){ - usedDims = [ - leavesLyt.dims[0] + nonLeavesLyt.dims[0] - opts.tileSpacing, - Math.max(leavesLyt.dims[1] + (sepArea != null && sepArea.used ? sepArea.dims[1] : 0), nonLeavesLyt.dims[1]) - + headerSz - ]; - } else { - usedDims = [ - Math.max(leavesLyt.dims[0] + (sepArea != null && sepArea.used ? sepArea.dims[0] : 0), nonLeavesLyt.dims[0]), - leavesLyt.dims[1] + nonLeavesLyt.dims[1] - opts.tileSpacing + headerSz - ]; - } - let empSpc = leavesLyt.empSpc + nonLeavesLyt.empSpc; - node.assignLayoutData(pos, usedDims, {showHeader, empSpc, sepSweptArea: null}); - return true; + return false; } // Lays out nodes like sqrLayout(), but may extend past the height limit to fit nodes // Does not recurse on child nodes with children |
