# (c) 2023-present Eroax # (c) 2023-present Yagich # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) class_name DeckNode ## A node in a [Deck]. ## ## Nodes are the essential building block of a [Deck] graph. They can have connections between ports ## and send data through them. ## The name initially shown to a renderer. var name: String ## A map of outgoing connections from this node, in the format[br] ## [code]Dictionary[int -> output port, Dictionary[String -> DeckNode#_id, Array[int -> input port]]][/code] var outgoing_connections: Dictionary ## A map of incoming connections to this node, in the format[br] ## [code]Dictionary[int -> input port, [Dictionary[String -> DeckNode#_id, int -> output port]][/code] var incoming_connections: Dictionary ## A list of [Port]s on this node. var ports: Array[Port] ## The deck this node belongs to. var _belonging_to: Deck ## A unique identifier for this node. var _id: String ## The type of this node, used for instantiation. var node_type: String ## The description of this node, shown to the user by a renderer. var description: String ## A list of aliases for this node, used by search. var aliases: Array[String] ## Controls whether this node should appear in [SearchProvider]. var appears_in_search: bool = true ## A list of additional properties to save when this node is saved. var props_to_serialize: Array[StringName] ## The position of this node relative to the parent graph. ## Only used by renderers. var position: Dictionary = {"x": 0.0, "y": 0.0} ## If [code]true[/code], the user can delete this node by normal means. ## The parent [Deck] can still delete the node by other means. var user_can_delete: bool = true enum PortType{ INPUT, ## Input port type (slot on the left). OUTPUT, ## Output port type (slot on the right). VIRTUAL, ## Virtual port type (no slot on left [i]or[/i] right). } ## Emitted when this node has been moved. signal position_updated(new_position: Dictionary) ## Emitted when a port has been added. signal port_added(port: int) ## Emitted when a port has been removed. signal port_removed(port: int) ## Emitted when a port or multiple ports have been updated (added or removed). signal ports_updated() ## Emitted when a connection from this node has been added. signal outgoing_connection_added(from_port: int) ## Emitted when a connection from this node has been removed. signal outgoing_connection_removed(from_port: int) ## Emitted when a connection to this node has been added. signal incoming_connection_added(from_port: int) ## Emitted when a connection to this node has been removed. signal incoming_connection_removed(from_port: int) signal port_value_updated(port_idx: int, new_value: Variant) signal renamed(new_name: String) ## Add an input port to this node. Usually only used at initialization. func add_input_port(type: DeckType.Types, label: String, descriptor: String = "") -> void: add_port(type, label, PortType.INPUT, get_input_ports().size(), descriptor) ## Add an output port to this node. Usually only used at initialization. func add_output_port(type: DeckType.Types, label: String, descriptor: String = "") -> void: add_port(type, label, PortType.OUTPUT, get_output_ports().size(), descriptor) ## Add a virtual port to this node. Usually only used at initialization. func add_virtual_port(type: DeckType.Types, label: String, descriptor: String = "") -> void: add_port(type, label, PortType.VIRTUAL, get_virtual_ports().size(), descriptor) ## Add a port to this node. Usually only used at initialization. func add_port(type: DeckType.Types, label: String, port_type: PortType, index_of_type: int, descriptor: String = "") -> void: var port := Port.new(type, label, ports.size(), port_type, index_of_type, descriptor) ports.append(port) port_added.emit(ports.size() - 1) port.value_updated.connect( func(new_value: Variant) -> void: port_value_updated.emit(port.index, new_value) ) ports_updated.emit() ## Remove a port from this node. ## @deprecated func remove_port(port_idx: int) -> void: outgoing_connections.erase(port_idx) incoming_connections.erase(port_idx) ports.remove_at(port_idx) port_removed.emit(port_idx) ## Send data to all outgoing connections on port [param from_output_port]. func send(from_output_port: int, data: Variant, extra_data: Array = []) -> void: if outgoing_connections.get(from_output_port) == null: return for node: String in outgoing_connections[from_output_port]: for input_port: int in outgoing_connections[from_output_port][node]: get_node(node)._receive(input_port, data, extra_data) ## Virtual function that's called when this node receives data from another node's [method send] call. @warning_ignore("unused_parameter") func _receive(to_input_port: int, data: Variant, extra_data: Array = []) -> void: pass ## Add a connection from the output port at [param from_port] to [param to_node]'s input port ## at [param to_port]. func add_outgoing_connection(from_port: int, to_node: String, to_port: int) -> void: var inner: Dictionary = outgoing_connections.get(from_port, {}) as Dictionary var inner_ports: Array = inner.get(to_node, []) as Array inner_ports.append(to_port) inner[to_node] = inner_ports outgoing_connections[from_port] = inner get_node(to_node).add_incoming_connection(to_port, _id, from_port) outgoing_connection_added.emit(from_port) ## Add an incoming connection from [param from_node]'s output port at [param from_port] to this node's ## input port at [param to_port]. func add_incoming_connection(to_port: int, from_node: String, from_port: int) -> void: var connection := {from_node: from_port} incoming_connections[to_port] = connection incoming_connection_added.emit(to_port) ## Asynchronously request a value from an incoming connection on this node's input port at [param on_port]. ## Returns [code]null[/code] if no incoming connection exists on that port. ## The connected node may also return [code]null[/code]. func request_value_async(on_port: int) -> Variant: if !incoming_connections.has(on_port): return null var connection: Dictionary = incoming_connections[on_port] var node := get_node(connection.keys()[0]) return await node._value_request(connection.values()[0]) ## Virtual function that's called when this node has been requested a value from the output port ## at [param from_port]. @warning_ignore("unused_parameter") func _value_request(from_port: int) -> Variant: return null ## Remove an outgoing connection from this node. ## Does [b]not[/b] remove the other node's incoming connection equivalent. func remove_outgoing_connection(from_port: int, to_node: String, to_port: int) -> void: var connections: Dictionary = outgoing_connections.get(from_port, {}) as Dictionary if connections.is_empty(): return var inner_ports: Array = connections.get(to_node, []) as Array inner_ports.erase(to_port) if inner_ports.is_empty(): (outgoing_connections[from_port] as Dictionary).erase(to_node) if (outgoing_connections[from_port] as Dictionary).is_empty(): outgoing_connections.erase(from_port) outgoing_connection_removed.emit(from_port) ## Remove an incoming connection to this node on the input port at [param to_port]. ## Does [b]not[/b] remove the other node's outgoing connection equivalent. func remove_incoming_connection(to_port: int) -> void: incoming_connections.erase(to_port) incoming_connection_removed.emit(to_port) ## Returns [code]true[/code] if the exact connection from this node exists. func has_outgoing_connection_exact(from_port: int, to_node: String, to_port: int) -> bool: if !outgoing_connections.has(from_port): return false var connection: Dictionary = outgoing_connections[from_port] if !connection.has(to_node): return false var connections_to_node: Array = connection[to_node] if !(to_port in connections_to_node): return false return true ## Returns [code]true[/code] if the node has an incoming connection at input port [param on_port]. func has_incoming_connection(on_port: int) -> bool: return incoming_connections.has(on_port) func rename(new_name: String) -> void: name = new_name renamed.emit(new_name) @warning_ignore("unused_parameter") func _event_received(event_name: StringName, event_data: Dictionary = {}) -> void: pass ## Returns a list of all input ports. func get_input_ports() -> Array[Port]: return ports.filter( func(port: Port) -> bool: return port.port_type == PortType.INPUT ) ## Returns a list of all output ports. func get_output_ports() -> Array[Port]: return ports.filter( func(port: Port) -> bool: return port.port_type == PortType.OUTPUT ) ## Returns a list of all virtual ports. func get_virtual_ports() -> Array[Port]: return ports.filter( func(port: Port) -> bool: return port.port_type == PortType.VIRTUAL ) ## Returns the global port index from the input port index at [param idx]. func get_global_port_idx_from_input(idx: int) -> int: if get_input_ports().size() > idx: return get_input_ports()[idx].index else: return -1 ## Returns the global port index from the output port index at [param idx]. func get_global_port_idx_from_output(idx: int) -> int: if get_output_ports().size() > idx: return get_output_ports()[idx].index else: return -1 ## Returns the global port index from the virtual port index at [param idx]. func get_global_port_idx_from_virtual(idx: int) -> int: if get_virtual_ports().size() > idx: return get_virtual_ports()[idx].index else: return -1 ## Get a port's local index from the global port index at [param idx]. func get_port_type_idx_from_global(idx: int) -> int: return ports[idx].index_of_type ## Returns the list of all ports. func get_all_ports() -> Array[Port]: return ports ## Get a sibling node by its' ID. func get_node(uuid: String) -> DeckNode: return _belonging_to.get_node(uuid) ## Virtual function that's called during deserialization before connections are loaded in. func _pre_connection() -> void: pass ## Virtual function that's called after the node has been deserialized. func _post_load() -> void: pass ## A helper function to get a value on an input port. Returns the best match in the following ## order of priority:[br] ## 1. The direct result of [method request value], called asynchronously. [br] ## 2. The result of [method Port.value_callback], if it's not equal to [param empty_value]. [br] ## 3. The input [Port] at index [param input_port]'s stored [member Port.value], if it's not equal to [param empty_value]. [br] ## 4. [code]null[/code].[br] func resolve_input_port_value_async(input_port: int, empty_value: Variant = null) -> Variant: var request = await request_value_async(input_port) if request != null: return request elif get_input_ports()[input_port].value_callback.get_object() && get_input_ports()[input_port].value_callback.call() != empty_value: return get_input_ports()[input_port].value_callback.call() elif get_input_ports()[input_port].value != empty_value: return get_input_ports()[input_port].value else: return null ## Returns a [Dictionary] representation of this node. func to_dict(with_meta: bool = true) -> Dictionary: var d := { "_id": _id, "name": name, "outgoing_connections": outgoing_connections.duplicate(true), "incoming_connections": incoming_connections.duplicate(true), "props": {}, "node_type": node_type, "port_values": [], "position": position.duplicate(), } for prop in props_to_serialize: d.props[prop] = get(prop) ports.map( func(port: Port) -> void: d.port_values.append(port.value) ) if with_meta: d["meta"] = {} for meta in get_meta_list(): d["meta"][meta] = var_to_str(get_meta(meta)) return d static func from_dict(data: Dictionary) -> DeckNode: var node := NodeDB.instance_node(data.node_type) #node._id = data._id node.name = data.name node.position = data.position for prop in data.props: node.set(prop, data.props[prop]) node._pre_connection() for from_port in data.outgoing_connections: var connection_data: Dictionary = data.outgoing_connections[from_port] node.outgoing_connections[int(from_port)] = {} for to_node in connection_data: var input_ports: Array = connection_data[to_node] node.outgoing_connections[int(from_port)][to_node] = [] for to_input_port in input_ports: node.outgoing_connections[int(from_port)][to_node].append(int(to_input_port)) for to_port in data.incoming_connections: var connection_data = data.incoming_connections[to_port] for connection in connection_data: connection_data[connection] = int(connection_data[connection]) node.incoming_connections[int(to_port)] = connection_data for i in node.ports.size(): var port_value: Variant if (data.port_values as Array).size() <= i: port_value = null else: port_value = (data.port_values as Array)[i] node.ports[i].value = port_value for key in data.meta: node.set_meta(key, str_to_var(data.meta[key])) node._post_load() return node ## Returns the node's [member position] as a [Vector2]. func position_as_vector2() -> Vector2: return Vector2(position.x, position.y)