owl-bt
Version:
owl-bt is editor for Behavior trees. It has been inspired by Unreal engine behavior trees in a way, that it supports special node items like decorators and services. This makes trees smaller and much more readable.
385 lines (343 loc) • 11 kB
JavaScript
'use strict';
(function () {
/**
* Common node functions
*/
class TreeNode {
constructor(_, ArrayHelper, TreeNodeByIdStore, TreeNodeItem) {
this._ = _;
this._ArrayHelper = ArrayHelper;
this._TreeNodeByIdStore = TreeNodeByIdStore;
this._TreeNodeItem = TreeNodeItem;
}
updateVersion(node) {
if (node.$meta.version === Number.MAX_SAFE_INTEGER) {
node.$meta.version = 1;
} else {
node.$meta.version++;
}
}
/**
* gets sub item at a given index
* @param {node} node
* @param {string} type - 'service' or 'decorator'
* @param {int} index
* @return sub item if exists
*/
getSubItemAt(node, type, index) {
this._checkSubItemType(type);
return node[type + 's'][index];
}
/**
* gets number of sub items
* @param {node} node
* @param {string} type - 'service' or 'decorator'
*/
getSubItemCount(node, type) {
this._checkSubItemType(type);
let array = node[type + 's'];
if (!array) {
return 0;
}
return array.length;
}
/**
* returns item that is above the specified node item (in render order (parent node/decorators/services)-(node/decorators/services))
* @param {Node} node - node that contains the specified item or the node alone
* @param {Node|Service|Decorator} item
* @return {Object} - def - above item def if exists
* @return {Node|Service|Decorator} - def.item - above item
* @return {Node} - def.node - node that contains the specified item, or the node itself
*/
getAboveNodeItem(node, item) {
if (node === item) { //node
let parentNode = this._TreeNodeByIdStore.getNode(item.$meta.parentId);
return {
node: parentNode,
item: this._getLastSubItem(parentNode) || parentNode
};
} else {
let index = this.indexOfSubItem(node, item);
let itemType = this.getSubItemType(node, item);
if (index === 0) {
if (itemType === 'service' && node.decorators && node.decorators.length) {
return {
node: node,
item: node.decorators[node.decorators.length - 1]
};
} else {
return {
node: node,
item: node
};
}
} else {
return {
node: node,
item: this.getSubItemAt(node, itemType, index - 1)
};
}
}
}
/**
* returns item that is below the specified node item (in render order (parent node/decorators/services)-(node/decorators/services))
* @param {Node} node - node that contains the specified item or the node alone
* @param {Node|Service|Decorator} item
* @return {Object} - def - below item def if exists
* @return {Node|Service|Decorator} - def.item - above item
* @return {Node} - def.node - node that contains the specified item, or the node itself
*/
getBelowNodeItem(node, item) {
if (node === item) { //node
if (node.decorators && node.decorators.length) {
return {
node: node,
item: node.decorators[0]
};
}
if (node.services && node.services.length) {
return {
node: node,
item: node.services[0]
};
}
return this._getBelowNodeSel(node);
} else {
let index = this.indexOfSubItem(node, item);
let itemType = this.getSubItemType(node, item);
let itemCnt = this.getSubItemCount(node, itemType);
if (index === itemCnt - 1) {
if (itemType === 'decorator' && node.services && node.services.length) {
return {
node: node,
item: node.services[0]
};
} else {
return this._getBelowNodeSel(node);
}
} else {
return {
node: node,
item: this.getSubItemAt(node, itemType, index + 1)
};
}
}
}
addService(node, service) {
this._addSubItem(node, 'services', service);
}
addDecorator(node, decorator) {
this._addSubItem(node, 'decorators', decorator);
}
/**
* add sub item
* @param {node} node
* @param {service|decorator} subItem
* @param {string} type - 'service' or 'decorator'
*/
addSubItem(node, subItem, type) {
this.addSubItemAt(node, subItem, type);
}
/**
* add sub item at a given index
* @param {node} node
* @param {service|decorator} subItem
* @param {string} type - 'service' or 'decorator'
* @param {int} index - (optional) Item is added to the end if not provided
*/
addSubItemAt(node, subItem, type, index) {
this._checkSubItemType(type);
this._addSubItem(node, type + 's', subItem, index);
}
addChildNode(node, childNode) {
this.addChildNodeAt(node, childNode);
}
/**
* add child node at a given index
* @param {node} node
* @param {node} childNode
* @param {int} index - (optional) Child node is added to the end if not provided
*/
addChildNodeAt(node, childNode, index) {
if (childNode.$meta.parentId && childNode.$meta.parentId !== node.$meta.id) {
throw new Error('Node is already child of another node');
}
if (!node.childNodes) {
node.childNodes = [];
}
if (this._.isUndefined(index)) {
index = node.childNodes.length;
}
node.childNodes.splice(index, 0, childNode);
childNode.$meta.parentId = node.$meta.id;
}
removeService(node, service) {
return this._removeSubItem(node, 'services', service);
}
removeDecorator(node, decorator) {
return this._removeSubItem(node, 'decorators', decorator);
}
removeSubItem(node, subItem) {
let subItemArrayName = this._getSubItemArrayName(node, subItem);
if (!subItemArrayName) {
return false;
}
return this._removeSubItem(node, subItemArrayName, subItem);
}
removeChildNode(node, childNode) {
if (!node.childNodes) {
return false;
}
if (this._ArrayHelper.remove(node.childNodes, childNode)) {
delete childNode.$meta.parentId;
return true;
}
return false;
}
indexOfChildNode(node, childNode) {
if (node.childNodes) {
return this._.indexOf(node.childNodes, childNode);
}
return -1;
}
indexOfService(node, service) {
if (node.services) {
return this._.indexOf(node.services, service);
}
return -1;
}
indexOfDecorator(node, decorator) {
if (node.decorators) {
return this._.indexOf(node.decorators, decorator);
}
return -1;
}
indexOfSubItem(node, subItem) {
let index = this.indexOfService(node, subItem);
if (index >= 0) {
return index;
}
return this.indexOfDecorator(node, subItem);
}
moveChildNode(node, childNode, left) {
if (!node.childNodes) {
return false;
}
if (left) {
return this._ArrayHelper.moveLeft(node.childNodes, childNode);
} else {
return this._ArrayHelper.moveRight(node.childNodes, childNode);
}
}
canMoveChildNode(node, childNode, left) {
if (!node.childNodes) {
return false;
}
if (left) {
return this._ArrayHelper.canMoveLeft(node.childNodes, childNode);
} else {
return this._ArrayHelper.canMoveRight(node.childNodes, childNode);
}
}
moveSubItem(node, subItem, up) {
let subItemArray = this._getSubItemArray(node, subItem);
if (!subItemArray) {
return false;
}
if (up) {
return this._ArrayHelper.moveLeft(subItemArray, subItem);
} else {
return this._ArrayHelper.moveRight(subItemArray, subItem);
}
}
canMoveSubItem(node, subItem, up) {
let subItemArray = this._getSubItemArray(node, subItem);
if (!subItemArray) {
return false;
}
if (up) {
return this._ArrayHelper.canMoveLeft(subItemArray, subItem);
} else {
return this._ArrayHelper.canMoveRight(subItemArray, subItem);
}
}
/**
* Get type (decorator/service) of the subItem in the node
* @param {node} node - node that contains the sub item
* @param {service|decorator} subItem
* @return {string} 'service' or 'decorator'
*/
getSubItemType(node, subItem) {
if (this.indexOfService(node, subItem) >= 0) {
return 'service';
}
if (this.indexOfDecorator(node, subItem) >= 0) {
return 'decorator';
}
}
propertiesAreValid(node) {
return this._TreeNodeItem.propertiesAreValid(node)
&& (!node.services || !node.services.find(s => !this._TreeNodeItem.propertiesAreValid(s)))
&& (!node.decorators || !node.decorators.find(d => !this._TreeNodeItem.propertiesAreValid(d)));
}
_getSubItemArrayName(node, subItem) {
let type = this.getSubItemType(node, subItem);
if (type) {
return type + 's';
}
}
_getSubItemArray(node, subItem) {
let subItemArrayName = this._getSubItemArrayName(node, subItem);
if (subItemArrayName) {
return node[subItemArrayName];
}
}
_addSubItem(node, subItemArrayName, subItem, index) {
if (subItem.$meta.nodeId && subItem.$meta.nodeId !== node.$meta.id) {
throw new Error('Sub item is already in another node');
}
if (!node[subItemArrayName]) {
node[subItemArrayName] = [];
}
if (this._.isUndefined(index)) {
index = node[subItemArrayName].length;
}
node[subItemArrayName].splice(index, 0, subItem);
subItem.$meta.nodeId = node.$meta.id;
}
_removeSubItem(node, subItemArrayName, subItem) {
if (!node[subItemArrayName]) {
return false;
}
if (this._ArrayHelper.remove(node[subItemArrayName], subItem)) {
delete subItem.$meta.nodeId;
return true;
}
return false;
}
_checkSubItemType(type) {
if (type !== 'service' && type !== 'decorator') {
throw new Error(`invalid sub item type ${type}`);
}
}
_getLastSubItem(node) {
if (node.services && node.services.length) {
return node.services[node.services.length - 1];
}
if (node.decorators && node.decorators.length) {
return node.decorators[node.decorators.length - 1];
}
}
_getBelowNodeSel(node) {
if (node.childNodes && node.childNodes.length) {
let bottomNode = node.childNodes[Math.floor((node.childNodes.length - 1) / 2)];
return {
node: bottomNode,
item: bottomNode
};
}
}
}
angular.module('editorApp')
.service('TreeNode', TreeNode);
})();