generated from mstar/godot-template
682 lines
23 KiB
GDScript3
682 lines
23 KiB
GDScript3
|
@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()
|