generated from mstar/godot-template
Code stuff
This commit is contained in:
parent
cf22890c16
commit
e58093b5a5
153 changed files with 11196 additions and 4 deletions
681
addons/imjp94.yafsm/scenes/flowchart/FlowChart.gd
Normal file
681
addons/imjp94.yafsm/scenes/flowchart/FlowChart.gd
Normal file
|
@ -0,0 +1,681 @@
|
|||
@tool
|
||||
extends Control
|
||||
|
||||
const Utils = preload("res://addons/imjp94.yafsm/scripts/Utils.gd")
|
||||
const CohenSutherland = Utils.CohenSutherland
|
||||
const FlowChartNode = preload("FlowChartNode.gd")
|
||||
const FlowChartNodeScene = preload("FlowChartNode.tscn")
|
||||
const FlowChartLine = preload("FlowChartLine.gd")
|
||||
const FlowChartLineScene = preload("FlowChartLine.tscn")
|
||||
const FlowChartLayer = preload("FlowChartLayer.gd")
|
||||
const FlowChartGrid = preload("FlowChartGrid.gd")
|
||||
const Connection = FlowChartLayer.Connection
|
||||
|
||||
signal connection(from, to, line) # When a connection established
|
||||
signal disconnection(from, to, line) # When a connection broken
|
||||
signal node_selected(node) # When a node selected
|
||||
signal node_deselected(node) # When a node deselected
|
||||
signal dragged(node, distance) # When a node dragged
|
||||
|
||||
# Margin of content from edge of FlowChart
|
||||
@export var scroll_margin: = 50
|
||||
# Offset between two line that interconnecting
|
||||
@export var interconnection_offset: = 10
|
||||
# Snap amount
|
||||
@export var snap: = 20
|
||||
# Zoom amount
|
||||
@export var zoom: = 1.0:
|
||||
set = set_zoom
|
||||
@export var zoom_step: = 0.2
|
||||
@export var max_zoom: = 2.0
|
||||
@export var min_zoom: = 0.5
|
||||
|
||||
var grid = FlowChartGrid.new() # Grid
|
||||
var content = Control.new() # Root node that hold anything drawn in the flowchart
|
||||
var current_layer
|
||||
var h_scroll = HScrollBar.new()
|
||||
var v_scroll = VScrollBar.new()
|
||||
var top_bar = VBoxContainer.new()
|
||||
var gadget = HBoxContainer.new() # Root node of top overlay controls
|
||||
var zoom_minus = Button.new()
|
||||
var zoom_reset = Button.new()
|
||||
var zoom_plus = Button.new()
|
||||
var snap_button = Button.new()
|
||||
var snap_amount = SpinBox.new()
|
||||
|
||||
var is_snapping = true
|
||||
var can_gui_select_node = true
|
||||
var can_gui_delete_node = true
|
||||
var can_gui_connect_node = true
|
||||
|
||||
var _is_connecting = false
|
||||
var _current_connection
|
||||
var _is_dragging = false
|
||||
var _is_dragging_node = false
|
||||
var _drag_start_pos = Vector2.ZERO
|
||||
var _drag_end_pos = Vector2.ZERO
|
||||
var _drag_origins = []
|
||||
var _selection = []
|
||||
var _copying_nodes = []
|
||||
|
||||
var selection_stylebox = StyleBoxFlat.new()
|
||||
var grid_major_color = Color(1, 1, 1, 0.2)
|
||||
var grid_minor_color = Color(1, 1, 1, 0.05)
|
||||
|
||||
|
||||
func _init():
|
||||
|
||||
focus_mode = FOCUS_ALL
|
||||
selection_stylebox.bg_color = Color(0, 0, 0, 0.3)
|
||||
selection_stylebox.set_border_width_all(1)
|
||||
|
||||
self.z_index = 0
|
||||
|
||||
content.mouse_filter = MOUSE_FILTER_IGNORE
|
||||
add_child(content)
|
||||
content.z_index = 1
|
||||
|
||||
grid.mouse_filter = MOUSE_FILTER_IGNORE
|
||||
content.add_child.call_deferred(grid)
|
||||
grid.z_index = -1
|
||||
|
||||
add_child(h_scroll)
|
||||
h_scroll.set_anchors_and_offsets_preset(PRESET_BOTTOM_WIDE)
|
||||
h_scroll.value_changed.connect(_on_h_scroll_changed)
|
||||
h_scroll.gui_input.connect(_on_h_scroll_gui_input)
|
||||
|
||||
add_child(v_scroll)
|
||||
v_scroll.set_anchors_and_offsets_preset(PRESET_RIGHT_WIDE)
|
||||
v_scroll.value_changed.connect(_on_v_scroll_changed)
|
||||
v_scroll.gui_input.connect(_on_v_scroll_gui_input)
|
||||
|
||||
h_scroll.offset_right = -v_scroll.size.x
|
||||
v_scroll.offset_bottom = -h_scroll.size.y
|
||||
|
||||
h_scroll.min_value = 0
|
||||
v_scroll.max_value = 0
|
||||
|
||||
add_layer_to(content)
|
||||
select_layer_at(0)
|
||||
|
||||
top_bar.set_anchors_and_offsets_preset(PRESET_TOP_WIDE)
|
||||
top_bar.mouse_filter = MOUSE_FILTER_IGNORE
|
||||
add_child(top_bar)
|
||||
|
||||
gadget.mouse_filter = MOUSE_FILTER_IGNORE
|
||||
top_bar.add_child(gadget)
|
||||
|
||||
zoom_minus.flat = true
|
||||
zoom_minus.tooltip_text = "Zoom Out"
|
||||
zoom_minus.pressed.connect(_on_zoom_minus_pressed)
|
||||
zoom_minus.focus_mode = FOCUS_NONE
|
||||
gadget.add_child(zoom_minus)
|
||||
|
||||
zoom_reset.flat = true
|
||||
zoom_reset.tooltip_text = "Zoom Reset"
|
||||
zoom_reset.pressed.connect(_on_zoom_reset_pressed)
|
||||
zoom_reset.focus_mode = FOCUS_NONE
|
||||
gadget.add_child(zoom_reset)
|
||||
|
||||
zoom_plus.flat = true
|
||||
zoom_plus.tooltip_text = "Zoom In"
|
||||
zoom_plus.pressed.connect(_on_zoom_plus_pressed)
|
||||
zoom_plus.focus_mode = FOCUS_NONE
|
||||
gadget.add_child(zoom_plus)
|
||||
|
||||
snap_button.flat = true
|
||||
snap_button.toggle_mode = true
|
||||
snap_button.tooltip_text = "Enable snap and show grid"
|
||||
snap_button.pressed.connect(_on_snap_button_pressed)
|
||||
snap_button.button_pressed = true
|
||||
snap_button.focus_mode = FOCUS_NONE
|
||||
gadget.add_child(snap_button)
|
||||
|
||||
snap_amount.value = snap
|
||||
snap_amount.value_changed.connect(_on_snap_amount_value_changed)
|
||||
gadget.add_child(snap_amount)
|
||||
|
||||
func _on_h_scroll_gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
var v = (h_scroll.max_value - h_scroll.min_value) * 0.01 # Scroll at 0.1% step
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
h_scroll.value -= v
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
h_scroll.value += v
|
||||
|
||||
func _on_v_scroll_gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
var v = (v_scroll.max_value - v_scroll.min_value) * 0.01 # Scroll at 0.1% step
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
v_scroll.value -= v # scroll left
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
v_scroll.value += v # scroll right
|
||||
|
||||
func _on_h_scroll_changed(value):
|
||||
content.position.x = -value
|
||||
|
||||
func _on_v_scroll_changed(value):
|
||||
content.position.y = -value
|
||||
|
||||
func set_zoom(v):
|
||||
zoom = clampf(v, min_zoom, max_zoom)
|
||||
content.scale = Vector2.ONE * zoom
|
||||
queue_redraw()
|
||||
grid.queue_redraw()
|
||||
|
||||
func _on_zoom_minus_pressed():
|
||||
set_zoom(zoom - zoom_step)
|
||||
queue_redraw()
|
||||
|
||||
func _on_zoom_reset_pressed():
|
||||
set_zoom(1.0)
|
||||
queue_redraw()
|
||||
|
||||
func _on_zoom_plus_pressed():
|
||||
set_zoom(zoom + zoom_step)
|
||||
queue_redraw()
|
||||
|
||||
func _on_snap_button_pressed():
|
||||
is_snapping = snap_button.button_pressed
|
||||
queue_redraw()
|
||||
|
||||
func _on_snap_amount_value_changed(value):
|
||||
snap = value
|
||||
queue_redraw()
|
||||
|
||||
func _draw():
|
||||
# Update scrolls
|
||||
var content_rect: Rect2 = get_scroll_rect(current_layer, 0)
|
||||
content.pivot_offset = content_rect.size / 2.0 # Scale from center
|
||||
var flowchart_rect: Rect2 = get_rect()
|
||||
# ENCLOSE CONDITIONS
|
||||
var is_content_enclosed = (flowchart_rect.size.x >= content_rect.size.x)
|
||||
is_content_enclosed = is_content_enclosed and (flowchart_rect.size.y >= content_rect.size.y)
|
||||
is_content_enclosed = is_content_enclosed and (flowchart_rect.position.x <= content_rect.position.x)
|
||||
is_content_enclosed = is_content_enclosed and (flowchart_rect.position.y >= content_rect.position.y)
|
||||
if not is_content_enclosed or (h_scroll.min_value==h_scroll.max_value) or (v_scroll.min_value==v_scroll.max_value):
|
||||
var h_min = 0 # content_rect.position.x - scroll_margin/2 - content_rect.get_center().x/2
|
||||
var h_max = content_rect.size.x - content_rect.position.x - size.x + scroll_margin + content_rect.get_center().x
|
||||
var v_min = 0 # content_rect.position.y - scroll_margin/2 - content_rect.get_center().y/2
|
||||
var v_max = content_rect.size.y - content_rect.position.y - size.y + scroll_margin + content_rect.get_center().y
|
||||
if h_min == h_max: # Otherwise scroll bar will complain no ratio
|
||||
h_min -= 0.1
|
||||
h_max += 0.1
|
||||
if v_min == v_max: # Otherwise scroll bar will complain no ratio
|
||||
v_min -= 0.1
|
||||
v_max += 0.1
|
||||
h_scroll.min_value = h_min
|
||||
h_scroll.max_value = h_max
|
||||
h_scroll.page = content_rect.size.x / 100
|
||||
v_scroll.min_value = v_min
|
||||
v_scroll.max_value = v_max
|
||||
v_scroll.page = content_rect.size.y / 100
|
||||
|
||||
# Draw selection box
|
||||
if not _is_dragging_node and not _is_connecting:
|
||||
var selection_box_rect = get_selection_box_rect()
|
||||
draw_style_box(selection_stylebox, selection_box_rect)
|
||||
|
||||
if is_snapping:
|
||||
grid.visible = true
|
||||
grid.queue_redraw()
|
||||
else:
|
||||
grid.visible = false
|
||||
|
||||
# Debug draw
|
||||
# for node in content_nodes.get_children():
|
||||
# var rect = get_transform() * (content.get_transform() * (node.get_rect()))
|
||||
# draw_style_box(selection_stylebox, rect)
|
||||
|
||||
# var connection_list = get_connection_list()
|
||||
# for i in connection_list.size():
|
||||
# var connection = _connections[connection_list[i].from][connection_list[i].to]
|
||||
# # Line's offset along its down-vector
|
||||
# var line_local_up_offset = connection.line.position - connection.line.get_transform() * (Vector2.UP * connection.offset)
|
||||
# var from_pos = content.get_transform() * (connection.get_from_pos() + line_local_up_offset)
|
||||
# var to_pos = content.get_transform() * (connection.get_to_pos() + line_local_up_offset)
|
||||
# draw_line(from_pos, to_pos, Color.yellow)
|
||||
|
||||
func _gui_input(event):
|
||||
|
||||
var OS_KEY_DELETE = KEY_BACKSPACE if ( ["macOS", "OSX"].has(OS.get_name()) ) else KEY_DELETE
|
||||
if event is InputEventKey:
|
||||
match event.keycode:
|
||||
OS_KEY_DELETE:
|
||||
if event.pressed and can_gui_delete_node:
|
||||
# Delete nodes
|
||||
for node in _selection.duplicate():
|
||||
if node is FlowChartLine:
|
||||
# TODO: More efficient way to get connection from Line node
|
||||
for connections_from in current_layer._connections.duplicate().values():
|
||||
for connection in connections_from.duplicate().values():
|
||||
if connection.line == node:
|
||||
disconnect_node(current_layer, connection.from_node.name, connection.to_node.name).queue_free()
|
||||
elif node is FlowChartNode:
|
||||
remove_node(current_layer, node.name)
|
||||
for connection_pair in current_layer.get_connection_list():
|
||||
if connection_pair.from == node.name or connection_pair.to == node.name:
|
||||
disconnect_node(current_layer, connection_pair.from, connection_pair.to).queue_free()
|
||||
accept_event()
|
||||
KEY_C:
|
||||
if event.pressed and event.ctrl_pressed:
|
||||
# Copy node
|
||||
_copying_nodes = _selection.duplicate()
|
||||
accept_event()
|
||||
KEY_D:
|
||||
if event.pressed and event.ctrl_pressed:
|
||||
# Duplicate node directly from selection
|
||||
duplicate_nodes(current_layer, _selection.duplicate())
|
||||
accept_event()
|
||||
KEY_V:
|
||||
if event.pressed and event.ctrl_pressed:
|
||||
# Paste node from _copying_nodes
|
||||
duplicate_nodes(current_layer, _copying_nodes)
|
||||
accept_event()
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
match event.button_mask:
|
||||
MOUSE_BUTTON_MASK_MIDDLE:
|
||||
# Panning
|
||||
h_scroll.value -= event.relative.x
|
||||
v_scroll.value -= event.relative.y
|
||||
queue_redraw()
|
||||
MOUSE_BUTTON_LEFT:
|
||||
# Dragging
|
||||
if _is_dragging:
|
||||
if _is_connecting:
|
||||
# Connecting
|
||||
if _current_connection:
|
||||
var pos = content_position(get_local_mouse_position())
|
||||
var clip_rects = [_current_connection.from_node.get_rect()]
|
||||
|
||||
# Snapping connecting line
|
||||
for i in current_layer.content_nodes.get_child_count():
|
||||
var child = current_layer.content_nodes.get_child(current_layer.content_nodes.get_child_count()-1 - i) # Inverse order to check from top to bottom of canvas
|
||||
if child is FlowChartNode and child.name != _current_connection.from_node.name:
|
||||
if _request_connect_to(current_layer, child.name):
|
||||
if child.get_rect().has_point(pos):
|
||||
pos = child.position + child.size / 2
|
||||
clip_rects.append(child.get_rect())
|
||||
break
|
||||
_current_connection.line.join(_current_connection.get_from_pos(), pos, Vector2.ZERO, clip_rects)
|
||||
elif _is_dragging_node:
|
||||
# Dragging nodes
|
||||
var dragged = content_position(_drag_end_pos) - content_position(_drag_start_pos)
|
||||
for i in _selection.size():
|
||||
var selected = _selection[i]
|
||||
if not (selected is FlowChartNode):
|
||||
continue
|
||||
selected.position = (_drag_origins[i] + selected.size / 2.0 + dragged)
|
||||
selected.modulate.a = 0.3
|
||||
if is_snapping:
|
||||
selected.position = selected.position.snapped(Vector2.ONE * snap)
|
||||
selected.position -= selected.size / 2.0
|
||||
_on_node_dragged(current_layer, selected, dragged)
|
||||
emit_signal("dragged", selected, dragged)
|
||||
# Update connection pos
|
||||
for from in current_layer._connections:
|
||||
var connections_from = current_layer._connections[from]
|
||||
for to in connections_from:
|
||||
if from == selected.name or to == selected.name:
|
||||
var connection = current_layer._connections[from][to]
|
||||
connection.join()
|
||||
_drag_end_pos = get_local_mouse_position()
|
||||
queue_redraw()
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_MIDDLE:
|
||||
# Reset zoom
|
||||
if event.double_click:
|
||||
set_zoom(1.0)
|
||||
queue_redraw()
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
# Zoom in
|
||||
set_zoom(zoom + zoom_step/10)
|
||||
queue_redraw()
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
# Zoom out
|
||||
set_zoom(zoom - zoom_step/10)
|
||||
queue_redraw()
|
||||
MOUSE_BUTTON_LEFT:
|
||||
# Hit detection
|
||||
var hit_node
|
||||
for i in current_layer.content_nodes.get_child_count():
|
||||
var child = current_layer.content_nodes.get_child(current_layer.content_nodes.get_child_count()-1 - i) # Inverse order to check from top to bottom of canvas
|
||||
if child is FlowChartNode:
|
||||
if child.get_rect().has_point(content_position(get_local_mouse_position())):
|
||||
hit_node = child
|
||||
break
|
||||
if not hit_node:
|
||||
# Test Line
|
||||
# Refer https://github.com/godotengine/godot/blob/master/editor/plugins/animation_state_machine_editor.cpp#L187
|
||||
var closest = -1
|
||||
var closest_d = 1e20
|
||||
var connection_list = get_connection_list()
|
||||
for i in connection_list.size():
|
||||
var connection = current_layer._connections[connection_list[i].from][connection_list[i].to]
|
||||
# Line's offset along its down-vector
|
||||
var line_local_up_offset = connection.line.position - connection.line.get_transform()*(Vector2.DOWN * connection.offset)
|
||||
var from_pos = connection.get_from_pos() + line_local_up_offset
|
||||
var to_pos = connection.get_to_pos() + line_local_up_offset
|
||||
var cp = Geometry2D.get_closest_point_to_segment(content_position(event.position), from_pos, to_pos)
|
||||
var d = cp.distance_to(content_position(event.position))
|
||||
if d > connection.line.size.y * 2:
|
||||
continue
|
||||
if d < closest_d:
|
||||
closest = i
|
||||
closest_d = d
|
||||
if closest >= 0:
|
||||
hit_node = current_layer._connections[connection_list[closest].from][connection_list[closest].to].line
|
||||
|
||||
if event.pressed:
|
||||
if not (hit_node in _selection) and not event.shift_pressed:
|
||||
# Click on empty space
|
||||
clear_selection()
|
||||
if hit_node:
|
||||
# Click on node(can be a line)
|
||||
_is_dragging_node = true
|
||||
if hit_node is FlowChartLine:
|
||||
current_layer.content_lines.move_child(hit_node, current_layer.content_lines.get_child_count()-1) # Raise selected line to top
|
||||
if event.shift_pressed and can_gui_connect_node:
|
||||
# Reconnection Start
|
||||
for from in current_layer._connections.keys():
|
||||
var from_connections = current_layer._connections[from]
|
||||
for to in from_connections.keys():
|
||||
var connection = from_connections[to]
|
||||
if connection.line == hit_node:
|
||||
_is_connecting = true
|
||||
_is_dragging_node = false
|
||||
_current_connection = connection
|
||||
_on_node_reconnect_begin(current_layer, from, to)
|
||||
break
|
||||
if hit_node is FlowChartNode:
|
||||
current_layer.content_nodes.move_child(hit_node, current_layer.content_nodes.get_child_count()-1) # Raise selected node to top
|
||||
if event.shift_pressed and can_gui_connect_node:
|
||||
# Connection start
|
||||
if _request_connect_from(current_layer, hit_node.name):
|
||||
_is_connecting = true
|
||||
_is_dragging_node = false
|
||||
var line = create_line_instance()
|
||||
var connection = Connection.new(line, hit_node, null)
|
||||
current_layer._connect_node(connection)
|
||||
_current_connection = connection
|
||||
_current_connection.line.join(_current_connection.get_from_pos(), content_position(event.position))
|
||||
accept_event()
|
||||
if _is_connecting:
|
||||
clear_selection()
|
||||
else:
|
||||
if can_gui_select_node:
|
||||
select(hit_node)
|
||||
if not _is_dragging:
|
||||
# Drag start
|
||||
_is_dragging = true
|
||||
for i in _selection.size():
|
||||
var selected = _selection[i]
|
||||
_drag_origins[i] = selected.position
|
||||
selected.modulate.a = 1.0
|
||||
_drag_start_pos = event.position
|
||||
_drag_end_pos = event.position
|
||||
else:
|
||||
var was_connecting = _is_connecting
|
||||
var was_dragging_node = _is_dragging_node
|
||||
if _current_connection:
|
||||
# Connection end
|
||||
var from = _current_connection.from_node.name
|
||||
var to = hit_node.name if hit_node else null
|
||||
if hit_node is FlowChartNode and _request_connect_to(current_layer, to) and from != to:
|
||||
# Connection success
|
||||
var line
|
||||
if _current_connection.to_node:
|
||||
# Reconnection
|
||||
line = disconnect_node(current_layer, from, _current_connection.to_node.name)
|
||||
_current_connection.to_node = hit_node
|
||||
_on_node_reconnect_end(current_layer, from, to)
|
||||
connect_node(current_layer, from, to, line)
|
||||
else:
|
||||
# New Connection
|
||||
current_layer.content_lines.remove_child(_current_connection.line)
|
||||
line = _current_connection.line
|
||||
_current_connection.to_node = hit_node
|
||||
connect_node(current_layer, from, to, line)
|
||||
else:
|
||||
# Connection failed
|
||||
if _current_connection.to_node:
|
||||
# Reconnection
|
||||
_current_connection.join()
|
||||
_on_node_reconnect_failed(current_layer, from, name)
|
||||
else:
|
||||
# New Connection
|
||||
_current_connection.line.queue_free()
|
||||
_on_node_connect_failed(current_layer, from)
|
||||
_is_connecting = false
|
||||
_current_connection = null
|
||||
accept_event()
|
||||
|
||||
if _is_dragging:
|
||||
# Drag end
|
||||
_is_dragging = false
|
||||
_is_dragging_node = false
|
||||
if not (was_connecting or was_dragging_node) and can_gui_select_node:
|
||||
var selection_box_rect = get_selection_box_rect()
|
||||
# Select node
|
||||
for node in current_layer.content_nodes.get_children():
|
||||
var rect = get_transform() * (content.get_transform() * (node.get_rect()))
|
||||
if selection_box_rect.intersects(rect):
|
||||
if node is FlowChartNode:
|
||||
select(node)
|
||||
# Select line
|
||||
var connection_list = get_connection_list()
|
||||
for i in connection_list.size():
|
||||
var connection = current_layer._connections[connection_list[i].from][connection_list[i].to]
|
||||
# Line's offset along its down-vector
|
||||
var line_local_up_offset = connection.line.position - connection.line.get_transform() * (Vector2.UP * connection.offset)
|
||||
var from_pos = content.get_transform() * (connection.get_from_pos() + line_local_up_offset)
|
||||
var to_pos = content.get_transform() * (connection.get_to_pos() + line_local_up_offset)
|
||||
if CohenSutherland.line_intersect_rectangle(from_pos, to_pos, selection_box_rect):
|
||||
select(connection.line)
|
||||
if was_dragging_node:
|
||||
# Update _drag_origins with new position after dragged
|
||||
for i in _selection.size():
|
||||
var selected = _selection[i]
|
||||
_drag_origins[i] = selected.position
|
||||
selected.modulate.a = 1.0
|
||||
_drag_start_pos = _drag_end_pos
|
||||
queue_redraw()
|
||||
|
||||
# Get selection box rect
|
||||
func get_selection_box_rect():
|
||||
var pos = Vector2(min(_drag_start_pos.x, _drag_end_pos.x), min(_drag_start_pos.y, _drag_end_pos.y))
|
||||
var size = (_drag_end_pos - _drag_start_pos).abs()
|
||||
return Rect2(pos, size)
|
||||
|
||||
# Get required scroll rect base on content
|
||||
func get_scroll_rect(layer=current_layer, force_scroll_margin=null):
|
||||
var _scroll_margin = scroll_margin
|
||||
if force_scroll_margin!=null:
|
||||
_scroll_margin = force_scroll_margin
|
||||
return layer.get_scroll_rect(_scroll_margin)
|
||||
|
||||
func add_layer_to(target):
|
||||
var layer = create_layer_instance()
|
||||
target.add_child(layer)
|
||||
return layer
|
||||
|
||||
func get_layer(np):
|
||||
return content.get_node_or_null(NodePath(np))
|
||||
|
||||
func select_layer_at(i):
|
||||
select_layer(content.get_child(i))
|
||||
|
||||
func select_layer(layer):
|
||||
var prev_layer = current_layer
|
||||
_on_layer_deselected(prev_layer)
|
||||
current_layer = layer
|
||||
_on_layer_selected(layer)
|
||||
|
||||
# Add node
|
||||
func add_node(layer, node):
|
||||
layer.add_node(node)
|
||||
_on_node_added(layer, node)
|
||||
|
||||
# Remove node
|
||||
func remove_node(layer, node_name):
|
||||
var node = layer.content_nodes.get_node_or_null(NodePath(node_name))
|
||||
if node:
|
||||
deselect(node) # Must deselct before remove to make sure _drag_origins synced with _selections
|
||||
layer.remove_node(node)
|
||||
_on_node_removed(layer, node_name)
|
||||
|
||||
# Called after connection established
|
||||
func _connect_node(line, from_pos, to_pos):
|
||||
pass
|
||||
|
||||
# Called after connection broken
|
||||
func _disconnect_node(line):
|
||||
if line in _selection:
|
||||
deselect(line)
|
||||
|
||||
func create_layer_instance():
|
||||
var layer = Control.new()
|
||||
layer.set_script(FlowChartLayer)
|
||||
return layer
|
||||
|
||||
# Return new line instance to use, called when connecting node
|
||||
func create_line_instance():
|
||||
return FlowChartLineScene.instantiate()
|
||||
|
||||
# Rename node
|
||||
func rename_node(layer, old, new):
|
||||
layer.rename_node(old, new)
|
||||
|
||||
# Connect two nodes with a line
|
||||
func connect_node(layer, from, to, line=null):
|
||||
if not line:
|
||||
line = create_line_instance()
|
||||
line.name = "%s>%s" % [from, to] # "From>To"
|
||||
layer.connect_node(line, from, to, interconnection_offset)
|
||||
_on_node_connected(layer, from, to)
|
||||
emit_signal("connection", from, to, line)
|
||||
|
||||
# Break a connection between two node
|
||||
func disconnect_node(layer, from, to):
|
||||
var line = layer.disconnect_node(from, to)
|
||||
deselect(line) # Since line is selectable as well
|
||||
_on_node_disconnected(layer, from, to)
|
||||
emit_signal("disconnection", from, to)
|
||||
return line
|
||||
|
||||
# Clear all connections
|
||||
func clear_connections(layer=current_layer):
|
||||
layer.clear_connections()
|
||||
|
||||
# Select a node(can be a line)
|
||||
func select(node):
|
||||
if node in _selection:
|
||||
return
|
||||
|
||||
_selection.append(node)
|
||||
node.selected = true
|
||||
_drag_origins.append(node.position)
|
||||
emit_signal("node_selected", node)
|
||||
|
||||
# Deselect a node
|
||||
func deselect(node):
|
||||
_selection.erase(node)
|
||||
if is_instance_valid(node):
|
||||
node.selected = false
|
||||
_drag_origins.pop_back()
|
||||
emit_signal("node_deselected", node)
|
||||
|
||||
# Clear all selection
|
||||
func clear_selection():
|
||||
for node in _selection.duplicate(): # duplicate _selection array as deselect() edit array
|
||||
if not node:
|
||||
continue
|
||||
deselect(node)
|
||||
_selection.clear()
|
||||
|
||||
# Duplicate given nodes in editor
|
||||
func duplicate_nodes(layer, nodes):
|
||||
clear_selection()
|
||||
var new_nodes = []
|
||||
for i in nodes.size():
|
||||
var node = nodes[i]
|
||||
if not (node is FlowChartNode):
|
||||
continue
|
||||
var new_node = node.duplicate(DUPLICATE_SIGNALS + DUPLICATE_SCRIPTS)
|
||||
var offset = content_position(get_local_mouse_position()) - content_position(_drag_end_pos)
|
||||
new_node.position = new_node.position + offset
|
||||
new_nodes.append(new_node)
|
||||
add_node(layer, new_node)
|
||||
select(new_node)
|
||||
# Duplicate connection within selection
|
||||
for i in nodes.size():
|
||||
var from_node = nodes[i]
|
||||
for connection_pair in get_connection_list():
|
||||
if from_node.name == connection_pair.from:
|
||||
for j in nodes.size():
|
||||
var to_node = nodes[j]
|
||||
if to_node.name == connection_pair.to:
|
||||
connect_node(layer, new_nodes[i].name, new_nodes[j].name)
|
||||
_on_duplicated(layer, nodes, new_nodes)
|
||||
|
||||
# Called after layer selected(current_layer changed)
|
||||
func _on_layer_selected(layer):
|
||||
pass
|
||||
|
||||
func _on_layer_deselected(layer):
|
||||
pass
|
||||
|
||||
# Called after a node added
|
||||
func _on_node_added(layer, node):
|
||||
pass
|
||||
|
||||
# Called after a node removed
|
||||
func _on_node_removed(layer, node):
|
||||
pass
|
||||
|
||||
# Called when a node dragged
|
||||
func _on_node_dragged(layer, node, dragged):
|
||||
pass
|
||||
|
||||
# Called when connection established between two nodes
|
||||
func _on_node_connected(layer, from, to):
|
||||
pass
|
||||
|
||||
# Called when connection broken
|
||||
func _on_node_disconnected(layer, from, to):
|
||||
pass
|
||||
|
||||
func _on_node_connect_failed(layer, from):
|
||||
pass
|
||||
|
||||
func _on_node_reconnect_begin(layer, from, to):
|
||||
pass
|
||||
|
||||
func _on_node_reconnect_end(layer, from, to):
|
||||
pass
|
||||
|
||||
func _on_node_reconnect_failed(layer, from, to):
|
||||
pass
|
||||
|
||||
func _request_connect_from(layer, from):
|
||||
return true
|
||||
|
||||
func _request_connect_to(layer, to):
|
||||
return true
|
||||
|
||||
# Called when nodes duplicated
|
||||
func _on_duplicated(layer, old_nodes, new_nodes):
|
||||
pass
|
||||
|
||||
# Convert position in FlowChart space to content(takes translation/scale of content into account)
|
||||
func content_position(pos):
|
||||
return (pos - content.position - content.pivot_offset * (Vector2.ONE - content.scale)) * 1.0/content.scale
|
||||
|
||||
# Return array of dictionary of connection as such [{"from1": "to1"}, {"from2": "to2"}]
|
||||
func get_connection_list(layer=current_layer):
|
||||
return layer.get_connection_list()
|
64
addons/imjp94.yafsm/scenes/flowchart/FlowChartGrid.gd
Normal file
64
addons/imjp94.yafsm/scenes/flowchart/FlowChartGrid.gd
Normal file
|
@ -0,0 +1,64 @@
|
|||
extends Control
|
||||
|
||||
var flowchart
|
||||
|
||||
func _ready():
|
||||
flowchart = get_parent().get_parent()
|
||||
queue_redraw()
|
||||
|
||||
# Original Draw in FlowChart.gd inspired by:
|
||||
# https://github.com/godotengine/godot/blob/6019dab0b45e1291e556e6d9e01b625b5076cc3c/scene/gui/graph_edit.cpp#L442
|
||||
func _draw():
|
||||
|
||||
self.position = flowchart.position
|
||||
# Extents of the grid.
|
||||
self.size = flowchart.size*100 # good with min_zoom = 0.5 e max_zoom = 2.0
|
||||
|
||||
var zoom = flowchart.zoom
|
||||
var snap = flowchart.snap
|
||||
|
||||
# Origin of the grid.
|
||||
var offset = -Vector2(1, 1)*10000 # good with min_zoom = 0.5 e max_zoom = 2.0
|
||||
|
||||
var corrected_size = size/zoom
|
||||
|
||||
var from = (offset / snap).floor()
|
||||
var l = (corrected_size / snap).floor() + Vector2(1, 1)
|
||||
|
||||
var grid_minor = flowchart.grid_minor_color
|
||||
var grid_major = flowchart.grid_major_color
|
||||
|
||||
var multi_line_vector_array: PackedVector2Array = PackedVector2Array()
|
||||
var multi_line_color_array: PackedColorArray = PackedColorArray ()
|
||||
|
||||
# for (int i = from.x; i < from.x + len.x; i++) {
|
||||
for i in range(from.x, from.x + l.x):
|
||||
var color
|
||||
|
||||
if (int(abs(i)) % 10 == 0):
|
||||
color = grid_major
|
||||
else:
|
||||
color = grid_minor
|
||||
|
||||
var base_ofs = i * snap
|
||||
|
||||
multi_line_vector_array.append(Vector2(base_ofs, offset.y))
|
||||
multi_line_vector_array.append(Vector2(base_ofs, corrected_size.y))
|
||||
multi_line_color_array.append(color)
|
||||
|
||||
# for (int i = from.y; i < from.y + len.y; i++) {
|
||||
for i in range(from.y, from.y + l.y):
|
||||
var color
|
||||
|
||||
if (int(abs(i)) % 10 == 0):
|
||||
color = grid_major
|
||||
else:
|
||||
color = grid_minor
|
||||
|
||||
var base_ofs = i * snap
|
||||
|
||||
multi_line_vector_array.append(Vector2(offset.x, base_ofs))
|
||||
multi_line_vector_array.append(Vector2(corrected_size.x, base_ofs))
|
||||
multi_line_color_array.append(color)
|
||||
|
||||
draw_multiline_colors(multi_line_vector_array, multi_line_color_array, -1)
|
157
addons/imjp94.yafsm/scenes/flowchart/FlowChartLayer.gd
Normal file
157
addons/imjp94.yafsm/scenes/flowchart/FlowChartLayer.gd
Normal file
|
@ -0,0 +1,157 @@
|
|||
@tool
|
||||
extends Control
|
||||
const FlowChartNode = preload("res://addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd")
|
||||
|
||||
var content_lines = Control.new() # Node that hold all flowchart lines
|
||||
var content_nodes = Control.new() # Node that hold all flowchart nodes
|
||||
|
||||
var _connections = {}
|
||||
|
||||
func _init():
|
||||
|
||||
name = "FlowChartLayer"
|
||||
mouse_filter = MOUSE_FILTER_IGNORE
|
||||
|
||||
content_lines.name = "content_lines"
|
||||
content_lines.mouse_filter = MOUSE_FILTER_IGNORE
|
||||
add_child(content_lines)
|
||||
move_child(content_lines, 0) # Make sure content_lines always behind nodes
|
||||
|
||||
content_nodes.name = "content_nodes"
|
||||
content_nodes.mouse_filter = MOUSE_FILTER_IGNORE
|
||||
add_child(content_nodes)
|
||||
|
||||
func hide_content():
|
||||
content_nodes.hide()
|
||||
content_lines.hide()
|
||||
|
||||
func show_content():
|
||||
content_nodes.show()
|
||||
content_lines.show()
|
||||
|
||||
# Get required scroll rect base on content
|
||||
func get_scroll_rect(scroll_margin=0):
|
||||
var rect = Rect2()
|
||||
for child in content_nodes.get_children():
|
||||
# Every child is a state/statemachine node
|
||||
var child_rect = child.get_rect()
|
||||
rect = rect.merge(child_rect)
|
||||
return rect.grow(scroll_margin)
|
||||
|
||||
# Add node
|
||||
func add_node(node):
|
||||
content_nodes.add_child(node)
|
||||
|
||||
# Remove node
|
||||
func remove_node(node):
|
||||
if node:
|
||||
content_nodes.remove_child(node)
|
||||
|
||||
# Called after connection established
|
||||
func _connect_node(connection):
|
||||
content_lines.add_child(connection.line)
|
||||
connection.join()
|
||||
|
||||
# Called after connection broken
|
||||
func _disconnect_node(connection):
|
||||
content_lines.remove_child(connection.line)
|
||||
return connection.line
|
||||
|
||||
# Rename node
|
||||
func rename_node(old, new):
|
||||
for from in _connections.keys():
|
||||
if from == old: # Connection from
|
||||
var from_connections = _connections[from]
|
||||
_connections.erase(old)
|
||||
_connections[new] = from_connections
|
||||
else: # Connection to
|
||||
for to in _connections[from].keys():
|
||||
if to == old:
|
||||
var from_connection = _connections[from]
|
||||
var value = from_connection[old]
|
||||
from_connection.erase(old)
|
||||
from_connection[new] = value
|
||||
|
||||
# Connect two nodes with a line
|
||||
func connect_node(line, from, to, interconnection_offset=0):
|
||||
if from == to:
|
||||
return # Connect to self
|
||||
var connections_from = _connections.get(from)
|
||||
if connections_from:
|
||||
if to in connections_from:
|
||||
return # Connection existed
|
||||
var connection = Connection.new(line, content_nodes.get_node(NodePath(from)), content_nodes.get_node(NodePath(to)))
|
||||
if connections_from == null:
|
||||
connections_from = {}
|
||||
_connections[from] = connections_from
|
||||
connections_from[to] = connection
|
||||
_connect_node(connection)
|
||||
|
||||
# Check if connection in both ways
|
||||
connections_from = _connections.get(to)
|
||||
if connections_from:
|
||||
var inv_connection = connections_from.get(from)
|
||||
if inv_connection:
|
||||
connection.offset = interconnection_offset
|
||||
inv_connection.offset = interconnection_offset
|
||||
connection.join()
|
||||
inv_connection.join()
|
||||
|
||||
# Break a connection between two node
|
||||
func disconnect_node(from, to):
|
||||
var connections_from = _connections.get(from)
|
||||
var connection = connections_from.get(to)
|
||||
if connection == null:
|
||||
return
|
||||
|
||||
_disconnect_node(connection)
|
||||
if connections_from.size() == 1:
|
||||
_connections.erase(from)
|
||||
else:
|
||||
connections_from.erase(to)
|
||||
|
||||
connections_from = _connections.get(to)
|
||||
if connections_from:
|
||||
var inv_connection = connections_from.get(from)
|
||||
if inv_connection:
|
||||
inv_connection.offset = 0
|
||||
inv_connection.join()
|
||||
return connection.line
|
||||
|
||||
# Clear all selection
|
||||
func clear_connections():
|
||||
for connections_from in _connections.values():
|
||||
for connection in connections_from.values():
|
||||
connection.line.queue_free()
|
||||
_connections.clear()
|
||||
|
||||
# Return array of dictionary of connection as such [{"from1": "to1"}, {"from2": "to2"}]
|
||||
func get_connection_list():
|
||||
var connection_list = []
|
||||
for connections_from in _connections.values():
|
||||
for connection in connections_from.values():
|
||||
connection_list.append({"from": connection.from_node.name, "to": connection.to_node.name})
|
||||
return connection_list
|
||||
|
||||
class Connection:
|
||||
var line # Control node that draw line
|
||||
var from_node
|
||||
var to_node
|
||||
var offset = 0 # line's y offset to make space for two interconnecting lines
|
||||
|
||||
func _init(p_line, p_from_node, p_to_node):
|
||||
line = p_line
|
||||
from_node = p_from_node
|
||||
to_node = p_to_node
|
||||
|
||||
# Update line position
|
||||
func join():
|
||||
line.join(get_from_pos(), get_to_pos(), offset, [from_node.get_rect() if from_node else Rect2(), to_node.get_rect() if to_node else Rect2()])
|
||||
|
||||
# Return start position of line
|
||||
func get_from_pos():
|
||||
return from_node.position + from_node.size / 2
|
||||
|
||||
# Return destination position of line
|
||||
func get_to_pos():
|
||||
return to_node.position + to_node.size / 2 if to_node else line.position
|
91
addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.gd
Normal file
91
addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.gd
Normal file
|
@ -0,0 +1,91 @@
|
|||
@tool
|
||||
extends Container
|
||||
# Custom style normal, focus, arrow
|
||||
|
||||
var selected: = false:
|
||||
set = set_selected
|
||||
|
||||
|
||||
func _init():
|
||||
|
||||
focus_mode = FOCUS_CLICK
|
||||
mouse_filter = MOUSE_FILTER_IGNORE
|
||||
|
||||
func _draw():
|
||||
pivot_at_line_start()
|
||||
var from = Vector2.ZERO
|
||||
from.y += size.y / 2.0
|
||||
var to = size
|
||||
to.y -= size.y / 2.0
|
||||
var arrow = get_theme_icon("arrow", "FlowChartLine")
|
||||
var tint = Color.WHITE
|
||||
if selected:
|
||||
tint = get_theme_stylebox("focus", "FlowChartLine").shadow_color
|
||||
draw_style_box(get_theme_stylebox("focus", "FlowChartLine"), Rect2(Vector2.ZERO, size))
|
||||
else:
|
||||
draw_style_box(get_theme_stylebox("normal", "FlowChartLine"), Rect2(Vector2.ZERO, size))
|
||||
|
||||
|
||||
draw_texture(arrow, Vector2.ZERO - arrow.get_size() / 2 + size / 2, tint)
|
||||
|
||||
func _get_minimum_size():
|
||||
return Vector2(0, 5)
|
||||
|
||||
func pivot_at_line_start():
|
||||
pivot_offset.x = 0
|
||||
pivot_offset.y = size.y / 2.0
|
||||
|
||||
func join(from, to, offset=Vector2.ZERO, clip_rects=[]):
|
||||
# Offset along perpendicular direction
|
||||
var perp_dir = from.direction_to(to).rotated(deg_to_rad(90.0)).normalized()
|
||||
from -= perp_dir * offset
|
||||
to -= perp_dir * offset
|
||||
|
||||
var dist = from.distance_to(to)
|
||||
var dir = from.direction_to(to)
|
||||
var center = from + dir * dist / 2
|
||||
|
||||
# Clip line with provided Rect2 array
|
||||
var clipped = [[from, to]]
|
||||
var line_from = from
|
||||
var line_to = to
|
||||
for clip_rect in clip_rects:
|
||||
if clipped.size() == 0:
|
||||
break
|
||||
|
||||
line_from = clipped[0][0]
|
||||
line_to = clipped[0][1]
|
||||
clipped = Geometry2D.clip_polyline_with_polygon(
|
||||
[line_from, line_to],
|
||||
[clip_rect.position, Vector2(clip_rect.position.x, clip_rect.end.y),
|
||||
clip_rect.end, Vector2(clip_rect.end.x, clip_rect.position.y)]
|
||||
)
|
||||
|
||||
if clipped.size() > 0:
|
||||
from = clipped[0][0]
|
||||
to = clipped[0][1]
|
||||
else: # Line is totally overlapped
|
||||
from = center
|
||||
to = center + dir * 0.1
|
||||
|
||||
# Extends line by 2px to minimise ugly seam
|
||||
from -= dir * 2.0
|
||||
to += dir * 2.0
|
||||
|
||||
size.x = to.distance_to(from)
|
||||
# size.y equals to the thickness of line
|
||||
position = from
|
||||
position.y -= size.y / 2.0
|
||||
rotation = Vector2.RIGHT.angle_to(dir)
|
||||
pivot_at_line_start()
|
||||
|
||||
func set_selected(v):
|
||||
if selected != v:
|
||||
selected = v
|
||||
queue_redraw()
|
||||
|
||||
func get_from_pos():
|
||||
return get_transform() * (position)
|
||||
|
||||
func get_to_pos():
|
||||
return get_transform() * (position + size)
|
44
addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.tscn
Normal file
44
addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.tscn
Normal file
File diff suppressed because one or more lines are too long
33
addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd
Normal file
33
addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd
Normal file
|
@ -0,0 +1,33 @@
|
|||
@tool
|
||||
extends Container
|
||||
# Custom style normal, focus
|
||||
|
||||
var selected: = false:
|
||||
set = set_selected
|
||||
|
||||
|
||||
func _init():
|
||||
|
||||
focus_mode = FOCUS_NONE # Let FlowChart has the focus to handle gui_input
|
||||
mouse_filter = MOUSE_FILTER_PASS
|
||||
|
||||
func _draw():
|
||||
if selected:
|
||||
draw_style_box(get_theme_stylebox("focus", "FlowChartNode"), Rect2(Vector2.ZERO, size))
|
||||
else:
|
||||
draw_style_box(get_theme_stylebox("normal", "FlowChartNode"), Rect2(Vector2.ZERO, size))
|
||||
|
||||
func _notification(what):
|
||||
match what:
|
||||
NOTIFICATION_SORT_CHILDREN:
|
||||
for child in get_children():
|
||||
if child is Control:
|
||||
fit_child_in_rect(child, Rect2(Vector2.ZERO, size))
|
||||
|
||||
func _get_minimum_size():
|
||||
return Vector2(50, 50)
|
||||
|
||||
func set_selected(v):
|
||||
if selected != v:
|
||||
selected = v
|
||||
queue_redraw()
|
34
addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.tscn
Normal file
34
addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.tscn
Normal file
|
@ -0,0 +1,34 @@
|
|||
[gd_scene load_steps=5 format=3 uid="uid://bar1eob74t82f"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd" id="1"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="1"]
|
||||
bg_color = Color(0.164706, 0.164706, 0.164706, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.901961, 0.756863, 0.243137, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="2"]
|
||||
bg_color = Color(0.164706, 0.164706, 0.164706, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="Theme" id="3"]
|
||||
FlowChartNode/styles/focus = SubResource("1")
|
||||
FlowChartNode/styles/normal = SubResource("2")
|
||||
|
||||
[node name="FlowChartNode" type="Container"]
|
||||
theme = SubResource("3")
|
||||
script = ExtResource("1")
|
Loading…
Add table
Add a link
Reference in a new issue