# (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 GraphEdit class_name DeckRendererGraphEdit ## Reference to the [DeckNodeRendererGraphNode] used for later instantiation @export var NODE_SCENE: PackedScene ## Reference to the [AddNodeMenu] used for later instantiation @export var ADD_NODE_MENU_SCENE: PackedScene ## The [PopupPanel] that holds the [AddNodeMenu] scene. var search_popup_panel: PopupPanel ## Stores instance of [AddNodeMenu] that is used under [member search_popup_panel] var add_node_menu: AddNodeMenu ## Used to specify the size of [member search_popup_panel]. @export var search_popup_size: Vector2i = Vector2i(500, 300) ## Stores the position of the [member search_popup_panel] for use when adding ## nodes in [method _on_add_node_menu_node_selected] var popup_position: Vector2 var rename_popup := RenamePopup.new() @export var rename_popup_size: Vector2i = Vector2i(200, 0) ## References the [Deck] that holds all the functional properties of this [DeckRendererGraphEdit] var deck: Deck: set(v): deck = v deck.node_added.connect(_on_deck_node_added) deck.node_removed.connect(_on_deck_node_removed) deck.nodes_disconnected.connect(_on_deck_nodes_disconnected) ## Emits when Group creation is requested. Ex. Hitting the "group_nodes" Hotkey. signal group_enter_requested(group_id: String) var dirty: bool = false: set(v): if change_dirty: dirty = v dirty_state_changed.emit() var is_group: bool = false var change_dirty: bool = true signal dirty_state_changed ## Sets up the [member search_popup_panel] with an instance of [member ADD_NODE_SCENE] ## stored in [member add_node_menu]. And sets its size of [member search_popup_panel] to ## [member add_node_popup_size] func _ready() -> void: add_node_menu = ADD_NODE_MENU_SCENE.instantiate() search_popup_panel = PopupPanel.new() search_popup_panel.add_child(add_node_menu) search_popup_panel.size = search_popup_size add_child(search_popup_panel, false, Node.INTERNAL_MODE_BACK) rename_popup = RenamePopup.new() rename_popup.rename_confirmed.connect(_on_rename_popup_rename_confirmed) rename_popup.close_requested.connect(_on_rename_popup_closed) add_child(rename_popup, false, Node.INTERNAL_MODE_FRONT) for t: DeckType.Types in DeckType.CONVERSION_MAP: for out_type: DeckType.Types in DeckType.CONVERSION_MAP[t]: add_valid_connection_type(t, out_type) add_node_menu.node_selected.connect(_on_add_node_menu_node_selected) connection_request.connect(attempt_connection) disconnection_request.connect(attempt_disconnect) ## Receives [signal GraphEdit.connection_request] and attempts to create a ## connection between the two [DeckNode]s involved, utilizes [NodeDB] for accessing them. func attempt_connection(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void: var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node_name)) var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node_name)) #var from_output := from_node_renderer.node.get_global_port_idx_from_output(from_port) #var to_input := to_node_renderer.node.get_global_port_idx_from_input(to_port) if deck.connect_nodes(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port): connect_node( from_node_renderer.name, from_port, to_node_renderer.name, to_port ) dirty = true func _is_node_hover_valid(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> bool: var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node)) var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node)) return deck.is_valid_connection(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port) ## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s ## involved, utilizes [NodeDB] for accessing them. func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void: var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node_name)) var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node_name)) #var from_output := from_node_renderer.node.get_global_port_idx_from_output(from_port) #var to_input := to_node_renderer.node.get_global_port_idx_from_input(to_port) deck.disconnect_nodes(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port) disconnect_node( from_node_renderer.name, from_port, to_node_renderer.name, to_port ) dirty = true ## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode]. ## Or [code]null[/code] if none is found. func get_node_renderer(node: DeckNode) -> DeckNodeRendererGraphNode: for i: DeckNodeRendererGraphNode in get_children(): if i.node == node: return i return null ## Updates [member Deck]s meta property "offset" whenever [member GraphEdit.scroll_offset] func _on_scroll_offset_changed(offset: Vector2) -> void: deck.set_meta("offset", offset) ## Setups all the data from the set [member deck] in this [DeckRendererGraphEdit] func initialize_from_deck() -> void: change_dirty = false for i in get_children(): i.queue_free() is_group = deck.is_group for node_id in deck.nodes: var node_renderer: DeckNodeRendererGraphNode = NODE_SCENE.instantiate() node_renderer.node = deck.nodes[node_id] add_child(node_renderer) node_renderer.position_offset = node_renderer.node.position_as_vector2() change_dirty = true dirty = false refresh_connections() var ofs = deck.get_meta("offset", Vector2()) set_scroll_offset.call_deferred(ofs) ## Loops through all [DeckNode]s in [member Deck.nodes] and calls ## [method GraphEdit.connect_node] for all the connections that exist in each func refresh_connections() -> void: for node_id in deck.nodes: var node: DeckNode = deck.nodes[node_id] var from_node: DeckNodeRendererGraphNode = get_children().filter( func(c: DeckNodeRendererGraphNode): return c.node._id == node_id )[0] for from_port in node.outgoing_connections: for to_node_id: String in node.outgoing_connections[from_port]: var to_node_ports = node.outgoing_connections[from_port][to_node_id] var renderer: Array = get_children().filter( func(c: DeckNodeRendererGraphNode): return c.node._id == to_node_id ) if renderer.is_empty(): break var to_node: DeckNodeRendererGraphNode = renderer[0] for to_node_port: int in to_node_ports: connect_node( from_node.name, from_port, to_node.name, to_node_port ) ## Connected to [signal Deck.node_added], used to instance the required ## [DeckNodeRendererGraphNode] and set it's [member DeckNodeRenderGraphNode.position_offset] func _on_deck_node_added(node: DeckNode) -> void: var inst: DeckNodeRendererGraphNode = NODE_SCENE.instantiate() inst.node = node add_child(inst) inst.position_offset = inst.node.position_as_vector2() dirty = true ## Connected to [signal Deck.node_added], used to remove the specified ## [DeckNodeRendererGraphNode] and queue_free it. func _on_deck_node_removed(node: DeckNode) -> void: for renderer: DeckNodeRendererGraphNode in get_children(): if renderer.node != node: continue # TODO: when multiple nodes are removed and they are connected, the renderer will break # trying to get an invalid node. (GraphEdit, this is not on us.) # consider a batch removed signal for Deck or a separate signal for grouping nodes in 0.0.6. renderer.queue_free() break dirty = true ## Utility function that gets all [DeckNodeRenderGraphNode]s that are selected ## See [member GraphNode.selected] func get_selected_nodes() -> Array: return get_children().filter( func(x: DeckNodeRendererGraphNode): return x.selected ) ## Executes functionality based off hotkey inputs. Specifically handles creating groups ## based off the action "group_nodes". func _gui_input(event: InputEvent) -> void: if event.is_action_pressed("group_nodes") and get_selected_nodes().size() > 0: clear_connections() var nodes = get_selected_nodes().map( func(x: DeckNodeRendererGraphNode): return x.node ) deck.group_nodes(nodes) refresh_connections() get_viewport().set_input_as_handled() if event.is_action_pressed("debug_make_unique") and get_selected_nodes().size() == 1: var node: DeckNode = get_selected_nodes().map( func(x: DeckNodeRendererGraphNode): return x.node )[0] if node.node_type != "group_node": return node.make_unique() if event.is_action_pressed("rename_node") and get_selected_nodes().size() == 1: var node: DeckNodeRendererGraphNode = get_selected_nodes()[0] var pos := get_viewport_rect().position + get_global_mouse_position() rename_popup.popup_on_parent(Rect2i(pos, rename_popup_size)) rename_popup.le.size.x = rename_popup_size.x rename_popup.set_text(node.title) ## Handles entering groups with action "enter_group". Done here to bypass neighbor ## functionality. func _input(event: InputEvent) -> void: if not has_focus(): return if event.is_action_pressed("enter_group") and get_selected_nodes().size() == 1: if (get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.node_type != "group_node": return group_enter_requested.emit((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.group_id) get_viewport().set_input_as_handled() func _on_rename_popup_rename_confirmed(new_name: String) -> void: var node: DeckNodeRendererGraphNode = get_selected_nodes()[0] node.title = new_name node.node.rename(new_name) dirty = true func _on_rename_popup_closed() -> void: pass ## Opens [member search_popup_panel] at the mouse position. Connected to [signal GraphEdit.popup_request] func _on_popup_request(p_popup_position: Vector2) -> void: var p := get_viewport_rect().position + get_global_mouse_position() p += Vector2(10, 10) var r := Rect2i(p, search_popup_panel.size) search_popup_panel.popup_on_parent(r) add_node_menu.focus_search_bar() popup_position = p_popup_position ## Connected to [signal AddNodeMenu.node_selected] and creates a [DeckNode] using ## [method NodeDB.instance_node]. Then placing it at the [member scroll_offset] + ## [member popup_position] / [member zoom] func _on_add_node_menu_node_selected(type: String) -> void: var node := NodeDB.instance_node(type) as DeckNode deck.add_node_inst(node) var node_pos := ((scroll_offset + popup_position) / zoom) if snapping_enabled: node_pos = node_pos.snapped(Vector2(snapping_distance, snapping_distance)) get_node_renderer(node).position_offset = node_pos search_popup_panel.hide() func _on_deck_nodes_disconnected(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> void: print("1") var from_node: DeckNodeRendererGraphNode = get_children().filter( func(x: DeckNodeRendererGraphNode): return x.node._id == from_node_id )[0] var to_node: DeckNodeRendererGraphNode = get_children().filter( func(x: DeckNodeRendererGraphNode): return x.node._id == to_node_id )[0] print(is_node_connected(from_node.name, from_output_port, to_node.name, to_input_port)) disconnect_node(from_node.name, from_output_port, to_node.name, to_input_port) func _on_delete_nodes_request(nodes: Array[StringName]) -> void: var node_ids := nodes.map( func(n: StringName): return (get_node(NodePath(n)) as DeckNodeRendererGraphNode).node._id ) clear_connections() if node_ids.is_empty(): return for node_id in node_ids: deck.remove_node(node_id, true) refresh_connections() dirty = true func _on_copy_nodes_request() -> void: var selected := get_selected_nodes() if selected.is_empty(): return var selected_ids: Array[String] = [] selected_ids.assign(selected.map( func(x: DeckNodeRendererGraphNode): return x.node._id )) DisplayServer.clipboard_set(deck.copy_nodes_json(selected_ids)) func _on_paste_nodes_request() -> void: var clip := DisplayServer.clipboard_get() var node_pos := (get_local_mouse_position() + scroll_offset - (Vector2(75.0, 0.0) * zoom)) / zoom if snapping_enabled: node_pos = node_pos.snapped(Vector2(snapping_distance, snapping_distance)) clear_connections() deck.paste_nodes_from_json(clip, node_pos) refresh_connections() dirty = true func _on_duplicate_nodes_request() -> void: var selected := get_selected_nodes() if selected.is_empty(): return var selected_ids: Array[String] = [] selected_ids.assign(selected.map( func(x: DeckNodeRendererGraphNode): return x.node._id )) clear_connections() deck.duplicate_nodes(selected_ids) refresh_connections() dirty = true class RenamePopup extends Popup: var le := LineEdit.new() signal rename_confirmed(new_text: String) func _ready() -> void: add_child(le) le.placeholder_text = "New Name" le.text_submitted.connect( func(new_text: String): if new_text.is_empty(): return rename_confirmed.emit(new_text) hide() ) func set_text(text: String) -> void: le.text = text le.select_all() le.grab_focus()