mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
3bff8e27e6
Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/167 Co-authored-by: Lera Elvoé <yagich@poto.cafe> Co-committed-by: Lera Elvoé <yagich@poto.cafe>
1192 lines
37 KiB
GDScript
1192 lines
37 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)
|
|
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
|