mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
51652ef277
first part of addressing #59 every `Port` now has a `usage_type` field that indicates whether it can be used for triggers (eg. sending and receiving events), value requests, or both. `Deck` has an additional method to validate if a potential connection is legal, which checks for the following in order: 1. the source and target nodes are not the same node; 2. the port usage is valid (trigger to trigger, value to value, both to any); 3. the port types are compatible 4. the connection doesn't already exist all node ports by default use the "both" usage, since that will be the most common use case (especially in cases where an input port can accept either a trigger and a value request but the output can only send one type), but it can be specified as an optional argument in `add_[input|output]_port()` usage types are represented in the renderer by different port icons: ![image](/attachments/28d3cfe9-c62c-4dd4-937d-64dbe87cb205) there is a reference implementation in the Compare Values and Twitch Chat Received nodes, since those were used as examples in #59. other nodes will be added as a separate PR later if this is merged, since behavior will vary greatly per node. Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/69 Co-authored-by: Lera Elvoé <yagich@poto.cafe> Co-committed-by: Lera Elvoé <yagich@poto.cafe>
403 lines
13 KiB
GDScript
403 lines
13 KiB
GDScript
# (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 = "",
|
|
usage: Port.UsageType = Port.UsageType.BOTH) -> void:
|
|
add_port(type, label, PortType.INPUT, get_input_ports().size(), descriptor, usage)
|
|
|
|
|
|
## Add an output port to this node. Usually only used at initialization.
|
|
func add_output_port(
|
|
type: DeckType.Types,
|
|
label: String,
|
|
descriptor: String = "",
|
|
usage: Port.UsageType = Port.UsageType.BOTH) -> void:
|
|
add_port(type, label, PortType.OUTPUT, get_output_ports().size(), descriptor, usage)
|
|
|
|
|
|
## Add a virtual port to this node. Usually only used at initialization.
|
|
func add_virtual_port(
|
|
type: DeckType.Types,
|
|
label: String,
|
|
descriptor: String = "",
|
|
usage: Port.UsageType = Port.UsageType.BOTH) -> void:
|
|
add_port(type, label, PortType.VIRTUAL, get_virtual_ports().size(), descriptor, usage)
|
|
|
|
|
|
## 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 = "",
|
|
usage: Port.UsageType = Port.UsageType.BOTH) -> void:
|
|
var port := Port.new(type, label, ports.size(), port_type, index_of_type, descriptor, usage)
|
|
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)
|