# (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() signal nodes_about_to_delete() ## 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: if deck.is_library: return false 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: if deck.is_library: return 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: return get_node_or_null(NodePath(node._id)) ## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode] [member DeckNode._id], ## or [code]null[/code] if none is found. func get_node_id_renderer(id: String) -> DeckNodeRendererGraphNode: return get_node_or_null(NodePath(id)) func focus_node(node: DeckNodeRendererGraphNode) -> void: set_selected(node) var t := create_tween() t.tween_property(self, ^"scroll_offset", (node.position_offset + node.size / 2.0) * zoom - size / 2.0, 0.3).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) func focus_selection() -> void: var nodes := get_selected_nodes() if nodes.size() == 0: return var midpoint := Vector2() for i in nodes: midpoint += i.position_offset + i.size / 2.0 midpoint *= zoom midpoint /= nodes.size() midpoint -= size / 2.0 var t := create_tween() t.tween_property(self, ^"scroll_offset", midpoint, 0.3).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) ## Updates [member Deck]s meta property "offset" whenever [member GraphEdit.scroll_offset] func _on_scroll_offset_changed(offset: Vector2) -> void: if is_instance_valid(deck): 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.get_node(node_id) node_renderer.name = node_id node_renderer.draggable = not deck.is_library add_child(node_renderer) node_renderer.position_offset = node_renderer.node.position_as_vector2() change_dirty = true dirty = false right_disconnects = not deck.is_library if deck.is_library: connection_drag_started.connect( func(from_node: StringName, from_port: int, is_output: bool): force_connection_drag_end() ) 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: if not deck.get_connections_dict().has(node_id): continue for from_port: int in deck.get_connections_dict()[node_id].outgoing: for outgoing: Deck.OutgoingConnection in deck.get_connections_dict()[node_id].outgoing[from_port]: connect_node( node_id, from_port, outgoing.to_node, outgoing.to_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 inst.name = node._id 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: # 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. refresh_connections() get_node_renderer(node).queue_free() 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 ) func _gui_input(event: InputEvent) -> void: if not deck.is_library: if RendererShortcuts.check_shortcut("group_nodes", event) 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() accept_event() if RendererShortcuts.check_shortcut("add_node", event): 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 = r.position accept_event() 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 RendererShortcuts.check_shortcut("rename_node", event) 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) accept_event() if RendererShortcuts.check_shortcut("duplicate_nodes", event) and get_selected_nodes().size() > 0: _on_duplicate_nodes_request() accept_event() if RendererShortcuts.check_shortcut("paste_nodes", event): _on_paste_nodes_request() accept_event() # copy/paste/duplicate/etc if RendererShortcuts.check_shortcut("copy_nodes", event) and get_selected_nodes().size() > 0: _on_copy_nodes_request() accept_event() if RendererShortcuts.check_shortcut("focus_nodes", event): focus_selection() func _input(event: InputEvent) -> void: if not has_focus(): return if RendererShortcuts.check_shortcut("enter_group", event) 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) accept_event() 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: if deck.is_library: return 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_pos := ((scroll_offset + popup_position) / zoom) if snapping_enabled: node_pos = node_pos.snapped(Vector2(snapping_distance, snapping_distance)) if not NodeDB.is_library(type): # var node := NodeDB.instance_node(type) as DeckNode # deck.add_node_inst(node) var node := deck.add_node_type(type) get_node_renderer(node).position_offset = node_pos else: var node := deck.add_lib_group_node(type) 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: var from_node := get_node_id_renderer(from_node_id) if from_node == null: return var to_node := get_node_id_renderer(to_node_id) if to_node == null: return 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 nodes_about_to_delete.emit() 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()