mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
f720efcc72
saving decks that use lib groups will no longer save the whole file path to that library (at the expense of the structure needing to be the same) also some ui/ux improvements: - more menus in sidebar remember their collapsed state between deck/node switches - adding a lib group will name the group node appropriately - save dialog properly remembers the most recent path when invoked via ctrl+s and the deck hasn't been saved before Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/162 Co-authored-by: Lera Elvoé <yagich@poto.cafe> Co-committed-by: Lera Elvoé <yagich@poto.cafe>
1177 lines
36 KiB
GDScript
1177 lines
36 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
|
|
## 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 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].
|
|
func add_node_inst(node: DeckNode, assign_id: String = "", assign_to_self: bool = true) -> DeckNode:
|
|
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
|
|
|
|
if node.node_type == "group_output":
|
|
group_output_node = node._id
|
|
|
|
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
|
|
)
|
|
|
|
group.get_node(node.input_node_id).group_node = node
|
|
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
|