diff options
| -rw-r--r-- | src/components/Tile.vue | 2 | ||||
| -rw-r--r-- | src/components/TileTree.vue | 26 | ||||
| -rw-r--r-- | src/lib.ts | 115 |
3 files changed, 66 insertions, 77 deletions
diff --git a/src/components/Tile.vue b/src/components/Tile.vue index 4009068..9b43082 100644 --- a/src/components/Tile.vue +++ b/src/components/Tile.vue @@ -90,7 +90,7 @@ export default defineComponent({ position: 'absolute', left: this.options.leafHeaderX + 'px', top: this.options.leafHeaderY + 'px', - maxWidth: this.layoutNode.hidden ? 0 : this.layoutNode.dims[0] - this.options.leafHeaderX * 2 + 'px', + maxWidth: (this.layoutNode.hidden ? 0 : this.layoutNode.dims[0] - this.options.leafHeaderX * 2) + 'px', height: this.options.leafHeaderFontSz + 'px', lineHeight: this.options.leafHeaderFontSz + 'px', fontSize: this.options.leafHeaderFontSz + 'px', diff --git a/src/components/TileTree.vue b/src/components/TileTree.vue index 2cc2d53..6c63f4f 100644 --- a/src/components/TileTree.vue +++ b/src/components/TileTree.vue @@ -1,7 +1,7 @@ <script lang="ts"> import {defineComponent} from 'vue'; import Tile from './Tile.vue'; -import {TolNode, LayoutTree, LayoutNode} from '../lib'; +import {TolNode, LayoutNode, initLayoutTree, tryLayout} from '../lib'; import type {LayoutOptions} from '../lib'; // Import paths lack a .ts or .js extension because .ts makes vue-tsc complain, and .js makes vite complain @@ -40,10 +40,10 @@ const defaultOtherOptions = { // Collects events about tile expansion/collapse and window-resize, and initiates relayout of tiles export default defineComponent({ data(){ - let layoutTree = new LayoutTree(tol, defaultLayoutOptions, 0); + let layoutTree = initLayoutTree(tol, 0); return { layoutTree: layoutTree, - activeRoot: layoutTree.root, + activeRoot: layoutTree, layoutOptions: {...defaultLayoutOptions}, otherOptions: {...defaultOtherOptions}, width: document.documentElement.clientWidth - (defaultOtherOptions.rootOffset * 2), @@ -57,34 +57,30 @@ export default defineComponent({ // Update data and relayout tiles this.width = document.documentElement.clientWidth - (this.otherOptions.rootOffset * 2); this.height = document.documentElement.clientHeight - (this.otherOptions.rootOffset * 2); - if (!this.layoutTree.tryLayout(this.activeRoot, [0,0], [this.width,this.height], true)){ - console.log('Unable to layout tree'); - } + tryLayout(this.activeRoot, [0,0], [this.width,this.height], this.layoutOptions, true); // Prevent re-triggering until after a delay this.resizeThrottled = true; setTimeout(() => {this.resizeThrottled = false;}, this.otherOptions.resizeDelay); } }, onInnerLeafClicked({layoutNode, domNode}: {layoutNode: LayoutNode, domNode: HTMLElement}){ - let success = this.layoutTree.tryLayout(this.activeRoot, [0,0], [this.width,this.height], false, + let success = tryLayout(this.activeRoot, [0,0], [this.width,this.height], this.layoutOptions, false, {type: 'expand', node: layoutNode}); if (!success){ // Trigger failure animation domNode.classList.remove('animate-expand-shrink'); domNode.offsetWidth; // Triggers reflow domNode.classList.add('animate-expand-shrink'); - //console.log('Unable to layout tree'); } }, onInnerHeaderClicked({layoutNode, domNode}: {layoutNode: LayoutNode, domNode: HTMLElement}){ - let success = this.layoutTree.tryLayout(this.activeRoot, [0,0], [this.width,this.height], false, + let success = tryLayout(this.activeRoot, [0,0], [this.width,this.height], this.layoutOptions, false, {type: 'collapse', node: layoutNode}); if (!success){ // Trigger failure animation domNode.classList.remove('animate-shrink-expand'); domNode.offsetWidth; // Triggers reflow domNode.classList.add('animate-shrink-expand'); - //console.log('Unable to layout tree'); } }, onInnerLeafDblClicked(layoutNode: LayoutNode){ @@ -94,7 +90,7 @@ export default defineComponent({ } LayoutNode.hideUpward(layoutNode); this.activeRoot = layoutNode; - this.layoutTree.tryLayout(layoutNode, [0,0], [this.width,this.height], true, + tryLayout(layoutNode, [0,0], [this.width,this.height], this.layoutOptions, true, {type: 'expand', node: layoutNode}); }, onInnerHeaderDblClicked(layoutNode: LayoutNode){ @@ -104,14 +100,12 @@ export default defineComponent({ } LayoutNode.hideUpward(layoutNode); this.activeRoot = layoutNode; - this.layoutTree.tryLayout(layoutNode, [0,0], [this.width,this.height], true); + tryLayout(layoutNode, [0,0], [this.width,this.height], this.layoutOptions, true); }, }, created(){ window.addEventListener('resize', this.onResize); - if (!this.layoutTree.tryLayout(this.activeRoot, [0,0], [this.width,this.height], true)){ - console.log('Unable to layout tree'); - } + tryLayout(this.activeRoot, [0,0], [this.width,this.height], this.layoutOptions, true); }, unmounted(){ window.removeEventListener('resize', this.onResize); @@ -124,7 +118,7 @@ export default defineComponent({ <template> <div class="h-screen bg-stone-800"> - <tile :layoutNode="layoutTree.root" + <tile :layoutNode="layoutTree" :headerSz="layoutOptions.headerSz" :tileSpacing="layoutOptions.tileSpacing" :transitionDuration="otherOptions.transitionDuration" @leaf-clicked="onInnerLeafClicked" @header-clicked="onInnerHeaderClicked" @@ -2,8 +2,9 @@ * Contains classes used for representing tree-of-life data, and tile-based layouts of such data. * * Generally, given a TolNode with child TolNodes representing tree-of-life T, - * a LayoutTree is created for a subtree of T, and represents a tile-based layout of that subtree. - * The LayoutTree holds LayoutNodes, each of which holds placement info for a linked TolNode. + * 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. */ // Represents a tree-of-life node/tree @@ -15,63 +16,6 @@ export class TolNode { this.children = children; } } -// Represents a tree of LayoutNode objects, and has methods for (re)computing layout -export class LayoutTree { - root: LayoutNode; - options: LayoutOptions; - // Creates an object representing a TolNode tree, up to a given depth (0 means just the root) - constructor(tol: TolNode, options: LayoutOptions, depth: number){ - this.root = this.initHelper(tol, depth); - this.options = options; - } - // Used by constructor to initialise the LayoutNode tree - initHelper(tolNode: TolNode, depthLeft: number, atDepth: number = 0): LayoutNode { - if (depthLeft == 0){ - let node = new LayoutNode(tolNode, []); - node.depth = atDepth; - return node; - } else { - let children = tolNode.children.map((n: TolNode) => this.initHelper(n, depthLeft-1, atDepth+1)); - let node = new LayoutNode(tolNode, children); - children.forEach(n => n.parent = node); - return node; - } - } - // Attempts layout of TolNode tree, for an area with given xy-coordinate and width+height (in pixels) - // 'allowCollapse' allows the layout algorithm to collapse nodes to avoid layout failure - // 'chg' allows for performing layout after expanding/collapsing a node - tryLayout(root: LayoutNode, pos: [number,number], dims: [number,number], allowCollapse: boolean = false, - chg?: LayoutTreeChg){ - // Create a new LayoutNode tree, keeping the old one in case of layout failure - let tempTree = root.cloneNodeTree(chg); - let success: boolean; - switch (this.options.layoutType){ - case 'sqr': success = sqrLayout(tempTree, pos, dims, true, allowCollapse, this.options); break; - case 'rect': success = rectLayout(tempTree, pos, dims, true, allowCollapse, this.options); break; - case 'sweep': success = sweepLayout(tempTree, pos, dims, true, allowCollapse, this.options); break; - } - if (success){ - // Center root 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 - tempTree.copyTreeForRender(root); - } - return success; - } -} -// Contains settings that affect how layout is done -export type LayoutOptions = { - tileSpacing: number; // Spacing between tiles, in pixels (ignoring borders) - headerSz: number; - minTileSz: number; // Minimum size of a tile edge, in pixels (ignoring borders) - maxTileSz: number; - layoutType: 'sqr' | 'rect' | 'sweep'; // The LayoutFn function to use - rectMode: 'horz' | 'vert' | 'linear' | 'auto'; // Layout in 1 row, 1 column, 1 row or column, or multiple rows - 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 -}; // Represents a node/tree, and holds layout data for a TolNode node/tree export class LayoutNode { tolNode: TolNode; @@ -177,6 +121,18 @@ export class LayoutNode { }); } } +// Contains settings that affect how layout is done +export type LayoutOptions = { + tileSpacing: number; // Spacing between tiles, in pixels (ignoring borders) + headerSz: number; + minTileSz: number; // Minimum size of a tile edge, in pixels (ignoring borders) + maxTileSz: number; + layoutType: 'sqr' | 'rect' | 'sweep'; // The LayoutFn function to use + rectMode: 'horz' | 'vert' | 'linear' | 'auto'; // Layout in 1 row, 1 column, 1 row or column, or multiple rows + 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 +}; export type LayoutTreeChg = { type: 'expand' | 'collapse'; node: LayoutNode; @@ -196,7 +152,46 @@ export class SepSweptArea { } } -// Type for functions called by LayoutTree to perform layout +// 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 { + function initHelper(tolNode: TolNode, depthLeft: number, atDepth: number = 0): LayoutNode { + if (depthLeft == 0){ + let node = new LayoutNode(tolNode, []); + node.depth = atDepth; + return node; + } else { + let children = tolNode.children.map((n: TolNode) => initHelper(n, depthLeft-1, atDepth+1)); + let node = new LayoutNode(tolNode, children); + children.forEach(n => n.parent = node); + return node; + } + } + return initHelper(tol, depth); +} +// 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, pos: [number,number], dims: [number,number], + options: LayoutOptions, allowCollapse: boolean = false, chg?: LayoutTreeChg){ + // Create a new LayoutNode tree, in case of layout failure + let tempTree = layoutTree.cloneNodeTree(chg); + let success: boolean; + switch (options.layoutType){ + case 'sqr': success = sqrLayout(tempTree, pos, dims, true, allowCollapse, options); break; + case 'rect': success = rectLayout(tempTree, pos, dims, true, allowCollapse, options); break; + case 'sweep': success = sweepLayout(tempTree, pos, dims, true, allowCollapse, options); break; + } + if (success){ + // 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 + tempTree.copyTreeForRender(layoutTree); + } + 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 // Returns a boolean indicating success type LayoutFn = ( |
