Code stuff

This commit is contained in:
Melody Becker 2025-01-26 20:23:56 +01:00
parent cf22890c16
commit e58093b5a5
Signed by: mstar
SSH key fingerprint: SHA256:vkXfS9FG2pVNVfvDrzd1VW9n8VJzqqdKQGljxxX8uK8
153 changed files with 11196 additions and 4 deletions

View 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()

View 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)

View 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

View 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)

File diff suppressed because one or more lines are too long

View 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()

View 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")