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