234 lines
6.1 KiB
TypeScript
234 lines
6.1 KiB
TypeScript
|
|
/**
|
|
* A tree type data structure used to build scene and ...
|
|
* @copyright MrKBear 2022
|
|
* @typeParam T - Child type of `TreeNode`
|
|
*/
|
|
class TreeNode<T extends TreeNode = TreeNode<any>> implements Iterable<T> {
|
|
|
|
/**
|
|
* Record all child nodes when this node is the root node.
|
|
* @remarks
|
|
* Only maintained when the node is the root node. \
|
|
* The array is arranged in depth first traversal order.
|
|
*/
|
|
private _progenyNodes: Array<T> | undefined = [ ];
|
|
|
|
/**
|
|
* Whether the structure of the current tree has changed.
|
|
* @remarks
|
|
* Only maintained when the node is the root node.
|
|
*/
|
|
private _isStructUpdated = false;
|
|
|
|
/**
|
|
* @remarks
|
|
* When a node is just created, it defaults the root node. \
|
|
* So this `rootNode` points to itself.
|
|
*/
|
|
private _rootNode: T | this = this;
|
|
|
|
private _parentNode: T | undefined = void 0;
|
|
|
|
private _childNodes: Array<T> = [];
|
|
|
|
/**
|
|
* Pointer to the root node.
|
|
*/
|
|
public get rootNode(): T | this { return this._rootNode; }
|
|
|
|
/**
|
|
* Pointer to the parent node.
|
|
*/
|
|
public get parentNode(): T | undefined { return this._parentNode; }
|
|
|
|
/**
|
|
* Points to the children of the current node.
|
|
*/
|
|
public get childNodes(): ReadonlyArray<T> { return this._childNodes; }
|
|
|
|
/**
|
|
* Is this a root node?
|
|
*/
|
|
public get isRootNode(): boolean { return !this._parentNode && this._rootNode === this; }
|
|
|
|
/**
|
|
* Depth first traverses the current `TreeNode` and its children `TreeNode`.
|
|
* @remarks
|
|
* This will use {@link https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR Pre-order (NLR) algorithm}. \
|
|
* So... Do not call this getter frequently.
|
|
* @example
|
|
* ``` javascript
|
|
* // ❌ Deprecated implementation !!!
|
|
* for (let i = 0; i < treeNode.progenyNodes.length; i++) {
|
|
* doSomeThing(treeNode.progenyNodes[i]);
|
|
* }
|
|
*
|
|
* // ✔️ Best Practices
|
|
* treeNode.progenyNodes.forEach((progenyNode)=> {
|
|
* doSomeThing(progenyNode);
|
|
* });
|
|
*
|
|
* // ✔️ Best Practices
|
|
* for(const progenyNode of treeNode.progenyNodes) {
|
|
* doSomeThing(progenyNode);
|
|
* }
|
|
* ```
|
|
* @return Progeny nodes list.
|
|
*/
|
|
public get progenyNodes(): ReadonlyArray<T> {
|
|
|
|
if (this.isRootNode && !this._isStructUpdated && this._progenyNodes) {
|
|
return this._progenyNodes;
|
|
}
|
|
|
|
else {
|
|
const cacheArray: Array<T> = [];
|
|
const stack: Array<T> = this._childNodes.slice().reverse();
|
|
|
|
while (stack.length > 0) {
|
|
const currentNode: T | undefined = stack.pop();
|
|
|
|
if (currentNode) {
|
|
cacheArray.push(currentNode);
|
|
|
|
// Push all child nodes in reverse order
|
|
for (let i = currentNode.childNodes.length - 1; i >= 0; i --) {
|
|
stack.push(<T>currentNode.childNodes[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.isRootNode) {
|
|
this._progenyNodes = cacheArray;
|
|
this._isStructUpdated = false;
|
|
}
|
|
|
|
return cacheArray;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An iterator implementation for {@link TreeNode.progenyNodes}
|
|
* @see {@link TreeNode.progenyNodes}
|
|
*/
|
|
public *[Symbol.iterator](): Iterator<T> { yield * this.progenyNodes; }
|
|
|
|
/**
|
|
* Remove `nodes` from child nodes.
|
|
* @param nodes - Nodes to be deleted.
|
|
* @returns Success Times.
|
|
*/
|
|
public removeChild(...nodes: ReadonlyArray<T>): number {
|
|
|
|
let successCount = 0;
|
|
for (const node of nodes) {
|
|
|
|
let removeNodesIndex = -1;
|
|
for (let i = 0; i < this._childNodes.length; i++) {
|
|
|
|
if (this._childNodes[i] === node) {
|
|
removeNodesIndex = i;
|
|
}
|
|
}
|
|
|
|
if (removeNodesIndex < 0) {
|
|
continue;
|
|
}
|
|
|
|
// Perform the remove operation
|
|
this._childNodes.splice(removeNodesIndex, 1);
|
|
|
|
node._parentNode = void 0;
|
|
node._rootNode = node;
|
|
node.markStructUpdated();
|
|
|
|
for (const progeny of node.progenyNodes) {
|
|
progeny._rootNode = node;
|
|
}
|
|
|
|
successCount ++;
|
|
}
|
|
|
|
if (successCount) {
|
|
this._rootNode.markStructUpdated();
|
|
}
|
|
|
|
return successCount;
|
|
}
|
|
|
|
/**
|
|
* Remove this node from the parent node
|
|
* @returns Success or not.
|
|
*/
|
|
public removeFromParent(): number {
|
|
|
|
if (this.isRootNode) {
|
|
return 0;
|
|
}
|
|
|
|
else {
|
|
return this._parentNode!.removeChild(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append `nodes` into child nodes.
|
|
* @param node - Nodes to be added.
|
|
* @returns Success Times.
|
|
*/
|
|
public appendChild(...nodes: ReadonlyArray<T>): number {
|
|
|
|
let successCount = 0;
|
|
for (const node of nodes) {
|
|
|
|
if (!node.isRootNode) {
|
|
node.removeFromParent();
|
|
}
|
|
|
|
for (const progeny of node.progenyNodes) {
|
|
progeny._rootNode = this._rootNode;
|
|
}
|
|
|
|
node._rootNode = this._rootNode;
|
|
node._parentNode = this;
|
|
node._progenyNodes = void 0;
|
|
|
|
this._childNodes.push(node);
|
|
|
|
successCount ++;
|
|
}
|
|
|
|
if (successCount) {
|
|
this._rootNode.markStructUpdated();
|
|
}
|
|
|
|
return successCount;
|
|
}
|
|
|
|
/**
|
|
* Mark that the current tree structure has been updated
|
|
* @returns true
|
|
*/
|
|
public markStructUpdated(): boolean {
|
|
if (this.isRootNode) {
|
|
return this._isStructUpdated = true;
|
|
}
|
|
else {
|
|
return this.rootNode.markStructUpdated();
|
|
}
|
|
}
|
|
}
|
|
|
|
class B extends TreeNode<B> { b(){}; }
|
|
class A extends TreeNode<A> { a(){}; }
|
|
class C extends A { c(){}; }
|
|
const a = new A();
|
|
const b = new B();
|
|
const c = new C();
|
|
a.rootNode.rootNode.appendChild(c);
|
|
// a.rootNode.rootNode.appendChild(a);
|
|
|
|
console.log(a);
|
|
|
|
export { TreeNode }; |