miggor-StreamGraph/classes/deck/deck.gd
Lera Elvoé 0ebe17399d fix connections being copied incorrectly (#42)
when copying nodes, incoming and outgoing connections could be copied in such a way that they had (empty) entries in the outgoing/incoming connections dictionaries despite not being connected to anything

Reviewed-on: https://codeberg.org/Eroax/StreamGraph/pulls/42
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
2024-01-17 08:15:02 +00:00

506 lines
18 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 Deck
## A deck/graph with nodes.
##
## A container for [DeckNode]s, managing connections between them.
## The [DeckNode]s that belong to this deck. The key is the node's id, the value
## is the [DeckNode] instance.
var nodes: Dictionary
## A map of variables set on this deck.
var variable_stack: Dictionary = {}
## The path to save this deck on the file system.
var save_path: String = ""
var is_group: bool = false
## List of groups belonging to this deck, in the format of[br]
## [code]Dictionary[String -> Deck.id, Deck][/code]
#var groups: Dictionary = {}
## A unique identifier for this deck, or an ID for the group this deck represents.
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.
#var _belonging_to: Deck # 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
## The ID of this group's input node. Used only if [member is_group] is [code]true[/code].
var group_output_node: String
## The ID of the group node this group is represented by, contained in this deck's parent deck.
## Used only if [member is_group] is [code]true[/code].
## @experimental
#var group_node: String
var emit_group_signals: bool = true
var emit_node_added_signal: bool = true
## Emitted when a node has been added to this deck.
signal node_added(node: DeckNode)
## Emitted when a node has been removed from this deck.
signal node_removed(node: DeckNode)
## Emitted when nodes have been disconnected
signal nodes_disconnected(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int)
#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)
#endregion
## Instantiate a node by its' [member DeckNode.node_type] and add it to this deck.[br]
## See [method add_node_inst] for parameter descriptions.
func add_node_type(type: String, assign_id: String = "", assign_to_self: bool = true) -> DeckNode:
var node_inst: DeckNode = NodeDB.instance_node(type)
return add_node_inst(node_inst, assign_id, assign_to_self)
## Add a [DeckNode] instance to this deck.[br]
## If [param assign_id] is empty, the node will get its' ID (re-)assigned.
## Otherwise, it will be assigned to be that value.[br]
## If [param assign_to_self] is [code]true[/code], the node's
## [member DeckNode._belonging_to] property will be set to [code]self[/code].
func add_node_inst(node: DeckNode, assign_id: String = "", assign_to_self: bool = true) -> DeckNode:
if assign_to_self:
node._belonging_to = self
if assign_id == "":
var uuid := UUID.v4()
nodes[uuid] = node
node._id = uuid
else:
nodes[assign_id] = node
node._id = assign_id
if emit_node_added_signal:
node_added.emit(node)
if is_group && emit_group_signals:
node_added_to_group.emit(node, node._id, assign_to_self, self)
node.port_value_updated.connect(
func(port_idx: int, new_value: Variant):
if is_group && emit_group_signals:
node_port_value_updated.emit(node._id, port_idx, new_value, self)
)
node.renamed.connect(
func(new_name: String):
if is_group && emit_group_signals:
node_renamed.emit(node._id, new_name, self)
)
node.position_updated.connect(
func(new_position: Dictionary):
if is_group && emit_group_signals:
node_moved.emit(node._id, new_position, self)
)
return node
## Get a node belonging to this deck by its' ID.
func get_node(uuid: String) -> DeckNode:
return nodes.get(uuid)
## 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:
var from_node := get_node(from_node_id)
var to_node := get_node(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
# refuse duplicate connections
if from_node.has_outgoing_connection_exact(from_output_port, to_node_id, to_input_port):
print("no duplicates")
return false
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 is_group && 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)
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:
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 && 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)
## Returns true if this deck has no nodes and no variables.
func is_empty() -> bool:
return nodes.is_empty() && variable_stack.is_empty()
## Remove a node from this deck.
func remove_node(uuid: String, remove_connections: bool = false, force: bool = false, keep_group_instances: bool = false) -> void:
var node := get_node(uuid)
if node == null:
return
if !node.user_can_delete && !force:
return
if node.node_type == "group_node" && !keep_group_instances:
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)
nodes.erase(uuid)
node_removed.emit(node)
if is_group && emit_group_signals:
node_removed_from_group.emit(uuid, remove_connections, self)
## Group the [param nodes_to_group] into a new deck and return it.
## Returns [code]null[/code] on failure.[br]
## Adds a group node to this deck, and adds group input and output nodes in the group.
func group_nodes(nodes_to_group: Array) -> Deck:
if nodes_to_group.is_empty():
return null
# don't include nodes that can't be grouped/deleted
nodes_to_group = nodes_to_group.filter(
func(x: DeckNode):
return x.user_can_delete
)
var node_ids_to_keep := nodes_to_group.map(
func(x: DeckNode):
return x._id
)
var group := DeckHolder.add_empty_group()
var midpoint := Vector2()
var rightmost := -INF
var leftmost := INF
for node: DeckNode in nodes_to_group:
#if node.node_type == "group_node":
#var _group_id: String = node.group_id
#var _group: Deck = groups[_group_id]
#groups.erase(_group)
#group.groups[_group_id] = _group
#_group._belonging_to = group
if node.position.x > rightmost:
rightmost = node.position.x
if node.position.x < leftmost:
leftmost = node.position.x
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 in node_ids_to_keep):
disconnect_nodes(node._id, to_node, from_port, 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 in node_ids_to_keep):
disconnect_nodes(from_node, node._id, incoming_connections[to_port][from_node], to_port)
midpoint += node.position_as_vector2()
remove_node(node._id, false, true)
group.add_node_inst(node, node._id)
midpoint /= nodes_to_group.size()
emit_node_added_signal = false
var _group_node := add_node_type("group_node")
_group_node.group_id = group.id
_group_node.group_instance_id = group.instance_id
_group_node.position.x = midpoint.x
_group_node.position.y = midpoint.y
_group_node.position_updated.emit(_group_node.position)
#group.group_node = _group_node._id
node_added.emit(_group_node)
emit_node_added_signal = true
var input_node := group.add_node_type("group_input")
var output_node := group.add_node_type("group_output")
group.group_input_node = input_node._id
group.group_output_node = output_node._id
input_node.position.x = leftmost - 350
output_node.position.x = rightmost + 350
input_node.position.y = midpoint.y
output_node.position.y = midpoint.y
input_node.position_updated.emit(input_node.position)
output_node.position_updated.emit(output_node.position)
input_node.group_node = _group_node
output_node.group_node = _group_node
_group_node.input_node_id = input_node._id
_group_node.output_node_id = output_node._id
#_group_node.setup_connections()
_group_node.init_io()
return group
## Get a group belonging to this deck by its ID.
#func get_group(uuid: String) -> Deck:
#return groups.get(uuid)
func copy_nodes(nodes_to_copy: Array[String]) -> Dictionary:
var d := {"nodes": {}}
for node_id: String in nodes_to_copy:
d.nodes[node_id] = get_node(node_id).to_dict()
for node: String in d.nodes:
var outgoing_connections: Dictionary = d.nodes[node].outgoing_connections.duplicate(true)
for from_port: int in outgoing_connections:
for to_node: String in outgoing_connections[from_port]:
if !(to_node in nodes_to_copy):
(d.nodes[node].outgoing_connections[from_port] as Dictionary).erase(to_node)
var outgoing_is_empty: bool = true
for from_port: int in d.nodes[node].outgoing_connections:
if !(d.nodes[node].outgoing_connections[from_port] as Dictionary).is_empty():
outgoing_is_empty = false
break
if outgoing_is_empty:
d.nodes[node].outgoing_connections = {}
var incoming_connections: Dictionary = d.nodes[node].incoming_connections.duplicate(true)
for to_port: int in incoming_connections:
for from_node: String in incoming_connections[to_port]:
if !(from_node in nodes_to_copy):
(d.nodes[node].incoming_connections[to_port] as Dictionary).erase(from_node)
var incoming_is_empty: bool = true
for to_port: int in d.nodes[node].incoming_connections:
if !(d.nodes[node].incoming_connections[to_port] as Dictionary).is_empty():
incoming_is_empty = false
break
if incoming_is_empty:
d.nodes[node].incoming_connections = {}
for node: Dictionary in d.nodes.values().slice(1):
node.position.x = node.position.x - d.nodes.values()[0].position.x
node.position.y = node.position.y - d.nodes.values()[0].position.y
d.nodes.values()[0].position.x = 0
d.nodes.values()[0].position.y = 0
return d
func copy_nodes_json(nodes_to_copy: Array[String]) -> String:
return JSON.stringify(copy_nodes(nodes_to_copy))
func allocate_ids(count: int) -> Array[String]:
var res: Array[String] = []
for i in count:
res.append(UUID.v4())
return res
func paste_nodes_from_dict(nodes: Dictionary, position: Vector2 = Vector2()) -> void:
if !nodes.get("nodes"):
return
var new_ids := allocate_ids(nodes.nodes.size())
var ids_map := {}
for i: int in nodes.nodes.keys().size():
var node_id: String = nodes.nodes.keys()[i]
ids_map[node_id] = new_ids[i]
for node_id: String in nodes.nodes:
nodes.nodes[node_id]._id = ids_map[node_id]
nodes.nodes[node_id].position.x += position.x
nodes.nodes[node_id].position.y += position.y
var outgoing_connections: Dictionary = nodes.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.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.nodes[node_id].outgoing_connections = outgoing_connections_res
nodes.nodes[node_id].incoming_connections = incoming_connections_res
var node := DeckNode.from_dict(nodes.nodes[node_id])
if node.node_type == "group_node":
var group := DeckHolder.make_new_group_instance(node.group_id)
node.group_instance_id = group.instance_id
group.get_node(group.group_input_node).group_node = node
group.get_node(group.group_output_node).group_node = node
node.input_node = group.get_node(group.group_input_node)
node.output_node = group.get_node(group.group_output_node)
node.init_io()
add_node_inst(node, ids_map[node_id])
func paste_nodes_from_json(json: String, position: Vector2 = Vector2()) -> void:
paste_nodes_from_dict(JSON.parse_string(json), position)
func duplicate_nodes(nodes: Array[String]) -> void:
if nodes.is_empty():
return
var position := get_node(nodes[0]).position_as_vector2() + Vector2(50, 50)
var d := copy_nodes(nodes)
paste_nodes_from_dict(d, position)
func send_event(event_name: StringName, event_data: Dictionary = {}) -> void:
for node: DeckNode in nodes.values():
node._event_received(event_name, event_data)
func get_referenced_groups() -> Array[String]:
# this is expensive
# recursively returns a list of all groups referenced by this deck
var res: Array[String] = []
for node_id: String in nodes:
var node := get_node(node_id)
if node.node_type != "group_node":
continue
res.append(node.group_id)
res.append_array(DeckHolder.get_deck(node.group_id).get_referenced_groups())
return res
## Returns a [Dictionary] representation of this deck.
func to_dict(with_meta: bool = true, group_ids: Array = []) -> Dictionary:
var inner := {
"nodes": {},
"variable_stack": variable_stack,
"id": id,
"groups": {}
}
for node_id: String in nodes.keys():
inner["nodes"][node_id] = nodes[node_id].to_dict(with_meta)
if (nodes[node_id] as DeckNode).node_type == "group_node":
if !(nodes[node_id].group_id in group_ids):
inner["groups"][nodes[node_id].group_id] = DeckHolder.get_deck(nodes[node_id].group_id).to_dict(with_meta, group_ids)
group_ids.append(nodes[node_id].group_id)
#for group_id in groups.keys():
#inner["groups"][group_id] = groups[group_id].to_dict(with_meta)
if is_group:
inner["instance_id"] = instance_id
inner["group_input_node"] = group_input_node
inner["group_output_node"] = group_output_node
var d := {"deck": inner}
if with_meta:
d["meta"] = {}
for meta in get_meta_list():
d["meta"][meta] = var_to_str(get_meta(meta))
return d
## Create a new deck from a [Dictionary] representation, such as one created by [method to_dict].
static func from_dict(data: Dictionary, path: String = "") -> Deck:
var deck := Deck.new()
deck.save_path = path
deck.variable_stack = data.deck.variable_stack
deck.id = data.deck.id
for key in data.meta:
deck.set_meta(key, str_to_var(data.meta[key]))
var nodes_data: Dictionary = data.deck.nodes as Dictionary
for node_id in nodes_data:
var node := DeckNode.from_dict(nodes_data[node_id])
deck.add_node_inst(node, node_id)
var groups_data: Dictionary = data.deck.groups as Dictionary
for node_id: String in deck.nodes:
var node := deck.get_node(node_id)
if node.node_type != "group_node":
continue
var group_id: String = node.group_id
var group_instance_id: String = node.group_instance_id
var group_data: Dictionary = groups_data[group_id]
var group := DeckHolder.add_group_from_dict(group_data, group_id, group_instance_id)
group.get_node(group.group_input_node).group_node = node
group.get_node(group.group_output_node).group_node = node
node.init_io()
#for group_id: String in groups_data:
#var group := Deck.from_dict(groups_data[group_id])
#group._belonging_to = deck
#group.is_group = true
#deck.groups[group_id] = group
#group.group_node = groups_data[group_id]["deck"]["group_node"]
#group.group_input_node = groups_data[group_id]["deck"]["group_input_node"]
#group.group_output_node = groups_data[group_id]["deck"]["group_output_node"]
#deck.get_node(group.group_node).init_io()
return deck