@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
634 lines (587 loc) • 20.7 kB
text/typescript
import {TypedNodeConnection} from './NodeConnection';
import {CoreGraphNode} from '../../../../core/graph/CoreGraphNode';
import {NodeEvent} from '../../../poly/NodeEvent';
import {BaseNodeByContextMap, NodeContext} from '../../../poly/NodeContext';
import {ConnectionPointTypeMap} from './connections/ConnectionMap';
import {TypedNode} from '../../_Base';
import {ContainerMap} from '../../../containers/utils/ContainerMap';
import {ClonedStatesController} from './utils/ClonedStatesController';
import {InputCloneMode} from '../../../poly/InputCloneMode';
import {BaseConnectionPoint} from './connections/_Base';
import {isString} from '../../../../core/Type';
import {arrayShallowClone} from '../../../../core/ArrayUtils';
type OnUpdateHook = () => void;
type OnEvalSingleInputListen = () => Promise<void>;
const _existingInputIndices: number[] = [];
export interface SetInputsOptions {
noExceptionOnInvalidInput?: boolean;
ignoreLockedState?: boolean;
}
const MAX_INPUTS_COUNT_UNSET = 0;
export class NodeInputsController<NC extends NodeContext> {
private _graphNode: CoreGraphNode | undefined;
private _graphNodeInputs: CoreGraphNode[] = [];
private _inputs: Array<BaseNodeByContextMap[NC] | null> = [];
private _has_named_inputs: boolean = false;
private _named_input_connection_points: ConnectionPointTypeMap[NC][] | undefined;
private _minInputsCount: number = 0;
private _maxInputsCount: number = MAX_INPUTS_COUNT_UNSET;
private _maxInputsCountOnInput: number = MAX_INPUTS_COUNT_UNSET;
private _depends_on_inputs: boolean = true;
// hooks
private _on_update_hooks: OnUpdateHook[] | undefined;
private _on_update_hook_names: string[] | undefined;
// clonable
dispose() {
if (this._graphNode) {
this._graphNode.dispose();
}
for (const graph_node of this._graphNodeInputs) {
if (graph_node) {
graph_node.dispose();
}
}
// hooks
this._on_update_hooks = undefined;
this._on_update_hook_names = undefined;
}
// private _user_inputs_clonable_states: InputCloneMode[] | undefined;
// private _inputs_clonable_states: InputCloneMode[] | undefined;
// private _inputs_cloned_state: boolean[] = [];
// private _override_clonable_state: boolean = false;
constructor(public node: TypedNode<NC, any>) {}
setDependsOnInputs(depends_on_inputs: boolean) {
this._depends_on_inputs = depends_on_inputs;
}
private setMinCount(minInputsCount: number) {
this._minInputsCount = minInputsCount;
}
minCount() {
return this._minInputsCount;
}
private setMaxCount(maxInputsCount: number) {
if (this._maxInputsCount == MAX_INPUTS_COUNT_UNSET) {
this._maxInputsCountOnInput = maxInputsCount;
}
this._maxInputsCount = maxInputsCount;
this._initGraphNodeInputs();
// we need to update the cloneRequiredState here,
// in case the inputsCount changes
this._updateCloneRequiredState();
}
private _singleInputIndexListenedTo: number | null = null;
listenToSingleInputIndex(index: number) {
this._singleInputIndexListenedTo = index;
}
private _onEnsureListenToSingleInputIndexUpdatedCallback: OnEvalSingleInputListen | undefined;
onEnsureListenToSingleInputIndexUpdated(callback: OnEvalSingleInputListen) {
this._onEnsureListenToSingleInputIndexUpdatedCallback = callback;
}
namedInputConnectionPointsByName(name: string): ConnectionPointTypeMap[NC] | undefined {
if (this._named_input_connection_points) {
for (const connection_point of this._named_input_connection_points) {
if (connection_point && connection_point.name() == name) {
return connection_point;
}
}
}
}
setNamedInputConnectionPoints(newConnectionPoints: ConnectionPointTypeMap[NC][]) {
this._has_named_inputs = true;
const connectionPointsToKeep =
this._named_input_connection_points?.filter((cp) => cp?.inNodeDefinition()) || [];
// ensure names are unique
const allNewConnectionPoints: ConnectionPointTypeMap[NC][] = arrayShallowClone(connectionPointsToKeep);
const currentNames: Set<string> = new Set();
for (const connectionPointToKeep of connectionPointsToKeep) {
if (connectionPointToKeep) {
currentNames.add(connectionPointToKeep.name());
}
}
for (const newConnectionPoint of newConnectionPoints) {
if (newConnectionPoint) {
if (!currentNames.has(newConnectionPoint.name())) {
currentNames.add(newConnectionPoint.name());
allNewConnectionPoints.push(newConnectionPoint);
}
}
}
// disconnect if the number of inputs changes
const connections = this.node.io.connections.inputConnections();
if (connections) {
for (const connection of connections) {
if (connection) {
// assume we only work with indices for now, not with connection point names
// so we only need to check again the new max number of connection points.
if (connection.inputIndex() >= allNewConnectionPoints.length) {
connection.disconnect({setInput: true, ignoreLockedState: true});
}
}
}
}
// update connections
this._named_input_connection_points = allNewConnectionPoints;
this.setMinCount(0);
this.setMaxCount(this._named_input_connection_points.length);
this._initGraphNodeInputs();
this.node.emit(NodeEvent.NAMED_INPUTS_UPDATED);
}
// private _has_connected_inputs() {
// for (let input of this._inputs) {
// if (input != null) {
// return true;
// }
// }
// return false;
// }
// private _check_name_changed(connection_points: ConnectionPointTypeMap[NC][]) {
// if (this._named_input_connection_points) {
// if (this._named_input_connection_points.length != connection_points.length) {
// return true;
// } else {
// for (let i = 0; i < this._named_input_connection_points.length; i++) {
// if (this._named_input_connection_points[i]?.name != connection_points[i]?.name) {
// return true;
// }
// }
// }
// }
// return false;
// }
hasNamedInputs() {
return this._has_named_inputs;
}
namedInputConnectionPoints(): ConnectionPointTypeMap[NC][] | undefined {
return this._named_input_connection_points;
}
private _initGraphNodeInputs() {
for (let i = 0; i < this._maxInputsCount; i++) {
this._graphNodeInputs[i] = this._graphNodeInputs[i] || this._createGraphNodeInput(i);
}
}
private _createGraphNodeInput(index: number): CoreGraphNode {
const graphInputNode = new CoreGraphNode(this.node.scene(), `input_${index}`);
this.graphNode().addGraphInput(graphInputNode, false);
return graphInputNode;
}
graphNode() {
return (this._graphNode = this._graphNode || this._createGraphNode());
}
private _createGraphNode() {
const graphNode = new CoreGraphNode(this.node.scene(), 'inputs');
this.node.addGraphInput(graphNode, false);
return graphNode;
}
maxInputsCount(): number {
return this._maxInputsCount || 0;
}
maxInputsCountOverriden(): boolean {
return this._maxInputsCount != this._maxInputsCountOnInput;
}
inputGraphNode(input_index: number): CoreGraphNode {
return this._graphNodeInputs[input_index];
}
setCount(min: number, max?: number) {
if (max == null) {
max = min;
}
this.setMinCount(min);
this.setMaxCount(max);
// this._clonable_states_controller.init_inputs_clonable_state();
this._initConnectionControllerInputs();
}
private _initConnectionControllerInputs() {
this.node.io.connections.initInputs();
}
isGraphNodeDirty() {
// Update:
// we cannot simply check if this._graphNode is dirty,
// as with the following case:
// a merge node has 2 nodes as input
// both those nodes are set to dirty (without using the scene batching)
// then
// - the merge node with be set dirty a first time as the first input is made dirty.
// - the merge node starts cooking
// - the merge node is made dirty a second time
// - the merge node completes its first cook, having only processed the new content of the first input
// - when the merge node completes, it realises it needs to cook a second time, since it was made dirty after starting the cook
// - but when it attempts to get the content of the second node, the .isAnyInputDirty returns false when it should return true
// because this._graphNode is not dirty anymore
// OR...
// we can instead make sure that this._graphNode is only made un-dirty if all inputs are clean too.
return this._graphNode?.isDirty() || false;
}
private _isAnyInputDirty() {
// let anyDirty=false
// const inputNodes = this.inputs()
for (const input of this._inputs) {
if (input && input.isDirty()) {
return true;
}
}
return false;
// if (this._maxInputsCount > 0) {
// for (let i = 0; i < this._inputs.length; i++) {
// if (this._inputs[i]?.isDirty()) {
// return true;
// }
// }
// } else {
// return false;
// }
}
containersWithoutEvaluation(target: Array<ContainerMap[NC] | null>) {
target.length = 0;
for (let i = 0; i < this._inputs.length; i++) {
const inputNode = this._inputs[i];
let container: ContainerMap[NC] | null = null;
if (inputNode) {
// container = (await inputNode.compute()) as ContainerMap[NC];
// we do not need a promise using await here,
// as we know that the input node is not dirty
// therefore we can simply request the container
// and only check if it is bypassed or not
container = inputNode.containerController.containerUnlessBypassed() as ContainerMap[NC] | null;
}
target.push(container);
}
return target;
}
private _existingInputIndices(target: number[]) {
target.length = 0;
if (this._maxInputsCount > 0) {
for (let i = 0; i < this._inputs.length; i++) {
if (this._inputs[i]) {
target.push(i);
}
}
}
return target;
}
async evalRequiredInputs(
target: Array<ContainerMap[NC] | null | undefined>
): Promise<Array<ContainerMap[NC] | null | undefined>> {
target.length = 0;
// let containers: Array<ContainerMap[NC] | null | undefined> = [];
if (this.node.disposed() == true) {
return target;
}
if (this._maxInputsCount > 0) {
this._existingInputIndices(_existingInputIndices);
if (_existingInputIndices.length < this._minInputsCount) {
this.node.states.error.set('inputs are missing');
} else {
if (_existingInputIndices.length > 0) {
if (this._onEnsureListenToSingleInputIndexUpdatedCallback) {
await this._onEnsureListenToSingleInputIndexUpdatedCallback();
}
if (this._maxInputsCount == 1) {
const container = await this.evalRequiredInput(0);
target.push(container as ContainerMap[NC]);
} else {
const promises: Promise<ContainerMap[NC] | null>[] = [];
if (this._singleInputIndexListenedTo != null) {
promises.push(
this.evalRequiredInput(this._singleInputIndexListenedTo) as Promise<ContainerMap[NC]>
);
} else {
const lastExistingInputIndex = _existingInputIndices[_existingInputIndices.length - 1];
// let input: BaseNodeByContextMap[NC] | null;
for (let i = 0; i < this._inputs.length; i++) {
const input = this._inputs[i];
if (input) {
// I tried here to only use a promise for dirty inputs,
// but that messes up with the order
// if (input.isDirty()) {
// containers.push(input.containerController.container as ContainerMap[NC]);
// } else {
promises.push(this.evalRequiredInput(i) as Promise<ContainerMap[NC]>);
// }
} else {
// we need to add an empty container,
// for non connected inputs.
// otherwise, if input 0 is not connected,
// and input 1 is, then we get only 1 container
// which appears to be from input 0
if (i <= lastExistingInputIndex) {
promises.push(undefined as any);
}
}
}
}
const results = await Promise.all(promises);
for (const result of results) {
target.push(result);
}
}
if (!this._isAnyInputDirty()) {
this._graphNode?.removeDirtyState();
}
}
}
}
return target;
}
async evalRequiredInput(inputIndex: number) {
let container: ContainerMap[NC] | undefined = undefined;
const inputNode = this.input(inputIndex);
// if (input_node && !input_node.isDirty()) {
// container = input_node.containerController.container as ContainerMap[NC] | null;
// } else {
// container = await this.node.containerController.requestInputContainer(input_index);
// this._graph_node_inputs[input_index].removeDirtyState();
// }
if (inputNode) {
container = (await inputNode.compute()) as ContainerMap[NC];
this._graphNodeInputs[inputIndex].removeDirtyState();
}
// we do not clone here, as we just check if a group is present
if (container && container.coreContent()) {
// return container;
} else {
// const input_node = this.input(input_index);
if (inputNode) {
const inputErrorMessage = inputNode.states.error.message();
if (inputErrorMessage && this.node.disposed() == false) {
this.node.states.error.set(`input ${inputIndex} is invalid (error: ${inputErrorMessage})`);
}
}
}
return container;
}
getNamedInputIndex(name: string): number {
if (this._named_input_connection_points) {
for (let i = 0; i < this._named_input_connection_points.length; i++) {
if (this._named_input_connection_points[i]?.name() == name) {
return i;
}
}
}
return -1;
}
getInputIndex(input_index_or_name: number | string): number {
if (isString(input_index_or_name)) {
if (this.hasNamedInputs()) {
return this.getNamedInputIndex(input_index_or_name);
} else {
throw new Error(`node ${this.node.path()} has no named inputs`);
}
} else {
return input_index_or_name;
}
}
setInput(
inputIndexOrName: number | string,
node: BaseNodeByContextMap[NC] | null,
outputIndexOrName?: number | string,
options?: Readonly<SetInputsOptions>
) {
const ignoreLockedState = options?.ignoreLockedState || false;
if (ignoreLockedState == false && this.node.insideALockedParent()) {
const lockedParent = this.node.lockedParent();
console.warn(
`node '${this.node.path()}' cannot have its inputs changed, since it is inside '${
lockedParent ? lockedParent.path() : ''
}', which is locked`
);
return;
}
if (outputIndexOrName == null) {
outputIndexOrName = 0;
}
const noExceptionOnInvalidInput = options?.noExceptionOnInvalidInput || false;
const inputIndex = this.getInputIndex(inputIndexOrName) || 0;
if (inputIndex < 0) {
const message = `invalid input (${inputIndexOrName}) for node ${this.node.path()}`;
if (!noExceptionOnInvalidInput) {
console.warn(message);
throw new Error(message);
} else {
return;
}
}
let outputIndex = 0;
if (node) {
if (node.io.outputs.hasNamedOutputs()) {
outputIndex = node.io.outputs.getOutputIndex(outputIndexOrName);
if (outputIndex == null || outputIndex < 0) {
const connection_points = node.io.outputs.namedOutputConnectionPoints() as
| BaseConnectionPoint[]
| undefined;
const names: string[] = connection_points ? connection_points.map((cp) => cp.name()) : [];
console.warn(
`node ${node.path()} does not have an output named ${outputIndexOrName}. inputs are: ${names.join(
', '
)}`
);
return;
}
}
// check that parents exists and are the same
const nodeParent = node.parent();
const currentNodeParent = this.node.parent();
if (!(nodeParent && currentNodeParent && nodeParent.graphNodeId() == currentNodeParent.graphNodeId())) {
console.warn(`node ${node.path()} does not have the same parent as ${this.node.path()}`);
return;
}
}
const graphInputNode = this._graphNodeInputs[inputIndex];
if (graphInputNode == null) {
const message = `no input at index ${inputIndex} (name: ${inputIndexOrName}) for node '${this.node.name()}' at path '${this.node.path()}'`;
console.warn(message);
throw new Error(message);
}
if (node && this.node.parent() != node.parent()) {
return;
}
const oldInputNode = this._inputs[inputIndex];
let oldOutputIndex: number | null = null;
let oldConnection: TypedNodeConnection<NC> | undefined = undefined;
if (this.node.io.connections) {
oldConnection = this.node.io.connections.inputConnection(inputIndex);
}
if (oldConnection) {
oldOutputIndex = oldConnection.outputIndex();
}
if (node !== oldInputNode || outputIndex != oldOutputIndex) {
// TODO: test: add test to make sure this is necessary
if (oldInputNode != null) {
if (this._depends_on_inputs) {
graphInputNode.removeGraphInput(oldInputNode);
}
}
if (node != null) {
const connectionResult = graphInputNode.addGraphInput(node);
if (connectionResult) {
// we do test if we can create the graph connection
// to ensure we are not in a cyclical graph,
// but we delete it right after
if (!this._depends_on_inputs) {
graphInputNode.removeGraphInput(node);
}
// this._input_connections[input_index] = new NodeConnection(node, this.self, outputIndex, input_index);
if (oldConnection) {
oldConnection.disconnect({setInput: false});
}
this._inputs[inputIndex] = node;
new TypedNodeConnection<NC>(
(<unknown>node) as TypedNode<NC, any>,
this.node,
outputIndex,
inputIndex
);
} else {
console.warn(`cannot connect ${node.path()} to ${this.node.path()}`);
}
} else {
this._inputs[inputIndex] = null;
if (oldConnection) {
oldConnection.disconnect({setInput: false});
}
// this._input_connections[input_index] = null;
}
this._run_on_set_input_hooks();
graphInputNode.setSuccessorsDirty();
// this.node.set_dirty(node);
this.node.emit(NodeEvent.INPUTS_UPDATED);
}
}
// remove_input(node: BaseNodeByContextMap[NC]) {
// const inputs = this.inputs();
// let input: BaseNodeByContextMap[NC] | null;
// for (let i = 0; i < inputs.length; i++) {
// input = inputs[i];
// if (input != null && node != null) {
// if (input.graphNodeId() === node.graphNodeId()) {
// this.setInput(i, null);
// }
// }
// }
// }
input(input_index: number): BaseNodeByContextMap[NC] | null {
return this._inputs[input_index];
}
named_input(input_name: string): BaseNodeByContextMap[NC] | null {
if (this.hasNamedInputs()) {
const input_index = this.getInputIndex(input_name);
return this._inputs[input_index];
} else {
return null;
}
}
named_input_connection_point(input_name: string): ConnectionPointTypeMap[NC] | undefined {
if (this.hasNamedInputs() && this._named_input_connection_points) {
const input_index = this.getInputIndex(input_name);
return this._named_input_connection_points[input_index];
}
}
has_named_input(name: string): boolean {
return this.getNamedInputIndex(name) >= 0;
}
hasInput(input_index: number): boolean {
return this._inputs[input_index] != null;
}
inputs() {
return this._inputs;
}
//
//
// CLONABLE STATES
//
//
private _clonedStatesController: ClonedStatesController<NC> | undefined;
initInputsClonedState(states: InputCloneMode | InputCloneMode[]) {
if (!this._clonedStatesController) {
this._clonedStatesController = new ClonedStatesController(this);
this._clonedStatesController.initInputsClonedState(states);
}
}
overrideClonedStateAllowed(): boolean {
return this._clonedStatesController?.overrideClonedStateAllowed() || false;
}
overrideClonedState(state: boolean) {
this._clonedStatesController?.overrideClonedState(state);
}
clonedStateOverriden() {
return this._clonedStatesController?.overriden() || false;
}
cloneRequired(index: number) {
const state = this._clonedStatesController?.cloneRequiredState(index);
if (state != null) {
return state;
}
return true;
}
cloneRequiredStates(): boolean | boolean[] {
const states = this._clonedStatesController?.cloneRequiredStates();
if (states != null) {
return states;
}
return true;
}
private _updateCloneRequiredState() {
this._clonedStatesController?.updateCloneRequiredState();
}
//
//
// HOOKS
//
//
add_on_set_input_hook(name: string, hook: OnUpdateHook) {
this._on_update_hooks = this._on_update_hooks || [];
this._on_update_hook_names = this._on_update_hook_names || [];
if (!this._on_update_hook_names.includes(name)) {
this._on_update_hooks.push(hook);
this._on_update_hook_names.push(name);
} else {
console.warn(`hook with name ${name} already exists`, this.node);
}
}
private _run_on_set_input_hooks() {
if (this._on_update_hooks) {
for (const hook of this._on_update_hooks) {
hook();
}
}
}
}