mirror of
https://gitlab.com/MaddoScientisto/cirnogodot.git
synced 2026-06-10 11:55:54 +00:00
Broken graph editor
This commit is contained in:
parent
6215008db7
commit
f996513dca
16 changed files with 1033 additions and 12 deletions
499
addons/bullet_script_graph_editor/ui/graph_editor_dock.gd
Normal file
499
addons/bullet_script_graph_editor/ui/graph_editor_dock.gd
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
@tool
|
||||
extends PanelContainer
|
||||
|
||||
const GraphAdapterScript := preload("res://addons/bullet_script_graph_editor/model/graph_adapter.gd")
|
||||
const META_LAYOUT_KEY := "_bullet_graph_layout_v1"
|
||||
|
||||
var _editor_interface: EditorInterface
|
||||
var _graph_adapter: RefCounted
|
||||
var _current_resource: Resource
|
||||
|
||||
var _path_edit: LineEdit
|
||||
var _status_label: Label
|
||||
var _graph_edit: GraphEdit
|
||||
var _file_dialog: FileDialog
|
||||
var _open_selected_button: Button
|
||||
var _inspect_selected_button: Button
|
||||
var _selected_node_resource: Resource
|
||||
var _wait_checkbox: CheckBox
|
||||
|
||||
func setup(editor_interface: EditorInterface) -> void:
|
||||
_editor_interface = editor_interface
|
||||
|
||||
func _ready() -> void:
|
||||
_graph_adapter = GraphAdapterScript.new()
|
||||
_build_ui()
|
||||
|
||||
func is_supported_resource(resource: Resource) -> bool:
|
||||
if _graph_adapter == null:
|
||||
return false
|
||||
return _graph_adapter.is_supported_resource(resource)
|
||||
|
||||
func open_resource(resource: Resource) -> void:
|
||||
if resource == null:
|
||||
_set_status("No resource selected.", true)
|
||||
return
|
||||
|
||||
if not is_supported_resource(resource):
|
||||
_set_status("Unsupported resource: %s" % resource.resource_path, true)
|
||||
return
|
||||
# Build the graph first to detect whether any nodes will be generated (diagnostic)
|
||||
var built_graph: Dictionary = _graph_adapter.build_graph(resource)
|
||||
var built_nodes: Dictionary = built_graph.get("nodes", {})
|
||||
_current_resource = resource
|
||||
_path_edit.text = resource.resource_path
|
||||
_render_graph()
|
||||
if built_nodes.size() == 0:
|
||||
_set_status("Opened: %s (no nodes detected)" % _display_path(resource), true)
|
||||
else:
|
||||
_set_status("Opened: %s" % _display_path(resource), false)
|
||||
|
||||
func _build_ui() -> void:
|
||||
var root := VBoxContainer.new()
|
||||
root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
root.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(root)
|
||||
|
||||
var toolbar := HBoxContainer.new()
|
||||
root.add_child(toolbar)
|
||||
|
||||
var open_button := Button.new()
|
||||
open_button.text = "Open Resource"
|
||||
open_button.pressed.connect(_on_open_pressed)
|
||||
toolbar.add_child(open_button)
|
||||
|
||||
var open_inspected_button := Button.new()
|
||||
open_inspected_button.text = "Open Inspected"
|
||||
open_inspected_button.pressed.connect(_on_open_inspected_pressed)
|
||||
toolbar.add_child(open_inspected_button)
|
||||
|
||||
var save_layout_button := Button.new()
|
||||
save_layout_button.text = "Save Layout"
|
||||
save_layout_button.pressed.connect(_save_layout_metadata)
|
||||
toolbar.add_child(save_layout_button)
|
||||
|
||||
_open_selected_button = Button.new()
|
||||
_open_selected_button.text = "Open Selected Node"
|
||||
_open_selected_button.disabled = true
|
||||
_open_selected_button.pressed.connect(_on_open_selected_node_pressed)
|
||||
toolbar.add_child(_open_selected_button)
|
||||
|
||||
_inspect_selected_button = Button.new()
|
||||
_inspect_selected_button.text = "Inspect Selected Node"
|
||||
_inspect_selected_button.disabled = true
|
||||
_inspect_selected_button.pressed.connect(_on_inspect_selected_node_pressed)
|
||||
toolbar.add_child(_inspect_selected_button)
|
||||
|
||||
_path_edit = LineEdit.new()
|
||||
_path_edit.placeholder_text = "res://Resources/..."
|
||||
_path_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_path_edit.text_submitted.connect(_on_path_submitted)
|
||||
toolbar.add_child(_path_edit)
|
||||
|
||||
_status_label = Label.new()
|
||||
_status_label.text = "Open a BossScript/BossPhase/BulletScript to render graph."
|
||||
root.add_child(_status_label)
|
||||
|
||||
_wait_checkbox = CheckBox.new()
|
||||
_wait_checkbox.text = "Wait For Completion (selected pattern)"
|
||||
_wait_checkbox.visible = false
|
||||
_wait_checkbox.toggled.connect(_on_wait_for_completion_toggled)
|
||||
root.add_child(_wait_checkbox)
|
||||
|
||||
_graph_edit = GraphEdit.new()
|
||||
_graph_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_graph_edit.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_graph_edit.right_disconnects = true
|
||||
_graph_edit.end_node_move.connect(_save_layout_metadata)
|
||||
_graph_edit.node_selected.connect(_on_graph_node_selected)
|
||||
root.add_child(_graph_edit)
|
||||
|
||||
_file_dialog = FileDialog.new()
|
||||
_file_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
_file_dialog.add_filter("*.tres; Godot Resource")
|
||||
_file_dialog.add_filter("*.res; Godot Resource")
|
||||
_file_dialog.file_selected.connect(_on_file_selected)
|
||||
add_child(_file_dialog)
|
||||
|
||||
func _on_open_pressed() -> void:
|
||||
_file_dialog.popup_centered_ratio(0.8)
|
||||
|
||||
func _on_open_inspected_pressed() -> void:
|
||||
if _editor_interface == null:
|
||||
_set_status("Editor interface is not available.", true)
|
||||
return
|
||||
var inspector := _editor_interface.get_inspector()
|
||||
if inspector == null:
|
||||
_set_status("Inspector is not available.", true)
|
||||
return
|
||||
|
||||
var edited_object := inspector.get_edited_object()
|
||||
if not (edited_object is Resource):
|
||||
_set_status("Inspector object is not a supported resource.", true)
|
||||
return
|
||||
|
||||
open_resource(edited_object)
|
||||
|
||||
func _on_file_selected(path: String) -> void:
|
||||
var loaded = ResourceLoader.load(path)
|
||||
if not (loaded is Resource):
|
||||
_set_status("Failed to load resource: %s" % path, true)
|
||||
return
|
||||
open_resource(loaded)
|
||||
|
||||
func _on_path_submitted(path: String) -> void:
|
||||
if path.strip_edges() == "":
|
||||
return
|
||||
_on_file_selected(path)
|
||||
|
||||
func _clear_graph() -> void:
|
||||
for child in _graph_edit.get_children():
|
||||
if child is GraphNode:
|
||||
child.queue_free()
|
||||
|
||||
func _render_graph() -> void:
|
||||
if _current_resource == null:
|
||||
return
|
||||
|
||||
_clear_graph()
|
||||
|
||||
var graph: Dictionary = _graph_adapter.build_graph(_current_resource)
|
||||
var nodes: Dictionary = graph.get("nodes", {})
|
||||
var edges: Array = graph.get("edges", [])
|
||||
var node_name_by_id := {}
|
||||
_selected_node_resource = null
|
||||
_open_selected_button.disabled = true
|
||||
_inspect_selected_button.disabled = true
|
||||
_wait_checkbox.visible = false
|
||||
|
||||
var layout := _load_layout_metadata()
|
||||
var auto_index := 0
|
||||
|
||||
for node_id in nodes.keys():
|
||||
var node_data: Dictionary = nodes[node_id]
|
||||
var graph_node := GraphNode.new()
|
||||
graph_node.name = _node_name_from_id(node_id)
|
||||
graph_node.title = node_data.get("title", "Node")
|
||||
graph_node.resizable = false
|
||||
graph_node.custom_minimum_size = Vector2(240, 84)
|
||||
graph_node.set_meta("graph_id", node_id)
|
||||
|
||||
var info := VBoxContainer.new()
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = node_data.get("subtitle", "")
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
subtitle.clip_text = true
|
||||
subtitle.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
info.add_child(subtitle)
|
||||
|
||||
# Collapsible details handled below when there are actual details to show
|
||||
|
||||
# Determine if there are details to show (description or exported properties or patterns add button)
|
||||
var potential_details := []
|
||||
var node_resource: Resource = node_data.get("resource", null)
|
||||
if node_resource != null:
|
||||
graph_node.set_meta("resource", node_resource)
|
||||
if node_resource != null:
|
||||
var desc_text := str(node_data.get("subtitle", ""))
|
||||
if desc_text != "":
|
||||
potential_details.append("desc")
|
||||
# check for exported properties (editor usage)
|
||||
for property_data in node_resource.get_property_list():
|
||||
if property_data is Dictionary:
|
||||
var pname := property_data.get("name", "")
|
||||
if pname == "" or pname.begins_with("_") or pname == "Patterns":
|
||||
continue
|
||||
var usage := int(property_data.get("usage", 0))
|
||||
if usage & PROPERTY_USAGE_EDITOR:
|
||||
potential_details.append(pname)
|
||||
# Add Pattern button is considered a detail for BulletScript3D
|
||||
if node_data.get("kind", "") == "BulletScript3D":
|
||||
potential_details.append("add_pattern")
|
||||
|
||||
var has_details := potential_details.size() > 0
|
||||
var details: VBoxContainer = null
|
||||
var toggle: Button = null
|
||||
if has_details:
|
||||
details = VBoxContainer.new()
|
||||
details.name = "details"
|
||||
details.visible = false
|
||||
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
info.add_child(details)
|
||||
|
||||
# Toggle button to expand/collapse
|
||||
toggle = Button.new()
|
||||
toggle.text = "Details"
|
||||
toggle.toggle_mode = true
|
||||
# default is not pressed; use setter to avoid assigning to a constant
|
||||
toggle.set_pressed(false)
|
||||
# bind graph_node and details so we can resize the node when toggling
|
||||
toggle.pressed.connect(Callable(self, "_on_toggle_details_pressed").bind(graph_node, details))
|
||||
info.add_child(toggle)
|
||||
|
||||
|
||||
graph_node.add_child(info)
|
||||
|
||||
# Add quick action buttons on each node for convenience
|
||||
if node_resource != null:
|
||||
var actions := HBoxContainer.new()
|
||||
actions.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
var btn_open := Button.new()
|
||||
btn_open.text = "Open"
|
||||
btn_open.pressed.connect(Callable(self, "_on_node_open_pressed").bind(graph_node))
|
||||
actions.add_child(btn_open)
|
||||
|
||||
var btn_inspect := Button.new()
|
||||
btn_inspect.text = "Inspect"
|
||||
btn_inspect.pressed.connect(Callable(self, "_on_node_inspect_pressed").bind(graph_node))
|
||||
actions.add_child(btn_inspect)
|
||||
|
||||
info.add_child(actions)
|
||||
graph_node.set_slot(0, true, 0, Color(0.75, 0.75, 0.9), true, 0, Color(0.75, 0.9, 0.75))
|
||||
|
||||
if layout.has(node_id):
|
||||
graph_node.position_offset = layout[node_id]
|
||||
else:
|
||||
graph_node.position_offset = Vector2(60 + (auto_index % 5) * 320, 36 + int(auto_index / 5) * 160)
|
||||
auto_index += 1
|
||||
|
||||
_graph_edit.add_child(graph_node)
|
||||
node_name_by_id[node_id] = graph_node.name
|
||||
|
||||
for edge_data in edges:
|
||||
var from_id: String = edge_data.get("from", "")
|
||||
var to_id: String = edge_data.get("to", "")
|
||||
if not (node_name_by_id.has(from_id) and node_name_by_id.has(to_id)):
|
||||
continue
|
||||
var from_name: StringName = StringName(node_name_by_id[from_id])
|
||||
var to_name: StringName = StringName(node_name_by_id[to_id])
|
||||
if from_name == to_name:
|
||||
continue
|
||||
if _graph_edit.is_node_connected(from_name, 0, to_name, 0):
|
||||
continue
|
||||
_graph_edit.connect_node(from_name, 0, to_name, 0)
|
||||
|
||||
func _load_layout_metadata() -> Dictionary:
|
||||
if _current_resource == null:
|
||||
return {}
|
||||
if not _current_resource.has_meta(META_LAYOUT_KEY):
|
||||
return {}
|
||||
var data = _current_resource.get_meta(META_LAYOUT_KEY, {})
|
||||
return data if data is Dictionary else {}
|
||||
|
||||
func _save_layout_metadata() -> void:
|
||||
if _current_resource == null:
|
||||
return
|
||||
|
||||
var layout := {}
|
||||
for child in _graph_edit.get_children():
|
||||
if not (child is GraphNode):
|
||||
continue
|
||||
var graph_id: String = child.get_meta("graph_id", "")
|
||||
if graph_id == "":
|
||||
continue
|
||||
layout[graph_id] = child.position_offset
|
||||
|
||||
_current_resource.set_meta(META_LAYOUT_KEY, layout)
|
||||
if _current_resource.resource_path != "":
|
||||
var err := ResourceSaver.save(_current_resource, _current_resource.resource_path)
|
||||
if err != OK:
|
||||
_set_status("Failed to save graph layout metadata.", true)
|
||||
return
|
||||
|
||||
_set_status("Saved graph layout metadata.", false)
|
||||
|
||||
func _node_name_from_id(node_id: String) -> String:
|
||||
return "n_%s" % str(abs(node_id.hash()))
|
||||
|
||||
func _display_path(resource: Resource) -> String:
|
||||
if resource == null:
|
||||
return ""
|
||||
if resource.resource_path != "":
|
||||
return resource.resource_path
|
||||
return "<subresource>"
|
||||
|
||||
func _set_status(message: String, is_error: bool) -> void:
|
||||
if _status_label == null:
|
||||
return
|
||||
_status_label.text = message
|
||||
_status_label.modulate = Color(1, 0.55, 0.55) if is_error else Color(1, 1, 1)
|
||||
|
||||
func _on_graph_node_selected(node_arg) -> void:
|
||||
# GraphEdit may send different types (StringName, NodePath, or the node object itself).
|
||||
var graph_node: GraphNode = null
|
||||
if typeof(node_arg) == TYPE_OBJECT and node_arg is GraphNode:
|
||||
graph_node = node_arg
|
||||
else:
|
||||
# Try to resolve by path or name
|
||||
var resolved := _graph_edit.get_node_or_null(node_arg)
|
||||
if resolved is GraphNode:
|
||||
graph_node = resolved
|
||||
|
||||
if not (graph_node is GraphNode):
|
||||
_selected_node_resource = null
|
||||
_open_selected_button.disabled = true
|
||||
_inspect_selected_button.disabled = true
|
||||
_wait_checkbox.visible = false
|
||||
return
|
||||
|
||||
_selected_node_resource = _get_graph_node_resource(graph_node)
|
||||
var enabled := _selected_node_resource != null
|
||||
_open_selected_button.disabled = not enabled
|
||||
_inspect_selected_button.disabled = not enabled
|
||||
|
||||
if enabled and _resource_has_property(_selected_node_resource, "WaitForCompletion"):
|
||||
_wait_checkbox.visible = true
|
||||
_wait_checkbox.set_pressed_no_signal(bool(_selected_node_resource.get("WaitForCompletion")))
|
||||
else:
|
||||
_wait_checkbox.visible = false
|
||||
|
||||
_set_status("Selected: %s" % _display_path(_selected_node_resource), false)
|
||||
|
||||
func _on_node_open_pressed(graph_node: GraphNode) -> void:
|
||||
if not (graph_node is GraphNode):
|
||||
return
|
||||
var res := _get_graph_node_resource(graph_node)
|
||||
if res == null:
|
||||
_set_status("Node has no resource to open.", true)
|
||||
return
|
||||
open_resource(res)
|
||||
|
||||
func _on_toggle_details_pressed(graph_node: GraphNode, details: VBoxContainer) -> void:
|
||||
if details == null or graph_node == null:
|
||||
return
|
||||
details.visible = not details.visible
|
||||
if details.visible:
|
||||
var extra := details.get_combined_minimum_size().y
|
||||
graph_node.custom_minimum_size = Vector2(240, 84 + extra)
|
||||
else:
|
||||
graph_node.custom_minimum_size = Vector2(240, 84)
|
||||
|
||||
func _on_prop_text_changed(resource: Resource, prop_name: String, new_text: String) -> void:
|
||||
if resource == null:
|
||||
return
|
||||
resource.set(prop_name, new_text)
|
||||
if resource.resource_path != "":
|
||||
ResourceSaver.save(resource, resource.resource_path)
|
||||
_set_status("Updated %s" % prop_name, false)
|
||||
|
||||
func _on_prop_float_changed(resource: Resource, prop_name: String, value: float) -> void:
|
||||
if resource == null:
|
||||
return
|
||||
resource.set(prop_name, value)
|
||||
if resource.resource_path != "":
|
||||
ResourceSaver.save(resource, resource.resource_path)
|
||||
_set_status("Updated %s" % prop_name, false)
|
||||
|
||||
func _on_prop_bool_changed(resource: Resource, prop_name: String, pressed: bool) -> void:
|
||||
if resource == null:
|
||||
return
|
||||
resource.set(prop_name, pressed)
|
||||
if resource.resource_path != "":
|
||||
ResourceSaver.save(resource, resource.resource_path)
|
||||
_set_status("Updated %s" % prop_name, false)
|
||||
|
||||
func _on_add_shooting_pattern_pressed(resource: Resource) -> void:
|
||||
if resource == null:
|
||||
_set_status("No resource to add pattern to.", true)
|
||||
return
|
||||
# Try to instantiate a ShootingPattern3D (GlobalClass C# type)
|
||||
var new_pattern = null
|
||||
# attempt to instantiate ShootingPattern3D; fall back to generic Resource if not available
|
||||
# Instantiating a C# AttackPattern from GDScript can be fragile; create a generic Resource for now.
|
||||
new_pattern = Resource.new()
|
||||
# Append to Patterns array if present
|
||||
if _resource_has_property(resource, "Patterns"):
|
||||
var arr := resource.get("Patterns")
|
||||
if not (arr is Array):
|
||||
arr = []
|
||||
arr.append(new_pattern)
|
||||
resource.set("Patterns", arr)
|
||||
if resource.resource_path != "":
|
||||
ResourceSaver.save(resource, resource.resource_path)
|
||||
_set_status("Added new pattern.", false)
|
||||
_render_graph()
|
||||
else:
|
||||
_set_status("Resource has no Patterns array.", true)
|
||||
|
||||
func _on_node_inspect_pressed(graph_node: GraphNode) -> void:
|
||||
if not (graph_node is GraphNode):
|
||||
return
|
||||
var res := _get_graph_node_resource(graph_node)
|
||||
if res == null:
|
||||
_set_status("Node has no resource to inspect.", true)
|
||||
return
|
||||
if _editor_interface == null:
|
||||
_set_status("Editor interface not available.", true)
|
||||
return
|
||||
# Defer opening the inspector to avoid any re-entrancy or script-parse conflicts
|
||||
call_deferred("_deferred_edit_resource", res)
|
||||
|
||||
# Immediate status update; the actual inspector open happens shortly after
|
||||
_set_status("Inspecting (deferred): %s" % _display_path(res), false)
|
||||
|
||||
func _deferred_edit_resource(res: Resource) -> void:
|
||||
if _editor_interface == null or res == null:
|
||||
return
|
||||
_editor_interface.edit_resource(res)
|
||||
_set_status("Inspecting: %s" % _display_path(res), false)
|
||||
|
||||
func _on_open_selected_node_pressed() -> void:
|
||||
if _selected_node_resource == null:
|
||||
_set_status("No node resource selected.", true)
|
||||
return
|
||||
open_resource(_selected_node_resource)
|
||||
|
||||
func _on_inspect_selected_node_pressed() -> void:
|
||||
if _selected_node_resource == null:
|
||||
_set_status("No node resource selected.", true)
|
||||
return
|
||||
if _editor_interface == null:
|
||||
_set_status("Editor interface is not available.", true)
|
||||
return
|
||||
_editor_interface.edit_resource(_selected_node_resource)
|
||||
_set_status("Inspected selected node resource.", false)
|
||||
|
||||
func _unique_strings(values: Array) -> PackedStringArray:
|
||||
var seen := {}
|
||||
var result := PackedStringArray()
|
||||
for value in values:
|
||||
var text := str(value)
|
||||
if text == "" or seen.has(text):
|
||||
continue
|
||||
seen[text] = true
|
||||
result.append(text)
|
||||
if result.is_empty():
|
||||
result.append("-")
|
||||
return result
|
||||
|
||||
func _on_wait_for_completion_toggled(pressed: bool) -> void:
|
||||
if _selected_node_resource == null:
|
||||
return
|
||||
if not _resource_has_property(_selected_node_resource, "WaitForCompletion"):
|
||||
return
|
||||
|
||||
_selected_node_resource.set("WaitForCompletion", pressed)
|
||||
if _selected_node_resource.resource_path != "":
|
||||
var err := ResourceSaver.save(_selected_node_resource, _selected_node_resource.resource_path)
|
||||
if err != OK:
|
||||
_set_status("Failed to save WaitForCompletion.", true)
|
||||
return
|
||||
|
||||
_set_status("Updated WaitForCompletion on selected pattern.", false)
|
||||
_render_graph()
|
||||
|
||||
func _resource_has_property(resource: Object, property_name: String) -> bool:
|
||||
for property_data in resource.get_property_list():
|
||||
if property_data is Dictionary and property_data.get("name", "") == property_name:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _get_graph_node_resource(graph_node: GraphNode) -> Resource:
|
||||
if graph_node == null:
|
||||
return null
|
||||
if not graph_node.has_meta("resource"):
|
||||
return null
|
||||
var value = graph_node.get_meta("resource")
|
||||
return value if value is Resource else null
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://duc4xqfaiyjq5
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/bullet_script_graph_editor/ui/graph_editor_dock.gd" id="1_5n6ty"]
|
||||
|
||||
[node name="GraphEditorDock" type="PanelContainer"]
|
||||
script = ExtResource("1_5n6ty")
|
||||
Loading…
Add table
Add a link
Reference in a new issue