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,12 @@
[gd_scene format=3 uid="uid://cflltb00e10be"]
[node name="ContextMenu" type="PopupMenu"]
size = Vector2i(104, 100)
visible = true
item_count = 3
item_0/text = "Add State"
item_0/id = 0
item_1/text = "Add Entry"
item_1/id = 1
item_2/text = "Add Exit"
item_2/id = 2

View file

@ -0,0 +1,63 @@
@tool
extends MarginContainer
@onready var grid = $PanelContainer/MarginContainer/VBoxContainer/GridContainer
@onready var button = $PanelContainer/MarginContainer/VBoxContainer/MarginContainer/Button
func _ready():
button.pressed.connect(_on_button_pressed)
func update_params(params, local_params):
# Remove erased parameters from param panel
for param in grid.get_children():
if not (param.name in params):
remove_param(param.name)
for param in params:
var value = params[param]
if value == null: # Ignore trigger
continue
set_param(param, str(value))
# Remove erased local parameters from param panel
for param in grid.get_children():
if not (param.name in local_params) and not (param.name in params):
remove_param(param.name)
for param in local_params:
var nested_params = local_params[param]
for nested_param in nested_params:
var value = nested_params[nested_param]
if value == null: # Ignore trigger
continue
set_param(str(param, "/", nested_param), str(value))
func set_param(param, value):
var label = grid.get_node_or_null(NodePath(param))
if not label:
label = Label.new()
label.name = param
grid.add_child(label)
label.text = "%s = %s" % [param, value]
func remove_param(param):
var label = grid.get_node_or_null(NodePath(param))
if label:
grid.remove_child(label)
label.queue_free()
set_anchors_preset(PRESET_BOTTOM_RIGHT)
func clear_params():
for child in grid.get_children():
grid.remove_child(child)
child.queue_free()
func _on_button_pressed():
grid.visible = !grid.visible
if grid.visible:
button.text = "Hide params"
else:
button.text = "Show params"
set_anchors_preset(PRESET_BOTTOM_RIGHT)

View file

@ -0,0 +1,64 @@
@tool
extends HBoxContainer
signal dir_pressed(dir, index)
func _init():
add_dir("root")
# Select parent dir & return its path
func back():
return select_dir(get_child(max(get_child_count()-1 - 1, 0)).name)
# Select dir & return its path
func select_dir(dir):
for i in get_child_count():
var child = get_child(i)
if child.name == dir:
remove_dir_until(i)
return get_dir_until(i)
# Add directory button
func add_dir(dir):
var button = Button.new()
button.name = dir
button.flat = true
button.text = dir
add_child(button)
button.pressed.connect(_on_button_pressed.bind(button))
return button
# Remove directory until index(exclusive)
func remove_dir_until(index):
var to_remove = []
for i in get_child_count():
if index == get_child_count()-1 - i:
break
var child = get_child(get_child_count()-1 - i)
to_remove.append(child)
for n in to_remove:
remove_child(n)
n.queue_free()
# Return current working directory
func get_cwd():
return get_dir_until(get_child_count()-1)
# Return path until index(inclusive) of directory
func get_dir_until(index):
var path = ""
for i in get_child_count():
if i > index:
break
var child = get_child(i)
if i == 0:
path = "root"
else:
path = str(path, "/", child.text)
return path
func _on_button_pressed(button):
var index = button.get_index()
var dir = button.name
emit_signal("dir_pressed", dir, index)

View file

@ -0,0 +1,761 @@
@tool
extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChart.gd"
const StateMachine = preload("../src/states/StateMachine.gd")
const Transition = preload("../src/transitions/Transition.gd")
const State = preload("../src/states/State.gd")
const StateDirectory = preload("../src/StateDirectory.gd")
const StateNode = preload("state_nodes/StateNode.tscn")
const TransitionLine = preload("transition_editors/TransitionLine.tscn")
const StateNodeScript = preload("state_nodes/StateNode.gd")
const StateMachineEditorLayer = preload("StateMachineEditorLayer.gd")
const PathViewer = preload("PathViewer.gd")
signal inspector_changed(property) # Inform plugin to refresh inspector
signal debug_mode_changed(new_debug_mode)
const ENTRY_STATE_MISSING_MSG = {
"key": "entry_state_missing",
"text": "Entry State missing, it will never get started. Right-click -> \"Add Entry\"."
}
const EXIT_STATE_MISSING_MSG = {
"key": "exit_state_missing",
"text": "Exit State missing, it will never exit from nested state. Right-click -> \"Add Exit\"."
}
const DEBUG_MODE_MSG = {
"key": "debug_mode",
"text": "Debug Mode"
}
@onready var context_menu = $ContextMenu
@onready var state_node_context_menu = $StateNodeContextMenu
@onready var convert_to_state_confirmation = $ConvertToStateConfirmation
@onready var save_dialog = $SaveDialog
@onready var create_new_state_machine_container = $MarginContainer
@onready var create_new_state_machine = $MarginContainer/CreateNewStateMachine
@onready var param_panel = $ParametersPanel
var path_viewer = HBoxContainer.new()
var condition_visibility = TextureButton.new()
var unsaved_indicator = Label.new()
var message_box = VBoxContainer.new()
var editor_accent_color = Color.WHITE
var transition_arrow_icon
var undo_redo: EditorUndoRedoManager
var debug_mode: = false:
set = set_debug_mode
var state_machine_player:
set = set_state_machine_player
var state_machine:
set = set_state_machine
var can_gui_name_edit = true
var can_gui_context_menu = true
var _reconnecting_connection
var _last_index = 0
var _last_path = ""
var _message_box_dict = {}
var _context_node
var _current_state = ""
var _last_stack = []
func _init():
super._init()
path_viewer.mouse_filter = MOUSE_FILTER_IGNORE
path_viewer.set_script(PathViewer)
path_viewer.dir_pressed.connect(_on_path_viewer_dir_pressed)
top_bar.add_child(path_viewer)
condition_visibility.tooltip_text = "Hide/Show Conditions on Transition Line"
condition_visibility.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
condition_visibility.toggle_mode = true
condition_visibility.size_flags_vertical = SIZE_SHRINK_CENTER
condition_visibility.focus_mode = FOCUS_NONE
condition_visibility.pressed.connect(_on_condition_visibility_pressed)
condition_visibility.button_pressed = true
gadget.add_child(condition_visibility)
unsaved_indicator.size_flags_vertical = SIZE_SHRINK_CENTER
unsaved_indicator.focus_mode = FOCUS_NONE
gadget.add_child(unsaved_indicator)
message_box.set_anchors_and_offsets_preset(PRESET_BOTTOM_WIDE)
message_box.grow_vertical = GROW_DIRECTION_BEGIN
add_child(message_box)
content.get_child(0).name = "root"
set_process(false)
func _ready():
create_new_state_machine_container.visible = false
create_new_state_machine.pressed.connect(_on_create_new_state_machine_pressed)
context_menu.index_pressed.connect(_on_context_menu_index_pressed)
state_node_context_menu.index_pressed.connect(_on_state_node_context_menu_index_pressed)
convert_to_state_confirmation.confirmed.connect(_on_convert_to_state_confirmation_confirmed)
save_dialog.confirmed.connect(_on_save_dialog_confirmed)
func _process(delta):
if not debug_mode:
set_process(false)
return
if not is_instance_valid(state_machine_player):
set_process(false)
set_debug_mode(false)
return
var stack = state_machine_player.get("Members/StackPlayer.gd/stack")
if ((stack == []) or (stack==null)):
set_process(false)
set_debug_mode(false)
return
if stack.size() == 1:
set_current_state(state_machine_player.get("Members/StackPlayer.gd/current"))
else:
var stack_max_index = stack.size() - 1
var prev_index = stack.find(_current_state)
if prev_index == -1:
if _last_stack.size() < stack.size():
# Reproduce transition, for example:
# [Entry, Idle, Walk]
# [Entry, Idle, Jump, Fall]
# Walk -> Idle
# Idle -> Jump
# Jump -> Fall
var common_index = -1
for i in _last_stack.size():
if _last_stack[i] == stack[i]:
common_index = i
break
if common_index > -1:
var count_from_last_stack = _last_stack.size()-1 - common_index -1
_last_stack.reverse()
# Transit back to common state
for i in count_from_last_stack:
set_current_state(_last_stack[i + 1])
# Transit to all missing state in current stack
for i in range(common_index + 1, stack.size()):
set_current_state(stack[i])
else:
set_current_state(stack.back())
else:
set_current_state(stack.back())
else:
# Set every skipped state
var missing_count = stack_max_index - prev_index
for i in range(1, missing_count + 1):
set_current_state(stack[prev_index + i])
_last_stack = stack
var params = state_machine_player.get("Members/_parameters")
var local_params = state_machine_player.get("Members/_local_parameters")
if params == null:
params = state_machine_player.get("Members/StateMachinePlayer.gd/_parameters")
local_params = state_machine_player.get("Members/StateMachinePlayer.gd/_local_parameters")
param_panel.update_params(params, local_params)
get_focused_layer(_current_state).debug_update(_current_state, params, local_params)
func _on_path_viewer_dir_pressed(dir, index):
var path = path_viewer.select_dir(dir)
select_layer(get_layer(path))
if _last_index > index:
# Going backward
var end_state_parent_path = StateMachinePlayer.path_backward(_last_path)
var end_state_name = StateMachinePlayer.path_end_dir(_last_path)
var layer = content.get_node_or_null(NodePath(end_state_parent_path))
if layer:
var node = layer.content_nodes.get_node_or_null(NodePath(end_state_name))
if node:
var cond_1 = (not ("states" in node.state)) or (node.state.states=={}) # states property not defined or empty
# Now check if, for some reason, there are an Entry and/or an Exit node inside this node
# not registered in the states variable above.
var nested_layer = content.get_node_or_null(NodePath(_last_path))
var cond_2 = (nested_layer.content_nodes.get_node_or_null(NodePath(State.ENTRY_STATE)) == null) # there is no entry state in the node
var cond_3 = (nested_layer.content_nodes.get_node_or_null(NodePath(State.EXIT_STATE)) == null) # there is no exit state in the node
if (cond_1 and cond_2 and cond_3):
# Convert state machine node back to state node
convert_to_state(layer, node)
_last_index = index
_last_path = path
func _on_context_menu_index_pressed(index):
var new_node = StateNode.instantiate()
new_node.theme.get_stylebox("focus", "FlowChartNode").border_color = editor_accent_color
match index:
0: # Add State
## Handle state name duplication (4.x changed how duplicates are
## automatically handled and gave a random index instead of
## a progressive one)
var default_new_state_name = "State"
var state_dup_index = 0
var new_name = default_new_state_name
for state_name in current_layer.state_machine.states:
if (state_name == new_name):
state_dup_index += 1
new_name = "%s%s" % [default_new_state_name, state_dup_index]
new_node.name = new_name
1: # Add Entry
if State.ENTRY_STATE in current_layer.state_machine.states:
push_warning("Entry node already exist")
return
new_node.name = State.ENTRY_STATE
2: # Add Exit
if State.EXIT_STATE in current_layer.state_machine.states:
push_warning("Exit node already exist")
return
new_node.name = State.EXIT_STATE
new_node.position = content_position(get_local_mouse_position())
add_node(current_layer, new_node)
func _on_state_node_context_menu_index_pressed(index):
if not _context_node:
return
match index:
0: # Copy
_copying_nodes = [_context_node]
_context_node = null
1: # Duplicate
duplicate_nodes(current_layer, [_context_node])
_context_node = null
2: # Delete
remove_node(current_layer, _context_node.name)
for connection_pair in current_layer.get_connection_list():
if connection_pair.from == _context_node.name or connection_pair.to == _context_node.name:
disconnect_node(current_layer, connection_pair.from, connection_pair.to).queue_free()
_context_node = null
3: # Separator
_context_node = null
4: # Convert
convert_to_state_confirmation.popup_centered()
func _on_convert_to_state_confirmation_confirmed():
convert_to_state(current_layer, _context_node)
_context_node.queue_redraw() # Update outlook of node
# Remove layer
var path = str(path_viewer.get_cwd(), "/", _context_node.name)
var layer = get_layer(path)
if layer:
layer.queue_free()
_context_node = null
func _on_save_dialog_confirmed():
save()
func _on_create_new_state_machine_pressed():
var new_state_machine = StateMachine.new()
undo_redo.create_action("Create New StateMachine")
undo_redo.add_do_reference(new_state_machine)
undo_redo.add_do_property(state_machine_player, "state_machine", new_state_machine)
undo_redo.add_do_method(self, "set_state_machine", new_state_machine)
undo_redo.add_do_property(create_new_state_machine_container, "visible", false)
undo_redo.add_do_method(self, "check_has_entry")
undo_redo.add_do_method(self, "emit_signal", "inspector_changed", "state_machine")
undo_redo.add_undo_property(state_machine_player, "state_machine", null)
undo_redo.add_undo_method(self, "set_state_machine", null)
undo_redo.add_undo_property(create_new_state_machine_container, "visible", true)
undo_redo.add_undo_method(self, "check_has_entry")
undo_redo.add_undo_method(self, "emit_signal", "inspector_changed", "state_machine")
undo_redo.commit_action()
func _on_condition_visibility_pressed():
for line in current_layer.content_lines.get_children():
line.vbox.visible = condition_visibility.button_pressed
func _on_debug_mode_changed(new_debug_mode):
if new_debug_mode:
param_panel.show()
add_message(DEBUG_MODE_MSG.key, DEBUG_MODE_MSG.text)
set_process(true)
# mouse_filter = MOUSE_FILTER_IGNORE
can_gui_select_node = false
can_gui_delete_node = false
can_gui_connect_node = false
can_gui_name_edit = false
can_gui_context_menu = false
else:
param_panel.clear_params()
param_panel.hide()
remove_message(DEBUG_MODE_MSG.key)
set_process(false)
can_gui_select_node = true
can_gui_delete_node = true
can_gui_connect_node = true
can_gui_name_edit = true
can_gui_context_menu = true
func _on_state_machine_player_changed(new_state_machine_player):
if not state_machine_player:
return
if new_state_machine_player.get_class() == "EditorDebuggerRemoteObject":
return
if new_state_machine_player:
create_new_state_machine_container.visible = !new_state_machine_player.state_machine
else:
create_new_state_machine_container.visible = false
func _on_state_machine_changed(new_state_machine):
var root_layer = get_layer("root")
path_viewer.select_dir("root") # Before select_layer, so path_viewer will be updated in _on_layer_selected
select_layer(root_layer)
clear_graph(root_layer)
# Reset layers & path viewer
for child in root_layer.get_children():
if child is FlowChartLayer:
root_layer.remove_child(child)
child.queue_free()
if new_state_machine:
root_layer.state_machine = state_machine
var validated = StateMachine.validate(new_state_machine)
if validated:
print_debug("gd-YAFSM: Corrupted StateMachine Resource fixed, save to apply the fix.")
draw_graph(root_layer)
check_has_entry()
func _gui_input(event):
super._gui_input(event)
if event is InputEventMouseButton:
match event.button_index:
MOUSE_BUTTON_RIGHT:
if event.pressed and can_gui_context_menu:
context_menu.set_item_disabled(1, current_layer.state_machine.has_entry())
context_menu.set_item_disabled(2, current_layer.state_machine.has_exit())
context_menu.position = get_window().position + Vector2i(get_viewport().get_mouse_position())
context_menu.popup()
func _input(event):
# Intercept save action
if visible:
if event is InputEventKey:
match event.keycode:
KEY_S:
if event.ctrl_pressed and event.pressed:
save_request()
func create_layer(node):
# Create/Move to new layer
var new_state_machine = convert_to_state_machine(current_layer, node)
# Determine current layer path
var parent_path = path_viewer.get_cwd()
var path = str(parent_path, "/", node.name)
var layer = get_layer(path)
path_viewer.add_dir(node.state.name) # Before select_layer, so path_viewer will be updated in _on_layer_selected
if not layer:
# New layer to spawn
layer = add_layer_to(get_layer(parent_path))
layer.name = node.state.name
layer.state_machine = new_state_machine
draw_graph(layer)
_last_index = path_viewer.get_child_count()-1
_last_path = path
return layer
func open_layer(path):
var dir = StateDirectory.new(path)
dir.goto(dir.get_end_index())
dir.back()
var next_layer = get_next_layer(dir, get_layer("root"))
select_layer(next_layer)
return next_layer
# Recursively get next layer
func get_next_layer(dir, base_layer):
var next_layer = base_layer
var np = dir.next()
if np:
next_layer = base_layer.get_node_or_null(NodePath(np))
if next_layer:
next_layer = get_next_layer(dir, next_layer)
else:
var to_dir = StateDirectory.new(dir.get_current())
to_dir.goto(to_dir.get_end_index())
to_dir.back()
var node = base_layer.content_nodes.get_node_or_null(NodePath(to_dir.get_current_end()))
next_layer = get_next_layer(dir, create_layer(node))
return next_layer
func get_focused_layer(state):
var current_dir = StateDirectory.new(state)
current_dir.goto(current_dir.get_end_index())
current_dir.back()
return get_layer(str("root/", current_dir.get_current()))
func _on_state_node_gui_input(event, node):
if node.state.is_entry() or node.state.is_exit():
return
if event is InputEventMouseButton:
match event.button_index:
MOUSE_BUTTON_LEFT:
if event.pressed:
if event.double_click:
if node.name_edit.get_rect().has_point(event.position) and can_gui_name_edit:
# Edit State name if within LineEdit
node.enable_name_edit(true)
accept_event()
else:
var layer = create_layer(node)
select_layer(layer)
accept_event()
MOUSE_BUTTON_RIGHT:
if event.pressed:
# State node context menu
_context_node = node
state_node_context_menu.position = get_window().position + Vector2i(get_viewport().get_mouse_position())
state_node_context_menu.popup()
state_node_context_menu.set_item_disabled(4, not (node.state is StateMachine))
accept_event()
func convert_to_state_machine(layer, node):
# Convert State to StateMachine
var new_state_machine
if node.state is StateMachine:
new_state_machine = node.state
else:
new_state_machine = StateMachine.new()
new_state_machine.name = node.state.name
new_state_machine.graph_offset = node.state.graph_offset
layer.state_machine.remove_state(node.state.name)
layer.state_machine.add_state(new_state_machine)
node.state = new_state_machine
return new_state_machine
func convert_to_state(layer, node):
# Convert StateMachine to State
var new_state
if node.state is StateMachine:
new_state = State.new()
new_state.name = node.state.name
new_state.graph_offset = node.state.graph_offset
layer.state_machine.remove_state(node.state.name)
layer.state_machine.add_state(new_state)
node.state = new_state
else:
new_state = node.state
return new_state
func create_layer_instance():
var layer = Control.new()
layer.set_script(StateMachineEditorLayer)
layer.editor_accent_color = editor_accent_color
return layer
func create_line_instance():
var line = TransitionLine.instantiate()
line.theme.get_stylebox("focus", "FlowChartLine").shadow_color = editor_accent_color
line.theme.set_icon("arrow", "FlowChartLine", transition_arrow_icon)
return line
# Request to save current editing StateMachine
func save_request():
if not can_save():
return
save_dialog.dialog_text = "Saving StateMachine to %s" % state_machine.resource_path
save_dialog.popup_centered()
# Save current editing StateMachine
func save():
if not can_save():
return
unsaved_indicator.text = ""
ResourceSaver.save(state_machine, state_machine.resource_path)
# Clear editor
func clear_graph(layer):
clear_connections()
for child in layer.content_nodes.get_children():
if child is StateNodeScript:
layer.content_nodes.remove_child(child)
child.queue_free()
queue_redraw()
unsaved_indicator.text = "" # Clear graph is not action by user
# Intialize editor with current editing StateMachine
func draw_graph(layer):
for state_key in layer.state_machine.states.keys():
var state = layer.state_machine.states[state_key]
var new_node = StateNode.instantiate()
new_node.theme.get_stylebox("focus", "FlowChartNode").border_color = editor_accent_color
new_node.name = state_key # Set before add_node to let engine handle duplicate name
add_node(layer, new_node)
# Set after add_node to make sure UIs are initialized
new_node.state = state
new_node.state.name = state_key
new_node.position = state.graph_offset
for state_key in layer.state_machine.states.keys():
var from_transitions = layer.state_machine.transitions.get(state_key)
if from_transitions:
for transition in from_transitions.values():
connect_node(layer, transition.from, transition.to)
layer._connections[transition.from][transition.to].line.transition = transition
queue_redraw()
unsaved_indicator.text = "" # Draw graph is not action by user
# Add message to message_box(overlay text at bottom of editor)
func add_message(key, text):
var label = Label.new()
label.text = text
_message_box_dict[key] = label
message_box.add_child(label)
return label
# Remove message from message_box
func remove_message(key):
var control = _message_box_dict.get(key)
if control:
_message_box_dict.erase(key)
message_box.remove_child(control)
# Weird behavior of VBoxContainer, only sort children properly after changing grow_direction
message_box.grow_vertical = GROW_DIRECTION_END
message_box.grow_vertical = GROW_DIRECTION_BEGIN
return true
return false
# Check if current editing StateMachine has entry, warns user if entry state missing
func check_has_entry():
if not current_layer.state_machine:
return
if not current_layer.state_machine.has_entry():
if not (ENTRY_STATE_MISSING_MSG.key in _message_box_dict):
add_message(ENTRY_STATE_MISSING_MSG.key, ENTRY_STATE_MISSING_MSG.text)
else:
if ENTRY_STATE_MISSING_MSG.key in _message_box_dict:
remove_message(ENTRY_STATE_MISSING_MSG.key)
# Check if current editing StateMachine is nested and has exit, warns user if exit state missing
func check_has_exit():
if not current_layer.state_machine:
return
if not path_viewer.get_cwd() == "root": # Nested state
if not current_layer.state_machine.has_exit():
if not (EXIT_STATE_MISSING_MSG.key in _message_box_dict):
add_message(EXIT_STATE_MISSING_MSG.key, EXIT_STATE_MISSING_MSG.text)
return
if EXIT_STATE_MISSING_MSG.key in _message_box_dict:
remove_message(EXIT_STATE_MISSING_MSG.key)
func _on_layer_selected(layer):
if layer:
layer.show_content()
check_has_entry()
check_has_exit()
func _on_layer_deselected(layer):
if layer:
layer.hide_content()
func _on_node_dragged(layer, node, dragged):
node.state.graph_offset = node.position
_on_edited()
func _on_node_added(layer, new_node):
# Godot 4 duplicates node with an internal @ name, which breaks everything
while String(new_node.name).begins_with("@"):
new_node.name = String(new_node.name).lstrip("@")
new_node.undo_redo = undo_redo
new_node.state.name = new_node.name
new_node.state.graph_offset = new_node.position
new_node.name_edit_entered.connect(_on_node_name_edit_entered.bind(new_node))
new_node.gui_input.connect(_on_state_node_gui_input.bind(new_node))
layer.state_machine.add_state(new_node.state)
check_has_entry()
check_has_exit()
_on_edited()
func _on_node_removed(layer, node_name):
var path = str(path_viewer.get_cwd(), "/", node_name)
var layer_to_remove = get_layer(path)
if layer_to_remove:
layer_to_remove.get_parent().remove_child(layer_to_remove)
layer_to_remove.queue_free()
var result = layer.state_machine.remove_state(node_name)
check_has_entry()
check_has_exit()
_on_edited()
return result
func _on_node_connected(layer, from, to):
if _reconnecting_connection:
# Reconnection will trigger _on_node_connected after _on_node_reconnect_end/_on_node_reconnect_failed
if is_instance_valid(_reconnecting_connection.from_node) and \
_reconnecting_connection.from_node.name == from and \
is_instance_valid(_reconnecting_connection.to_node) and \
_reconnecting_connection.to_node.name == to:
_reconnecting_connection = null
return
if layer.state_machine.transitions.has(from):
if layer.state_machine.transitions[from].has(to):
return # Already existed as it is loaded from file
var line = layer._connections[from][to].line
var new_transition = Transition.new(from, to)
line.transition = new_transition
layer.state_machine.add_transition(new_transition)
clear_selection()
select(line)
_on_edited()
func _on_node_disconnected(layer, from, to):
layer.state_machine.remove_transition(from, to)
_on_edited()
func _on_node_reconnect_begin(layer, from, to):
_reconnecting_connection = layer._connections[from][to]
layer.state_machine.remove_transition(from, to)
func _on_node_reconnect_end(layer, from, to):
var transition = _reconnecting_connection.line.transition
transition.to = to
layer.state_machine.add_transition(transition)
clear_selection()
select(_reconnecting_connection.line)
func _on_node_reconnect_failed(layer, from, to):
var transition = _reconnecting_connection.line.transition
layer.state_machine.add_transition(transition)
clear_selection()
select(_reconnecting_connection.line)
func _request_connect_from(layer, from):
if from == State.EXIT_STATE:
return false
return true
func _request_connect_to(layer, to):
if to == State.ENTRY_STATE:
return false
return true
func _on_duplicated(layer, old_nodes, new_nodes):
# Duplicate condition as well
for i in old_nodes.size():
var from_node = old_nodes[i]
for connection_pair in get_connection_list():
if from_node.name == connection_pair.from:
for j in old_nodes.size():
var to_node = old_nodes[j]
if to_node.name == connection_pair.to:
var old_connection = layer._connections[connection_pair.from][connection_pair.to]
var new_connection = layer._connections[new_nodes[i].name][new_nodes[j].name]
for condition in old_connection.line.transition.conditions.values():
new_connection.line.transition.add_condition(condition.duplicate())
_on_edited()
func _on_node_name_edit_entered(new_name, node):
var old = node.state.name
var new = new_name
if old == new:
return
if "/" in new or "\\" in new: # No back/forward-slash
push_warning("Illegal State Name: / and \\ are not allowed in State name(%s)" % new)
node.name_edit.text = old
return
if current_layer.state_machine.change_state_name(old, new):
rename_node(current_layer, node.name, new)
node.name = new
# Rename layer as well
var path = str(path_viewer.get_cwd(), "/", node.name)
var layer = get_layer(path)
if layer:
layer.name = new
for child in path_viewer.get_children():
if child.text == old:
child.text = new
break
_on_edited()
else:
node.name_edit.text = old
func _on_edited():
unsaved_indicator.text = "*"
func _on_remote_transited(from, to):
var from_dir = StateDirectory.new(from)
var to_dir = StateDirectory.new(to)
var focused_layer = get_focused_layer(from)
if from:
if focused_layer:
focused_layer.debug_transit_out(from, to)
if to:
if from_dir.is_nested() and from_dir.is_exit():
if focused_layer:
var path = path_viewer.back()
select_layer(get_layer(path))
elif to_dir.is_nested():
if to_dir.is_entry() and focused_layer:
# Open into next layer
to_dir.goto(to_dir.get_end_index())
to_dir.back()
var node = focused_layer.content_nodes.get_node_or_null(NodePath(to_dir.get_current_end()))
if node:
var layer = create_layer(node)
select_layer(layer)
# In case where, "from" state is nested yet not an exit state,
# while "to" state is on different level, then jump to destination layer directly.
# This happens when StateMachinePlayer transit to state that existing in the stack,
# which trigger StackPlayer.reset() and cause multiple states removed from stack within one frame
elif from_dir.is_nested() and not from_dir.is_exit():
if to_dir._dirs.size() != from_dir._dirs.size():
to_dir.goto(to_dir.get_end_index())
var n = to_dir.back()
if not n:
n = "root"
var layer = get_layer(n)
path_viewer.select_dir(layer.name)
select_layer(layer)
focused_layer = get_focused_layer(to)
if not focused_layer:
focused_layer = open_layer(to)
focused_layer.debug_transit_in(from, to)
# Return if current editing StateMachine can be saved, ignore built-in resource
func can_save():
if not state_machine:
return false
var resource_path = state_machine.resource_path
if resource_path.is_empty():
return false
if ".scn" in resource_path or ".tscn" in resource_path: # Built-in resource will be saved by scene
return false
return true
func set_debug_mode(v):
if debug_mode != v:
debug_mode = v
_on_debug_mode_changed(v)
emit_signal("debug_mode_changed", debug_mode)
func set_state_machine_player(smp):
if state_machine_player != smp:
state_machine_player = smp
_on_state_machine_player_changed(smp)
func set_state_machine(sm):
if state_machine != sm:
state_machine = sm
_on_state_machine_changed(sm)
func set_current_state(v):
if _current_state != v:
var from = _current_state
var to = v
_current_state = v
_on_remote_transited(from, to)

View file

@ -0,0 +1,101 @@
[gd_scene load_steps=5 format=3 uid="uid://bp2f3rs2sgn8g"]
[ext_resource type="PackedScene" uid="uid://ccv81pntbud75" path="res://addons/imjp94.yafsm/scenes/StateNodeContextMenu.tscn" id="1"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/StateMachineEditor.gd" id="2"]
[ext_resource type="PackedScene" uid="uid://cflltb00e10be" path="res://addons/imjp94.yafsm/scenes/ContextMenu.tscn" id="3"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/ParametersPanel.gd" id="4"]
[node name="StateMachineEditor" type="Control"]
visible = false
clip_contents = true
custom_minimum_size = Vector2i(0, 200)
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
mouse_filter = 1
script = ExtResource("2")
[node name="MarginContainer" type="MarginContainer" parent="."]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Panel" type="Panel" parent="MarginContainer"]
layout_mode = 2
offset_right = 1152.0
offset_bottom = 648.0
[node name="CreateNewStateMachine" type="Button" parent="MarginContainer"]
layout_mode = 2
offset_left = 473.0
offset_top = 308.0
offset_right = 679.0
offset_bottom = 339.0
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_colors/font_color = Color(0.87451, 0.87451, 0.87451, 1)
text = "Create new StateMachine"
[node name="ContextMenu" parent="." instance=ExtResource("3")]
visible = false
[node name="StateNodeContextMenu" parent="." instance=ExtResource("1")]
visible = false
[node name="SaveDialog" type="ConfirmationDialog" parent="."]
[node name="ConvertToStateConfirmation" type="ConfirmationDialog" parent="."]
dialog_text = "All nested states beneath it will be lost, are you sure about that?"
dialog_autowrap = true
[node name="ParametersPanel" type="MarginContainer" parent="."]
visible = false
layout_mode = 1
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 0
grow_vertical = 0
script = ExtResource("4")
[node name="PanelContainer" type="PanelContainer" parent="ParametersPanel"]
layout_mode = 2
offset_right = 113.0
offset_bottom = 31.0
[node name="MarginContainer" type="MarginContainer" parent="ParametersPanel/PanelContainer"]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="ParametersPanel/PanelContainer/MarginContainer"]
layout_mode = 2
offset_right = 113.0
offset_bottom = 31.0
[node name="MarginContainer" type="MarginContainer" parent="ParametersPanel/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Button" type="Button" parent="ParametersPanel/PanelContainer/MarginContainer/VBoxContainer/MarginContainer"]
layout_mode = 2
offset_right = 113.0
offset_bottom = 31.0
size_flags_horizontal = 10
text = "Show Params"
[node name="GridContainer" type="GridContainer" parent="ParametersPanel/PanelContainer/MarginContainer/VBoxContainer"]
visible = false
layout_mode = 2
columns = 4

View file

@ -0,0 +1,149 @@
@tool
extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLayer.gd"
const Utils = preload("res://addons/imjp94.yafsm/scripts/Utils.gd")
const StateNode = preload("res://addons/imjp94.yafsm/scenes/state_nodes/StateNode.tscn")
const StateNodeScript = preload("res://addons/imjp94.yafsm/scenes/state_nodes/StateNode.gd")
const StateDirectory = preload("../src/StateDirectory.gd")
var editor_accent_color: = Color.WHITE:
set = set_editor_accent_color
var editor_complementary_color = Color.WHITE
var state_machine
var tween_lines
var tween_labels
var tween_nodes
func debug_update(current_state, parameters, local_parameters):
_init_tweens()
if not state_machine:
return
var current_dir = StateDirectory.new(current_state)
var transitions = state_machine.transitions.get(current_state, {})
if current_dir.is_nested():
transitions = state_machine.transitions.get(current_dir.get_end(), {})
for transition in transitions.values():
# Check all possible transitions from current state, update labels, color them accordingly
var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to]))
if line:
# Blinking alpha of TransitionLine
var color1 = Color.WHITE
color1.a = 0.1
var color2 = Color.WHITE
color2.a = 0.5
if line.self_modulate == color1:
tween_lines.tween_property(line, "self_modulate", color2, 0.5)
elif line.self_modulate == color2:
tween_lines.tween_property(line, "self_modulate", color1, 0.5)
elif line.self_modulate == Color.WHITE:
tween_lines.tween_property(line, "self_modulate", color2, 0.5)
# Update TransitionLine condition labels
for condition in transition.conditions.values():
if not ("value" in condition): # Ignore trigger
continue
var value = parameters.get(str(condition.name))
value = str(value) if value != null else "?"
var label = line.vbox.get_node_or_null(NodePath(str(condition.name)))
var override_template_var = line._template_var.get(str(condition.name))
if override_template_var == null:
override_template_var = {}
line._template_var[str(condition.name)] = override_template_var
override_template_var["value"] = str(value)
line.update_label()
# Condition label color based on comparation
var cond_1: bool = condition.compare(parameters.get(str(condition.name)))
var cond_2: bool = condition.compare(local_parameters.get(str(condition.name)))
if cond_1 or cond_2:
tween_labels.tween_property(label, "self_modulate", Color.GREEN.lightened(0.5), 0.01)
else:
tween_labels.tween_property(label, "self_modulate", Color.RED.lightened(0.5), 0.01)
_start_tweens()
func debug_transit_out(from, to):
_init_tweens()
var from_dir = StateDirectory.new(from)
var to_dir = StateDirectory.new(to)
var from_node = content_nodes.get_node_or_null(NodePath(from_dir.get_end()))
if from_node != null:
tween_nodes.tween_property(from_node, "self_modulate", editor_complementary_color, 0.01)
tween_nodes.tween_property(from_node, "self_modulate", Color.WHITE, 1)
var transitions = state_machine.transitions.get(from, {})
if from_dir.is_nested():
transitions = state_machine.transitions.get(from_dir.get_end(), {})
# Fade out color of StateNode
for transition in transitions.values():
var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to]))
if line:
line.template = "{condition_name} {condition_comparation} {condition_value}"
line.update_label()
if transition.to == to_dir.get_end():
tween_lines.tween_property(line, "self_modulate", editor_complementary_color, 0.01)
tween_lines.tween_property(line, "self_modulate", Color.WHITE, 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_IN)
# Highlight all the conditions of the transition that just happened
for condition in transition.conditions.values():
if not ("value" in condition): # Ignore trigger
continue
var label = line.vbox.get_node_or_null(NodePath(condition.name))
tween_labels.tween_property(label, "self_modulate", editor_complementary_color, 0.01)
tween_labels.tween_property(label, "self_modulate", Color.WHITE, 1)
else:
tween_lines.tween_property(line, "self_modulate", Color.WHITE, 0.1)
# Revert color of TransitionLine condition labels
for condition in transition.conditions.values():
if not ("value" in condition): # Ignore trigger
continue
var label = line.vbox.get_node_or_null(NodePath(condition.name))
if label.self_modulate != Color.WHITE:
tween_labels.tween_property(label, "self_modulate", Color.WHITE, 0.5)
if from_dir.is_nested() and from_dir.is_exit():
# Transition from nested state
transitions = state_machine.transitions.get(from_dir.get_base(), {})
tween_lines.set_parallel(true)
for transition in transitions.values():
var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to]))
if line:
tween_lines.tween_property(line, "self_modulate", editor_complementary_color.lightened(0.5), 0.1)
for transition in transitions.values():
var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to]))
if line:
tween_lines.tween_property(line, "self_modulate", Color.WHITE, 0.1)
_start_tweens()
func debug_transit_in(from, to):
_init_tweens()
var to_dir = StateDirectory.new(to)
var to_node = content_nodes.get_node_or_null(NodePath(to_dir.get_end()))
if to_node:
tween_nodes.tween_property(to_node, "self_modulate", editor_complementary_color, 0.5)
var transitions = state_machine.transitions.get(to, {})
if to_dir.is_nested():
transitions = state_machine.transitions.get(to_dir.get_end(), {})
# Change string template for current TransitionLines
for transition in transitions.values():
var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to]))
line.template = "{condition_name} {condition_comparation} {condition_value}({value})"
_start_tweens()
func set_editor_accent_color(color):
editor_accent_color = color
editor_complementary_color = Utils.get_complementary_color(color)
func _init_tweens():
tween_lines = get_tree().create_tween()
tween_lines.stop()
tween_labels = get_tree().create_tween()
tween_labels.stop()
tween_nodes = get_tree().create_tween()
tween_nodes.stop()
func _start_tweens():
tween_lines.tween_interval(0.001)
tween_lines.play()
tween_labels.tween_interval(0.001)
tween_labels.play()
tween_nodes.tween_interval(0.001)
tween_nodes.play()

View file

@ -0,0 +1,17 @@
[gd_scene format=3 uid="uid://ccv81pntbud75"]
[node name="StateNodeContextMenu" type="PopupMenu"]
size = Vector2i(154, 120)
visible = true
item_count = 5
item_0/text = "Copy"
item_0/id = 0
item_1/text = "Duplicate"
item_1/id = 1
item_2/text = "Delete"
item_2/id = 4
item_3/text = ""
item_3/id = 2
item_3/separator = true
item_4/text = "Convert to State"
item_4/id = 3

View file

@ -0,0 +1,22 @@
@tool
extends "ValueConditionEditor.gd"
@onready var boolean_value = $MarginContainer/BooleanValue
func _ready():
super._ready()
boolean_value.pressed.connect(_on_boolean_value_pressed)
func _on_value_changed(new_value):
if boolean_value.button_pressed != new_value:
boolean_value.button_pressed = new_value
func _on_boolean_value_pressed():
change_value_action(condition.value, boolean_value.button_pressed)
func _on_condition_changed(new_condition):
super._on_condition_changed(new_condition)
if new_condition:
boolean_value.button_pressed = new_condition.value

View file

@ -0,0 +1,35 @@
[gd_scene load_steps=3 format=3 uid="uid://bmpwx6h3ckekr"]
[ext_resource type="PackedScene" uid="uid://blnscdhcxvpmk" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn" id="1"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/BoolConditionEditor.gd" id="2"]
[node name="BoolConditionEditor" instance=ExtResource("1")]
script = ExtResource("2")
[node name="Name" parent="." index="0"]
layout_mode = 2
[node name="Comparation" parent="." index="1"]
layout_mode = 2
[node name="PopupMenu" parent="Comparation" index="0"]
item_count = 2
[node name="MarginContainer" parent="." index="2"]
layout_mode = 2
offset_top = 3.0
offset_right = 146.0
offset_bottom = 27.0
size_flags_horizontal = 3
size_flags_vertical = 4
[node name="BooleanValue" type="CheckButton" parent="MarginContainer" index="0"]
layout_mode = 2
offset_right = 44.0
offset_bottom = 24.0
size_flags_horizontal = 6
[node name="Remove" parent="." index="3"]
layout_mode = 2
offset_left = 150.0
offset_right = 176.0

View file

@ -0,0 +1,72 @@
@tool
extends HBoxContainer
@onready var name_edit = $Name
@onready var remove = $Remove
var undo_redo
var condition:
set = set_condition
func _ready():
name_edit.text_submitted.connect(_on_name_edit_text_submitted)
name_edit.focus_entered.connect(_on_name_edit_focus_entered)
name_edit.focus_exited.connect(_on_name_edit_focus_exited)
name_edit.text_changed.connect(_on_name_edit_text_changed)
set_process_input(false)
func _input(event):
if event is InputEventMouseButton:
if event.pressed:
if get_viewport().gui_get_focus_owner() == name_edit:
var local_event = name_edit.make_input_local(event)
if not name_edit.get_rect().has_point(local_event.position):
name_edit.release_focus()
func _on_name_edit_text_changed(new_text):
# name_edit.release_focus()
if condition.name == new_text: # Avoid infinite loop
return
rename_edit_action(new_text)
func _on_name_edit_focus_entered():
set_process_input(true)
func _on_name_edit_focus_exited():
set_process_input(false)
if condition.name == name_edit.text:
return
rename_edit_action(name_edit.text)
func _on_name_edit_text_submitted(new_text):
name_edit.tooltip_text = new_text
func change_name_edit(from, to):
var transition = get_parent().get_parent().get_parent().transition # TODO: Better way to get Transition object
if transition.change_condition_name(from, to):
if name_edit.text != to: # Manually update name_edit.text, in case called from undo_redo
name_edit.text = to
else:
name_edit.text = from
push_warning("Change Condition name_edit from (%s) to (%s) failed, name_edit existed" % [from, to])
func rename_edit_action(new_name_edit):
var old_name_edit = condition.name
undo_redo.create_action("Rename_edit Condition")
undo_redo.add_do_method(self, "change_name_edit", old_name_edit, new_name_edit)
undo_redo.add_undo_method(self, "change_name_edit", new_name_edit, old_name_edit)
undo_redo.commit_action()
func _on_condition_changed(new_condition):
if new_condition:
name_edit.text = new_condition.name
name_edit.tooltip_text = name_edit.text
func set_condition(c):
if condition != c:
condition = c
_on_condition_changed(c)

View file

@ -0,0 +1,24 @@
[gd_scene load_steps=3 format=3 uid="uid://cie8lb6ww58ck"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/ConditionEditor.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://l78bjwo7shm" path="res://addons/imjp94.yafsm/assets/icons/close-white-18dp.svg" id="2"]
[node name="ConditionEditor" type="HBoxContainer"]
script = ExtResource("1")
[node name="Name" type="LineEdit" parent="."]
layout_mode = 2
offset_right = 67.0
offset_bottom = 31.0
size_flags_horizontal = 3
size_flags_vertical = 4
text = "Param"
[node name="Remove" type="Button" parent="."]
layout_mode = 2
offset_left = 71.0
offset_right = 97.0
offset_bottom = 31.0
size_flags_horizontal = 9
icon = ExtResource("2")
flat = true

View file

@ -0,0 +1,44 @@
@tool
extends "ValueConditionEditor.gd"
@onready var float_value = $MarginContainer/FloatValue
var _old_value = 0.0
func _ready():
super._ready()
float_value.text_submitted.connect(_on_float_value_text_submitted)
float_value.focus_entered.connect(_on_float_value_focus_entered)
float_value.focus_exited.connect(_on_float_value_focus_exited)
set_process_input(false)
func _input(event):
super._input(event)
if event is InputEventMouseButton:
if event.pressed:
if get_viewport().gui_get_focus_owner() == float_value:
var local_event = float_value.make_input_local(event)
if not float_value.get_rect().has_point(local_event.position):
float_value.release_focus()
func _on_value_changed(new_value):
float_value.text = str(snapped(new_value, 0.01)).pad_decimals(2)
func _on_float_value_text_submitted(new_text):
change_value_action(_old_value, float(new_text))
float_value.release_focus()
func _on_float_value_focus_entered():
set_process_input(true)
_old_value = float(float_value.text)
func _on_float_value_focus_exited():
set_process_input(false)
change_value_action(_old_value, float(float_value.text))
func _on_condition_changed(new_condition):
super._on_condition_changed(new_condition)
if new_condition:
float_value.text = str(snapped(new_condition.value, 0.01)).pad_decimals(2)

View file

@ -0,0 +1,26 @@
[gd_scene load_steps=3 format=3 uid="uid://doq6lkdh20j15"]
[ext_resource type="PackedScene" uid="uid://blnscdhcxvpmk" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn" id="1"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/FloatConditionEditor.gd" id="2"]
[node name="ValueConditionEditor" instance=ExtResource("1")]
script = ExtResource("2")
[node name="Comparation" parent="." index="1"]
layout_mode = 2
[node name="MarginContainer" parent="." index="2"]
layout_mode = 2
offset_right = 169.0
size_flags_horizontal = 3
size_flags_vertical = 4
[node name="FloatValue" type="LineEdit" parent="MarginContainer" index="0"]
layout_mode = 2
offset_right = 67.0
offset_bottom = 31.0
size_flags_horizontal = 3
[node name="Remove" parent="." index="3"]
offset_left = 173.0
offset_right = 199.0

View file

@ -0,0 +1,45 @@
@tool
extends "ValueConditionEditor.gd"
@onready var integer_value = $MarginContainer/IntegerValue
var _old_value = 0
func _ready():
super._ready()
integer_value.text_submitted.connect(_on_integer_value_text_submitted)
integer_value.focus_entered.connect(_on_integer_value_focus_entered)
integer_value.focus_exited.connect(_on_integer_value_focus_exited)
set_process_input(false)
func _input(event):
super._input(event)
if event is InputEventMouseButton:
if event.pressed:
if get_viewport().gui_get_focus_owner() == integer_value:
var local_event = integer_value.make_input_local(event)
if not integer_value.get_rect().has_point(local_event.position):
integer_value.release_focus()
func _on_value_changed(new_value):
integer_value.text = str(new_value)
func _on_integer_value_text_submitted(new_text):
change_value_action(_old_value, int(new_text))
integer_value.release_focus()
func _on_integer_value_focus_entered():
set_process_input(true)
_old_value = int(integer_value.text)
func _on_integer_value_focus_exited():
set_process_input(false)
change_value_action(_old_value, int(integer_value.text))
func _on_condition_changed(new_condition):
super._on_condition_changed(new_condition)
if new_condition:
integer_value.text = str(new_condition.value)

View file

@ -0,0 +1,26 @@
[gd_scene load_steps=3 format=3 uid="uid://d1ib30424prpf"]
[ext_resource type="PackedScene" uid="uid://blnscdhcxvpmk" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn" id="1"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/IntegerConditionEditor.gd" id="2"]
[node name="IntegerConditionEditor" instance=ExtResource("1")]
script = ExtResource("2")
[node name="Comparation" parent="." index="1"]
layout_mode = 2
[node name="MarginContainer" parent="." index="2"]
layout_mode = 2
offset_right = 169.0
size_flags_horizontal = 3
size_flags_vertical = 4
[node name="IntegerValue" type="LineEdit" parent="MarginContainer" index="0"]
layout_mode = 2
offset_right = 67.0
offset_bottom = 31.0
size_flags_horizontal = 3
[node name="Remove" parent="." index="3"]
offset_left = 173.0
offset_right = 199.0

View file

@ -0,0 +1,46 @@
@tool
extends "res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.gd"
@onready var string_value = $MarginContainer/StringValue
var _old_value = 0
func _ready():
super._ready()
string_value.text_submitted.connect(_on_string_value_text_submitted)
string_value.focus_entered.connect(_on_string_value_focus_entered)
string_value.focus_exited.connect(_on_string_value_focus_exited)
set_process_input(false)
func _input(event):
super._input(event)
if event is InputEventMouseButton:
if event.pressed:
if get_viewport().gui_get_focus_owner() == string_value:
var local_event = string_value.make_input_local(event)
if not string_value.get_rect().has_point(local_event.position):
string_value.release_focus()
func _on_value_changed(new_value):
string_value.text = new_value
func _on_string_value_text_submitted(new_text):
change_value_action(_old_value, new_text)
string_value.release_focus()
func _on_string_value_focus_entered():
set_process_input(true)
_old_value = string_value.text
func _on_string_value_focus_exited():
set_process_input(false)
change_value_action(_old_value, string_value.text)
func _on_condition_changed(new_condition):
super._on_condition_changed(new_condition)
if new_condition:
string_value.text = new_condition.value

View file

@ -0,0 +1,29 @@
[gd_scene load_steps=3 format=3 uid="uid://qfw0snt5kss6"]
[ext_resource type="PackedScene" uid="uid://blnscdhcxvpmk" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn" id="1"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/StringConditionEditor.gd" id="2"]
[node name="StringConditionEditor" instance=ExtResource("1")]
script = ExtResource("2")
[node name="Comparation" parent="." index="1"]
layout_mode = 2
[node name="PopupMenu" parent="Comparation" index="0"]
item_count = 2
[node name="MarginContainer" parent="." index="2"]
layout_mode = 2
offset_right = 169.0
size_flags_horizontal = 3
size_flags_vertical = 4
[node name="StringValue" type="LineEdit" parent="MarginContainer" index="0"]
layout_mode = 2
offset_right = 67.0
offset_bottom = 31.0
size_flags_horizontal = 3
[node name="Remove" parent="." index="3"]
offset_left = 173.0
offset_right = 199.0

View file

@ -0,0 +1,57 @@
@tool
extends "ConditionEditor.gd"
const Utils = preload("../../scripts/Utils.gd")
const Comparation = preload("../../src/conditions/ValueCondition.gd").Comparation
@onready var comparation_button = $Comparation
@onready var comparation_popup_menu = $Comparation/PopupMenu
func _ready():
super._ready()
comparation_button.pressed.connect(_on_comparation_button_pressed)
comparation_popup_menu.id_pressed.connect(_on_comparation_popup_menu_id_pressed)
func _on_comparation_button_pressed():
Utils.popup_on_target(comparation_popup_menu, comparation_button)
func _on_comparation_popup_menu_id_pressed(id):
change_comparation_action(id)
func _on_condition_changed(new_condition):
super._on_condition_changed(new_condition)
if new_condition:
comparation_button.text = comparation_popup_menu.get_item_text(new_condition.comparation)
func _on_value_changed(new_value):
pass
func change_comparation(id):
if id > Comparation.size() - 1:
push_error("Unexpected id(%d) from PopupMenu" % id)
return
condition.comparation = id
comparation_button.text = comparation_popup_menu.get_item_text(id)
func change_comparation_action(id):
var from = condition.comparation
var to = id
undo_redo.create_action("Change Condition Comparation")
undo_redo.add_do_method(self, "change_comparation", to)
undo_redo.add_undo_method(self, "change_comparation", from)
undo_redo.commit_action()
func set_value(v):
if condition.value != v:
condition.value = v
_on_value_changed(v)
func change_value_action(from, to):
if from == to:
return
undo_redo.create_action("Change Condition Value")
undo_redo.add_do_method(self, "set_value", to)
undo_redo.add_undo_method(self, "set_value", from)
undo_redo.commit_action()

View file

@ -0,0 +1,42 @@
[gd_scene load_steps=3 format=3 uid="uid://blnscdhcxvpmk"]
[ext_resource type="PackedScene" uid="uid://cie8lb6ww58ck" path="res://addons/imjp94.yafsm/scenes/condition_editors/ConditionEditor.tscn" id="1"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.gd" id="2"]
[node name="ValueConditionEditor" instance=ExtResource("1")]
script = ExtResource("2")
[node name="Comparation" type="Button" parent="." index="1"]
layout_mode = 2
offset_left = 71.0
offset_right = 98.0
offset_bottom = 31.0
size_flags_horizontal = 5
size_flags_vertical = 4
text = "=="
[node name="PopupMenu" type="PopupMenu" parent="Comparation" index="0"]
item_count = 6
item_0/text = "=="
item_0/id = 0
item_1/text = "!="
item_1/id = 1
item_2/text = ">"
item_2/id = 2
item_3/text = "<"
item_3/id = 3
item_4/text = "≥"
item_4/id = 4
item_5/text = "≤"
item_5/id = 5
[node name="MarginContainer" type="MarginContainer" parent="." index="2"]
layout_mode = 2
offset_left = 102.0
offset_right = 102.0
offset_bottom = 31.0
[node name="Remove" parent="." index="3"]
offset_left = 106.0
offset_right = 132.0
tooltip_text = "Remove Condition"

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

View file

@ -0,0 +1,11 @@
extends EditorInspectorPlugin
const State = preload("res://addons/imjp94.yafsm/src/states/State.gd")
func _can_handle(object):
return object is State
func _parse_property(object, type, path, hint, hint_text, usage, wide) -> bool:
return false
# Hide all property
return true

View file

@ -0,0 +1,82 @@
@tool
extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd"
const State = preload("../../src/states/State.gd")
const StateMachine = preload("../../src/states/StateMachine.gd")
signal name_edit_entered(new_name) # Emits when focused exit or Enter pressed
@onready var name_edit = $MarginContainer/NameEdit
var undo_redo
var state:
set = set_state
func _init():
super._init()
set_state(State.new())
func _ready():
name_edit.focus_exited.connect(_on_NameEdit_focus_exited)
name_edit.text_submitted.connect(_on_NameEdit_text_submitted)
set_process_input(false) # _input only required when name_edit enabled to check mouse click outside
func _draw():
if state is StateMachine:
if selected:
draw_style_box(get_theme_stylebox("nested_focus", "StateNode"), Rect2(Vector2.ZERO, size))
else:
draw_style_box(get_theme_stylebox("nested_normal", "StateNode"), Rect2(Vector2.ZERO, size))
else:
super._draw()
func _input(event):
if event is InputEventMouseButton:
if event.pressed:
# Detect click outside rect
if get_viewport().gui_get_focus_owner() == name_edit:
var local_event = make_input_local(event)
if not name_edit.get_rect().has_point(local_event.position):
name_edit.release_focus()
func enable_name_edit(v):
if v:
set_process_input(true)
name_edit.editable = true
name_edit.selecting_enabled = true
name_edit.mouse_filter = MOUSE_FILTER_PASS
mouse_default_cursor_shape = CURSOR_IBEAM
name_edit.grab_focus()
else:
set_process_input(false)
name_edit.editable = false
name_edit.selecting_enabled = false
name_edit.mouse_filter = MOUSE_FILTER_IGNORE
mouse_default_cursor_shape = CURSOR_ARROW
name_edit.release_focus()
func _on_state_name_changed(new_name):
name_edit.text = new_name
size.x = 0 # Force reset horizontal size
func _on_state_changed(new_state):
if state:
state.name_changed.connect(_on_state_name_changed)
if name_edit:
name_edit.text = state.name
func _on_NameEdit_focus_exited():
enable_name_edit(false)
name_edit.deselect()
emit_signal("name_edit_entered", name_edit.text)
func _on_NameEdit_text_submitted(new_text):
enable_name_edit(false)
emit_signal("name_edit_entered", new_text)
func set_state(s):
if state != s:
state = s
_on_state_changed(s)

View file

@ -0,0 +1,79 @@
[gd_scene load_steps=8 format=3 uid="uid://l3mqbqjwjkc3"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/state_nodes/StateNode.gd" id="2"]
[ext_resource type="SystemFont" uid="uid://dmcxm8gxsonbq" path="res://addons/imjp94.yafsm/assets/fonts/sans_serif.tres" id="2_352m3"]
[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.44, 0.73, 0.98, 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
corner_detail = 2
[sub_resource type="StyleBoxFlat" id="3"]
bg_color = Color(0.164706, 0.164706, 0.164706, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.960784, 0.772549, 0.333333, 1)
shadow_size = 2
[sub_resource type="StyleBoxFlat" id="4"]
bg_color = Color(0.164706, 0.164706, 0.164706, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
shadow_size = 2
[sub_resource type="Theme" id="5"]
FlowChartNode/styles/focus = SubResource("1")
FlowChartNode/styles/normal = SubResource("2")
StateNode/styles/nested_focus = SubResource("3")
StateNode/styles/nested_normal = SubResource("4")
[node name="StateNode" type="HBoxContainer"]
grow_horizontal = 2
grow_vertical = 2
theme = SubResource("5")
script = ExtResource("2")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
mouse_filter = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="NameEdit" type="LineEdit" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
mouse_filter = 2
mouse_default_cursor_shape = 0
theme_override_fonts/font = ExtResource("2_352m3")
text = "State"
alignment = 1
editable = false
expand_to_text_length = true
selecting_enabled = false
caret_blink = true

View file

@ -0,0 +1,185 @@
@tool
extends VBoxContainer
const Utils = preload("../../scripts/Utils.gd")
const ConditionEditor = preload("../condition_editors/ConditionEditor.tscn")
const BoolConditionEditor = preload("../condition_editors/BoolConditionEditor.tscn")
const IntegerConditionEditor = preload("../condition_editors/IntegerConditionEditor.tscn")
const FloatConditionEditor = preload("../condition_editors/FloatConditionEditor.tscn")
const StringConditionEditor = preload("../condition_editors/StringConditionEditor.tscn")
@onready var header = $HeaderContainer/Header
@onready var title = $HeaderContainer/Header/Title
@onready var title_icon = $HeaderContainer/Header/Title/Icon
@onready var from = $HeaderContainer/Header/Title/From
@onready var to = $HeaderContainer/Header/Title/To
@onready var condition_count_icon = $HeaderContainer/Header/ConditionCount/Icon
@onready var condition_count_label = $HeaderContainer/Header/ConditionCount/Label
@onready var priority_icon = $HeaderContainer/Header/Priority/Icon
@onready var priority_spinbox = $HeaderContainer/Header/Priority/SpinBox
@onready var add = $HeaderContainer/Header/HBoxContainer/Add
@onready var add_popup_menu = $HeaderContainer/Header/HBoxContainer/Add/PopupMenu
@onready var content_container = $MarginContainer
@onready var condition_list = $MarginContainer/Conditions
var undo_redo
var transition:
set = set_transition
var _to_free
func _init():
_to_free = []
func _ready():
header.gui_input.connect(_on_header_gui_input)
priority_spinbox.value_changed.connect(_on_priority_spinbox_value_changed)
add.pressed.connect(_on_add_pressed)
add_popup_menu.index_pressed.connect(_on_add_popup_menu_index_pressed)
condition_count_icon.texture = get_theme_icon("MirrorX", "EditorIcons")
priority_icon.texture = get_theme_icon("AnimationTrackGroup", "EditorIcons")
func _exit_tree():
free_node_from_undo_redo() # Managed by EditorInspector
func _on_header_gui_input(event):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
toggle_conditions()
func _on_priority_spinbox_value_changed(val: int) -> void:
set_priority(val)
func _on_add_pressed():
Utils.popup_on_target(add_popup_menu, add)
func _on_add_popup_menu_index_pressed(index):
## Handle condition name duplication (4.x changed how duplicates are
## automatically handled and gave a random index instead of a progressive one)
var default_new_condition_name = "Param"
var condition_dup_index = 0
var new_name = default_new_condition_name
for condition_editor in condition_list.get_children():
var condition_name = condition_editor.condition.name
if (condition_name == new_name):
condition_dup_index += 1
new_name = "%s%s" % [default_new_condition_name, condition_dup_index]
var condition
match index:
0: # Trigger
condition = Condition.new(new_name)
1: # Boolean
condition = BooleanCondition.new(new_name)
2: # Integer
condition = IntegerCondition.new(new_name)
3: # Float
condition = FloatCondition.new(new_name)
4: # String
condition = StringCondition.new(new_name)
_:
push_error("Unexpected index(%d) from PopupMenu" % index)
var editor = create_condition_editor(condition)
add_condition_editor_action(editor, condition)
func _on_ConditionEditorRemove_pressed(editor):
remove_condition_editor_action(editor)
func _on_transition_changed(new_transition):
if not new_transition:
return
for condition in transition.conditions.values():
var editor = create_condition_editor(condition)
add_condition_editor(editor, condition)
update_title()
update_condition_count()
update_priority_spinbox_value()
func _on_condition_editor_added(editor):
editor.undo_redo = undo_redo
if not editor.remove.pressed.is_connected(_on_ConditionEditorRemove_pressed):
editor.remove.pressed.connect(_on_ConditionEditorRemove_pressed.bind(editor))
transition.add_condition(editor.condition)
update_condition_count()
func add_condition_editor(editor, condition):
condition_list.add_child(editor)
editor.condition = condition # Must be assigned after enter tree, as assignment would trigger ui code
_on_condition_editor_added(editor)
func remove_condition_editor(editor):
transition.remove_condition(editor.condition.name)
condition_list.remove_child(editor)
_to_free.append(editor) # Freeing immediately after removal will break undo/redo
update_condition_count()
func update_title():
from.text = transition.from
to.text = transition.to
func update_condition_count():
var count = transition.conditions.size()
condition_count_label.text = str(count)
if count == 0:
hide_conditions()
else:
show_conditions()
func update_priority_spinbox_value():
priority_spinbox.value = transition.priority
priority_spinbox.apply()
func set_priority(value):
transition.priority = value
func show_conditions():
content_container.visible = true
func hide_conditions():
content_container.visible = false
func toggle_conditions():
content_container.visible = !content_container.visible
func create_condition_editor(condition):
var editor
if condition is BooleanCondition:
editor = BoolConditionEditor.instantiate()
elif condition is IntegerCondition:
editor = IntegerConditionEditor.instantiate()
elif condition is FloatCondition:
editor = FloatConditionEditor.instantiate()
elif condition is StringCondition:
editor = StringConditionEditor.instantiate()
else:
editor = ConditionEditor.instantiate()
return editor
func add_condition_editor_action(editor, condition):
undo_redo.create_action("Add Transition Condition")
undo_redo.add_do_method(self, "add_condition_editor", editor, condition)
undo_redo.add_undo_method(self, "remove_condition_editor", editor)
undo_redo.commit_action()
func remove_condition_editor_action(editor):
undo_redo.create_action("Remove Transition Condition")
undo_redo.add_do_method(self, "remove_condition_editor", editor)
undo_redo.add_undo_method(self, "add_condition_editor", editor, editor.condition)
undo_redo.commit_action()
func set_transition(t):
if transition != t:
transition = t
_on_transition_changed(t)
# Free nodes cached in UndoRedo stack
func free_node_from_undo_redo():
for node in _to_free:
if is_instance_valid(node):
var history_id = undo_redo.get_object_history_id(node)
undo_redo.get_history_undo_redo(history_id).clear_history(false) # TODO: Should be handled by plugin.gd (Temporary solution as only TransitionEditor support undo/redo)
node.queue_free()
_to_free.clear()

View file

@ -0,0 +1,133 @@
[gd_scene load_steps=7 format=3 uid="uid://dw0ecw2wdeosi"]
[ext_resource type="Texture2D" uid="uid://dg8cmn5ubq6r5" path="res://addons/imjp94.yafsm/assets/icons/add-white-18dp.svg" id="1"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/transition_editors/TransitionEditor.gd" id="3"]
[sub_resource type="Gradient" id="Gradient_hw7k8"]
offsets = PackedFloat32Array(1)
colors = PackedColorArray(1, 1, 1, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_ipxab"]
gradient = SubResource("Gradient_hw7k8")
width = 18
height = 18
[sub_resource type="Image" id="Image_o35y7"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_v636r"]
image = SubResource("Image_o35y7")
[node name="TransitionEditor" type="VBoxContainer"]
script = ExtResource("3")
[node name="HeaderContainer" type="MarginContainer" parent="."]
layout_mode = 2
[node name="Panel" type="Panel" parent="HeaderContainer"]
layout_mode = 2
[node name="Header" type="HBoxContainer" parent="HeaderContainer"]
layout_mode = 2
[node name="Title" type="HBoxContainer" parent="HeaderContainer/Header"]
layout_mode = 2
tooltip_text = "Next State"
[node name="From" type="Label" parent="HeaderContainer/Header/Title"]
layout_mode = 2
size_flags_horizontal = 3
text = "From"
[node name="Icon" type="TextureRect" parent="HeaderContainer/Header/Title"]
texture_filter = 1
layout_mode = 2
texture = SubResource("GradientTexture2D_ipxab")
expand_mode = 3
stretch_mode = 3
[node name="To" type="Label" parent="HeaderContainer/Header/Title"]
layout_mode = 2
size_flags_horizontal = 3
text = "To"
[node name="VSeparator" type="VSeparator" parent="HeaderContainer/Header"]
layout_mode = 2
[node name="ConditionCount" type="HBoxContainer" parent="HeaderContainer/Header"]
layout_mode = 2
tooltip_text = "Number of Conditions"
[node name="Icon" type="TextureRect" parent="HeaderContainer/Header/ConditionCount"]
texture_filter = 1
layout_mode = 2
texture = SubResource("ImageTexture_v636r")
expand_mode = 3
stretch_mode = 3
[node name="Label" type="Label" parent="HeaderContainer/Header/ConditionCount"]
layout_mode = 2
text = "No."
[node name="VSeparator2" type="VSeparator" parent="HeaderContainer/Header"]
layout_mode = 2
[node name="Priority" type="HBoxContainer" parent="HeaderContainer/Header"]
layout_mode = 2
tooltip_text = "Priority"
[node name="Icon" type="TextureRect" parent="HeaderContainer/Header/Priority"]
texture_filter = 1
layout_mode = 2
texture = SubResource("ImageTexture_v636r")
expand_mode = 3
stretch_mode = 3
[node name="SpinBox" type="SpinBox" parent="HeaderContainer/Header/Priority"]
layout_mode = 2
max_value = 10.0
rounded = true
allow_greater = true
[node name="VSeparator3" type="VSeparator" parent="HeaderContainer/Header"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="HeaderContainer/Header"]
layout_mode = 2
size_flags_horizontal = 10
[node name="Add" type="Button" parent="HeaderContainer/Header/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add Condition"
icon = ExtResource("1")
flat = true
[node name="PopupMenu" type="PopupMenu" parent="HeaderContainer/Header/HBoxContainer/Add"]
item_count = 5
item_0/text = "Trigger"
item_0/id = 0
item_1/text = "Boolean"
item_1/id = 1
item_2/text = "Integer"
item_2/id = 2
item_3/text = "Float"
item_3/id = 3
item_4/text = "String"
item_4/id = 4
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
[node name="Panel" type="Panel" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Conditions" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2

View file

@ -0,0 +1,33 @@
@tool
extends EditorInspectorPlugin
const Transition = preload("res://addons/imjp94.yafsm/src/transitions/Transition.gd")
const TransitionEditor = preload("res://addons/imjp94.yafsm/scenes/transition_editors/TransitionEditor.tscn")
var undo_redo
var transition_icon
func _can_handle(object):
return object is Transition
func _parse_property(object, type, path, hint, hint_text, usage, wide) -> bool:
match path:
"from":
return true
"to":
return true
"conditions":
var transition_editor = TransitionEditor.instantiate() # Will be freed by editor
transition_editor.undo_redo = undo_redo
add_custom_control(transition_editor)
transition_editor.ready.connect(_on_transition_editor_tree_entered.bind(transition_editor, object))
return true
"priority":
return true
return false
func _on_transition_editor_tree_entered(editor, transition):
editor.transition = transition
if transition_icon:
editor.title_icon.texture = transition_icon

View file

@ -0,0 +1,112 @@
@tool
extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.gd"
const Transition = preload("../../src/transitions/Transition.gd")
const ValueCondition = preload("../../src/conditions/ValueCondition.gd")
const hi_res_font: Font = preload("res://addons/imjp94.yafsm/assets/fonts/sans_serif.tres")
@export var upright_angle_range: = 5.0
@onready var label_margin = $MarginContainer
@onready var vbox = $MarginContainer/VBoxContainer
var undo_redo
var transition:
set = set_transition
var template = "{condition_name} {condition_comparation} {condition_value}"
var _template_var = {}
func _init():
super._init()
set_transition(Transition.new())
func _draw():
super._draw()
var abs_rotation = abs(rotation)
var is_flip = abs_rotation > deg_to_rad(90.0)
var is_upright = (abs_rotation > (deg_to_rad(90.0) - deg_to_rad(upright_angle_range))) and (abs_rotation < (deg_to_rad(90.0) + deg_to_rad(upright_angle_range)))
if is_upright:
var x_offset = label_margin.size.x / 2
var y_offset = -label_margin.size.y
label_margin.position = Vector2((size.x - x_offset) / 2, 0)
else:
var x_offset = label_margin.size.x
var y_offset = -label_margin.size.y
if is_flip:
label_margin.rotation = deg_to_rad(180)
label_margin.position = Vector2((size.x + x_offset) / 2, 0)
else:
label_margin.rotation = deg_to_rad(0)
label_margin.position = Vector2((size.x - x_offset) / 2, y_offset)
# Update overlay text
func update_label():
if transition:
var template_var = {"condition_name": "", "condition_comparation": "", "condition_value": null}
for label in vbox.get_children():
if not (str(label.name) in transition.conditions.keys()): # Names of nodes are now of type StringName, not simple strings!
vbox.remove_child(label)
label.queue_free()
for condition in transition.conditions.values():
var label = vbox.get_node_or_null(NodePath(condition.name))
if not label:
label = Label.new()
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.add_theme_font_override("font", hi_res_font)
label.name = condition.name
vbox.add_child(label)
if "value" in condition:
template_var["condition_name"] = condition.name
template_var["condition_comparation"] = ValueCondition.COMPARATION_SYMBOLS[condition.comparation]
template_var["condition_value"] = condition.get_value_string()
label.text = template.format(template_var)
var override_template_var = _template_var.get(condition.name)
if override_template_var:
label.text = label.text.format(override_template_var)
else:
label.text = condition.name
queue_redraw()
func _on_transition_changed(new_transition):
if not is_inside_tree():
return
if new_transition:
new_transition.condition_added.connect(_on_transition_condition_added)
new_transition.condition_removed.connect(_on_transition_condition_removed)
for condition in new_transition.conditions.values():
condition.name_changed.connect(_on_condition_name_changed)
condition.display_string_changed.connect(_on_condition_display_string_changed)
update_label()
func _on_transition_condition_added(condition):
condition.name_changed.connect(_on_condition_name_changed)
condition.display_string_changed.connect(_on_condition_display_string_changed)
update_label()
func _on_transition_condition_removed(condition):
condition.name_changed.disconnect(_on_condition_name_changed)
condition.display_string_changed.disconnect(_on_condition_display_string_changed)
update_label()
func _on_condition_name_changed(from, to):
var label = vbox.get_node_or_null(NodePath(from))
if label:
label.name = to
update_label()
func _on_condition_display_string_changed(display_string):
update_label()
func set_transition(t):
if transition != t:
if transition:
if transition.condition_added.is_connected(_on_transition_condition_added):
transition.condition_added.disconnect(_on_transition_condition_added)
transition = t
_on_transition_changed(transition)

View file

@ -0,0 +1,26 @@
[gd_scene load_steps=4 format=3 uid="uid://cwb2nrjai7fao"]
[ext_resource type="PackedScene" uid="uid://creoglbeckyhs" path="res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.tscn" id="1"]
[ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/transition_editors/TransitionLine.gd" id="2"]
[ext_resource type="SystemFont" uid="uid://dmcxm8gxsonbq" path="res://addons/imjp94.yafsm/assets/fonts/sans_serif.tres" id="3_y6xyv"]
[node name="TransitionLine" instance=ExtResource("1")]
script = ExtResource("2")
upright_angle_range = 0.0
[node name="MarginContainer" type="MarginContainer" parent="." index="0"]
layout_mode = 2
mouse_filter = 2
[node name="Label" type="Label" parent="MarginContainer" index="0"]
visible = false
layout_mode = 2
size_flags_horizontal = 6
size_flags_vertical = 6
theme_override_fonts/font = ExtResource("3_y6xyv")
text = "Transition"
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" index="1"]
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4