polygonjs-engine
Version:
node-based webgl 3D engine https://polygonjs.com
480 lines (440 loc) • 14.9 kB
text/typescript
import {TypedNodeConnection} from './NodeConnection';
import {CoreGraphNode} from '../../../../core/graph/CoreGraphNode';
import {NodeEvent} from '../../../poly/NodeEvent';
import {NodeContext} from '../../../poly/NodeContext';
import {ConnectionPointTypeMap} from './connections/ConnectionMap';
import {TypedNode} from '../../_Base';
import {ContainerMap, NodeTypeMap} from '../../../containers/utils/ContainerMap';
import {ClonedStatesController} from './utils/ClonedStatesController';
import {InputCloneMode} from '../../../poly/InputCloneMode';
import {BaseConnectionPoint} from './connections/_Base';
import {CoreType} from '../../../../core/Type';
type OnUpdateHook = () => void;
export class InputsController<NC extends NodeContext> {
private _graph_node: CoreGraphNode | undefined;
private _graph_node_inputs: CoreGraphNode[] = [];
private _inputs: Array<NodeTypeMap[NC] | null> = [];
private _has_named_inputs: boolean = false;
private _named_input_connection_points: ConnectionPointTypeMap[NC][] | undefined;
private _min_inputs_count: number = 0;
private _max_inputs_count: number = 0;
private _depends_on_inputs: boolean = true;
// hooks
private _on_update_hooks: OnUpdateHook[] | undefined;
private _on_update_hook_names: string[] | undefined;
// clonable
dispose() {
if (this._graph_node) {
this._graph_node.dispose();
}
for (let graph_node of this._graph_node_inputs) {
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>) {}
set_depends_on_inputs(depends_on_inputs: boolean) {
this._depends_on_inputs = depends_on_inputs;
}
private set_min_inputs_count(min_inputs_count: number) {
this._min_inputs_count = min_inputs_count;
}
private set_max_inputs_count(max_inputs_count: number) {
this._max_inputs_count = max_inputs_count;
this.init_graph_node_inputs();
}
named_input_connection_points_by_name(name: string): ConnectionPointTypeMap[NC] | undefined {
if (this._named_input_connection_points) {
for (let connection_point of this._named_input_connection_points) {
if (connection_point && connection_point.name() == name) {
return connection_point;
}
}
}
}
setNamedInputConnectionPoints(connection_points: ConnectionPointTypeMap[NC][]) {
this._has_named_inputs = true;
const connections = this.node.io.connections.input_connections();
if (connections) {
for (let 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.input_index >= connection_points.length) {
connection.disconnect({setInput: true});
}
}
}
}
// update connections
this._named_input_connection_points = connection_points;
this.set_min_inputs_count(0);
this.set_max_inputs_count(connection_points.length);
this.init_graph_node_inputs();
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;
// }
get has_named_inputs() {
return this._has_named_inputs;
}
get named_input_connection_points(): ConnectionPointTypeMap[NC][] {
return this._named_input_connection_points || [];
}
private init_graph_node_inputs() {
for (let i = 0; i < this._max_inputs_count; i++) {
this._graph_node_inputs[i] = this._graph_node_inputs[i] || this._create_graph_node_input(i);
}
}
private _create_graph_node_input(index: number): CoreGraphNode {
const graph_input_node = new CoreGraphNode(this.node.scene(), `input_${index}`);
// graph_input_node.set_scene(this.node.scene);
if (!this._graph_node) {
this._graph_node = new CoreGraphNode(this.node.scene(), 'inputs');
this.node.addGraphInput(this._graph_node, false);
}
this._graph_node.addGraphInput(graph_input_node, false);
return graph_input_node;
}
get max_inputs_count(): number {
return this._max_inputs_count || 0;
}
input_graph_node(input_index: number): CoreGraphNode {
return this._graph_node_inputs[input_index];
}
setCount(min: number, max?: number) {
if (max == null) {
max = min;
}
this.set_min_inputs_count(min);
this.set_max_inputs_count(max);
// this._clonable_states_controller.init_inputs_clonable_state();
this.init_connections_controller_inputs();
}
private init_connections_controller_inputs() {
this.node.io.connections.init_inputs();
}
is_any_input_dirty() {
return this._graph_node?.isDirty() || false;
// if (this._max_inputs_count > 0) {
// for (let i = 0; i < this._inputs.length; i++) {
// if (this._inputs[i]?.isDirty()) {
// return true;
// }
// }
// } else {
// return false;
// }
}
async containers_without_evaluation() {
const containers: Array<ContainerMap[NC] | undefined> = [];
for (let i = 0; i < this._inputs.length; i++) {
const input_node = this._inputs[i];
let container: ContainerMap[NC] | undefined = undefined;
if (input_node) {
container = (await input_node.requestContainer()) as ContainerMap[NC];
}
containers.push(container);
}
return containers;
}
existing_input_indices() {
const existing_input_indices: number[] = [];
if (this._max_inputs_count > 0) {
for (let i = 0; i < this._inputs.length; i++) {
if (this._inputs[i]) {
existing_input_indices.push(i);
}
}
}
return existing_input_indices;
}
async eval_required_inputs() {
let containers: Array<ContainerMap[NC] | null | undefined> = [];
if (this._max_inputs_count > 0) {
const existing_input_indices = this.existing_input_indices();
if (existing_input_indices.length < this._min_inputs_count) {
this.node.states.error.set('inputs are missing');
} else {
if (existing_input_indices.length > 0) {
const promises: Promise<ContainerMap[NC] | null>[] = [];
let input: NodeTypeMap[NC] | null;
for (let i = 0; i < this._inputs.length; i++) {
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.container_controller.container as ContainerMap[NC]);
// } else {
promises.push(this.eval_required_input(i) as Promise<ContainerMap[NC]>);
// }
}
}
containers = await Promise.all(promises);
// containers = containers.concat(promised_containers);
this._graph_node?.removeDirtyState();
}
}
}
return containers;
}
async eval_required_input(input_index: number) {
let container: ContainerMap[NC] | undefined = undefined;
const input_node = this.input(input_index);
// if (input_node && !input_node.isDirty()) {
// container = input_node.container_controller.container as ContainerMap[NC] | null;
// } else {
// container = await this.node.container_controller.requestInputContainer(input_index);
// this._graph_node_inputs[input_index].removeDirtyState();
// }
if (input_node) {
container = (await input_node.requestContainer()) as ContainerMap[NC];
this._graph_node_inputs[input_index].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 (input_node) {
const input_error_message = input_node.states.error.message();
if (input_error_message) {
this.node.states.error.set(`input ${input_index} is invalid (error: ${input_error_message})`);
}
}
}
return container;
}
get_named_input_index(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;
}
get_input_index(input_index_or_name: number | string): number {
if (CoreType.isString(input_index_or_name)) {
if (this.has_named_inputs) {
return this.get_named_input_index(input_index_or_name);
} else {
throw new Error(`node ${this.node.fullPath()} has no named inputs`);
}
} else {
return input_index_or_name;
}
}
setInput(
input_index_or_name: number | string,
node: NodeTypeMap[NC] | null,
output_index_or_name: number | string = 0
) {
const input_index = this.get_input_index(input_index_or_name) || 0;
if (input_index < 0) {
const message = `invalid input (${input_index_or_name}) for node ${this.node.fullPath()}`;
console.warn(message);
throw new Error(message);
}
let output_index = 0;
if (node) {
if (node.io.outputs.has_named_outputs) {
output_index = node.io.outputs.get_output_index(output_index_or_name);
if (output_index == null || output_index < 0) {
const connection_points = node.io.outputs.named_output_connection_points as BaseConnectionPoint[];
const names = connection_points.map((cp) => cp.name());
console.warn(
`node ${node.fullPath()} does not have an output named ${output_index_or_name}. inputs are: ${names.join(
', '
)}`
);
return;
}
}
}
const graph_input_node = this._graph_node_inputs[input_index];
if (graph_input_node == null) {
const message = `graph_input_node not found at index ${input_index}`;
console.warn(message);
throw new Error(message);
}
if (node && this.node.parent() != node.parent()) {
return;
}
const old_input_node = this._inputs[input_index];
let old_output_index: number | null = null;
let old_connection: TypedNodeConnection<NC> | undefined = undefined;
if (this.node.io.connections) {
old_connection = this.node.io.connections.input_connection(input_index);
}
if (old_connection) {
old_output_index = old_connection.output_index;
}
if (node !== old_input_node || output_index != old_output_index) {
// TODO: test: add test to make sure this is necessary
if (old_input_node != null) {
if (this._depends_on_inputs) {
graph_input_node.removeGraphInput(old_input_node);
}
}
if (node != null) {
if (graph_input_node.addGraphInput(node)) {
// 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) {
graph_input_node.removeGraphInput(node);
}
//this._input_connections[input_index] = new NodeConnection(node, this.self, output_index, input_index);
if (old_connection) {
old_connection.disconnect({setInput: false});
}
this._inputs[input_index] = node;
new TypedNodeConnection<NC>(
(<unknown>node) as TypedNode<NC, any>,
this.node,
output_index,
input_index
);
} else {
console.warn(`cannot connect ${node.fullPath()} to ${this.node.fullPath()}`);
}
} else {
this._inputs[input_index] = null;
if (old_connection) {
old_connection.disconnect({setInput: false});
}
// this._input_connections[input_index] = null;
}
this._run_on_set_input_hooks();
graph_input_node.setSuccessorsDirty();
// this.node.set_dirty(node);
this.node.emit(NodeEvent.INPUTS_UPDATED);
}
}
remove_input(node: NodeTypeMap[NC]) {
const inputs = this.inputs();
let input: NodeTypeMap[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): NodeTypeMap[NC] | null {
return this._inputs[input_index];
}
named_input(input_name: string): NodeTypeMap[NC] | null {
if (this.has_named_inputs) {
const input_index = this.get_input_index(input_name);
return this._inputs[input_index];
} else {
return null;
}
}
named_input_connection_point(input_name: string): ConnectionPointTypeMap[NC] | undefined {
if (this.has_named_inputs && this._named_input_connection_points) {
const input_index = this.get_input_index(input_name);
return this._named_input_connection_points[input_index];
}
}
has_named_input(name: string): boolean {
return this.get_named_input_index(name) >= 0;
}
has_input(input_index: number): boolean {
return this._inputs[input_index] != null;
}
inputs() {
return this._inputs;
}
//
//
// CLONABLE STATES
//
//
private _cloned_states_controller: ClonedStatesController<NC> | undefined;
initInputsClonedState(states: InputCloneMode | InputCloneMode[]) {
if (!this._cloned_states_controller) {
this._cloned_states_controller = new ClonedStatesController(this);
this._cloned_states_controller.init_inputs_cloned_state(states);
}
}
override_cloned_state_allowed(): boolean {
return this._cloned_states_controller?.override_cloned_state_allowed() || false;
}
override_cloned_state(state: boolean) {
this._cloned_states_controller?.override_cloned_state(state);
}
cloned_state_overriden() {
return this._cloned_states_controller?.overriden() || false;
}
clone_required(index: number) {
const state = this._cloned_states_controller?.clone_required_state(index);
if (state != null) {
return state;
}
return true;
}
clone_required_states(): boolean | boolean[] {
const states = this._cloned_states_controller?.clone_required_states();
if (states != null) {
return states;
}
return true;
}
//
//
// 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 (let hook of this._on_update_hooks) {
hook();
}
}
}
}