# (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 Object 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 = "" ## The connections graph. var connections := NodeConnections.new() ## Whether this deck is a group instance. var is_group: bool = false ## Whether this deck is a library. Implies [member is_group]. var is_library: bool = false var group_descriptors := GroupDescriptors.new() #region library group props ## The initial name of the library. Displayed by the group node and [SearchProvider]. var lib_name: String ## The description of this library, shown to the user by a renderer. var lib_description: String ## A list of aliases for this library, used by search. var lib_aliases: Array[String] #endregion ## 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. @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 ## The ID of this group's input node. Used only if [member is_group] is [code]true[/code]. var group_output_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 connected. signal nodes_connected(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) ## 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() ## Emitted when a node has been moved to a different deck. signal node_moved_to_deck(node: DeckNode, other: Deck) #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 node_port_value_updated(node_id: String, port_idx: int, new_value: Variant, deck: Deck) signal node_ports_updated(node_id: String, 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 func _init() -> void: group_descriptors.deck = self ## 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].[br] ## Returns [code]null[/code] if the node cannot be insantiated. func add_node_inst(node: DeckNode, assign_id: String = "", assign_to_self: bool = true) -> DeckNode: if node.node_type == "group_input" and not group_input_node.is_empty(): DeckHolder.logger.toast_error("Cannot add group input node, one already exists!") node.free() return null if node.node_type == "group_output" and not group_output_node.is_empty(): DeckHolder.logger.toast_error("Cannot add group output node, one already exists!") node.free() return null if assign_to_self: node._belonging_to = self #node._belonging_to_instance = instance_id if assign_id == "": var uuid := UUID.v4() nodes[uuid] = node node._id = uuid else: nodes[assign_id] = node node._id = assign_id if node.node_type == "group_input": group_input_node = node._id node.about_to_free.connect( func(): group_input_node = "" ) if node.node_type == "group_output": group_output_node = node._id node.about_to_free.connect( func(): group_output_node = "" ) if emit_node_added_signal: node_added.emit(node) if is_group and 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 and 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 and emit_group_signals: node_renamed.emit(node._id, new_name, self) ) node.position_updated.connect( func(new_position: Dictionary): if is_group and emit_group_signals: node_moved.emit(node._id, new_position, self) ) node.ports_updated.connect( func(): if is_group and emit_group_signals: node_ports_updated.emit(node._id, self) ) print_verbose("Deck %s::%s: added node %s, id %s" % [id, instance_id, node.node_type, node.get_instance_id()]) return node func add_lib_group_node(type: String) -> DeckNode: var group_node := add_node_type("group_node") var lib := DeckHolder.add_lib_instance(type, id) group_node.is_library = true group_node.group_id = lib.id group_node.group_instance_id = lib.instance_id for node_id: String in lib.nodes: var node := lib.get_node(node_id) if node.node_type == "group_input": group_node.input_node = node group_node.input_node_id = node._id node.group_node = group_node lib.group_input_node = node._id continue if node.node_type == "group_output": group_node.output_node = node group_node.output_node_id = node._id node.group_node = group_node lib.group_output_node = node._id continue group_node.init_io() group_node.rename(NodeDB.get_library_descriptor(type).name) return group_node ## Get a node belonging to this deck by its' ID. 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 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) and (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 not DeckType.can_convert(type_from, type_to): return false # duplicate connection if connections.has_outgoing_connection_exact(from_node_id, 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 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 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) 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.outgoing_connection_removed.emit(from_output_port) to_node.incoming_connection_removed.emit(to_input_port) func disconnect_pair(pair: ConnectionPair) -> void: connections.remove_pair(pair) var from_node_id := pair.incoming.from_node var to_node_id := pair.outgoing.to_node var from_output_port := pair.incoming.from_port var to_input_port := pair.outgoing.to_port 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.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. func is_empty() -> bool: return nodes.is_empty() and 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 not node.user_can_delete and not force: return if node.node_type == "group_node" and not keep_group_instances: DeckHolder.close_group_instance(node.group_id, node.group_instance_id) if remove_connections: # var outgoing_connections := connections.get_all_outgoing_connections(uuid) # for from_port: int in outgoing_connections.keys(): # 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.keys(): # var incoming := connections.get_incoming_connection(uuid, to_port) # disconnect_nodes(incoming.from_node, uuid, incoming.from_port, to_port) var pairs := connections.get_node_pairs(uuid) for pair in pairs: disconnect_pair(pair) nodes.erase(uuid) node_removed.emit(node) if is_group and emit_group_signals: node_removed_from_group.emit(uuid, remove_connections, self) print_verbose("Deck %s::%s: freeing node %s, id %s" % [id, instance_id, node.node_type, node.get_instance_id()]) node.free() func pre_exit_cleanup() -> void: for node_id: String in nodes: var node := get_node(node_id) print_verbose("Deck %s::%s: freeing node %s, id %s" % [id, instance_id, node.node_type, node.get_instance_id()]) node.free() ## 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(id) var connection_pairs := connections.filter_pairs(node_ids_to_keep) for pair in connection_pairs: group.connections.add_pair(pair) var midpoint := Vector2() var rightmost := -INF var leftmost := INF for node: DeckNode in nodes_to_group: if node.position.x > rightmost: rightmost = node.position.x if node.position.x < leftmost: leftmost = node.position.x var pairs := connections.get_node_pairs(node._id) for pair in pairs: disconnect_pair(pair) midpoint += node.position_as_vector2() # remove_node(node._id, false, true) # group.add_node_inst(node, node._id) move_node_to_deck(node, group) 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 func move_node_to_deck(node: DeckNode, other: Deck) -> void: if not node._id in nodes: return nodes.erase(node._id) other.nodes[node._id] = node # node_moved_to_deck.emit(node, other) node._belonging_to = other node_removed.emit(node) other.node_added.emit(node) if is_group and emit_group_signals: node_removed_from_group.emit(node._id, false, self) if other.is_group and other.emit_group_signals: node_added_to_group.emit(node, node._id, true, other) func copy_nodes(nodes_to_copy: Array[String]) -> Dictionary: 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() 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: 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]: var res: Array[String] = [] for i in count: res.append(UUID.v4()) return res func paste_nodes_from_dict(nodes_to_paste: Dictionary, position: Vector2 = Vector2()) -> void: 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 node := DeckNode.from_dict(nodes_to_paste.nodes[node_id]) var group_needs_unique := false if node.node_type == "group_node": if not node.is_library: var group := DeckHolder.make_new_group_instance(node.group_id, id) var old_group := DeckHolder.get_group_instance(node.group_id, node.group_instance_id) group_needs_unique = old_group._belonging_to != 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() else: var type: String = node.group_id.get_file().trim_suffix(".deck") var lib := DeckHolder.add_lib_instance(type, id) node.group_instance_id = lib.instance_id for l_node_id: String in lib.nodes: var l_node := lib.get_node(l_node_id) if l_node.node_type == "group_input": node.input_node = l_node node.input_node_id = l_node._id l_node.group_node = node lib.group_input_node = l_node_id continue if l_node.node_type == "group_output": node.output_node = l_node node.output_node_id = l_node._id l_node.group_node = node lib.group_output_node = l_node_id continue node.init_io() add_node_inst(node, ids_map[node_id]) if group_needs_unique: node.make_unique() DeckHolder.logger.toast_warn("Group was made unique.") func paste_nodes_from_json(json: String, position: Vector2 = Vector2()) -> void: var d = JSON.parse_string(json) if not d: DeckHolder.logger.toast_error("Paste failed.") return paste_nodes_from_dict(d, position) func duplicate_nodes(nodes_to_copy: Array[String]) -> void: if nodes_to_copy.is_empty(): return var position := get_node(nodes_to_copy[0]).position_as_vector2() + Vector2(50, 50) var d := copy_nodes(nodes_to_copy) 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) 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" and not node.is_library: res.append(node.group_id) res.append_array(DeckHolder.get_deck(node.group_id).get_referenced_groups()) return res func get_referenced_group_instances() -> Array[Dictionary]: var res: Array[Dictionary] = [] for node_id: String in nodes: var node := get_node(node_id) if node.node_type == "group_node" and not node.is_library: res.append({ "group_id": node.group_id, "instance_id": node.group_instance_id, "parent_id": node._belonging_to, "group_node": node_id, }) res.append_array(DeckHolder.get_group_instance(node.group_id, node.group_instance_id).get_referenced_group_instances()) return res func _to_string() -> String: if not is_group: return "DeckNode:%s" % id else: return "DeckNode:%s::%s" % [id, instance_id] ## 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": {}, "connections": connections.to_dict(), } if not group_descriptors.is_empty(): inner["group_descriptors"] = group_descriptors.to_dict() for node_id: String in nodes.keys(): inner["nodes"][node_id] = nodes[node_id].to_dict(with_meta) if get_node(node_id).node_type == "group_node" and not get_node(node_id).is_library: if nodes[node_id].group_id not 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 # don't save library stuff if this is not a library if not lib_name.is_empty(): var lib := { "lib_name": lib_name, "lib_description": lib_description, "lib_aliases": lib_aliases, } inner["library"] = lib 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 deck.connections = NodeConnections.from_dict(data.deck.connections) if data.deck.has("library"): var lib_data: Dictionary = data.deck.library deck.lib_name = lib_data.get("lib_name", "") deck.lib_description = lib_data.get("lib_description", "") # deck.lib_aliases = lib_data.get("lib_aliases", []) if data.deck.has("group_descriptors"): deck.group_descriptors = GroupDescriptors.from_dict(data.deck.group_descriptors) deck.group_descriptors.deck = deck 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.connections) deck.add_node_inst(node, node_id) node._post_deck_load() 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: Deck if not node.is_library: var group_data: Dictionary = groups_data[group_id] group = DeckHolder.add_group_from_dict(group_data, group_id, group_instance_id, deck.id) else: group = DeckHolder.add_lib_instance( node.group_id.get_file(), deck.id, node.group_instance_id ) if not node.input_node_id.is_empty(): group.get_node(node.input_node_id).group_node = node if not node.output_node_id.is_empty(): group.get_node(node.output_node_id).group_node = node node.init_io() 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 # remove if empty if data[from_node_id] == _create_empty_connection(): data.erase(from_node_id) if data[to_node_id] == _create_empty_connection(): data.erase(to_node_id) if (data[from_node_id].outgoing[from_output_port] as Array).is_empty(): (data[from_node_id].outgoing as Dictionary).erase(from_output_port) 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 [] if not (data[node_id].outgoing as Dictionary).has(from_port): 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 get_node_pairs(node: String) -> Array[ConnectionPair]: var res: Array[ConnectionPair] = [] var connections: Dictionary = data.get(node, _create_empty_connection()) for to_port: int in connections.outgoing: for outgoing: OutgoingConnection in connections.outgoing[to_port]: res.append(ConnectionPair.from_outgoing(outgoing)) for from_port: int in connections.incoming: res.append(ConnectionPair.from_incoming(connections.incoming[from_port])) 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 remove_pair(pair: ConnectionPair) -> void: var outgoing := pair.outgoing var incoming := pair.incoming remove_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} class GroupDescriptors: var input_ports: Dictionary = {} # Dictionary[int -> input port idx, GroupPort] var output_ports: Dictionary = {} # Dictionary[int -> output port idx, GroupPort] var deck: Deck func is_empty() -> bool: if input_ports.is_empty() and output_ports.is_empty(): return true var test := func(e: GroupPort) -> bool: return not e.is_empty() var has_inputs = input_ports.values().any(test) var has_outputs = output_ports.values().any(test) if has_inputs or has_outputs: return false return true func _on_input_updated(port_idx: int) -> void: var node := deck.get_node(deck.group_input_node) var port := node.get_output_ports()[port_idx] var port_override := get_input_port(port_idx) if port_override.label.is_empty(): port.label = "Input %s" % port_idx else: port.label = port_override.label port.type = port_override.type port.descriptor = port_override.descriptor port.usage_type = port_override.usage_type node.ports_updated.emit() func _on_output_updated(port_idx: int) -> void: var node := deck.get_node(deck.group_output_node) var port := node.get_input_ports()[port_idx] var port_override := get_output_port(port_idx) if port_override.label.is_empty(): port.label = "Output %s" % port_idx else: port.label = port_override.label port.type = port_override.type port.descriptor = port_override.descriptor port.usage_type = port_override.usage_type node.ports_updated.emit() func _get_or_create_input(port_idx: int) -> GroupPort: var res := input_ports.get(port_idx, null) as GroupPort if res == null: var gr := GroupPort.new() input_ports[port_idx] = gr res = gr Util.safe_connect(res.updated, _on_input_updated.bind(port_idx)) return res func _get_or_create_output(port_idx: int) -> GroupPort: var res := output_ports.get(port_idx, null) as GroupPort if res == null: var gr := GroupPort.new() output_ports[port_idx] = gr res = gr Util.safe_connect(res.updated, _on_output_updated.bind(port_idx)) return res func get_input_port(port_idx: int) -> GroupPort: return _get_or_create_input(port_idx) func get_output_port(port_idx: int) -> GroupPort: return _get_or_create_output(port_idx) func to_dict() -> Dictionary: var res := { "input_ports": {}, "output_ports": {}, } for port_idx: int in input_ports: var gp := input_ports[port_idx] as GroupPort if gp.is_empty(): continue res["input_ports"][port_idx] = gp.to_dict() for port_idx: int in output_ports: var gp := output_ports[port_idx] as GroupPort if gp.is_empty(): continue res["output_ports"][port_idx] = gp.to_dict() return res static func from_dict(d: Dictionary) -> GroupDescriptors: var res := GroupDescriptors.new() for port_idx in d.input_ports: res.input_ports[int(port_idx)] = GroupPort.from_dict(d.input_ports[port_idx]) for port_idx in d.output_ports: res.output_ports[int(port_idx)] = GroupPort.from_dict(d.output_ports[port_idx]) return res class GroupPort: var type: DeckType.Types = DeckType.Types.ANY: set(v): type = v updated.emit() var label: String: set(v): label = v updated.emit() var descriptor: String: set(v): descriptor = v updated.emit() var usage_type: Port.UsageType = Port.UsageType.BOTH: set(v): usage_type = v updated.emit() signal updated() func is_empty() -> bool: return\ type == DeckType.Types.ANY and\ label.is_empty() and\ descriptor.is_empty() and\ usage_type == Port.UsageType.BOTH func to_dict() -> Dictionary: var res := { "type": type, "label": label, "descriptor": descriptor, "usage_type": usage_type, } return res static func from_dict(d: Dictionary) -> GroupPort: var res := GroupPort.new() res.type = d.type as DeckType.Types res.label = d.label res.descriptor = d.descriptor res.usage_type = d.usage_type as Port.UsageType return res