# (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) ## Emitted when the [member variable_stack] has been modified. 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) #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) ## 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 if from_node_id == to_node_id: return false var from_node := get_node(from_node_id) var to_node := get_node(to_node_id) 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 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): 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) ## Set a variable on this deck. func set_variable(var_name: String, value: Variant) -> void: variable_stack[var_name] = value variables_updated.emit() func update_variable(old_name: String, new_name: String, new_value: Variant) -> void: variable_stack.erase(old_name) variable_stack[new_name] = new_value func remove_variable(name: String) -> void: variable_stack.erase(name) ## 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