@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
413 lines (368 loc) • 14.4 kB
text/typescript
import {Constructor} from '../../../../types/GlobalTypes';
import {CoreString} from '../../../../core/String';
import {BaseNodeClass, BaseNodeType} from '../../_Base';
import {NodeEvent} from '../../../poly/NodeEvent';
import {NodeContext} from '../../../poly/NodeContext';
import {CoreNodeSelection} from '../../../../core/NodeSelection';
import {Poly} from '../../../Poly';
import {ParamsInitData} from '../io/IOController';
import {CoreGraphNodeId} from '../../../../core/graph/CoreGraph';
import {BaseOperationContainer} from '../../../operations/container/_Base';
import {SopOperationContainer} from '../../../operations/container/sop';
import {BaseSopOperation} from '../../../operations/sop/_Base';
import {
addToSetAtEntry,
pushOnArrayAtEntry,
removeFromSetAtEntry,
popFromArrayAtEntry,
} from '../../../../core/MapUtils';
import {NameController} from '../NameController';
import {CoreNodeSerializer} from '../CoreNodeSerializer';
import {TypedNodeConnection} from '../io/NodeConnection';
import {arrayCopy} from '../../../../core/ArrayUtils';
type OutputNodeFindMethod = (() => BaseNodeType) | undefined;
type TraverseNodeCallback = (node: BaseNodeType) => void;
type TraverseNodeConditionCallback = (node: BaseNodeType) => boolean;
export interface NodeCreateOptions {
paramsInitValueOverrides?: ParamsInitData;
nodeName?: string;
serializerClass?: typeof CoreNodeSerializer;
}
export class HierarchyChildrenController {
private _childrenByName: Map<string, BaseNodeType> = new Map();
private _childrenIdByType: Map<string, Set<CoreGraphNodeId>> = new Map();
private _childrenByType: Map<string, Array<BaseNodeType>> = new Map();
private _childrenAndGrandchildrenByContext: Map<NodeContext, Set<CoreGraphNodeId>> = new Map();
private _selection: CoreNodeSelection | undefined;
get selection(): CoreNodeSelection {
return (this._selection = this._selection || new CoreNodeSelection(this.node));
}
constructor(protected node: BaseNodeType, private _context: NodeContext) {}
dispose() {
const _tmpChildren: BaseNodeType[] = [];
arrayCopy(this.children(), _tmpChildren);
for (const child of _tmpChildren) {
this.node.removeNode(child);
}
_tmpChildren.length = 0;
this._selection = undefined;
}
get context() {
return this._context;
}
//
//
// OUTPUT NODE
//
//
private _outputNodeFindMethod: (() => BaseNodeType) | undefined;
setOutputNodeFindMethod(method: OutputNodeFindMethod) {
this._outputNodeFindMethod = method;
}
outputNode() {
if (this._outputNodeFindMethod) {
return this._outputNodeFindMethod();
}
}
//
//
//
//
//
setChildName(node: BaseNodeType, newName: string): void {
let currentChildWithName: BaseNodeType | undefined;
newName = CoreString.sanitizeName(newName);
if ((currentChildWithName = this._childrenByName.get(newName)) != null) {
// only return if found node is same as argument node, and if new_name is same as current_name
if (node.name() === newName && currentChildWithName.graphNodeId() === node.graphNodeId()) {
return;
}
// increment new_name
newName = CoreString.increment(newName);
return this.setChildName(node, newName);
} else {
const currentName = node.name();
// delete old entry if node was in _children with old name
const currentChild = this._childrenByName.get(currentName);
if (currentChild) {
this._childrenByName.delete(currentName);
}
// add to new name
this._childrenByName.set(newName, node);
this._updateCache();
node.nameController.updateNameFromParent(newName);
this.node.scene().nodesController.addToInstanciatedNode(node);
this.node.scene().graphNodesController.notifyNodePathChanged(node);
}
}
private _nextAvailableChildName(nodeName: string): string {
nodeName = CoreString.sanitizeName(nodeName);
return this._childrenByName.get(nodeName)
? this._nextAvailableChildName(CoreString.increment(nodeName))
: nodeName;
}
nodeContextSignature() {
return `${this.node.context()}/${this.node.type()}`;
}
availableChildrenClasses() {
return Poly.registeredNodes(this.node);
}
isValidChildType(node_type: string): boolean {
const node_class = this.availableChildrenClasses()[node_type];
return node_class != null;
}
// create_node(node_type: string, options?: NodeCreateOptions): BaseNodeType {
// const node_class = this.available_children_classes()[node_type];
// if (node_class == null) {
// const message = `child node type '${node_type}' not found for node '${this.node.path()}'. Available types are: ${Object.keys(
// this.available_children_classes()
// ).join(', ')}, ${this._context}, ${this.node.type}`;
// console.error(message);
// throw message;
// } else {
// const child_node = new node_class(this.node.scene, `child_node_${node_type}`, paramsInitValueOverrides);
// child_node.initialize_base_and_node();
// this.add_node(child_node);
// child_node.lifecycle.set_creation_completed();
// return child_node;
// }
// }
createNode<K extends BaseNodeType>(nodeClassOrString: string | Constructor<K>, options?: NodeCreateOptions): K {
// adding a test here means that this function could return undefined
// which would all the existing calls
// if (this.node.lockedOrInsideALockedParent()) {
// const lockedParent = this.node.lockedParent();
// console.error(
// `node '${this.node.path()}' cannot create nodes, since it is inside '${
// lockedParent ? lockedParent.path() : ''
// }', which is locked`
// );
// return;
// }
if (typeof nodeClassOrString == 'string') {
const nodeClass = this._findNodeClass(nodeClassOrString);
return this._createAndInitNode(nodeClass, options) as K;
} else {
return this._createAndInitNode(nodeClassOrString, options);
}
}
private _createAndInitNode<K extends BaseNodeType>(nodeClass: Constructor<K>, options?: NodeCreateOptions) {
const requestedNodeName =
options?.nodeName || NameController.baseName((<unknown>nodeClass) as typeof BaseNodeClass);
const nodeName = this._nextAvailableChildName(requestedNodeName);
const childNode = new nodeClass(this.node.scene(), nodeName, {
...options,
serializerClass: this.node.serializer?.constructor,
});
childNode.initializeBaseAndNode();
this._addNode(childNode);
childNode.lifecycle.setCreationCompleted();
return childNode;
}
private _findNodeClass(node_type: string) {
const nodeClass = this.availableChildrenClasses()[node_type.toLowerCase()];
if (nodeClass == null) {
const message = `child node type '${node_type}' not found for node '${this.node.path()}'. Available types are: ${Object.keys(
this.availableChildrenClasses()
).join(', ')}, ${this._context}, ${this.node.type()}`;
console.error(message);
throw message;
}
return nodeClass;
}
createOperationContainer(
operationType: string,
operationContainerName: string,
options?: NodeCreateOptions
): BaseOperationContainer<any> {
const operationClass = Poly.registeredOperation(this._context, operationType);
if (operationClass == null) {
const message = `no operation found with context ${this._context}/${operationType}`;
console.error(message);
throw message;
} else {
const operation = new operationClass(this.node.scene()) as BaseSopOperation;
const operation_container = new SopOperationContainer(
operation,
operationContainerName,
options?.paramsInitValueOverrides || {}
);
return operation_container;
}
}
private _addNode(childNode: BaseNodeType) {
childNode.setParent(this.node);
this._addToNodesByType(childNode);
childNode.params.init();
childNode.parentController.onSetParent();
childNode.nameController.runPostSetFullPathHooks();
if (childNode.childrenAllowed() && childNode.childrenController) {
for (const child of childNode.childrenController.children()) {
child.nameController.runPostSetFullPathHooks();
}
}
if (this.node.serializer) {
const childNodeJSON = childNode.toJSON();
if (childNodeJSON) {
this.node.emit(NodeEvent.CREATED, {child_node_json: childNodeJSON});
}
}
if (this.node.scene().lifecycleController.onAfterCreatedCallbackAllowed()) {
childNode.lifecycle.runOnAfterCreatedCallbacks();
}
childNode.lifecycle.runOnAfterAddedCallbacks();
this.node.lifecycle.runOnChildAddCallbacks(childNode);
if (childNode.requireWebGL2()) {
this.node.scene().webglController.setRequireWebGL2();
}
this.node.scene().missingExpressionReferencesController.checkForMissingNodeReferences(childNode);
return childNode;
}
removeNode(childNode: BaseNodeType): void {
if (this.node.lockedOrInsideALockedParent()) {
const lockedNode = this.node.selfOrLockedParent();
const reason =
lockedNode == this.node
? `it is locked`
: `it is inside '${lockedNode ? lockedNode.path() : ''}', which is locked`;
console.warn(`node '${this.node.path()}' cannot remove nodes, since ${reason}`);
console.log(this.node.graphNodeId(), this.node.name());
return;
}
if (childNode.parent() != this.node) {
return console.warn(`node ${childNode.name()} not under parent ${this.node.path()}`);
} else {
childNode.polyNodeController?.setLockedState(false); // makes it easier to dispose its content
childNode.lifecycle.runOnBeforeDeleteCallbacks();
if (this.selection.contains(childNode)) {
this.selection.remove([childNode]);
}
const firstConnection = childNode.io.connections.firstInputConnection();
const inputConnections = childNode.io.connections.inputConnections();
const outputConnections: TypedNodeConnection<NodeContext>[] = [];
childNode.io.connections.outputConnections(outputConnections);
if (inputConnections) {
for (const inputConnection of inputConnections) {
if (inputConnection) {
inputConnection.disconnect({setInput: true});
}
}
}
if (outputConnections) {
for (const outputConnection of outputConnections) {
if (outputConnection) {
outputConnection.disconnect({setInput: true});
if (firstConnection) {
const oldSrc = firstConnection.nodeSrc();
const oldOutputIndex = firstConnection.outputIndex();
const oldDest = outputConnection.nodeDest();
const oldInputIndex = outputConnection.inputIndex();
oldDest.io.inputs.setInput(oldInputIndex, oldSrc, oldOutputIndex);
}
}
}
}
// remove from children
childNode.setParent(null);
this._childrenByName.delete(childNode.name());
this._updateCache();
this._removeFromNodesByType(childNode);
this.node.scene().nodesController.removeFromInstanciatedNode(childNode);
// set other dependencies dirty
// Note that this call to set_dirty was initially before this._children_node.remove_graph_input
// but that prevented the obj/geo node to properly clear its sopGroup if this was the last node
// if (this._is_dependent_on_children && this._children_node) {
// this._children_node.set_successors_dirty(this.node);
// }
childNode.setSuccessorsDirty(this.node);
// disconnect successors
childNode.graphDisconnectSuccessors();
this.node.lifecycle.runOnChildRemoveCallbacks(childNode);
childNode.lifecycle.runOnDeleteCallbacks();
childNode.dispose();
childNode.emit(NodeEvent.DELETED, {parent_id: this.node.graphNodeId()});
this.node.scene().graphNodesController.notifyNodePathChanged(childNode);
}
}
private _addToNodesByType(node: BaseNodeType) {
const nodeId = node.graphNodeId();
const type = node.type();
addToSetAtEntry(this._childrenIdByType, type, nodeId);
pushOnArrayAtEntry(this._childrenByType, type, node);
this._addToChildrenAndGrandchildrenByContext(node);
}
private _removeFromNodesByType(node: BaseNodeType) {
const nodeId = node.graphNodeId();
const type = node.type();
removeFromSetAtEntry(this._childrenIdByType, type, nodeId);
popFromArrayAtEntry(this._childrenByType, type, node);
this._removeFromChildrenAndGrandchildrenByContext(node);
}
private _addToChildrenAndGrandchildrenByContext(node: BaseNodeType) {
const nodeId = node.graphNodeId();
const nodeContext = node.context();
addToSetAtEntry(this._childrenAndGrandchildrenByContext, nodeContext, nodeId);
const parent = this.node.parent();
if (parent && parent.childrenAllowed()) {
parent.childrenController?._addToChildrenAndGrandchildrenByContext(node);
}
}
private _removeFromChildrenAndGrandchildrenByContext(node: BaseNodeType) {
const nodeId = node.graphNodeId();
const type = node.context();
removeFromSetAtEntry(this._childrenAndGrandchildrenByContext, type, nodeId);
const parent = this.node.parent();
if (parent && parent.childrenAllowed()) {
parent.childrenController?._removeFromChildrenAndGrandchildrenByContext(node);
}
}
nodesByType(type: string, target: BaseNodeType[] = []): BaseNodeType[] {
const nodes = this._childrenByType.get(type);
// if (!nodes) {
// nodes = [];
// this._childrenByType.set(type, nodes);
// }
// ensure we don't return the same array,
// otherwise a previous call to .nodesByType()
// would share the same array,
// which is not what we want, when we aim to compare
// different results after different node creation/deletion
target.length = nodes ? nodes.length : 0;
if (nodes) {
for (let i = 0; i < nodes.length; i++) {
target[i] = nodes[i];
}
}
return target;
}
childByName(name: string) {
return this._childrenByName.get(name) || null;
}
hasChildrenAndGrandchildrenWithContext(context: NodeContext) {
return this._childrenAndGrandchildrenByContext.get(context) != null;
}
private _children: BaseNodeType[] = [];
private _childrenNames: string[] = [];
private _updateCache() {
this._children.length = 0;
this._childrenNames.length = 0;
this._childrenByName.forEach((node) => {
this._children.push(node);
this._childrenNames.push(node.name());
});
}
children(): Readonly<BaseNodeType[]> {
return this._children;
}
childrenNames(): Readonly<string[]> {
return this._childrenNames;
}
traverseChildren(callback: TraverseNodeCallback, conditionCallback?: TraverseNodeConditionCallback) {
this._childrenByName.forEach((childNode) => {
callback(childNode);
if (conditionCallback == null || conditionCallback(childNode) == true) {
childNode.childrenController?.traverseChildren(callback);
}
});
}
}