aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue2
-rw-r--r--src/components/SettingsModal.vue17
-rw-r--r--src/layout.ts358
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