store connections inside the deck (#147)

- node/port connections are now stored in the deck they belong to
- share the connections object between group instances for free syncing
- add compatibility code to allow loading decks made before this change
- disabled the ability to open multiple decks in the renderer for now since it'd cause problems when opening multiple decks and one of them is incompatible; to be redesigned later

closes #140

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/147
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
This commit is contained in:
Lera Elvoé 2024-04-17 00:24:55 +00:00 committed by yagich
parent 1ce3cd0367
commit f24906715d
10 changed files with 521 additions and 252 deletions

View file

@ -16,6 +16,9 @@ var variable_stack: Dictionary = {}
## The path to save this deck on the file system.
var save_path: String = ""
## The connections graph.
var connections := NodeConnections.new()
var is_group: bool = false
## List of groups belonging to this deck, in the format of[br]
## [code]Dictionary[String -> Deck.id, Deck][/code]
@ -25,6 +28,7 @@ var id: String = ""
## If this is a group, this is the local ID of this instance of the group.
var instance_id: String = ""
## The parent deck of this deck, if this is a group.
@warning_ignore("unused_private_class_variable")
var _belonging_to: String = "" # for groups
## The ID of this group's input node. Used only if [member is_group] is [code]true[/code].
var group_input_node: String
@ -52,8 +56,6 @@ signal variables_updated()
#region group signals
signal node_added_to_group(node: DeckNode, assign_id: String, assign_to_self: bool, deck: Deck)
signal node_removed_from_group(node_id: String, remove_connections: bool, deck: Deck)
signal nodes_connected_in_group(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int, deck: Deck)
signal nodes_disconnected_in_group(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int, deck: Deck)
signal node_port_value_updated(node_id: String, port_idx: int, new_value: Variant, deck: Deck)
signal node_renamed(node_id: String, new_name: String, deck: Deck)
signal node_moved(node_id: String, new_position: Dictionary, deck: Deck)
@ -129,6 +131,10 @@ func get_node(uuid: String) -> DeckNode:
return nodes.get(uuid)
func get_connections_dict() -> Dictionary:
return connections.data
## Returns [code]true[/code] if the connection between two nodes is legal.
func is_valid_connection(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> bool:
# do not connect to self
@ -152,7 +158,7 @@ func is_valid_connection(from_node_id: String, to_node_id: String, from_output_p
return false
# duplicate connection
if from_node.has_outgoing_connection_exact(from_output_port, to_node_id, to_input_port):
if connections.has_outgoing_connection_exact(from_node_id, from_output_port, to_node_id, to_input_port):
return false
return true
@ -162,34 +168,30 @@ func is_valid_connection(from_node_id: String, to_node_id: String, from_output_p
func connect_nodes(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> bool:
if not 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):
var connection: Dictionary = to_node.incoming_connections[to_input_port]
var node_id: String = connection.keys()[0]
var node_out_port: int = connection.values()[0]
disconnect_nodes(node_id, to_node_id, node_out_port, to_input_port)
if connections.has_incoming_connection(to_node_id, to_input_port):
var connection := connections.get_incoming_connection(to_node_id, to_input_port)
disconnect_nodes(connection.from_node, to_node_id, connection.from_port, to_input_port)
if is_group and emit_group_signals:
nodes_connected_in_group.emit(from_node_id, to_node_id, from_output_port, to_input_port, self)
from_node.add_outgoing_connection(from_output_port, to_node._id, to_input_port)
connections.add_connection(from_node_id, to_node_id, from_output_port, to_input_port)
nodes_connected.emit(from_node_id, to_node_id, from_output_port, to_input_port)
from_node.outgoing_connection_added.emit(from_output_port)
to_node.incoming_connection_added.emit(to_input_port)
return true
## Remove a connection from two nodes.
func disconnect_nodes(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> void:
connections.remove_connection(from_node_id, to_node_id, from_output_port, to_input_port)
nodes_disconnected.emit(from_node_id, to_node_id, from_output_port, to_input_port)
var from_node := get_node(from_node_id)
var to_node := get_node(to_node_id)
from_node.remove_outgoing_connection(from_output_port, to_node_id, to_input_port)
to_node.remove_incoming_connection(to_input_port)
if is_group and emit_group_signals:
nodes_disconnected_in_group.emit(from_node_id, to_node_id, from_output_port, to_input_port, self)
nodes_disconnected.emit(from_node_id, to_node_id, from_output_port, to_input_port)
from_node.outgoing_connection_removed.emit(from_output_port)
to_node.incoming_connection_removed.emit(to_input_port)
## Returns true if this deck has no nodes and no variables.
@ -210,19 +212,17 @@ func remove_node(uuid: String, remove_connections: bool = false, force: bool = f
DeckHolder.close_group_instance(node.group_id, node.group_instance_id)
if remove_connections:
var outgoing_connections := node.outgoing_connections.duplicate(true)
for output_port: int in outgoing_connections:
for to_node: String in outgoing_connections[output_port]:
for to_port: int in outgoing_connections[output_port][to_node]:
disconnect_nodes(uuid, to_node, output_port, to_port)
var incoming_connections := node.incoming_connections.duplicate(true)
for input_port: int in incoming_connections:
for from_node: String in incoming_connections[input_port]:
disconnect_nodes(from_node, uuid, incoming_connections[input_port][from_node], input_port)
var outgoing_connections := connections.get_all_outgoing_connections(uuid)
for from_port: int in outgoing_connections:
for outgoing: OutgoingConnection in outgoing_connections[from_port]:
var incoming := outgoing.counterpart.get_ref() as IncomingConnection
disconnect_nodes(uuid, outgoing.to_node, incoming.from_port, outgoing.to_port)
var incoming_connections := connections.get_all_incoming_connections(uuid)
for to_port: int in incoming_connections:
var incoming := connections.get_incoming_connection(uuid, to_port)
disconnect_nodes(incoming.from_node, uuid, incoming.from_port, to_port)
nodes.erase(uuid)
node_removed.emit(node)
@ -265,6 +265,10 @@ func group_nodes(nodes_to_group: Array) -> Deck:
)
var group := DeckHolder.add_empty_group(id)
var connection_pairs := connections.filter_pairs(node_ids_to_keep)
for pair in connection_pairs:
group.connections.add_pair(pair)
var midpoint := Vector2()
@ -283,20 +287,31 @@ func group_nodes(nodes_to_group: Array) -> Deck:
if node.position.x < leftmost:
leftmost = node.position.x
var outgoing_connections := node.outgoing_connections.duplicate(true)
#var outgoing_connections := node.outgoing_connections.duplicate(true)
#
#for from_port: int in outgoing_connections:
#for to_node: String in outgoing_connections[from_port]:
#for to_port: int in outgoing_connections[from_port][to_node]:
#if to_node not in node_ids_to_keep:
#disconnect_nodes(node._id, to_node, from_port, to_port)
var outgoing_connections := connections.get_all_outgoing_connections(node._id)
for from_port: int in outgoing_connections:
for to_node: String in outgoing_connections[from_port]:
for to_port: int in outgoing_connections[from_port][to_node]:
if to_node not in node_ids_to_keep:
disconnect_nodes(node._id, to_node, from_port, to_port)
var incoming_connections := node.incoming_connections.duplicate(true)
for outgoing: OutgoingConnection in outgoing_connections[from_port]:
var incoming := outgoing.counterpart.get_ref() as IncomingConnection
disconnect_nodes(node._id, outgoing.to_node, incoming.from_port, outgoing.to_port)
#var incoming_connections := node.incoming_connections.duplicate(true)
#
#for to_port: int in incoming_connections:
#for from_node: String in incoming_connections[to_port]:
#if from_node not in node_ids_to_keep:
#disconnect_nodes(from_node, node._id, incoming_connections[to_port][from_node], to_port)
var incoming_connections := connections.get_all_incoming_connections(node._id)
for to_port: int in incoming_connections:
for from_node: String in incoming_connections[to_port]:
if from_node not in node_ids_to_keep:
disconnect_nodes(from_node, node._id, incoming_connections[to_port][from_node], to_port)
var incoming := connections.get_incoming_connection(node._id, to_port)
disconnect_nodes(incoming.from_node, node._id, incoming.from_port, to_port)
midpoint += node.position_as_vector2()
remove_node(node._id, false, true)
@ -344,7 +359,10 @@ func group_nodes(nodes_to_group: Array) -> Deck:
func copy_nodes(nodes_to_copy: Array[String]) -> Dictionary:
var d := {"nodes": {}}
var d := {
"nodes": {},
"connections": connections.filter_pairs(nodes_to_copy),
}
for node_id: String in nodes_to_copy:
d.nodes[node_id] = get_node(node_id).to_dict()
@ -392,7 +410,12 @@ func copy_nodes(nodes_to_copy: Array[String]) -> Dictionary:
func copy_nodes_json(nodes_to_copy: Array[String]) -> String:
return JSON.stringify(copy_nodes(nodes_to_copy))
var res := copy_nodes(nodes_to_copy)
res.connections = res.connections.map(
func(x: ConnectionPair):
return x.to_dict()
)
return JSON.stringify(res)
func allocate_ids(count: int) -> Array[String]:
@ -406,35 +429,30 @@ func paste_nodes_from_dict(nodes_to_paste: Dictionary, position: Vector2 = Vecto
if not nodes_to_paste.get("nodes"):
return
var pairs = (nodes_to_paste.connections as Array).map(
func(x: Dictionary):
return ConnectionPair.from_dict(x)
)
var new_ids := allocate_ids(nodes_to_paste.nodes.size())
var ids_map := {}
for i: int in nodes_to_paste.nodes.keys().size():
var node_id: String = nodes_to_paste.nodes.keys()[i]
ids_map[node_id] = new_ids[i]
for old_id: String in ids_map:
for pair: ConnectionPair in pairs:
pair.remap_id(old_id, ids_map[old_id])
for pair: ConnectionPair in pairs:
connections.add_pair(pair)
for node_id: String in nodes_to_paste.nodes:
nodes_to_paste.nodes[node_id]._id = ids_map[node_id]
nodes_to_paste.nodes[node_id].position.x += position.x
nodes_to_paste.nodes[node_id].position.y += position.y
var outgoing_connections: Dictionary = nodes_to_paste.nodes[node_id].outgoing_connections as Dictionary
var outgoing_connections_res := {}
for from_port in outgoing_connections:
outgoing_connections_res[from_port] = {}
for to_node_id in outgoing_connections[from_port]:
outgoing_connections_res[from_port][ids_map[to_node_id]] = outgoing_connections[from_port][to_node_id]
var incoming_connections: Dictionary = nodes_to_paste.nodes[node_id].incoming_connections as Dictionary
var incoming_connections_res := {}
for to_port in incoming_connections:
incoming_connections_res[to_port] = {}
for from_node_id in incoming_connections[to_port]:
incoming_connections_res[to_port][ids_map[from_node_id]] = incoming_connections[to_port][from_node_id]
nodes_to_paste.nodes[node_id].outgoing_connections = outgoing_connections_res
nodes_to_paste.nodes[node_id].incoming_connections = incoming_connections_res
var node := DeckNode.from_dict(nodes_to_paste.nodes[node_id])
var group_needs_unique := false
if node.node_type == "group_node":
@ -465,6 +483,29 @@ func duplicate_nodes(nodes_to_copy: Array[String]) -> void:
paste_nodes_from_dict(d, position)
## Send [param data] from [param from_node_id] to all outgoing connections on port [param from_output_port].[br]
## See [method DeckNode.send].
func send(from_node_id: String, from_output_port: int, data: Variant, send_id: String) -> void:
var outgoing_connections := connections.get_outgoing_connections(from_node_id, from_output_port)
for connection in outgoing_connections:
var node := get_node(connection.to_node)
node.handle_receive(connection.to_port, data, send_id)
## Asynchronously request a value from an incoming connection on the node at [param node_id]'s input port at [param on_input_port].
## Returns [code]null[/code] if no incoming connection exists on that port.
## The connected node may also return [code]null[/code].[br]
## See [method DeckNode.request_value_async].
func request_value_async(node_id: String, on_input_port: int) -> Variant:
var connection := connections.get_incoming_connection(node_id, on_input_port)
if connection == null:
return null
var other_node := get_node(connection.from_node)
return await other_node._value_request(connection.from_port)
func send_event(event_name: StringName, event_data: Dictionary = {}) -> void:
for node: DeckNode in nodes.values():
node._event_received(event_name, event_data)
@ -512,7 +553,8 @@ func to_dict(with_meta: bool = true, group_ids: Array = []) -> Dictionary:
"nodes": {},
"variable_stack": variable_stack,
"id": id,
"groups": {}
"groups": {},
"connections": connections.to_dict(),
}
for node_id: String in nodes.keys():
@ -545,6 +587,7 @@ static func from_dict(data: Dictionary, path: String = "") -> Deck:
deck.save_path = path
deck.variable_stack = data.deck.variable_stack
deck.id = data.deck.id
deck.connections = NodeConnections.from_dict(data.deck.connections)
for key in data.meta:
deck.set_meta(key, str_to_var(data.meta[key]))
@ -581,3 +624,269 @@ static func from_dict(data: Dictionary, path: String = "") -> Deck:
return deck
class NodeConnections:
# Dictionary[String -> node id, Dictionary["incoming": Dictionary[int -> input port idx, IncomingConnection], "outgoing": Dictionary[int - > output port idx, Array[OutgoingConnection]]]
var data := {}
func add_connection(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> void:
var from_connection: Dictionary = data.get(from_node_id, _create_empty_connection())
var to_connection: Dictionary = data.get(to_node_id, _create_empty_connection())
var pair := ConnectionPair.new(from_node_id, to_node_id, from_output_port, to_input_port)
var out_list: Array = (from_connection.outgoing as Dictionary).get(from_output_port, [])
out_list.append(pair.outgoing)
(from_connection.outgoing as Dictionary)[from_output_port] = out_list
(to_connection.incoming as Dictionary)[to_input_port] = pair.incoming
data[from_node_id] = from_connection
data[to_node_id] = to_connection
func remove_connection(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> void:
var from_connection: Dictionary = data.get(from_node_id, {})
if from_connection.is_empty():
return
var out_list: Array = (from_connection.outgoing as Dictionary).get(from_output_port, [])
if out_list.is_empty():
return
var comp := ConnectionPair.new(from_node_id, to_node_id, from_output_port, to_input_port)
for i in out_list.size():
var out: OutgoingConnection = out_list[i]
var pair := ConnectionPair.from_outgoing(out)
if pair.is_equivalent(comp):
var to_connection: Dictionary = data.get(to_node_id).incoming
to_connection.erase(to_input_port)
out_list.erase(out)
break
func has_incoming_connection(node_id: String, on_port: int) -> bool:
return data.get(node_id, _create_empty_connection()).incoming.has(on_port)
func get_incoming_connection(node_id: String, on_port: int) -> IncomingConnection:
return data.get(node_id, _create_empty_connection()).incoming.get(on_port)
func has_outgoing_connection_exact(node_id: String, from_port: int, to_node: String, to_port: int) -> bool:
if not data.get(node_id, _create_empty_connection()).outgoing.has(from_port):
return false
var has = false
for connection: OutgoingConnection in data.get(node_id, _create_empty_connection()).outgoing.get(from_port):
var inc := connection.counterpart.get_ref() as IncomingConnection
if connection.to_node == to_node \
and connection.to_port == to_port\
and inc.from_port == from_port:
has = true
return has
func get_outgoing_connections(node_id: String, from_port: int) -> Array[OutgoingConnection]:
if data.get(node_id, _create_empty_connection()).outgoing.is_empty():
return []
var res: Array[OutgoingConnection] = []
res.assign(data[node_id].outgoing[from_port])
return res
func get_all_outgoing_connections(node_id: String) -> Dictionary:
return data.get(node_id, _create_empty_connection()).outgoing
func get_all_incoming_connections(node_id: String) -> Dictionary:
return data.get(node_id, _create_empty_connection()).incoming
func filter_pairs(nodes: Array) -> Array[ConnectionPair]:
var res: Array[ConnectionPair] = []
for node_id: String in nodes:
var connections: Dictionary = data.get(node_id, _create_empty_connection())
for to_port: int in connections.outgoing:
for outgoing: OutgoingConnection in connections.outgoing[to_port]:
if outgoing.to_node in nodes:
res.append(ConnectionPair.from_outgoing(outgoing))
return res
func add_pair(pair: ConnectionPair) -> void:
var outgoing := pair.outgoing
var incoming := pair.incoming
add_connection(incoming.from_node, outgoing.to_node, incoming.from_port, outgoing.to_port)
func _create_empty_connection() -> Dictionary:
return {
"incoming": {},
"outgoing": {},
}
func to_dict() -> Dictionary:
var res := {}
for node_id: String in data:
res[node_id] = _create_empty_connection()
for to_port: int in data[node_id].incoming:
res[node_id].incoming[to_port] = (data[node_id].incoming[to_port] as IncomingConnection).to_dict()
for from_port: int in data[node_id].outgoing:
for i: OutgoingConnection in data[node_id].outgoing[from_port]:
var arr: Array = res[node_id].outgoing.get(from_port, [])
arr.append(i.to_dict())
res[node_id].outgoing[from_port] = arr
return res
static func from_dict(d: Dictionary) -> NodeConnections:
var res := NodeConnections.new()
for node_id: String in d:
for from_port in d[node_id].outgoing:
for connection: Dictionary in d[node_id].outgoing[from_port]:
res.add_connection(node_id, connection.to_node, int(from_port), int(connection.to_port))
return res
class ConnectionPair:
var from_node_id: String
var to_node_id: String
var from_output_port: int
var to_input_port: int
var incoming: IncomingConnection
var outgoing: OutgoingConnection
func _init(
p_from_node_id: String,
p_to_node_id: String,
p_from_output_port: int,
p_to_input_port: int,
p_outgoing: OutgoingConnection = null,
p_incoming: IncomingConnection = null
) -> void:
from_node_id = p_from_node_id
to_node_id = p_to_node_id
from_output_port = p_from_output_port
to_input_port = p_to_input_port
if not p_outgoing:
outgoing = OutgoingConnection.new()
outgoing.to_node = to_node_id
outgoing.to_port = to_input_port
incoming = IncomingConnection.new()
incoming.from_port = from_output_port
incoming.from_node = from_node_id
incoming.counterpart = weakref(outgoing)
outgoing.counterpart = weakref(incoming)
else:
outgoing = p_outgoing
incoming = p_incoming
func remap_id(old_id: String, new_id: String) -> void:
if old_id == from_node_id:
from_node_id = new_id
incoming.from_node = new_id
elif old_id == to_node_id:
to_node_id = new_id
outgoing.to_node = new_id
func is_equivalent(other: ConnectionPair) -> bool:
if from_node_id == other.from_node_id and \
to_node_id == other.to_node_id and \
from_output_port == other.from_output_port and \
to_input_port == other.to_input_port:
return true
return false
func to_dict() -> Dictionary:
return {
"incoming": incoming.to_dict(),
"outgoing": outgoing.to_dict(),
}
static func from_incoming(p_incoming: IncomingConnection) -> ConnectionPair:
@warning_ignore("shadowed_variable")
var outgoing := p_incoming.counterpart.get_ref() as OutgoingConnection
return ConnectionPair.new(
p_incoming.from_node, outgoing.to_node,
p_incoming.from_port, outgoing.to_port,
outgoing, p_incoming
)
static func from_outgoing(p_outgoing: OutgoingConnection) -> ConnectionPair:
@warning_ignore("shadowed_variable")
var incoming := p_outgoing.counterpart.get_ref() as IncomingConnection
return ConnectionPair.new(
incoming.from_node, p_outgoing.to_node,
incoming.from_port, p_outgoing.to_port,
p_outgoing, incoming
)
static func from_dict(d: Dictionary) -> ConnectionPair:
@warning_ignore("shadowed_variable")
var outgoing := OutgoingConnection.new()
outgoing.to_node = d.outgoing.to_node
outgoing.to_port = d.outgoing.to_port
@warning_ignore("shadowed_variable")
var incoming := IncomingConnection.new()
incoming.from_node = d.incoming.from_node
incoming.from_port = d.incoming.from_port
outgoing.counterpart = weakref(incoming)
incoming.counterpart = weakref(outgoing)
return from_incoming(incoming)
class IncomingConnection:
var from_node: String
var from_port: int
var counterpart: WeakRef # OutgoingConnection
func _to_string() -> String:
return str({"from_node": from_node, "from_port": from_port})
func to_dict() -> Dictionary:
return {"from_node": from_node, "from_port": from_port}
class OutgoingConnection:
var to_node: String
var to_port: int
var counterpart: WeakRef # IncomingConnection
func _to_string() -> String:
return str({"to_node": to_node, "to_port": to_port})
func to_dict() -> Dictionary:
return {"to_node": to_node, "to_port": to_port}

View file

@ -14,6 +14,10 @@ static var groups_emitted: Array[String]
static var logger := Logger.new()
static var signals := Signals.new()
enum Compat {
CONNECTIONS_IN_DECK = 1 << 0,
}
static func _static_init() -> void:
signals.deck_added.connect(RPCSignalLayer._on_deck_added)
@ -31,6 +35,28 @@ static func add_empty_deck() -> Deck:
return deck
static func get_deck_compat(data: Dictionary) -> int:
var res := 0
if not data.deck.has("connections"):
res |= Compat.CONNECTIONS_IN_DECK
return res
static func apply_compat_patches(data: Dictionary, compat: int) -> void:
if compat & Compat.CONNECTIONS_IN_DECK:
# convert pre-0.0.6 connections to be stored in the deck instead
var connections := Deck.NodeConnections.new()
for node: String in data.deck.nodes:
for from_port in data.deck.nodes[node].outgoing_connections:
for to_node: String in data.deck.nodes[node].outgoing_connections[from_port]:
for to_port: int in data.deck.nodes[node].outgoing_connections[from_port][to_node]:
connections.add_connection(node, to_node, int(from_port), to_port)
data.deck.connections = connections.to_dict()
## Opens a deck from the [param path].
static func open_deck_from_file(path: String) -> Deck:
var f := FileAccess.open(path, FileAccess.READ)
@ -42,6 +68,8 @@ static func open_deck_from_file(path: String) -> Deck:
static func open_deck_from_dict(data: Dictionary, path := "") -> Deck:
if get_deck_compat(data) != 0:
apply_compat_patches(data, get_deck_compat(data))
var deck := Deck.from_dict(data, path)
decks[deck.id] = deck
deck.connect_rpc_signals()
@ -72,7 +100,10 @@ static func add_group_from_dict(data: Dictionary, deck_id: String, instance_id:
static func make_new_group_instance(group_id: String, parent: String = "") -> Deck:
var group := get_deck(group_id)
var data := group.to_dict()
return add_group_from_dict(data, group_id, UUID.v4(), parent)
var inst := add_group_from_dict(data, group_id, UUID.v4(), parent)
# copy connections
inst.connections = group.connections
return inst
static func make_group_instance_unique(group_id: String, instance_id: String, parent_deck_id: String, group_node_id: String) -> Deck:
@ -119,8 +150,6 @@ static func add_empty_group(parent: String = "") -> Deck:
static func connect_group_signals(group: Deck) -> void:
group.node_added_to_group.connect(DeckHolder._on_node_added_to_group)
group.node_removed_from_group.connect(DeckHolder._on_node_removed_from_group)
group.nodes_connected_in_group.connect(DeckHolder._on_nodes_connected_in_group)
group.nodes_disconnected_in_group.connect(DeckHolder._on_nodes_disconnected_in_group)
group.node_port_value_updated.connect(DeckHolder._on_node_port_value_updated)
group.node_renamed.connect(DeckHolder._on_node_renamed)
group.node_moved.connect(DeckHolder._on_node_moved)
@ -216,30 +245,6 @@ static func _on_node_removed_from_group(node_id: String, remove_connections: boo
instance.emit_group_signals = true
static func _on_nodes_connected_in_group(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int, deck: Deck) -> void:
var group_id := deck.id
for instance_id: String in decks[group_id]:
if instance_id == deck.instance_id:
continue
var instance: Deck = get_group_instance(group_id, instance_id)
instance.emit_group_signals = false
instance.connect_nodes(from_node_id, to_node_id, from_output_port, to_input_port)
instance.emit_group_signals = true
static func _on_nodes_disconnected_in_group(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int, deck: Deck) -> void:
var group_id := deck.id
for instance_id: String in decks[group_id]:
if instance_id == deck.instance_id:
continue
var instance: Deck = get_group_instance(group_id, instance_id)
instance.emit_group_signals = false
instance.disconnect_nodes(from_node_id, to_node_id, from_output_port, to_input_port)
instance.emit_group_signals = true
static func _on_node_port_value_updated(node_id: String, port_idx: int, new_value: Variant, deck: Deck) -> void:
var group_id := deck.id
for instance_id: String in decks[group_id]:

View file

@ -10,12 +10,6 @@ class_name DeckNode
## 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]
@ -127,23 +121,9 @@ func add_port(type: DeckType.Types,
return port
## 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, send_id: String = UUID.v4()) -> 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).handle_receive(input_port, data, send_id)
_belonging_to.send(_id, from_output_port, data, send_id)
func handle_receive(to_input_port: int, data: Variant, send_id: String) -> void:
@ -160,36 +140,11 @@ func _receive(to_input_port: int, data: Variant) -> 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 not 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])
return await _belonging_to.request_value_async(_id, on_port)
## Virtual function that's called when this node has been requested a value from the output port
@ -199,52 +154,6 @@ 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 not outgoing_connections.has(from_port):
return false
var connection: Dictionary = outgoing_connections[from_port]
if not connection.has(to_node):
return false
var connections_to_node: Array = connection[to_node]
if to_port not 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)
@ -324,8 +233,8 @@ 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:
## Virtual function that's called during deserialization before connections are loaded in.[br]
func _pre_port_load() -> void:
pass
@ -357,8 +266,8 @@ 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),
#"outgoing_connections": outgoing_connections.duplicate(true),
#"incoming_connections": incoming_connections.duplicate(true),
"props": {},
"node_type": node_type,
"port_values": [],
@ -389,22 +298,22 @@ static func from_dict(data: Dictionary) -> DeckNode:
for prop in data.props:
node.set(prop, data.props[prop])
node._pre_connection()
node._pre_port_load()
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 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

View file

@ -37,13 +37,10 @@ func _on_outgoing_connection_added(port_idx: int) -> void:
func _on_outgoing_connection_removed(port_idx: int) -> void:
var last_connected_port := 0
#for port: int in outgoing_connections.keys().slice(1):
#if !(outgoing_connections[port] as Array).is_empty():
#last_connected_port = port
for port: int in outgoing_connections.keys().slice(1):
if not (outgoing_connections.get(port, {}) as Dictionary).is_empty():
var outgoing_connections = _belonging_to.connections.get_all_outgoing_connections(_id)
for port: int in outgoing_connections:
if not (outgoing_connections[port] as Array).is_empty():
last_connected_port = port
#prints("l:", last_connected_port, "p:", port_idx)
if port_idx < last_connected_port:
@ -54,7 +51,7 @@ func _on_outgoing_connection_removed(port_idx: int) -> void:
ports_updated.emit()
func _pre_connection() -> void:
func _pre_port_load() -> void:
for i in output_count + 1:
add_output_port(
DeckType.Types.ANY,
@ -65,6 +62,7 @@ func _pre_connection() -> void:
func _post_load() -> void:
# ensure we have enough ports after connections
var last_connected_port := 0
var outgoing_connections = _belonging_to.connections.get_all_outgoing_connections(_id)
for port: int in outgoing_connections:
last_connected_port = port if outgoing_connections.has(port) else last_connected_port

View file

@ -21,7 +21,7 @@ func _init() -> void:
appears_in_search = false
func _pre_connection() -> void:
func _pre_port_load() -> void:
for port_type: PortType in extra_ports:
match port_type:
PortType.OUTPUT:

View file

@ -38,9 +38,9 @@ func _on_incoming_connection_added(port_idx: int) -> void:
func _on_incoming_connection_removed(port_idx: int) -> void:
var last_connected_port := 0
for port: int in incoming_connections.keys().slice(1):
if not (incoming_connections[port] as Dictionary).is_empty():
last_connected_port = port
var incoming_connections := _belonging_to.connections.get_all_incoming_connections(_id)
if not incoming_connections.is_empty():
last_connected_port = incoming_connections.keys()[-1]
#prints("l:", last_connected_port, "p:", port_idx)
@ -52,7 +52,7 @@ func _on_incoming_connection_removed(port_idx: int) -> void:
ports_updated.emit()
func _pre_connection() -> void:
func _pre_port_load() -> void:
for i in input_count + 1:
add_input_port(
DeckType.Types.ANY,
@ -63,6 +63,7 @@ func _pre_connection() -> void:
func _post_load() -> void:
# ensure we have enough ports after connections
var last_connected_port := 0
var incoming_connections := _belonging_to.connections.get_all_incoming_connections(_id)
for port: int in incoming_connections:
last_connected_port = port if incoming_connections.has(port) else last_connected_port

View file

@ -0,0 +1,18 @@
[gd_scene format=3 uid="uid://cd1t0gvi022gx"]
[node name="CompatDialog" type="ConfirmationDialog"]
initial_position = 1
size = Vector2i(440, 175)
ok_button_text = "Open"
dialog_autowrap = true
[node name="Label" type="Label" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 432.0
offset_bottom = 129.0
text = "Warning: This deck was last saved with an earlier version of StreamGraph.
Open anyway? The file will be modified, and opening it in the version it was saved with may lead to errors."
horizontal_alignment = 1
autowrap_mode = 2

View file

@ -77,6 +77,8 @@ var _deck_to_save: WeakRef
@onready var sidebar_split: HSplitContainer = %SidebarSplit
@onready var sidebar: Sidebar = %Sidebar as Sidebar
@onready var compat_dialog: ConfirmationDialog = %CompatDialog
signal quit_completed()
signal rpc_start_requested(port: int)
signal rpc_stop_requested()
@ -330,6 +332,7 @@ func close_tab(tab: int) -> void:
if tab_container.get_tab_count() == 0:
bottom_dock.variable_viewer.disable_new_button()
## Opens [member file_dialog] with the mode [member FileDialog.FILE_MODE_SAVE_FILE]
## as well as getting a weakref to the active [Deck]
func open_save_dialog(path: String) -> void:
@ -340,14 +343,20 @@ func open_save_dialog(path: String) -> void:
file_dialog.popup_centered()
file_dialog.file_selected.connect(_on_file_dialog_save_file, CONNECT_ONE_SHOT)
## Opens [member file_dialog] with the mode [FileDialog.FILE_MODE_OPEN_FILES]
## with the supplied [param path]
func open_open_dialog(path: String) -> void:
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILES
file_dialog.title = "Open Deck(s)"
#file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILES
# TODO: disabled opening multiple for now until better compat dialog method is found
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
#file_dialog.title = "Open Deck(s)"
file_dialog.title = "Open Deck"
file_dialog.current_path = path + "/"
file_dialog.popup_centered()
file_dialog.files_selected.connect(_on_file_dialog_open_files, CONNECT_ONE_SHOT)
#file_dialog.files_selected.connect(_on_file_dialog_open_files, CONNECT_ONE_SHOT)
file_dialog.file_selected.connect(open_deck_at_path, CONNECT_ONE_SHOT)
## Connected to [signal FileDialog.save_file] on [member file_dialog].
## Saves the selected [Deck] if it still exists.
@ -381,8 +390,23 @@ func open_deck_at_path(path: String) -> void:
if tab_container.get_tab_metadata(tab, "path") == path:
tab_container.set_current_tab(tab)
return
var deck := DeckHolder.open_deck_from_file(path)
var f := FileAccess.open(path, FileAccess.READ)
if f.get_error() != OK:
return
var deck_data: Dictionary = JSON.parse_string(f.get_as_text())
if DeckHolder.get_deck_compat(deck_data) != 0:
compat_dialog.set_meta(&"path", path)
file_dialog.hide()
compat_dialog.popup_centered()
return
open_deck_from_dict(deck_data, path)
func open_deck_from_dict(data: Dictionary, path: String) -> void:
var deck := DeckHolder.open_deck_from_dict(data, path)
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
inst.deck = deck
var tab := tab_container.add_content(inst, path.get_file())
@ -450,6 +474,10 @@ func disconnect_file_dialog_signals() -> void:
if file_dialog.files_selected.is_connected(_on_file_dialog_open_files):
file_dialog.files_selected.disconnect(_on_file_dialog_open_files)
if file_dialog.file_selected.is_connected(open_deck_at_path):
file_dialog.file_selected.disconnect(open_deck_at_path)
## Connected to [signal DeckRenderGraphEdit.group_entered_request] to allow entering
## groups based off the given [param group_id] and [param deck]. As well as adding
@ -652,3 +680,19 @@ func _on_help_id_pressed(id: int) -> void:
HelpMenuId.DOCS:
OS.shell_open("https://codeberg.org/Eroax/StreamGraph/wiki")
func _on_compat_dialog_confirmed() -> void:
var path: String = compat_dialog.get_meta(&"path", "")
var f := FileAccess.open(path, FileAccess.READ)
if f.get_error() != OK:
return
# save a backup
if path.split(".")[-2] != "old":
var backup_path := "%s.old.deck" % path.trim_suffix(".deck")
DirAccess.copy_absolute(path, backup_path)
var deck_data: Dictionary = JSON.parse_string(f.get_as_text())
open_deck_from_dict(deck_data, path)
# set it as dirty to make sure the user resaves
get_active_deck_renderer().dirty = true

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=17 format=3 uid="uid://duaah5x0jhkn6"]
[gd_scene load_steps=18 format=3 uid="uid://duaah5x0jhkn6"]
[ext_resource type="Script" path="res://graph_node_renderer/deck_holder_renderer.gd" id="1_67g2g"]
[ext_resource type="PackedScene" uid="uid://b84f2ngtcm5b8" path="res://graph_node_renderer/tab_container_custom.tscn" id="1_s3ug2"]
@ -16,6 +16,7 @@
[ext_resource type="PackedScene" uid="uid://bu466w2w3q08c" path="res://graph_node_renderer/about_dialog.tscn" id="11_6ln7n"]
[ext_resource type="PackedScene" uid="uid://brfrufvkjwcor" path="res://graph_node_renderer/rpc_setup_dialog.tscn" id="12_1xrfk"]
[ext_resource type="PackedScene" uid="uid://dodqetbke5wji" path="res://graph_node_renderer/settings_dialog.tscn" id="16_rktri"]
[ext_resource type="PackedScene" uid="uid://cd1t0gvi022gx" path="res://graph_node_renderer/compat_dialog.tscn" id="17_2ndnq"]
[node name="DeckHolderRenderer" type="Control"]
layout_mode = 3
@ -170,6 +171,9 @@ unique_name_in_owner = true
[node name="SettingsDialog" parent="." instance=ExtResource("16_rktri")]
unique_name_in_owner = true
[node name="CompatDialog" parent="." instance=ExtResource("17_2ndnq")]
unique_name_in_owner = true
[connection signal="id_pressed" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/File" to="." method="_on_file_id_pressed"]
[connection signal="about_to_popup" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/Edit" to="." method="_on_edit_about_to_popup"]
[connection signal="id_pressed" from="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer/MenuBar/Edit" to="." method="_on_edit_id_pressed"]
@ -180,3 +184,4 @@ unique_name_in_owner = true
[connection signal="confirmed" from="UnsavedChangesDialogSingleDeck" to="." method="_on_unsaved_changes_dialog_single_deck_confirmed"]
[connection signal="custom_action" from="UnsavedChangesDialogSingleDeck" to="." method="_on_unsaved_changes_dialog_single_deck_custom_action"]
[connection signal="confirmed" from="UnsavedChangesDialog" to="." method="_on_unsaved_changes_dialog_confirmed"]
[connection signal="confirmed" from="CompatDialog" to="." method="_on_compat_dialog_confirmed"]

View file

@ -119,11 +119,7 @@ func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name
## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode].
## Or [code]null[/code] if none is found.
func get_node_renderer(node: DeckNode) -> DeckNodeRendererGraphNode:
for i: DeckNodeRendererGraphNode in get_children():
if i.node == node:
return i
return null
return get_node_or_null(NodePath(node._id))
func focus_node(node: DeckNodeRendererGraphNode) -> void:
@ -162,7 +158,8 @@ func initialize_from_deck() -> void:
is_group = deck.is_group
for node_id in deck.nodes:
var node_renderer: DeckNodeRendererGraphNode = NODE_SCENE.instantiate()
node_renderer.node = deck.nodes[node_id]
node_renderer.node = deck.get_node(node_id)
node_renderer.name = node_id
add_child(node_renderer)
node_renderer.position_offset = node_renderer.node.position_as_vector2()
change_dirty = true
@ -178,29 +175,16 @@ func initialize_from_deck() -> void:
## [method GraphEdit.connect_node] for all the connections that exist in each
func refresh_connections() -> void:
for node_id in deck.nodes:
var node: DeckNode = deck.nodes[node_id]
var from_node: DeckNodeRendererGraphNode = get_children().filter(
func(c: DeckNodeRendererGraphNode):
return c.node._id == node_id
)[0]
for from_port in node.outgoing_connections:
for to_node_id: String in node.outgoing_connections[from_port]:
var to_node_ports = node.outgoing_connections[from_port][to_node_id]
var renderer: Array = get_children().filter(
func(c: DeckNodeRendererGraphNode):
return c.node._id == to_node_id
if not deck.get_connections_dict().has(node_id):
continue
for from_port: int in deck.get_connections_dict()[node_id].outgoing:
for outgoing: Deck.OutgoingConnection in deck.get_connections_dict()[node_id].outgoing[from_port]:
connect_node(
node_id,
from_port,
outgoing.to_node,
outgoing.to_port
)
if renderer.is_empty():
break
var to_node: DeckNodeRendererGraphNode = renderer[0]
for to_node_port: int in to_node_ports:
connect_node(
from_node.name,
from_port,
to_node.name,
to_node_port
)
## Connected to [signal Deck.node_added], used to instance the required
@ -208,6 +192,7 @@ func refresh_connections() -> void:
func _on_deck_node_added(node: DeckNode) -> void:
var inst: DeckNodeRendererGraphNode = NODE_SCENE.instantiate()
inst.node = node
inst.name = node._id
add_child(inst)
inst.position_offset = inst.node.position_as_vector2()
dirty = true
@ -236,8 +221,6 @@ func get_selected_nodes() -> Array:
)
## Executes functionality based off hotkey inputs. Specifically handles creating groups
## based off the action "group_nodes".
func _gui_input(event: InputEvent) -> void:
if RendererShortcuts.check_shortcut("group_nodes", event) and get_selected_nodes().size() > 0:
clear_connections()
@ -293,8 +276,6 @@ func _gui_input(event: InputEvent) -> void:
focus_selection()
## Handles entering groups with action "enter_group". Done here to bypass neighbor
## functionality.
func _input(event: InputEvent) -> void:
if not has_focus():
return
@ -343,7 +324,6 @@ func _on_add_node_menu_node_selected(type: String) -> void:
func _on_deck_nodes_disconnected(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> void:
print("1")
var from_node: DeckNodeRendererGraphNode = get_children().filter(
func(x: DeckNodeRendererGraphNode):
return x.node._id == from_node_id