add beehave

This commit is contained in:
2025-01-28 13:53:59 +08:00
parent d6b69c14ac
commit 144a01556d
112 changed files with 6075 additions and 205 deletions
+96
View File
@@ -0,0 +1,96 @@
@tool
extends EditorDebuggerPlugin
const DebuggerTab := preload("debugger_tab.gd")
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
var debugger_tab := DebuggerTab.new()
var floating_window: Window
var session: EditorDebuggerSession
func _has_capture(prefix: String) -> bool:
return prefix == "beehave"
func _capture(message: String, data: Array, session_id: int) -> bool:
# in case the behavior tree has invalid setup this might be null
if debugger_tab == null:
return false
if message == "beehave:register_tree":
debugger_tab.register_tree(data[0])
return true
if message == "beehave:unregister_tree":
debugger_tab.unregister_tree(data[0])
return true
if message == "beehave:process_tick":
debugger_tab.graph.process_tick(data[0], data[1], data[2])
return true
if message == "beehave:process_begin":
debugger_tab.graph.process_begin(data[0], data[1])
return true
if message == "beehave:process_end":
debugger_tab.graph.process_end(data[0], data[1])
return true
return false
func _setup_session(session_id: int) -> void:
session = get_session(session_id)
session.started.connect(debugger_tab.start)
session.stopped.connect(debugger_tab.stop)
debugger_tab.name = "🐝 Beehave"
debugger_tab.make_floating.connect(_on_make_floating)
debugger_tab.session = session
session.add_session_tab(debugger_tab)
func _on_make_floating() -> void:
var plugin := BeehaveUtils.get_plugin()
if not plugin:
return
if floating_window:
_on_window_close_requested()
return
var border_size := Vector2(4, 4) * BeehaveUtils.get_editor_scale()
var editor_interface: EditorInterface = plugin.get_editor_interface()
var editor_main_screen = editor_interface.get_editor_main_screen()
debugger_tab.get_parent().remove_child(debugger_tab)
floating_window = Window.new()
var panel := Panel.new()
panel.add_theme_stylebox_override(
"panel",
editor_interface.get_base_control().get_theme_stylebox("PanelForeground", "EditorStyles")
)
panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
floating_window.add_child(panel)
var margin := MarginContainer.new()
margin.add_child(debugger_tab)
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_right", border_size.x)
margin.add_theme_constant_override("margin_left", border_size.x)
margin.add_theme_constant_override("margin_top", border_size.y)
margin.add_theme_constant_override("margin_bottom", border_size.y)
panel.add_child(margin)
floating_window.title = "🐝 Beehave"
floating_window.wrap_controls = true
floating_window.min_size = Vector2i(600, 350)
floating_window.size = debugger_tab.size
floating_window.position = editor_main_screen.global_position
floating_window.transient = true
floating_window.close_requested.connect(_on_window_close_requested)
editor_interface.get_base_control().add_child(floating_window)
func _on_window_close_requested() -> void:
debugger_tab.get_parent().remove_child(debugger_tab)
session.add_session_tab(debugger_tab)
floating_window.queue_free()
floating_window = null
+30
View File
@@ -0,0 +1,30 @@
class_name BeehaveDebuggerMessages
static func can_send_message() -> bool:
return not Engine.is_editor_hint() and OS.has_feature("editor")
static func register_tree(beehave_tree: Dictionary) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:register_tree", [beehave_tree])
static func unregister_tree(instance_id: int) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:unregister_tree", [instance_id])
static func process_tick(instance_id: int, status: int, blackboard: Dictionary = {}) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_tick", [instance_id, status, blackboard])
static func process_begin(instance_id: int, blackboard: Dictionary = {}) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_begin", [instance_id, blackboard])
static func process_end(instance_id: int, blackboard: Dictionary = {}) -> void:
if can_send_message():
EngineDebugger.send_message("beehave:process_end", [instance_id, blackboard])
+157
View File
@@ -0,0 +1,157 @@
@tool
class_name BeehaveDebuggerTab extends PanelContainer
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
signal make_floating
const OldBeehaveGraphEdit := preload("old_graph_edit.gd")
const NewBeehaveGraphEdit := preload("new_graph_edit.gd")
const NewNodeBlackBoard := preload("new_node_blackboard.gd")
const TREE_ICON := preload("../icons/tree.svg")
var graph
var container: HSplitContainer
var graph_container: HSplitContainer
var item_list: ItemList
var blackboard_vbox: VBoxContainer
var message: Label
var active_trees: Dictionary
var active_tree_id: int = -1
var session: EditorDebuggerSession
func _ready() -> void:
container = HSplitContainer.new()
add_child(container)
item_list = ItemList.new()
item_list.custom_minimum_size = Vector2(200, 0)
item_list.item_selected.connect(_on_item_selected)
container.add_child(item_list)
graph_container = HSplitContainer.new()
graph_container.split_offset = 1920
graph_container.set_anchors_preset(Control.PRESET_FULL_RECT)
container.add_child(graph_container)
if Engine.get_version_info().minor >= 2:
graph = NewBeehaveGraphEdit.new(BeehaveUtils.get_frames())
else:
graph = OldBeehaveGraphEdit.new(BeehaveUtils.get_frames())
graph.node_selected.connect(_on_graph_node_selected)
graph.node_deselected.connect(_on_graph_node_deselected)
graph_container.add_child(graph)
blackboard_vbox = VBoxContainer.new()
blackboard_vbox.custom_minimum_size = Vector2(200, 0)
blackboard_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
graph_container.add_child(blackboard_vbox)
message = Label.new()
message.text = "Run Project for debugging"
message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
message.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
message.set_anchors_preset(Control.PRESET_CENTER)
add_child(message)
var button := Button.new()
button.flat = true
button.name = "MakeFloatingButton"
button.icon = get_theme_icon(&"ExternalLink", &"EditorIcons")
button.pressed.connect(func(): make_floating.emit())
button.tooltip_text = "Make floating"
button.focus_mode = Control.FOCUS_NONE
graph.get_menu_container().add_child(button)
var toggle_button := Button.new()
toggle_button.flat = true
toggle_button.name = "TogglePanelButton"
toggle_button.icon = get_theme_icon(&"Back", &"EditorIcons")
toggle_button.pressed.connect(_on_toggle_button_pressed.bind(toggle_button))
toggle_button.tooltip_text = "Toggle Panel"
toggle_button.focus_mode = Control.FOCUS_NONE
graph.get_menu_container().add_child(toggle_button)
graph.get_menu_container().move_child(toggle_button, 0)
stop()
visibility_changed.connect(_on_visibility_changed)
func start() -> void:
container.visible = true
message.visible = false
func stop() -> void:
container.visible = false
message.visible = true
active_trees.clear()
item_list.clear()
graph.beehave_tree = {}
func register_tree(data: Dictionary) -> void:
if not active_trees.has(data.id):
var idx := item_list.add_item(data.name, TREE_ICON)
item_list.set_item_tooltip(idx, data.path)
item_list.set_item_metadata(idx, data.id)
active_trees[data.id] = data
if active_tree_id == data.id.to_int():
graph.beehave_tree = data
func unregister_tree(instance_id: int) -> void:
var id := str(instance_id)
for i in item_list.item_count:
if item_list.get_item_metadata(i) == id:
item_list.remove_item(i)
break
active_trees.erase(id)
if graph.beehave_tree.get("id", "") == id:
graph.beehave_tree = {}
func _on_toggle_button_pressed(toggle_button: Button) -> void:
item_list.visible = !item_list.visible
toggle_button.icon = get_theme_icon(
&"Back" if item_list.visible else &"Forward", &"EditorIcons"
)
func _on_item_selected(idx: int) -> void:
var id: StringName = item_list.get_item_metadata(idx)
graph.beehave_tree = active_trees.get(id, {})
# Clear our any loaded blackboards
for child in blackboard_vbox.get_children():
child.free()
active_tree_id = id.to_int()
if session != null:
session.send_message("beehave:activate_tree", [active_tree_id])
func _on_graph_node_selected(node: GraphNode) -> void:
var node_blackboard: VBoxContainer = NewNodeBlackBoard.new(BeehaveUtils.get_frames(), node)
blackboard_vbox.add_child(node_blackboard)
func _on_graph_node_deselected(node: GraphNode) -> void:
var matches: Array = blackboard_vbox\
.get_children()\
.filter(func (child): return child.name == node.name)
for child in matches:
child.free()
func _on_visibility_changed() -> void:
if session != null:
session.send_message("beehave:visibility_changed", [visible and is_visible_in_tree()])
+38
View File
@@ -0,0 +1,38 @@
extends Node
var _registered_trees: Dictionary
var _active_tree
func _enter_tree() -> void:
EngineDebugger.register_message_capture("beehave", _on_debug_message)
func _on_debug_message(message: String, data: Array) -> bool:
if message == "activate_tree":
_set_active_tree(data[0])
return true
if message == "visibility_changed":
if _active_tree && is_instance_valid(_active_tree):
_active_tree._can_send_message = data[0]
return true
return false
func _set_active_tree(tree_id: int) -> void:
var tree = _registered_trees.get(tree_id, null)
if not tree:
return
if _active_tree && is_instance_valid(_active_tree):
_active_tree._can_send_message = false
_active_tree = tree
_active_tree._can_send_message = true
func register_tree(tree) -> void:
_registered_trees[tree.get_instance_id()] = tree
func unregister_tree(tree) -> void:
_registered_trees.erase(tree.get_instance_id())
@@ -0,0 +1 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="matrix(0 -1 1 0 0 16)"><rect height="6" rx="1" stroke-width=".6" width="6" y="10"/><rect height="6" rx="1" stroke-width=".780723" width="6" x="5"/><rect height="6" rx="1" stroke-width=".780723" width="6" x="10" y="10"/><path d="m7 5h2v4h-2z" stroke-width=".768491"/><rect height="4" rx="1" ry="0" stroke-width=".768491" width="2" x="12" y="7"/><rect height="5" rx="1" stroke-width=".859" width="2" x="2" y="7"/><path d="m3 7h10v2h-10z" stroke-width="1.09113"/></g></svg>

After

Width:  |  Height:  |  Size: 562 B

@@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bah77esichnyx"
path="res://.godot/imported/horizontal_layout.svg-d2a7af351e44f9bf61d0c938b6f47fac.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/debug/icons/horizontal_layout.svg"
dest_files=["res://.godot/imported/horizontal_layout.svg-d2a7af351e44f9bf61d0c938b6f47fac.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
@@ -0,0 +1 @@
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg"><path d="m10 4a5 5 0 0 1 -2.5000001 4.3301271 5 5 0 0 1 -5-.0000002 5 5 0 0 1 -2.4999999-4.3301269" fill="#fff" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 222 B

@@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://da3b236rjbqns"
path="res://.godot/imported/port_bottom.svg-e5c5c61b642a79ab9c2b66ff56603d34.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/debug/icons/port_bottom.svg"
dest_files=["res://.godot/imported/port_bottom.svg-e5c5c61b642a79ab9c2b66ff56603d34.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
+1
View File
@@ -0,0 +1 @@
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg"><path d="m5 0a5 5 0 0 0 -4.33012712 2.5000001 5 5 0 0 0 .0000002 5 5 5 0 0 0 4.33012692 2.4999999" fill="#fff" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 221 B

@@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnufc8p6spdtn"
path="res://.godot/imported/port_left.svg-69cd927c4db555f1edbb8d1f553ea2fd.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/debug/icons/port_left.svg"
dest_files=["res://.godot/imported/port_left.svg-69cd927c4db555f1edbb8d1f553ea2fd.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
@@ -0,0 +1 @@
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg"><path d="m4.5 10a5 5 0 0 0 4.3301271-2.5000002 5 5 0 0 0 -.0000002-4.9999999 5 5 0 0 0 -4.3301269-2.4999999" fill="#fff" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 231 B

@@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bbmd6vk23ympm"
path="res://.godot/imported/port_right.svg-f760bd8be2dd613d0d3848c998c92a2a.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/debug/icons/port_right.svg"
dest_files=["res://.godot/imported/port_right.svg-f760bd8be2dd613d0d3848c998c92a2a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
+1
View File
@@ -0,0 +1 @@
<svg height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg"><path d="m10-6a5 5 0 0 1 -2.5000001 4.3301271 5 5 0 0 1 -5-.0000002 5 5 0 0 1 -2.4999999-4.3301269" fill="#fff" fill-rule="evenodd" transform="scale(1 -1)"/></svg>

After

Width:  |  Height:  |  Size: 246 B

@@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bw8wmxdfom8eh"
path="res://.godot/imported/port_top.svg-d1b336cdc6a0dd570305782a1e56f61d.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/debug/icons/port_top.svg"
dest_files=["res://.godot/imported/port_top.svg-d1b336cdc6a0dd570305782a1e56f61d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
@@ -0,0 +1 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0"><rect height="6" rx="1" stroke-width=".6" width="6" y="10"/><rect height="6" rx="1" stroke-width=".780723" width="6" x="5"/><rect height="6" rx="1" stroke-width=".780723" width="6" x="10" y="10"/><path d="m7 5h2v4h-2z" stroke-width=".768491"/><rect height="4" rx="1" ry="0" stroke-width=".768491" width="2" x="12" y="7"/><rect height="5" rx="1" stroke-width=".859" width="2" x="2" y="7"/><path d="m3 7h10v2h-10z" stroke-width="1.09113"/></g></svg>

After

Width:  |  Height:  |  Size: 528 B

@@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bpyxu6i1dx5qh"
path="res://.godot/imported/vertical_layout.svg-1a08fee4b09812a05bcf3defb8afcc4c.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/beehave/debug/icons/vertical_layout.svg"
dest_files=["res://.godot/imported/vertical_layout.svg-1a08fee4b09812a05bcf3defb8afcc4c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
+69
View File
@@ -0,0 +1,69 @@
@tool
extends RefCounted
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
const SUCCESS_COLOR := Color("#07783a")
const NORMAL_COLOR := Color("#15181e")
const FAILURE_COLOR := Color("#82010b")
const RUNNING_COLOR := Color("#c29c06")
var panel_normal: StyleBoxFlat
var panel_success: StyleBoxFlat
var panel_failure: StyleBoxFlat
var panel_running: StyleBoxFlat
var titlebar_normal: StyleBoxFlat
var titlebar_success: StyleBoxFlat
var titlebar_failure: StyleBoxFlat
var titlebar_running: StyleBoxFlat
func _init() -> void:
var plugin := BeehaveUtils.get_plugin()
if not plugin:
return
titlebar_normal = (
plugin
.get_editor_interface()
.get_base_control()
.get_theme_stylebox(&"titlebar", &"GraphNode")\
.duplicate()
)
titlebar_success = titlebar_normal.duplicate()
titlebar_failure = titlebar_normal.duplicate()
titlebar_running = titlebar_normal.duplicate()
titlebar_success.bg_color = SUCCESS_COLOR
titlebar_failure.bg_color = FAILURE_COLOR
titlebar_running.bg_color = RUNNING_COLOR
titlebar_success.border_color = SUCCESS_COLOR
titlebar_failure.border_color = FAILURE_COLOR
titlebar_running.border_color = RUNNING_COLOR
panel_normal = (
plugin
.get_editor_interface()
.get_base_control()
.get_theme_stylebox(&"panel", &"GraphNode")
.duplicate()
)
panel_success = (
plugin
.get_editor_interface()
.get_base_control()
.get_theme_stylebox(&"panel_selected", &"GraphNode")
.duplicate()
)
panel_failure = panel_success.duplicate()
panel_running = panel_success.duplicate()
panel_success.border_color = SUCCESS_COLOR
panel_failure.border_color = FAILURE_COLOR
panel_running.border_color = RUNNING_COLOR
+297
View File
@@ -0,0 +1,297 @@
@tool
extends GraphEdit
const BeehaveGraphNode := preload("new_graph_node.gd")
const HORIZONTAL_LAYOUT_ICON := preload("icons/horizontal_layout.svg")
const VERTICAL_LAYOUT_ICON := preload("icons/vertical_layout.svg")
const PROGRESS_SHIFT: int = 50
const INACTIVE_COLOR: Color = Color("#898989")
const ACTIVE_COLOR: Color = Color("#c29c06")
const SUCCESS_COLOR: Color = Color("#07783a")
var updating_graph: bool = false
var arraging_nodes: bool = false
var beehave_tree: Dictionary:
set(value):
if beehave_tree == value:
return
beehave_tree = value
active_nodes.clear()
_update_graph()
var horizontal_layout: bool = false:
set(value):
if updating_graph or arraging_nodes:
return
if horizontal_layout == value:
return
horizontal_layout = value
_update_layout_button()
_update_graph()
var frames:RefCounted
var active_nodes: Array[String]
var progress: int = 0
var layout_button: Button
func _init(frames:RefCounted) -> void:
self.frames = frames
func _ready() -> void:
custom_minimum_size = Vector2(100, 300)
set("show_arrange_button", true)
minimap_enabled = false
layout_button = Button.new()
layout_button.flat = true
layout_button.focus_mode = Control.FOCUS_NONE
layout_button.pressed.connect(func(): horizontal_layout = not horizontal_layout)
get_menu_container().add_child(layout_button)
_update_layout_button()
func _update_graph() -> void:
if updating_graph:
return
updating_graph = true
clear_connections()
for child in _get_child_nodes():
remove_child(child)
child.queue_free()
if not beehave_tree.is_empty():
_add_nodes(beehave_tree)
_connect_nodes(beehave_tree)
_arrange_nodes.call_deferred(beehave_tree)
updating_graph = false
func _add_nodes(node: Dictionary) -> void:
if node.is_empty():
return
var gnode := BeehaveGraphNode.new(frames, horizontal_layout)
add_child(gnode)
gnode.title_text = node.name
gnode.name = node.id
gnode.icon = _get_icon(node.type.back())
if node.type.has(&"BeehaveTree"):
gnode.set_slots(false, true)
elif node.type.has(&"Leaf"):
gnode.set_slots(true, false)
elif node.type.has(&"Composite") or node.type.has(&"Decorator"):
gnode.set_slots(true, true)
for child in node.get("children", []):
_add_nodes(child)
func _connect_nodes(node: Dictionary) -> void:
for child in node.get("children", []):
connect_node(node.id, 0, child.id, 0)
_connect_nodes(child)
func _arrange_nodes(node: Dictionary) -> void:
if arraging_nodes:
return
arraging_nodes = true
var tree_node := _create_tree_nodes(node)
tree_node.update_positions(horizontal_layout)
_place_nodes(tree_node)
arraging_nodes = false
func _create_tree_nodes(node: Dictionary, root: TreeNode = null) -> TreeNode:
var tree_node := TreeNode.new(get_node(node.id), root)
for child in node.get("children", []):
var child_node := _create_tree_nodes(child, tree_node)
tree_node.children.push_back(child_node)
return tree_node
func _place_nodes(node: TreeNode) -> void:
node.item.position_offset = Vector2(node.x, node.y)
for child in node.children:
_place_nodes(child)
func _get_icon(type: StringName) -> Texture2D:
var classes := ProjectSettings.get_global_class_list()
for c in classes:
if c["class"] == type:
var icon_path := c.get("icon", String())
if not icon_path.is_empty():
return load(icon_path)
return null
func get_menu_container() -> Control:
return call("get_menu_hbox")
func get_status(status: int) -> String:
if status == 0:
return "SUCCESS"
elif status == 1:
return "FAILURE"
return "RUNNING"
func process_begin(instance_id: int, blackboard = null) -> void:
if not _is_same_tree(instance_id):
return
for child in _get_child_nodes():
child.set_meta("status", -1)
func process_tick(instance_id: int, status: int, blackboard = null) -> void:
var node := get_node_or_null(str(instance_id))
if node:
node.text = "Status: %s" % get_status(status)
node.set_status(status)
node.set_meta("status", status)
node.blackboard = blackboard
if status == BeehaveNode.SUCCESS or status == BeehaveNode.RUNNING:
if not active_nodes.has(node.name):
active_nodes.push_back(node.name)
func process_end(instance_id: int, blackboard = null) -> void:
if not _is_same_tree(instance_id):
return
for child in _get_child_nodes():
var status := child.get_meta("status", -1)
match status:
BeehaveNode.SUCCESS:
active_nodes.erase(child.name)
child.set_color(SUCCESS_COLOR)
BeehaveNode.FAILURE:
active_nodes.erase(child.name)
child.set_color(INACTIVE_COLOR)
BeehaveNode.RUNNING:
child.set_color(ACTIVE_COLOR)
_:
child.text = " "
child.set_status(status)
child.set_color(INACTIVE_COLOR)
func _is_same_tree(instance_id: int) -> bool:
return str(instance_id) == beehave_tree.get("id", "")
func _get_child_nodes() -> Array[Node]:
return get_children().filter(func(child): return child is BeehaveGraphNode)
func _get_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array:
for child in _get_child_nodes():
for port in child.get_input_port_count():
if not (child.position_offset + child.get_input_port_position(port)).is_equal_approx(to_position):
continue
to_position = child.position_offset + child.get_custom_input_port_position(horizontal_layout)
for port in child.get_output_port_count():
if not (child.position_offset + child.get_output_port_position(port)).is_equal_approx(from_position):
continue
from_position = child.position_offset + child.get_custom_output_port_position(horizontal_layout)
return _get_elbow_connection_line(from_position, to_position)
func _get_elbow_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array:
var points: PackedVector2Array
points.push_back(from_position)
var mid_position := ((to_position + from_position) / 2).round()
if horizontal_layout:
points.push_back(Vector2(mid_position.x, from_position.y))
points.push_back(Vector2(mid_position.x, to_position.y))
else:
points.push_back(Vector2(from_position.x, mid_position.y))
points.push_back(Vector2(to_position.x, mid_position.y))
points.push_back(to_position)
return points
func _process(delta: float) -> void:
if not active_nodes.is_empty():
progress += 10 if delta >= 0.05 else 1
if progress >= 1000:
progress = 0
queue_redraw()
func _draw() -> void:
if active_nodes.is_empty():
return
var circle_size: float = max(3, 6 * zoom)
var progress_shift: float = PROGRESS_SHIFT * zoom
var connections := get_connection_list()
for c in connections:
var from_node: StringName
var to_node: StringName
from_node = c.from_node
to_node = c.to_node
if not from_node in active_nodes or not c.to_node in active_nodes:
continue
var from := get_node(String(from_node))
var to := get_node(String(to_node))
if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0:
return
var output_port_position: Vector2
var input_port_position: Vector2
var scale_factor: float = from.get_rect().size.x / from.size.x
var line := _get_elbow_connection_line(
from.position + from.get_custom_output_port_position(horizontal_layout) * scale_factor,
to.position + to.get_custom_input_port_position(horizontal_layout) * scale_factor
)
var curve = Curve2D.new()
for l in line:
curve.add_point(l)
var max_steps := int(curve.get_baked_length())
var current_shift := progress % max_steps
var p := curve.sample_baked(current_shift)
draw_circle(p, circle_size, ACTIVE_COLOR)
var shift := current_shift - progress_shift
while shift >= 0:
draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR)
shift -= progress_shift
shift = current_shift + progress_shift
while shift <= curve.get_baked_length():
draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR)
shift += progress_shift
func _update_layout_button() -> void:
layout_button.icon = VERTICAL_LAYOUT_ICON if horizontal_layout else HORIZONTAL_LAYOUT_ICON
layout_button.tooltip_text = "Switch to Vertical layout" if horizontal_layout else "Switch to Horizontal layout"
+192
View File
@@ -0,0 +1,192 @@
@tool
extends GraphNode
signal blackboard_updated
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
const PORT_TOP_ICON := preload("icons/port_top.svg")
const PORT_BOTTOM_ICON := preload("icons/port_bottom.svg")
const PORT_LEFT_ICON := preload("icons/port_left.svg")
const PORT_RIGHT_ICON := preload("icons/port_right.svg")
@export var title_text: String:
set(value):
title_text = value
if title_label:
title_label.text = value
@export var text: String:
set(value):
text = value
if label:
label.text = " " if text.is_empty() else text
@export var icon: Texture2D:
set(value):
icon = value
if icon_rect:
icon_rect.texture = value
@export var blackboard: Dictionary:
set(value):
blackboard = value
blackboard_updated.emit()
var layout_size: float:
get:
return size.y if horizontal else size.x
var icon_rect: TextureRect
var title_label: Label
var label: Label
var titlebar_hbox: HBoxContainer
var frames: RefCounted
var horizontal: bool = false
var panels_tween: Tween
func _init(frames:RefCounted, horizontal: bool = false) -> void:
self.frames = frames
self.horizontal = horizontal
func _ready() -> void:
custom_minimum_size = Vector2(50, 50) * BeehaveUtils.get_editor_scale()
draggable = false
add_theme_color_override("close_color", Color.TRANSPARENT)
add_theme_icon_override("close", ImageTexture.new())
# For top port
var top_port: Control = Control.new()
add_child(top_port)
icon_rect = TextureRect.new()
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
titlebar_hbox = get_titlebar_hbox()
titlebar_hbox.get_child(0).queue_free()
titlebar_hbox.alignment = BoxContainer.ALIGNMENT_BEGIN
titlebar_hbox.add_child(icon_rect)
title_label = Label.new()
title_label.add_theme_color_override("font_color", Color.WHITE)
var title_font: Font = get_theme_font("title_font").duplicate()
if title_font is FontVariation:
title_font.variation_embolden = 1
elif title_font is FontFile:
title_font.font_weight = 700
title_label.add_theme_font_override("font", title_font)
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title_label.text = title_text
titlebar_hbox.add_child(title_label)
label = Label.new()
label.text = " " if text.is_empty() else text
add_child(label)
# For bottom port
add_child(Control.new())
minimum_size_changed.connect(_on_size_changed)
_on_size_changed.call_deferred()
func _draw_port(slot_index: int, port_position: Vector2i, left: bool, color: Color) -> void:
if horizontal:
if is_slot_enabled_left(1):
draw_texture(PORT_LEFT_ICON, Vector2(0, size.y / 2) + Vector2(-4, -5), color)
if is_slot_enabled_right(1):
draw_texture(PORT_RIGHT_ICON, Vector2(size.x, size.y / 2) + Vector2(-5, -4.5), color)
else:
if slot_index == 0 and is_slot_enabled_left(0):
draw_texture(PORT_TOP_ICON, Vector2(size.x / 2, 0) + Vector2(-4.5, -7), color)
elif slot_index == 1:
draw_texture(PORT_BOTTOM_ICON, Vector2(size.x / 2, size.y) + Vector2(-4.5, -5), color)
func get_custom_input_port_position(horizontal: bool) -> Vector2:
if horizontal:
return Vector2(0, size.y / 2)
else:
return Vector2(size.x/2, 0)
func get_custom_output_port_position(horizontal: bool) -> Vector2:
if horizontal:
return Vector2(size.x, size.y / 2)
else:
return Vector2(size.x / 2, size.y)
func set_status(status: int) -> void:
match status:
BeehaveNode.SUCCESS: _set_stylebox_overrides(frames.panel_success, frames.titlebar_success)
BeehaveNode.FAILURE: _set_stylebox_overrides(frames.panel_failure, frames.titlebar_failure)
BeehaveNode.RUNNING: _set_stylebox_overrides(frames.panel_running, frames.titlebar_running)
_: _set_stylebox_overrides(frames.panel_normal, frames.titlebar_normal)
func set_slots(left_enabled: bool, right_enabled: bool) -> void:
if horizontal:
set_slot(1, left_enabled, -1, Color.WHITE, right_enabled, -1, Color.WHITE, PORT_LEFT_ICON, PORT_RIGHT_ICON)
else:
set_slot(0, left_enabled, -1, Color.WHITE, false, -1, Color.TRANSPARENT, PORT_TOP_ICON, null)
set_slot(2, false, -1, Color.TRANSPARENT, right_enabled, -1, Color.WHITE, null, PORT_BOTTOM_ICON)
func set_color(color: Color) -> void:
set_input_color(color)
set_output_color(color)
func set_input_color(color: Color) -> void:
set_slot_color_left(1 if horizontal else 0, color)
func set_output_color(color: Color) -> void:
set_slot_color_right(1 if horizontal else 2, color)
func _set_stylebox_overrides(panel_stylebox: StyleBox, titlebar_stylebox: StyleBox) -> void:
# First update and any status change gets immediate panel update
if not has_theme_stylebox_override("panel") or panel_stylebox != frames.panel_normal:
if panels_tween:
panels_tween.kill()
panels_tween = null
add_theme_stylebox_override("panel", panel_stylebox)
add_theme_stylebox_override("titlebar", titlebar_stylebox)
return
# Don't need to do anything if we're already tweening back to normal
if panels_tween:
return
# Don't need to do anything if our colors are already the same as a normal
var cur_panel_stylebox: StyleBox = get_theme_stylebox("panel")
var cur_titlebar_stylebox: StyleBox = get_theme_stylebox("titlebar")
if cur_panel_stylebox.bg_color == frames.panel_normal.bg_color:
return
# Apply a duplicate of our current panels that we can tween
add_theme_stylebox_override("panel", cur_panel_stylebox.duplicate())
add_theme_stylebox_override("titlebar", cur_titlebar_stylebox.duplicate())
cur_panel_stylebox = get_theme_stylebox("panel")
cur_titlebar_stylebox = get_theme_stylebox("titlebar")
# Going back to normal is a fade
panels_tween = create_tween()
panels_tween.parallel().tween_property(cur_panel_stylebox, "bg_color", panel_stylebox.bg_color, 1.0)
panels_tween.parallel().tween_property(cur_panel_stylebox, "border_color", panel_stylebox.border_color, 1.0)
panels_tween.parallel().tween_property(cur_titlebar_stylebox, "bg_color", panel_stylebox.bg_color, 1.0)
panels_tween.parallel().tween_property(cur_titlebar_stylebox, "border_color", panel_stylebox.border_color, 1.0)
func _on_size_changed():
add_theme_constant_override("port_offset", 12 * BeehaveUtils.get_editor_scale() if horizontal else round(size.x))
@@ -0,0 +1,76 @@
extends VBoxContainer
var frames: RefCounted
var graph_node: GraphNode
var item_tree: Tree
func _init(frames: RefCounted, node: GraphNode) -> void:
self.frames = frames
graph_node = node
graph_node.blackboard_updated.connect(_update_list)
func _ready() -> void:
name = graph_node.name
set_anchors_preset(Control.PRESET_FULL_RECT)
var title_panel: Panel = Panel.new()
title_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
title_panel.custom_minimum_size = Vector2(200, 50)
add_child(title_panel)
var title_hbox: HBoxContainer = HBoxContainer.new()
title_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
title_hbox.set_anchors_preset(Control.PRESET_FULL_RECT)
title_panel.add_child(title_hbox)
var icon_rect: TextureRect = TextureRect.new()
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_rect.texture = graph_node.icon
icon_rect.set_size(Vector2(20, 20))
title_hbox.add_child(icon_rect)
var title: Label = Label.new()
title.text = graph_node.title_text
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.set_anchors_preset(Control.PRESET_FULL_RECT)
title_hbox.add_child(title)
item_tree = Tree.new()
item_tree.custom_minimum_size = Vector2(200, 400)
item_tree.hide_root = true
item_tree.allow_search = false
item_tree.columns = 2
add_child(item_tree)
_update_list()
func _update_list() -> void:
item_tree.clear()
var root: TreeItem = item_tree.create_item()
if graph_node.blackboard.size() == 0:
var no_bb_message: TreeItem = item_tree.create_item(root)
no_bb_message.set_text(0, "No blackboard data")
return
for bb_name in graph_node.blackboard:
var bb_name_branch: TreeItem = item_tree.create_item(root)
bb_name_branch.set_text(0, bb_name)
#print(graph_node.blackboard.get(bb_name))
for key in graph_node.blackboard.get(bb_name):
var bb_kv_leaf: TreeItem = item_tree.create_item(bb_name_branch)
bb_kv_leaf.set_text(0, str(key))
bb_kv_leaf.set_text(1, str(graph_node.blackboard.get(bb_name).get(key)))
func _get_icon(type: StringName) -> Texture2D:
var classes := ProjectSettings.get_global_class_list()
for c in classes:
if c["class"] == type:
var icon_path := c.get("icon", String())
if not icon_path.is_empty():
return load(icon_path)
return null
+47
View File
@@ -0,0 +1,47 @@
@tool
extends RefCounted
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
const SUCCESS_COLOR := Color("#009944c8")
const NORMAL_COLOR := Color("#15181e")
const FAILURE_COLOR := Color("#cf000f80")
const RUNNING_COLOR := Color("#ffcc00c8")
var empty: StyleBoxEmpty
var normal: StyleBoxFlat
var success: StyleBoxFlat
var failure: StyleBoxFlat
var running: StyleBoxFlat
func _init() -> void:
var plugin := BeehaveUtils.get_plugin()
if not plugin:
return
var editor_scale := BeehaveUtils.get_editor_scale()
empty = StyleBoxEmpty.new()
normal = (
plugin
. get_editor_interface()
. get_base_control()
. get_theme_stylebox(&"frame", &"GraphNode")
. duplicate()
)
success = (
plugin
. get_editor_interface()
. get_base_control()
. get_theme_stylebox(&"selected_frame", &"GraphNode")
. duplicate()
)
failure = success.duplicate()
running = success.duplicate()
success.border_color = SUCCESS_COLOR
failure.border_color = FAILURE_COLOR
running.border_color = RUNNING_COLOR
+286
View File
@@ -0,0 +1,286 @@
@tool
extends GraphEdit
const BeehaveGraphNode := preload("old_graph_node.gd")
const HORIZONTAL_LAYOUT_ICON := preload("icons/horizontal_layout.svg")
const VERTICAL_LAYOUT_ICON := preload("icons/vertical_layout.svg")
const PROGRESS_SHIFT: int = 50
const INACTIVE_COLOR: Color = Color("#898989aa")
const ACTIVE_COLOR: Color = Color("#ffcc00c8")
const SUCCESS_COLOR: Color = Color("#009944c8")
var updating_graph: bool = false
var arraging_nodes: bool = false
var beehave_tree: Dictionary:
set(value):
if beehave_tree == value:
return
beehave_tree = value
active_nodes.clear()
_update_graph()
var horizontal_layout: bool = false:
set(value):
if updating_graph or arraging_nodes:
return
if horizontal_layout == value:
return
horizontal_layout = value
_update_layout_button()
_update_graph()
var frames: RefCounted
var active_nodes: Array[String]
var progress: int = 0
var layout_button: Button
func _init(frames: RefCounted) -> void:
self.frames = frames
func _ready() -> void:
custom_minimum_size = Vector2(100, 300)
set("arrange_nodes_button_hidden", true)
minimap_enabled = false
layout_button = Button.new()
layout_button.flat = true
layout_button.focus_mode = Control.FOCUS_NONE
layout_button.pressed.connect(func(): horizontal_layout = not horizontal_layout)
get_menu_container().add_child(layout_button)
_update_layout_button()
func _update_graph() -> void:
if updating_graph:
return
updating_graph = true
clear_connections()
for child in _get_child_nodes():
remove_child(child)
child.queue_free()
if not beehave_tree.is_empty():
_add_nodes(beehave_tree)
_connect_nodes(beehave_tree)
_arrange_nodes.call_deferred(beehave_tree)
updating_graph = false
func _add_nodes(node: Dictionary) -> void:
if node.is_empty():
return
var gnode := BeehaveGraphNode.new(frames, horizontal_layout)
add_child(gnode)
gnode.title_text = node.name
gnode.name = node.id
gnode.icon = _get_icon(node.type.back())
if node.type.has(&"BeehaveTree"):
gnode.set_slots(false, true)
elif node.type.has(&"Leaf"):
gnode.set_slots(true, false)
elif node.type.has(&"Composite") or node.type.has(&"Decorator"):
gnode.set_slots(true, true)
for child in node.get("children", []):
_add_nodes(child)
func _connect_nodes(node: Dictionary) -> void:
for child in node.get("children", []):
connect_node(node.id, 0, child.id, 0)
_connect_nodes(child)
func _arrange_nodes(node: Dictionary) -> void:
if arraging_nodes:
return
arraging_nodes = true
var tree_node := _create_tree_nodes(node)
tree_node.update_positions(horizontal_layout)
_place_nodes(tree_node)
arraging_nodes = false
func _create_tree_nodes(node: Dictionary, root: TreeNode = null) -> TreeNode:
var tree_node := TreeNode.new(get_node(node.id), root)
for child in node.get("children", []):
var child_node := _create_tree_nodes(child, tree_node)
tree_node.children.push_back(child_node)
return tree_node
func _place_nodes(node: TreeNode) -> void:
node.item.position_offset = Vector2(node.x, node.y)
for child in node.children:
_place_nodes(child)
func _get_icon(type: StringName) -> Texture2D:
var classes := ProjectSettings.get_global_class_list()
for c in classes:
if c["class"] == type:
var icon_path := c.get("icon", String())
if not icon_path.is_empty():
return load(icon_path)
return null
func get_menu_container() -> Control:
return call("get_zoom_hbox")
func get_status(status: int) -> String:
if status == 0:
return "SUCCESS"
elif status == 1:
return "FAILURE"
return "RUNNING"
func process_begin(instance_id: int) -> void:
if not _is_same_tree(instance_id):
return
for child in _get_child_nodes():
child.set_meta("status", -1)
func process_tick(instance_id: int, status: int) -> void:
var node := get_node_or_null(str(instance_id))
if node:
node.text = "Status: %s" % get_status(status)
node.set_status(status)
node.set_meta("status", status)
if status == BeehaveNode.SUCCESS or status == BeehaveNode.RUNNING:
if not active_nodes.has(node.name):
active_nodes.push_back(node.name)
func process_end(instance_id: int) -> void:
if not _is_same_tree(instance_id):
return
for child in _get_child_nodes():
var status := child.get_meta("status", -1)
match status:
BeehaveNode.SUCCESS:
active_nodes.erase(child.name)
child.set_color(SUCCESS_COLOR)
BeehaveNode.FAILURE:
active_nodes.erase(child.name)
child.set_color(INACTIVE_COLOR)
BeehaveNode.RUNNING:
child.set_color(ACTIVE_COLOR)
_:
child.text = " "
child.set_status(status)
child.set_color(INACTIVE_COLOR)
func _is_same_tree(instance_id: int) -> bool:
return str(instance_id) == beehave_tree.get("id", "")
func _get_child_nodes() -> Array[Node]:
return get_children().filter(func(child): return child is BeehaveGraphNode)
func _get_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array:
var points: PackedVector2Array
from_position = from_position.round()
to_position = to_position.round()
points.push_back(from_position)
var mid_position := ((to_position + from_position) / 2).round()
if horizontal_layout:
points.push_back(Vector2(mid_position.x, from_position.y))
points.push_back(Vector2(mid_position.x, to_position.y))
else:
points.push_back(Vector2(from_position.x, mid_position.y))
points.push_back(Vector2(to_position.x, mid_position.y))
points.push_back(to_position)
return points
func _process(delta: float) -> void:
if not active_nodes.is_empty():
progress += 10 if delta >= 0.05 else 1
if progress >= 1000:
progress = 0
queue_redraw()
func _draw() -> void:
if active_nodes.is_empty():
return
var circle_size: float = max(3, 6 * zoom)
var progress_shift: float = PROGRESS_SHIFT * zoom
var connections := get_connection_list()
for c in connections:
var from_node: StringName
var to_node: StringName
from_node = c.from
to_node = c.to
if not from_node in active_nodes or not c.to_node in active_nodes:
continue
var from := get_node(String(from_node))
var to := get_node(String(to_node))
if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0:
return
var output_port_position: Vector2
var input_port_position: Vector2
output_port_position = (
from.position + from.call("get_connection_output_position", c.from_port)
)
input_port_position = to.position + to.call("get_connection_input_position", c.to_port)
var line := _get_connection_line(output_port_position, input_port_position)
var curve = Curve2D.new()
for l in line:
curve.add_point(l)
var max_steps := int(curve.get_baked_length())
var current_shift := progress % max_steps
var p := curve.sample_baked(current_shift)
draw_circle(p, circle_size, ACTIVE_COLOR)
var shift := current_shift - progress_shift
while shift >= 0:
draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR)
shift -= progress_shift
shift = current_shift + progress_shift
while shift <= curve.get_baked_length():
draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR)
shift += progress_shift
func _update_layout_button() -> void:
layout_button.icon = VERTICAL_LAYOUT_ICON if horizontal_layout else HORIZONTAL_LAYOUT_ICON
layout_button.tooltip_text = (
"Switch to Vertical layout" if horizontal_layout else "Switch to Horizontal layout"
)
+166
View File
@@ -0,0 +1,166 @@
@tool
extends GraphNode
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
const DEFAULT_COLOR := Color("#dad4cb")
const PORT_TOP_ICON := preload("icons/port_top.svg")
const PORT_BOTTOM_ICON := preload("icons/port_bottom.svg")
const PORT_LEFT_ICON := preload("icons/port_left.svg")
const PORT_RIGHT_ICON := preload("icons/port_right.svg")
@export var title_text: String:
set(value):
title_text = value
if title_label:
title_label.text = value
@export var text: String:
set(value):
text = value
if label:
label.text = " " if text.is_empty() else text
@export var icon: Texture2D:
set(value):
icon = value
if icon_rect:
icon_rect.texture = value
var layout_size: float:
get:
return size.y if horizontal else size.x
var panel: PanelContainer
var icon_rect: TextureRect
var title_label: Label
var container: VBoxContainer
var label: Label
var frames: RefCounted
var horizontal: bool = false
func _init(frames: RefCounted, horizontal: bool = false) -> void:
self.frames = frames
self.horizontal = horizontal
func _ready() -> void:
custom_minimum_size = Vector2(50, 50) * BeehaveUtils.get_editor_scale()
draggable = false
add_theme_stylebox_override("frame", frames.empty if frames != null else null)
add_theme_stylebox_override("selected_frame", frames.empty if frames != null else null)
add_theme_color_override("close_color", Color.TRANSPARENT)
add_theme_icon_override("close", ImageTexture.new())
# For top port
add_child(Control.new())
panel = PanelContainer.new()
panel.mouse_filter = Control.MOUSE_FILTER_PASS
panel.add_theme_stylebox_override("panel", frames.normal if frames != null else null)
add_child(panel)
var vbox_container := VBoxContainer.new()
panel.add_child(vbox_container)
var title_size := 24 * BeehaveUtils.get_editor_scale()
var margin_container := MarginContainer.new()
margin_container.add_theme_constant_override(
"margin_top", -title_size - 2 * BeehaveUtils.get_editor_scale()
)
margin_container.mouse_filter = Control.MOUSE_FILTER_PASS
vbox_container.add_child(margin_container)
var title_container := HBoxContainer.new()
title_container.add_child(Control.new())
title_container.mouse_filter = Control.MOUSE_FILTER_PASS
title_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
margin_container.add_child(title_container)
icon_rect = TextureRect.new()
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
title_container.add_child(icon_rect)
title_label = Label.new()
title_label.add_theme_color_override("font_color", DEFAULT_COLOR)
title_label.add_theme_font_override("font", get_theme_font("title_font"))
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title_label.text = title_text
title_container.add_child(title_label)
title_container.add_child(Control.new())
container = VBoxContainer.new()
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_child(container)
label = Label.new()
label.text = " " if text.is_empty() else text
container.add_child(label)
# For bottom port
add_child(Control.new())
minimum_size_changed.connect(_on_size_changed)
_on_size_changed.call_deferred()
func set_status(status: int) -> void:
panel.add_theme_stylebox_override("panel", _get_stylebox(status))
func _get_stylebox(status: int) -> StyleBox:
match status:
BeehaveNode.SUCCESS:
return frames.success
BeehaveNode.FAILURE:
return frames.failure
BeehaveNode.RUNNING:
return frames.running
_:
return frames.normal
func set_slots(left_enabled: bool, right_enabled: bool) -> void:
if horizontal:
set_slot(
1,
left_enabled,
0,
Color.WHITE,
right_enabled,
0,
Color.WHITE,
PORT_LEFT_ICON,
PORT_RIGHT_ICON
)
else:
set_slot(0, left_enabled, 0, Color.WHITE, false, -2, Color.TRANSPARENT, PORT_TOP_ICON, null)
set_slot(
2, false, -1, Color.TRANSPARENT, right_enabled, 0, Color.WHITE, null, PORT_BOTTOM_ICON
)
func set_color(color: Color) -> void:
set_input_color(color)
set_output_color(color)
func set_input_color(color: Color) -> void:
set_slot_color_left(1 if horizontal else 0, color)
func set_output_color(color: Color) -> void:
set_slot_color_right(1 if horizontal else 2, color)
func _on_size_changed():
add_theme_constant_override(
"port_offset", 12 * BeehaveUtils.get_editor_scale() if horizontal else round(size.x / 2.0)
)
+275
View File
@@ -0,0 +1,275 @@
class_name TreeNode
extends RefCounted
# Based on https://rachel53461.wordpress.com/2014/04/20/algorithm-for-drawing-trees/
const SIBLING_DISTANCE: float = 20.0
const LEVEL_DISTANCE: float = 40.0
const BeehaveUtils := preload("res://addons/beehave/utils/utils.gd")
var x: float
var y: float
var mod: float
var parent: TreeNode
var children: Array[TreeNode]
var item: GraphNode
func _init(p_item: GraphNode = null, p_parent: TreeNode = null) -> void:
parent = p_parent
item = p_item
func is_leaf() -> bool:
return children.is_empty()
func is_most_left() -> bool:
if not parent:
return true
return parent.children.front() == self
func is_most_right() -> bool:
if not parent:
return true
return parent.children.back() == self
func get_previous_sibling() -> TreeNode:
if not parent or is_most_left():
return null
return parent.children[parent.children.find(self) - 1]
func get_next_sibling() -> TreeNode:
if not parent or is_most_right():
return null
return parent.children[parent.children.find(self) + 1]
func get_most_left_sibling() -> TreeNode:
if not parent:
return null
if is_most_left():
return self
return parent.children.front()
func get_most_left_child() -> TreeNode:
if children.is_empty():
return null
return children.front()
func get_most_right_child() -> TreeNode:
if children.is_empty():
return null
return children.back()
func update_positions(horizontally: bool = false) -> void:
_initialize_nodes(self, 0)
_calculate_initial_x(self)
_check_all_children_on_screen(self)
_calculate_final_positions(self, 0)
if horizontally:
_swap_x_y(self)
_calculate_x(self, 0)
else:
_calculate_y(self, 0)
func _initialize_nodes(node: TreeNode, depth: int) -> void:
node.x = -1
node.y = depth
node.mod = 0
for child in node.children:
_initialize_nodes(child, depth + 1)
func _calculate_initial_x(node: TreeNode) -> void:
for child in node.children:
_calculate_initial_x(child)
if node.is_leaf():
if not node.is_most_left():
node.x = (
node.get_previous_sibling().x
+ node.get_previous_sibling().item.layout_size
+ SIBLING_DISTANCE
)
else:
node.x = 0
else:
var mid: float
if node.children.size() == 1:
var offset: float = (node.children.front().item.layout_size - node.item.layout_size) / 2
mid = node.children.front().x + offset
else:
var left_child := node.get_most_left_child()
var right_child := node.get_most_right_child()
mid = (
(
left_child.x
+ right_child.x
+ right_child.item.layout_size
- node.item.layout_size
)
/ 2
)
if node.is_most_left():
node.x = mid
else:
node.x = (
node.get_previous_sibling().x
+ node.get_previous_sibling().item.layout_size
+ SIBLING_DISTANCE
)
node.mod = node.x - mid
if not node.is_leaf() and not node.is_most_left():
_check_for_conflicts(node)
func _calculate_final_positions(node: TreeNode, mod_sum: float) -> void:
node.x += mod_sum
mod_sum += node.mod
for child in node.children:
_calculate_final_positions(child, mod_sum)
func _check_all_children_on_screen(node: TreeNode) -> void:
var node_contour: Dictionary = {}
_get_left_contour(node, 0, node_contour)
var shift_amount: float = 0
for y in node_contour.keys():
if node_contour[y] + shift_amount < 0:
shift_amount = (node_contour[y] * -1)
if shift_amount > 0:
node.x += shift_amount
node.mod += shift_amount
func _check_for_conflicts(node: TreeNode) -> void:
var min_distance := SIBLING_DISTANCE
var shift_value: float = 0
var shift_sibling: TreeNode = null
var node_contour: Dictionary = {} # { int, float }
_get_left_contour(node, 0, node_contour)
var sibling := node.get_most_left_sibling()
while sibling != null and sibling != node:
var sibling_contour: Dictionary = {}
_get_right_contour(sibling, 0, sibling_contour)
for level in range(
node.y + 1, min(sibling_contour.keys().max(), node_contour.keys().max()) + 1
):
var distance: float = node_contour[level] - sibling_contour[level]
if distance + shift_value < min_distance:
shift_value = min_distance - distance
shift_sibling = sibling
sibling = sibling.get_next_sibling()
if shift_value > 0:
node.x += shift_value
node.mod += shift_value
_center_nodes_between(shift_sibling, node)
func _center_nodes_between(left_node: TreeNode, right_node: TreeNode) -> void:
var left_index := left_node.parent.children.find(left_node)
var right_index := left_node.parent.children.find(right_node)
var num_nodes_between: int = (right_index - left_index) - 1
if num_nodes_between > 0:
# The extra distance that needs to be split into num_nodes_between + 1
# in order to find the new node spacing so that nodes are equally spaced
var distance_to_allocate: float = right_node.x - left_node.x - left_node.item.layout_size
# Subtract sizes on nodes in between
for i in range(left_index + 1, right_index):
distance_to_allocate -= left_node.parent.children[i].item.layout_size
# Divide space equally
var distance_between_nodes: float = distance_to_allocate / (num_nodes_between + 1)
var prev_node := left_node
var middle_node := left_node.get_next_sibling()
while middle_node != right_node:
var desire_x: float = prev_node.x + prev_node.item.layout_size + distance_between_nodes
var offset := desire_x - middle_node.x
middle_node.x += offset
middle_node.mod += offset
prev_node = middle_node
middle_node = middle_node.get_next_sibling()
func _get_left_contour(node: TreeNode, mod_sum: float, values: Dictionary) -> void:
var node_left: float = node.x + mod_sum
var depth := int(node.y)
if not values.has(depth):
values[depth] = node_left
else:
values[depth] = min(values[depth], node_left)
mod_sum += node.mod
for child in node.children:
_get_left_contour(child, mod_sum, values)
func _get_right_contour(node: TreeNode, mod_sum: float, values: Dictionary) -> void:
var node_right: float = node.x + mod_sum + node.item.layout_size
var depth := int(node.y)
if not values.has(depth):
values[depth] = node_right
else:
values[depth] = max(values[depth], node_right)
mod_sum += node.mod
for child in node.children:
_get_right_contour(child, mod_sum, values)
func _swap_x_y(node: TreeNode) -> void:
for child in node.children:
_swap_x_y(child)
var temp := node.x
node.x = node.y
node.y = temp
func _calculate_x(node: TreeNode, offset: int) -> void:
node.x = offset
var sibling := node.get_most_left_sibling()
var max_size: int = node.item.size.x
while sibling != null:
max_size = max(sibling.item.size.x, max_size)
sibling = sibling.get_next_sibling()
for child in node.children:
_calculate_x(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale())
func _calculate_y(node: TreeNode, offset: int) -> void:
node.y = offset
var sibling := node.get_most_left_sibling()
var max_size: int = node.item.size.y
while sibling != null:
max_size = max(sibling.item.size.y, max_size)
sibling = sibling.get_next_sibling()
for child in node.children:
_calculate_y(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale())