add port usage type to Port (#69)

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>
This commit is contained in:
Lera Elvoé 2024-02-21 04:08:36 +00:00 committed by yagich
parent 759f6eff73
commit 51652ef277
14 changed files with 246 additions and 27 deletions

View file

@ -114,32 +114,49 @@ func get_node(uuid: String) -> DeckNode:
return nodes.get(uuid) return nodes.get(uuid)
## Attempt to connect two nodes. Returns [code]true[/code] if the connection succeeded. ## Returns [code]true[/code] if the connection between two nodes is legal.
func connect_nodes(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> bool: func is_valid_connection(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> bool:
var from_node := get_node(from_node_id) # do not connect to self
var to_node := get_node(to_node_id) if from_node_id == to_node_id:
# check that we can do the type conversion
var type_a: DeckType.Types = from_node.get_output_ports()[from_output_port].type
var type_b: DeckType.Types = to_node.get_input_ports()[to_input_port].type
if !DeckType.can_convert(type_a, type_b):
DeckHolder.logger.log_deck(
"Can not convert from %s to %s." % [DeckType.type_str(type_a), DeckType.type_str(type_b)],
Logger.LogType.ERROR
)
return false return false
# refuse duplicate connections var from_node := get_node(from_node_id)
if from_node.has_outgoing_connection_exact(from_output_port, to_node_id, to_input_port): var to_node := get_node(to_node_id)
print("no duplicates")
var usage_from: Port.UsageType = from_node.get_output_ports()[from_output_port].usage_type
var usage_to: Port.UsageType = to_node.get_input_ports()[to_input_port].usage_type
# incompatible usages
if (usage_from != Port.UsageType.BOTH) && (usage_to != Port.UsageType.BOTH):
if usage_from != usage_to:
return false return false
var type_from: DeckType.Types = from_node.get_output_ports()[from_output_port].type
var type_to: DeckType.Types = to_node.get_input_ports()[to_input_port].type
# incompatible types
if !DeckType.can_convert(type_from, type_to):
return false
# duplicate connection
if from_node.has_outgoing_connection_exact(from_output_port, to_node_id, to_input_port):
return false
return true
## Attempt to connect two nodes. Returns [code]true[/code] if the connection succeeded.
func connect_nodes(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> bool:
if !is_valid_connection(from_node_id, to_node_id, from_output_port, to_input_port):
return false
var from_node := get_node(from_node_id)
var to_node := get_node(to_node_id)
if to_node.has_incoming_connection(to_input_port): if to_node.has_incoming_connection(to_input_port):
var connection: Dictionary = to_node.incoming_connections[to_input_port] var connection: Dictionary = to_node.incoming_connections[to_input_port]
var node_id: String = connection.keys()[0] var node_id: String = connection.keys()[0]
var node_out_port: int = connection.values()[0] var node_out_port: int = connection.values()[0]
disconnect_nodes(node_id, to_node_id, node_out_port, to_input_port) disconnect_nodes(node_id, to_node_id, node_out_port, to_input_port)
if is_group && emit_group_signals: if is_group && emit_group_signals:
nodes_connected_in_group.emit(from_node_id, to_node_id, from_output_port, to_input_port, self) nodes_connected_in_group.emit(from_node_id, to_node_id, from_output_port, to_input_port, self)

View file

@ -74,23 +74,40 @@ signal renamed(new_name: String)
## Add an input port to this node. Usually only used at initialization. ## Add an input port to this node. Usually only used at initialization.
func add_input_port(type: DeckType.Types, label: String, descriptor: String = "") -> void: func add_input_port(
add_port(type, label, PortType.INPUT, get_input_ports().size(), descriptor) 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. ## Add an output port to this node. Usually only used at initialization.
func add_output_port(type: DeckType.Types, label: String, descriptor: String = "") -> void: func add_output_port(
add_port(type, label, PortType.OUTPUT, get_output_ports().size(), descriptor) 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. ## Add a virtual port to this node. Usually only used at initialization.
func add_virtual_port(type: DeckType.Types, label: String, descriptor: String = "") -> void: func add_virtual_port(
add_port(type, label, PortType.VIRTUAL, get_virtual_ports().size(), descriptor) 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. ## 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: func add_port(type: DeckType.Types,
var port := Port.new(type, label, ports.size(), port_type, index_of_type, descriptor) 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) ports.append(port)
port_added.emit(ports.size() - 1) port_added.emit(ports.size() - 1)
port.value_updated.connect( port.value_updated.connect(

View file

@ -32,3 +32,14 @@ func _value_request(on_port: int) -> Variant:
var b = await resolve_input_port_value_async(1) var b = await resolve_input_port_value_async(1)
return a == b return a == b
func _receive(on_port: int, data: Variant, extra_data: Array = []) -> void:
if on_port == 0:
var b = await resolve_input_port_value_async(1)
if data == b:
send(0, true)
else:
var b = await resolve_input_port_value_async(0)
if data == b:
send(0, true)

View file

@ -0,0 +1,33 @@
# (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)
extends DeckNode
func _init() -> void:
name = "Test Usage"
node_type = name.to_snake_case()
add_input_port(
DeckType.Types.ANY, "Both", "", Port.UsageType.BOTH
)
add_input_port(
DeckType.Types.ANY, "Trigger", "", Port.UsageType.TRIGGER
)
add_input_port(
DeckType.Types.ANY, "Value", "", Port.UsageType.VALUE_REQUEST
)
add_output_port(
DeckType.Types.ANY, "Both", "", Port.UsageType.BOTH
)
add_output_port(
DeckType.Types.ANY, "Trigger", "", Port.UsageType.TRIGGER
)
add_output_port(
DeckType.Types.ANY, "Value", "", Port.UsageType.VALUE_REQUEST
)

View file

@ -14,10 +14,10 @@ func _init():
node_type = "twitch_chat_received" node_type = "twitch_chat_received"
description = "Receives Twitch chat events from a Twitch connection." description = "Receives Twitch chat events from a Twitch connection."
add_output_port(DeckType.Types.STRING, "Username") add_output_port(DeckType.Types.STRING, "Username", "", Port.UsageType.TRIGGER)
add_output_port(DeckType.Types.STRING, "Message") add_output_port(DeckType.Types.STRING, "Message", "", Port.UsageType.TRIGGER)
add_output_port(DeckType.Types.STRING, "Channel") add_output_port(DeckType.Types.STRING, "Channel", "", Port.UsageType.TRIGGER)
add_output_port(DeckType.Types.DICTIONARY, "Tags") add_output_port(DeckType.Types.DICTIONARY, "Tags", "", Port.UsageType.TRIGGER)
add_output_port( add_output_port(
DeckType.Types.BOOL, DeckType.Types.BOOL,
@ -37,7 +37,10 @@ func _event_received(event_name : StringName, event_data : Dictionary = {}):
message = event_data.message message = event_data.message
channel = event_data.channel channel = event_data.channel
tags = event_data tags = event_data
send(0, username)
send(1, message)
send(2, channel)
send(3, tags)
send(4, true) send(4, true)

View file

@ -7,6 +7,12 @@ class_name Port
## Ports are used for connections between [DeckNode]s and can contain data that is passed between ## Ports are used for connections between [DeckNode]s and can contain data that is passed between
## them on a node. ## them on a node.
enum UsageType {
TRIGGER, ## Port can send or receive events, not request values.
VALUE_REQUEST, ## Port can request values and respond to value requests, not send or receive events.
BOTH, ## Port can send or receive events [b]and[/b] request and respond to value requests.
}
## The type index of this port. ## The type index of this port.
var type: DeckType.Types var type: DeckType.Types
## The label of this port. Used by the renderer to display. How it's displayed depends on the renderer ## The label of this port. Used by the renderer to display. How it's displayed depends on the renderer
@ -22,6 +28,8 @@ var value_callback: Callable
## The type of this port (input, output or virtual) ## The type of this port (input, output or virtual)
var port_type: DeckNode.PortType var port_type: DeckNode.PortType
## The usage type of this port (see [enum UsageType]).
var usage_type: UsageType
## The local index of this port. ## The local index of this port.
var index_of_type: int var index_of_type: int
## The global index of this port. ## The global index of this port.
@ -41,6 +49,7 @@ func _init(
p_index_of_type: int, p_index_of_type: int,
p_descriptor: String = "", p_descriptor: String = "",
# p_value_callback: Callable = Callable(), # p_value_callback: Callable = Callable(),
p_usage_type: UsageType = UsageType.BOTH,
) -> void: ) -> void:
type = p_type type = p_type
label = p_label label = p_label
@ -50,6 +59,7 @@ func _init(
port_type = p_port_type port_type = p_port_type
index_of_type = p_index_of_type index_of_type = p_index_of_type
index = p_index index = p_index
usage_type = p_usage_type
func set_value(v: Variant) -> void: func set_value(v: Variant) -> void:

View file

@ -19,6 +19,11 @@ const TYPE_COLORS := {
DeckType.Types.ANY: Color.WHITE, DeckType.Types.ANY: Color.WHITE,
} }
const PORT_USAGE_ICONS := {
Port.UsageType.TRIGGER: preload("res://graph_node_renderer/textures/port_trigger_12.svg"),
Port.UsageType.VALUE_REQUEST: preload("res://graph_node_renderer/textures/port_data_request_12.svg"),
Port.UsageType.BOTH: preload("res://graph_node_renderer/textures/port_any_12.svg"),
}
## Setups up all the properties based off [member node]. Including looping through ## Setups up all the properties based off [member node]. Including looping through
## [method DeckNode.get_all_ports()] and setting up all the descriptors. ## [method DeckNode.get_all_ports()] and setting up all the descriptors.
@ -204,4 +209,6 @@ func update_port(port: Port) -> void:
port.port_type == DeckNode.PortType.OUTPUT, port.port_type == DeckNode.PortType.OUTPUT,
port.type, port.type,
TYPE_COLORS[port.type], TYPE_COLORS[port.type],
PORT_USAGE_ICONS[port.usage_type],
PORT_USAGE_ICONS[port.usage_type],
) )

View file

@ -87,6 +87,13 @@ func attempt_connection(from_node_name: StringName, from_port: int, to_node_name
) )
dirty = true dirty = true
func _is_node_hover_valid(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> bool:
var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node))
var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node))
return deck.is_valid_connection(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port)
## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s ## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s
## involved, utilizes [NodeDB] for accessing them. ## involved, utilizes [NodeDB] for accessing them.
func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void: func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void:

View file

@ -0,0 +1 @@
<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg"><circle cx="6" cy="6" r="5" fill="#dedede" stroke="#fff" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 144 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c0qh34njvu4xt"
path="res://.godot/imported/port_any_12.svg-64aca00b3bf28084e8a38d718780e0ff.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://graph_node_renderer/textures/port_any_12.svg"
dest_files=["res://.godot/imported/port_any_12.svg-64aca00b3bf28084e8a38d718780e0ff.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1 @@
<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="10" height="10" rx="1" ry="1" fill="#dedede" stroke="#fff" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 171 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b5houge8j0lij"
path="res://.godot/imported/port_data_request_12.svg-da181e9908b91f72dfc70f3dd7196720.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://graph_node_renderer/textures/port_data_request_12.svg"
dest_files=["res://.godot/imported/port_data_request_12.svg-da181e9908b91f72dfc70f3dd7196720.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1 @@
<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg"><path d="M9.75 6 1 10.25 1 1.75Z" fill="#dedede" stroke="#fff" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 150 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://tbxgx46ch210"
path="res://.godot/imported/port_trigger_12.svg-ddfb2ada8bfb56f098470a2797045ba0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://graph_node_renderer/textures/port_trigger_12.svg"
dest_files=["res://.godot/imported/port_trigger_12.svg-ddfb2ada8bfb56f098470a2797045ba0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false