Skip to content

Instantly share code, notes, and snippets.

@YuriSizov
Last active June 7, 2021 09:13
Show Gist options
  • Save YuriSizov/7a88b55697138b646f69da0de40f71ac to your computer and use it in GitHub Desktop.
Save YuriSizov/7a88b55697138b646f69da0de40f71ac to your computer and use it in GitHub Desktop.
GraphEdit Minimap GDScript implementation

Create a Panel control inside your GraphEdit. Attach GraphMinimap.gd script to it. Make it hidden by default (you can adjust that later). Set rect_min_size to desired dimensions, I use it with 240x160.

Don't forget to set a toolbar_icon in the Inspector, so that the button is visible on the GraphEdit toolbar.

If you add nodes dynamically, don't forget to graph_minimap.raise() your minimap each time, so it stays on top of everything.

To colorize nodes on the minimap, define a minimap_color property on them. You can also modify code to deduce color somehow.

Supports displaying regular and comment nodes, connections, camera position. Can be clicked and dragged on to move the camera around.

https://i.imgur.com/ts1vktL.png

tool
extends Panel
# Node references
var graph_edit : GraphEdit
var graph_hscroll : HScrollBar
var graph_vscroll : VScrollBar
var minimap_button : Button
# Public properties
export var toolbar_icon : Texture
# Private properties
var _graph_nodes : Array = [] # of Dictionary
var _graph_lines : Array = [] # of Dictionary
var _camera_position : Vector2 = Vector2(100, 50)
var _camera_size : Vector2 = Vector2(200, 200)
var _graph_proportions : Vector2 = Vector2(1, 1)
var _zoom_level : float = 1.0
var _map_padding : Vector2 = Vector2(5, 5)
var _graph_padding : Vector2 = Vector2(0, 0)
var _is_pressing : bool = false
var _icon_modulate_color : Color = Color.white.linear_interpolate(Color.black, 0.25)
var _icon_modulate_active_color : Color = Color.white
func _enter_tree() -> void:
_update_node_references()
func _ready() -> void:
_update_theme()
_update_map()
if (graph_edit):
minimap_button = ToolButton.new()
minimap_button.icon = toolbar_icon
minimap_button.modulate = _icon_modulate_color
minimap_button.hint_tooltip = "Toggle graph minimap."
graph_edit.get_zoom_hbox().add_child(minimap_button)
minimap_button.connect("pressed", self, "_on_minimap_button_pressed")
graph_edit.connect("draw", self, "_on_graph_edit_changed")
graph_edit.connect("update_minimap", self, "_on_graph_edit_changed")
func _gui_input(event : InputEvent) -> void:
if (event is InputEventMouseButton && event.button_index == BUTTON_LEFT):
if (event.is_pressed()):
_is_pressing = true
var mouse_event = (event as InputEventMouseButton)
var click_location = _convert_to_graph_position(mouse_event.position - _map_padding) - _graph_padding
if (graph_edit):
var scroll_offset = get_scroll_offset()
graph_edit.scroll_offset = click_location + scroll_offset - _camera_size / 2
elif (_is_pressing):
_is_pressing = false
accept_event()
elif (event is InputEventMouseMotion && _is_pressing):
var mouse_event = (event as InputEventMouseMotion)
var click_location = _convert_to_graph_position(mouse_event.position - _map_padding) - _graph_padding
if (graph_edit):
var scroll_offset = get_scroll_offset()
graph_edit.scroll_offset = click_location + scroll_offset - _camera_size / 2
accept_event()
func _draw() -> void:
var centering_offset = _convert_from_graph_position(_graph_padding)
var map_offset = _map_padding + centering_offset
for node in _graph_nodes:
var position = _convert_from_graph_position(node.position) + map_offset
var size = _convert_from_graph_position(node.size)
if (node.is_comment):
var node_shape = Rect2(position - size / 2, size)
var comment_background = node.node_color.linear_interpolate(Color.black, 0.65)
draw_rect(node_shape, comment_background, true)
draw_rect(node_shape, node.node_color, false)
else:
draw_circle(position, 3, node.node_color)
for line in _graph_lines:
var from_position = _convert_from_graph_position(line.from_position) + map_offset
var to_position = _convert_from_graph_position(line.to_position) + map_offset
var bezier_len_pos = get_constant("bezier_len_pos", "GraphEdit") / 2
var bezier_len_neg = get_constant("bezier_len_neg", "GraphEdit") / 2
DrawUtils.draw_cos_line(self, from_position, to_position, line.from_color, line.to_color, bezier_len_pos, bezier_len_neg)
var camera_center = _convert_from_graph_position(_camera_position + _camera_size / 2) + map_offset
var camera_viewport = _convert_from_graph_position(_camera_size)
var camera_position = (camera_center - camera_viewport / 2)
draw_rect(Rect2(camera_position, camera_viewport), Color(0.65, 0.65, 0.65, 0.2), true)
draw_rect(Rect2(camera_position, camera_viewport), Color(0.65, 0.65, 0.65, 0.45), false)
func _update_theme() -> void:
if (!Engine.editor_hint || !is_inside_tree()):
return
modulate.a = 0.85
_icon_modulate_color = get_color("font_color", "Button")
_icon_modulate_active_color = get_color("accent_color", "Editor")
func _update_map() -> void:
if (!graph_edit):
return
# Update graph spatial information
var scroll_offset = get_scroll_offset()
var graph_size = get_graph_size()
_zoom_level = graph_edit.zoom
_camera_position = graph_edit.scroll_offset - scroll_offset
_camera_size = graph_edit.rect_size
var render_size = get_render_size()
var target_ratio = render_size.x / render_size.y
var graph_ratio = graph_size.x / graph_size.y
_graph_proportions = graph_size
_graph_padding = Vector2(0, 0)
if (graph_ratio > target_ratio):
_graph_proportions.x = graph_size.x
_graph_proportions.y = graph_size.x / target_ratio
_graph_padding.y = abs(graph_size.y - _graph_proportions.y) / 2
else:
_graph_proportions.x = graph_size.y * target_ratio
_graph_proportions.y = graph_size.y
_graph_padding.x = abs(graph_size.x - _graph_proportions.x) / 2
# Update node information
_graph_nodes = []
_graph_lines = []
for child in graph_edit.get_children():
if !(child is GraphNode):
continue
var node_data := {
"position": (child.offset + child.rect_size / 2) * _zoom_level - scroll_offset,
"size": child.rect_size * _zoom_level,
"node_color": Color.white,
"is_comment": child.comment,
}
var child_color = child.get("minimap_color")
if (child_color):
node_data.node_color = child_color
_graph_nodes.append(node_data)
for connection in graph_edit.get_connection_list():
var from_child = graph_edit.get_node_or_null(connection.from)
var to_child = graph_edit.get_node_or_null(connection.to)
if (!from_child || !to_child):
continue # We got caught in between two resources switching, some data is outdated
var from_slot_position = from_child.rect_size if from_child.comment else from_child.rect_size / 2
var to_slot_position = 0 if from_child.comment else to_child.rect_size / 2
var line_data := {
"from_position": (from_child.offset + from_slot_position) * _zoom_level - scroll_offset,
"to_position": (to_child.offset + to_slot_position) * _zoom_level - scroll_offset,
"from_color": Color.white,
"to_color": Color.white,
}
var from_child_color = from_child.get("minimap_color")
if (from_child_color):
line_data.from_color = from_child_color
var to_child_color = to_child.get("minimap_color")
if (to_child_color):
line_data.to_color = to_child_color
_graph_lines.append(line_data)
update()
func _update_node_references() -> void:
var parent_node = get_parent()
if !(parent_node is GraphEdit):
return
graph_edit = (parent_node as GraphEdit)
var top_layer = NodeUtils.get_child_by_class(graph_edit, "GraphEditFilter")
graph_hscroll = NodeUtils.get_child_by_class(top_layer, "HScrollBar")
graph_vscroll = NodeUtils.get_child_by_class(top_layer, "VScrollBar")
### Helpers
func _convert_from_graph_position(graph_position : Vector2) -> Vector2:
var position = Vector2(0, 0)
var render_size = get_render_size()
position.x = graph_position.x * render_size.x / _graph_proportions.x
position.y = graph_position.y * render_size.y / _graph_proportions.y
return position
func _convert_to_graph_position(position : Vector2) -> Vector2:
var graph_position = Vector2(0, 0)
var render_size = get_render_size()
graph_position.x = position.x * _graph_proportions.x / render_size.x
graph_position.y = position.y * _graph_proportions.y / render_size.y
return graph_position
func get_render_size() -> Vector2:
if (!is_inside_tree()):
return Vector2.ZERO
return rect_size - 2 * _map_padding
func get_scroll_offset() -> Vector2:
if (!graph_hscroll || !graph_vscroll):
return Vector2(0, 0)
return Vector2(graph_hscroll.min_value, graph_vscroll.min_value)
func get_graph_size() -> Vector2:
if (!graph_hscroll || !graph_vscroll):
return Vector2(1, 1)
var scroll_offset = get_scroll_offset()
var graph_size = Vector2(graph_hscroll.max_value, graph_vscroll.max_value) - scroll_offset
if (graph_size.x == 0):
graph_size.x = 1
if (graph_size.y == 0):
graph_size.y = 1
return graph_size
### Event handlers
func _on_graph_edit_changed() -> void:
_update_map()
func _on_minimap_button_pressed() -> void:
if (visible):
hide()
minimap_button.modulate = _icon_modulate_color
else:
show()
minimap_button.modulate = _icon_modulate_active_color
extends Object
class_name DrawUtils
# Extracted from graph_node.cpp
static func draw_cos_line(node : Node, from_position : Vector2, to_position : Vector2, from_color : Color, to_color : Color, bezier_len_pos : int, bezier_len_neg : int, line_width : float = 1.0, shadow : bool = false) -> void:
var diff = to_position.x - from_position.x
var cp_offset
var cp_len = bezier_len_pos
var cp_neg_len = bezier_len_neg
if (diff > 0):
cp_offset = min(cp_len, diff * 0.5);
else:
cp_offset = max(min(cp_len - diff, cp_neg_len), -diff * 0.5);
var c1 = Vector2(cp_offset, 0)
var c2 = Vector2(-cp_offset, 0)
var points := []
var colors := []
points.push_back(from_position);
colors.push_back(from_color);
_bake_segment2d(points, colors, 0, 1, from_position, c1, to_position, c2, 0, 3, 9, 3, from_color, to_color)
points.push_back(to_position);
colors.push_back(to_color);
if (shadow):
var shadow_points := PoolVector2Array()
var shadow_colors := PoolColorArray()
for point in points:
shadow_points.append(point + Vector2(1, 1))
for color in colors:
shadow_colors.append(color.linear_interpolate(Color.black, 0.76))
node.draw_polyline_colors(shadow_points, shadow_colors, line_width, true)
node.draw_polyline_colors(PoolVector2Array(points), PoolColorArray(colors), line_width, true)
# Extracted from graph_node.cpp
static func _bake_segment2d(points : Array, colors : Array, p_begin : float, p_end : float, p_a : Vector2, p_out : Vector2, p_b : Vector2, p_in : Vector2, p_depth : int, p_min_depth : int, p_max_depth : int, p_tol : float, p_color : Color, p_to_color : Color) -> void:
var mp = p_begin + (p_end - p_begin) * 0.5
var beg = MathUtils.cubic_bezier(p_a, p_a + p_out, p_b + p_in, p_b, p_begin)
var mid = MathUtils.cubic_bezier(p_a, p_a + p_out, p_b + p_in, p_b, mp)
var end = MathUtils.cubic_bezier(p_a, p_a + p_out, p_b + p_in, p_b, p_end)
var na = (mid - beg).normalized()
var nb = (end - mid).normalized()
var dp = rad2deg(acos(na.dot(nb)))
if (p_depth >= p_min_depth && (dp < p_tol || p_depth >= p_max_depth)):
points.push_back((beg + end) * 0.5)
colors.push_back(p_color.linear_interpolate(p_to_color, mp))
else:
_bake_segment2d(points, colors, p_begin, mp, p_a, p_out, p_b, p_in, p_depth + 1, p_min_depth, p_max_depth, p_tol, p_color, p_to_color)
_bake_segment2d(points, colors, mp, p_end, p_a, p_out, p_b, p_in, p_depth + 1, p_min_depth, p_max_depth, p_tol, p_color, p_to_color)
extends Object
class_name NodeUtils
static func get_child_by_class(node : Node, child_class : String, counter : int = 1) -> Node:
var match_counter = 0
var node_children = node.get_children()
for child in node_children:
if (child.get_class() == child_class):
match_counter += 1
if (match_counter == counter):
return child
return null
static func get_child_by_name(node : Node, child_name : String) -> Node:
var node_children = node.get_children()
for child in node_children:
if (child.name == child_name):
return child
return null
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment