aboutsummaryrefslogtreecommitdiff
path: root/src/layout.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout.ts')
-rw-r--r--src/layout.ts172
1 files changed, 93 insertions, 79 deletions
diff --git a/src/layout.ts b/src/layout.ts
index 711f20a..d863fa0 100644
--- a/src/layout.ts
+++ b/src/layout.ts
@@ -1,52 +1,53 @@
/*
- * Contains classes used for representing tile-based layouts of tree-of-life data.
+ * Contains classes for representing tile-based layouts of tree-of-life data.
*
- * Generally, given a TolNode tree T,initLayoutTree() produces a tree structure representing a subtree of T,
- * which is passed to tryLayout(), which alters data fields to represent a tile-based layout.
- * The tree structure consists of LayoutNode objects, each of which holds placement info for a linked TolNode.
+ * Generally, given a TolNode tree T, initLayoutTree() produces a
+ * subtree-analagous LayoutNode tree, for which tryLayout() can attempt to
+ * find a tile-based layout, filling in node fields to represent placement.
*/
import {TolNode} from './tol';
import {range, arraySum, limitVals, updateAscSeq} from './util';
-// Represents a node/tree, and holds layout data for a TolNode node/tree
+// Represents a node/tree that holds layout data for a TolNode node/tree
export class LayoutNode {
tolNode: TolNode;
+ // Tree-structure related
children: LayoutNode[];
parent: LayoutNode | null;
- // Used for rendering a corresponding tile
+ dCount: number; // Number of descendant leaf nodes
+ depth: number; // Number of ancestor nodes
+ // Layout data
pos: [number, number];
dims: [number, number];
showHeader: boolean;
- sepSweptArea: SepSweptArea | null;
- hidden: boolean;
- hasFocus: boolean;
- collapseFailFlag: boolean; // Used to trigger failure animations
- expandFailFlag: boolean; // Used to trigger failure animations
- // Used for layout heuristics and info display
- dCount: number; // Number of descendant leaf nodes
- depth: number; // Number of ancestor nodes
- empSpc: number; // Amount of unused space (in pixels)
- // Creates object with given fields ('parent' are 'depth' are generally initialised later, 'dCount' is computed)
+ sepSweptArea: SepSweptArea | null; // Used with layout option 'sweepToParent'
+ empSpc: number; // Amount of unused layout space (in pixels)
+ // Other
+ hidden: boolean; // Used to hide nodes upon an expand-to-view
+ hasFocus: boolean; // Used by search and auto-mode to highlight a tile
+ failFlag: boolean; // Used to trigger failure animations
+ // Constructor ('parent' are 'depth' are generally initialised later, 'dCount' is computed)
constructor(tolNode: TolNode, children: LayoutNode[]){
this.tolNode = tolNode;
this.children = children;
this.parent = null;
+ this.dCount = children.length == 0 ? 1 : arraySum(children.map(n => n.dCount));
+ this.depth = 0;
+ //
this.pos = [0,0];
this.dims = [0,0];
this.showHeader = false;
this.sepSweptArea = null;
+ this.empSpc = 0;
+ //
this.hidden = false;
this.hasFocus = false;
- this.collapseFailFlag = false;
- this.expandFailFlag = false;
- this.dCount = children.length == 0 ? 1 : arraySum(children.map(n => n.dCount));
- this.depth = 0;
- this.empSpc = 0;
+ this.failFlag = false;
}
- // Creates new node tree with the same structure (fields like 'pos' are set to defaults)
+ // Returns a new tree with the same structure and TolNode linkage
// 'chg' is usable to apply a change to the resultant tree
- cloneNodeTree(chg?: LayoutTreeChg){
+ cloneNodeTree(chg?: LayoutTreeChg | null): LayoutNode {
let newNode: LayoutNode;
if (chg != null && this == chg.node){
switch (chg.type){
@@ -70,15 +71,16 @@ export class LayoutNode {
newNode.depth = this.depth;
return newNode;
}
- // Copies render-relevant data to a given LayoutNode tree
+ // Copies layout data to a given LayoutNode tree
// If a target node has more/less children, removes/gives own children
- copyTreeForRender(target: LayoutNode, map?: LayoutMap): void {
+ // If 'map' is provided, it is updated to represent node additions/removals
+ copyTreeForRender(target: LayoutNode, map?: LayoutMap | null): void {
target.pos = this.pos;
target.dims = this.dims;
target.showHeader = this.showHeader;
target.sepSweptArea = this.sepSweptArea;
target.dCount = this.dCount; // Copied for structural-consistency
- target.empSpc = this.empSpc; // Currently redundant, but maintains data-consistency
+ target.empSpc = this.empSpc; // Note: Currently redundant, but maintains data-consistency
// Handle children
if (this.children.length == target.children.length){
this.children.forEach((n,i) => n.copyTreeForRender(target.children[i], map));
@@ -95,9 +97,9 @@ export class LayoutNode {
}
}
}
- // Assigns render-relevant data to this single node
- assignLayoutData(pos=[0,0] as [number,number], dims=[0,0] as [number,number],
- {showHeader=false, sepSweptArea=null as SepSweptArea|null, empSpc=0} = {}){
+ // Assigns layout data to this single node
+ assignLayoutData(pos = [0,0] as [number,number], dims = [0,0] as [number,number],
+ {showHeader = false, sepSweptArea = null as SepSweptArea | null, empSpc = 0} = {}): void {
this.pos = [...pos];
this.dims = [...dims];
this.showHeader = showHeader;
@@ -111,19 +113,19 @@ export class LayoutNode {
node = node.parent;
}
}
- // Used to hide/show parent nodes upon expand-to-view
- static hideUpward(node: LayoutNode){
+ // These are used to hide/show parent nodes upon an expand-to-view
+ static hideUpward(node: LayoutNode): void {
if (node.parent != null){
node.parent.hidden = true;
node.parent.children.filter(n => n != node).forEach(n => LayoutNode.hideDownward(n));
LayoutNode.hideUpward(node.parent);
}
}
- static hideDownward(node: LayoutNode){
+ static hideDownward(node: LayoutNode): void {
node.hidden = true;
node.children.forEach(n => LayoutNode.hideDownward(n));
}
- static showDownward(node: LayoutNode){
+ static showDownward(node: LayoutNode): void {
if (node.hidden){
node.hidden = false;
node.children.forEach(n => LayoutNode.showDownward(n));
@@ -132,22 +134,24 @@ export class LayoutNode {
}
// Contains settings that affect how layout is done
export type LayoutOptions = {
- tileSpacing: number; // Spacing between tiles, in pixels (ignoring borders)
+ tileSpacing: number; // Spacing between tiles, in pixels
headerSz: number;
- minTileSz: number; // Minimum size of a tile edge, in pixels (ignoring borders)
+ minTileSz: number; // Minimum size of a tile edge, in pixels
maxTileSz: number;
+ // Layout-algorithm related
layoutType: 'sqr' | 'rect' | 'sweep'; // The LayoutFn function to use
rectMode: 'horz' | 'vert' | 'linear' | 'auto' | 'auto first-row';
- // Layout in 1 row, 1 column, 1 row or column, or multiple rows (with/without first-row-heuristic)
+ // 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
- sweepingToParent: boolean; // Allow swept nodes to occupy empty space in a parent's swept-leaves area
+ sweepToParent: boolean; // Allow swept nodes to occupy empty space in a parent's swept-leaves area
};
+// Represents a change to a LayoutNode tree
export type LayoutTreeChg = {
type: 'expand' | 'collapse';
node: LayoutNode;
}
-// Used with layout option 'sweepingToParent', and represents, for a LayoutNode, a parent area to place leaf nodes in
+// Used with layout option 'sweepToParent', and represents, for a LayoutNode, a parent area to place leaf nodes in
export class SepSweptArea {
pos: [number, number];
dims: [number, number];
@@ -161,8 +165,29 @@ export class SepSweptArea {
return new SepSweptArea([...this.pos], [...this.dims], this.sweptLeft);
}
}
-//
+
+// Represents a map from TolNode names to nodes in a LayoutNode tree
export type LayoutMap = Map<string, LayoutNode>;
+// Creates a LayoutMap for a given tree
+export function initLayoutMap(layoutTree: LayoutNode): LayoutMap {
+ function helper(node: LayoutNode, map: LayoutMap): void {
+ map.set(node.tolNode.name, node);
+ node.children.forEach(n => helper(n, map));
+ }
+ let map = new Map();
+ helper(layoutTree, map);
+ return map;
+}
+// Adds a node and it's descendants' names to a LayoutMap
+function addToLayoutMap(node: LayoutNode, map: LayoutMap): void {
+ map.set(node.tolNode.name, node);
+ node.children.forEach(n => addToLayoutMap(n, map));
+}
+// Removes a node and it's descendants' names from a LayoutMap
+function removeFromLayoutMap(node: LayoutNode, map: LayoutMap): void {
+ map.delete(node.tolNode.name);
+ node.children.forEach(n => removeFromLayoutMap(n, map));
+}
// Creates a LayoutNode representing a TolNode tree, up to a given depth (0 means just the root)
export function initLayoutTree(tol: TolNode, depth: number): LayoutNode {
@@ -180,28 +205,14 @@ export function initLayoutTree(tol: TolNode, depth: number): LayoutNode {
}
return initHelper(tol, depth);
}
-export function initLayoutMap(node: LayoutNode): LayoutMap {
- function helper(node: LayoutNode, map: LayoutMap){
- map.set(node.tolNode.name, node);
- node.children.forEach(n => helper(n, map));
- }
- let map = new Map();
- helper(node, map);
- return map;
-}
-function removeFromLayoutMap(node: LayoutNode, map: LayoutMap){
- map.delete(node.tolNode.name);
- node.children.forEach(n => removeFromLayoutMap(n, map));
-}
-function addToLayoutMap(node: LayoutNode, map: LayoutMap){
- map.set(node.tolNode.name, node);
- node.children.forEach(n => addToLayoutMap(n, map));
-}
// Attempts layout on a LayoutNode's corresponding TolNode tree, for an area with given xy-position and width+height
// 'allowCollapse' allows the layout algorithm to collapse nodes to avoid layout failure
// 'chg' allows for performing layout after expanding/collapsing a node
-export function tryLayout(layoutTree: LayoutNode, layoutMap: LayoutMap, pos: [number,number], dims: [number,number],
- options: LayoutOptions, allowCollapse: boolean = false, chg?: LayoutTreeChg){
+// 'layoutMap' provides a LayoutMap to update with added/removed children
+export function tryLayout(
+ layoutTree: LayoutNode, pos: [number,number], dims: [number,number], options: LayoutOptions,
+ {allowCollapse = false, chg = null as LayoutTreeChg | null, layoutMap = null as LayoutMap | null} = {}
+ ): boolean {
// Create a new LayoutNode tree, in case of layout failure
let tempTree = layoutTree.cloneNodeTree(chg);
let success: boolean;
@@ -214,14 +225,14 @@ export function tryLayout(layoutTree: LayoutNode, layoutMap: LayoutMap, pos: [nu
// Center in layout area
tempTree.pos[0] += (dims[0] - tempTree.dims[0]) / 2;
tempTree.pos[1] += (dims[1] - tempTree.dims[1]) / 2;
- // Apply to active LayoutNode tree
+ // Copy to given LayoutNode tree
tempTree.copyTreeForRender(layoutTree, layoutMap);
}
return success;
}
-// Type for functions called by tryLayout() to perform layout
-// Given a LayoutNode tree, determines and records a new layout by setting fields of nodes in the tree
+// Type for functions called by tryLayout() to attempt layout
+// Takes similar parameters to tryLayout(), with 'showHeader' and 'ownOpts' generally used by other LayoutFns
// Returns a boolean indicating success
type LayoutFn = (
node: LayoutNode,
@@ -280,6 +291,7 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
usedTileSz = tileSz;
}
}
+ // Check if unable to find grid
if (lowestEmpSpc == Number.POSITIVE_INFINITY){
if (allowCollapse){
node.children = [];
@@ -341,14 +353,15 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
let numChildren = node.children.length;
let rowBrks: number[] = []; // Will hold indices for nodes at which each row starts
let lowestEmpSpc = Number.POSITIVE_INFINITY;
- let usedTree: LayoutNode | null = null, usedEmpRight = 0, usedEmpBottom = 0;
+ 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
opts.minTileSz + opts.tileSpacing +
(opts.layoutType == 'sweep' ? opts.tileSpacing*2 : 0),
opts.minTileSz + opts.tileSpacing +
(opts.layoutType == 'sweep' ? opts.tileSpacing*2 + opts.headerSz : 0)
];
- rowBrksLoop:
+ RowBrksLoop:
while (true){
// Update rowBrks or exit loop
switch (opts.rectMode){
@@ -356,14 +369,14 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
if (rowBrks.length == 0){
rowBrks = [0];
} else {
- break rowBrksLoop;
+ break RowBrksLoop;
}
break;
case 'vert':
if (rowBrks.length == 0){
rowBrks = range(numChildren);
} else {
- break rowBrksLoop;
+ break RowBrksLoop;
}
break;
case 'linear':
@@ -372,7 +385,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
} else if (rowBrks.length == numChildren){
rowBrks = range(numChildren);
} else {
- break rowBrksLoop;
+ break RowBrksLoop;
}
break;
case 'auto':
@@ -381,7 +394,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
} else {
let updated = updateAscSeq(rowBrks, numChildren);
if (!updated){
- break rowBrksLoop;
+ break RowBrksLoop;
}
}
break;
@@ -392,7 +405,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
// Get next possible first row
let idxFirstRowLastEl = (rowBrks.length == 1 ? numChildren : rowBrks[1]) - 1;
if (idxFirstRowLastEl == 0){
- break rowBrksLoop;
+ break RowBrksLoop;
}
rowBrks = [0];
rowBrks.push(idxFirstRowLastEl);
@@ -435,13 +448,13 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
for (let rowIdx = 0; rowIdx < rowsOfCnts.length; rowIdx++){
let newWs = limitVals(cellWs[rowIdx], minCellDims[0], Number.POSITIVE_INFINITY);
if (newWs == null){
- continue rowBrksLoop;
+ continue RowBrksLoop;
}
cellWs[rowIdx] = newWs;
}
cellHs = limitVals(cellHs, minCellDims[1], Number.POSITIVE_INFINITY)!;
if (cellHs == null){
- continue rowBrksLoop;
+ continue RowBrksLoop;
}
// Get cell xy-coordinates
let cellXs: number[][] = new Array(rowsOfCnts.length);
@@ -477,7 +490,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
success = layoutFn(child, childPos, childDims, true, allowCollapse, opts);
}
if (!success){
- continue rowBrksLoop;
+ continue RowBrksLoop;
}
// Remove horizontal empty space by trimming cell and moving/expanding any next cell
let horzEmp = childDims[0] - child.dims[0];
@@ -513,7 +526,8 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
usedEmpBottom = empBottom;
}
}
- if (usedTree == null){ // If no found layout
+ // Check if no found layout
+ if (usedTree == null){
if (allowCollapse){
node.children = [];
LayoutNode.updateDCounts(node, 1 - node.dCount);
@@ -528,9 +542,9 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
return true;
}
// Lays out nodes by pushing leaves to one side, and using rectLayout() for the non-leaves
-// With layout option 'sweepingToParent', leaves from child nodes may occupy a parent's leaf-section
-//'sepArea' represents a usable leaf-section area from a direct parent,
- //and is altered to represent the area used, which the parent can use for reducing empty space
+// With layout option 'sweepToParent', leaves from child nodes may occupy a parent's leaf-section
+// 'sepArea' represents a usable leaf-section area from a direct parent,
+ //and is changed to represent the area used, with changes visibile to the parent for reducing empty space
let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts,
ownOpts?: {sepArea?: SepSweptArea}){
// Separate leaf and non-leaf nodes
@@ -549,13 +563,13 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
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 using parent-provided area
- let parentArea = (opts.sweepingToParent && ownOpts) ? ownOpts.sepArea : null; // Represents area provided by parent
+ let parentArea = (opts.sweepToParent && ownOpts) ? ownOpts.sepArea : null; // Represents area provided by parent
let usingParentArea = false;
if (parentArea != null){
// Attempt leaves layout
sweptLeft = parentArea.sweptLeft;
leavesLyt = new LayoutNode(new TolNode('SWEEP_' + node.tolNode.name), leaves);
- // Not updating child nodes to point to tempTree as a parent seems acceptable here
+ // Note: Intentionally neglecting to update child nodes' 'parent' or 'depth' fields here
let leavesSuccess = sqrLayout(leavesLyt, [0,0], parentArea.dims, !sweptLeft, false, opts);
if (leavesSuccess){
// Move leaves to parent area
@@ -720,7 +734,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
if (sweptLeft){
sepAreaLen = newDims[1] - leavesLyt.dims[1] - opts.tileSpacing;
sepArea = new SepSweptArea(
- [-leavesLyt.dims[0] + opts.tileSpacing, leavesLyt.dims[1] - opts.tileSpacing], //Relative to child
+ [-leavesLyt.dims[0] + opts.tileSpacing, leavesLyt.dims[1] - opts.tileSpacing], // Relative to child
[leavesLyt.dims[0], sepAreaLen],
sweptLeft
);
@@ -752,7 +766,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
lyt.pos[1] += newPos[1];
});
}
- // Combine layouts
+ // Create combined layout
if (leavesLyt == null || nonLeavesLyt == null){ //hint for typescript
return false;
}