Code stuff
1
.plugged/Asset-Drawer
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit f29aac8fb01cab02a76758aa49d355079ac99825
|
1
.plugged/UIDesignTool
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 0e3bdbbfe966a4ff27ab9b711eff55a049968fd9
|
1
.plugged/gd-YAFSM
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 5b220c7b5bb070f22ca8874198a553a198adab42
|
1
.plugged/gd-blender-3d-shortcuts
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit ff9cf37f3d813745d871ba835be758874d2faac8
|
|
@ -1,3 +1,74 @@
|
|||
[plugin]
|
||||
|
||||
installed={}
|
||||
installed={
|
||||
"Asset-Drawer": {
|
||||
"branch": "",
|
||||
"commit": "",
|
||||
"dest_files": ["res://addons/Asset_Drawer/LICENSE", "res://addons/Asset_Drawer/plugin.cfg", "res://addons/Asset_Drawer/AssetDrawerShortcut.tres", "res://addons/Asset_Drawer/FileSystem.gd"],
|
||||
"dev": true,
|
||||
"exclude": [],
|
||||
"include": [],
|
||||
"install_root": "",
|
||||
"name": "Asset-Drawer",
|
||||
"on_updated": "",
|
||||
"plug_dir": "res://.plugged/Asset-Drawer",
|
||||
"tag": "",
|
||||
"url": "https://git::@github.com/newjoker6/Asset-Drawer.git"
|
||||
},
|
||||
"UIDesignTool": {
|
||||
"branch": "",
|
||||
"commit": "",
|
||||
"dest_files": ["res://addons/ui_design_tool/scripts/Utils.gd", "res://addons/ui_design_tool/scripts/FontManager.gd", "res://addons/ui_design_tool/plugin.cfg", "res://addons/ui_design_tool/scenes/OverlayTextEdit.gd", "res://addons/ui_design_tool/scenes/Toolbar.tscn", "res://addons/ui_design_tool/scenes/OverlayTextEdit.tscn", "res://addons/ui_design_tool/scenes/Toolbar.gd", "res://addons/ui_design_tool/plugin.gd", "res://addons/ui_design_tool/assets/icons/folder_open-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format_color_reset-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format_align_left-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format_italic-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/vertical_align_top-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/vertical_align_center-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/format_align_left-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/folder_open-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/format_align_center-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format_italic-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format-color-text.png.import", "res://addons/ui_design_tool/assets/icons/more_horiz-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format_align_center-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/vertical_align_top-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/vertical_align_bottom-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/more_vert-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/refresh-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/more_horiz-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/format_underlined-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/format_bold-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/vertical_align_bottom-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/format_underlined-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format_align_right-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/marker.png", "res://addons/ui_design_tool/assets/icons/format_clear-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/photo_size_select_small-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format-color-text.png", "res://addons/ui_design_tool/assets/icons/format_align_right-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/format_color_reset-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/vertical_align_center-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/format_bold-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/photo_size_select_small-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/more_vert-white-18dp.svg.import", "res://addons/ui_design_tool/assets/icons/marker.png.import", "res://addons/ui_design_tool/assets/icons/format_clear-white-18dp.svg", "res://addons/ui_design_tool/assets/icons/refresh-white-18dp.svg.import"],
|
||||
"dev": true,
|
||||
"exclude": [],
|
||||
"include": [],
|
||||
"install_root": "",
|
||||
"name": "UIDesignTool",
|
||||
"on_updated": "",
|
||||
"plug_dir": "res://.plugged/UIDesignTool",
|
||||
"tag": "",
|
||||
"url": "https://git::@github.com/imjp94/UIDesignTool.git"
|
||||
},
|
||||
"gd-YAFSM": {
|
||||
"branch": "",
|
||||
"commit": "",
|
||||
"dest_files": ["res://addons/imjp94.yafsm/YAFSM.gd", "res://addons/imjp94.yafsm/src/StackPlayer.gd", "res://addons/imjp94.yafsm/src/transitions/Transition.gd", "res://addons/imjp94.yafsm/src/StateDirectory.gd", "res://addons/imjp94.yafsm/src/states/State.gd", "res://addons/imjp94.yafsm/src/states/StateMachine.gd", "res://addons/imjp94.yafsm/src/StateMachinePlayer.gd", "res://addons/imjp94.yafsm/src/debugger/StackPlayerDebugger.gd", "res://addons/imjp94.yafsm/src/debugger/StackItem.tscn", "res://addons/imjp94.yafsm/src/debugger/StackPlayerDebugger.tscn", "res://addons/imjp94.yafsm/src/conditions/ValueCondition.gd", "res://addons/imjp94.yafsm/src/conditions/BooleanCondition.gd", "res://addons/imjp94.yafsm/src/conditions/StringCondition.gd", "res://addons/imjp94.yafsm/src/conditions/IntegerCondition.gd", "res://addons/imjp94.yafsm/src/conditions/Condition.gd", "res://addons/imjp94.yafsm/src/conditions/FloatCondition.gd", "res://addons/imjp94.yafsm/README.md", "res://addons/imjp94.yafsm/scripts/Utils.gd", "res://addons/imjp94.yafsm/plugin.cfg", "res://addons/imjp94.yafsm/scenes/StateMachineEditor.tscn", "res://addons/imjp94.yafsm/scenes/StateMachineEditorLayer.gd", "res://addons/imjp94.yafsm/scenes/condition_editors/StringConditionEditor.gd", "res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn", "res://addons/imjp94.yafsm/scenes/condition_editors/StringConditionEditor.tscn", "res://addons/imjp94.yafsm/scenes/condition_editors/BoolConditionEditor.gd", "res://addons/imjp94.yafsm/scenes/condition_editors/ConditionEditor.tscn", "res://addons/imjp94.yafsm/scenes/condition_editors/FloatConditionEditor.tscn", "res://addons/imjp94.yafsm/scenes/condition_editors/ConditionEditor.gd", "res://addons/imjp94.yafsm/scenes/condition_editors/FloatConditionEditor.gd", "res://addons/imjp94.yafsm/scenes/condition_editors/IntegerConditionEditor.tscn", "res://addons/imjp94.yafsm/scenes/condition_editors/IntegerConditionEditor.gd", "res://addons/imjp94.yafsm/scenes/condition_editors/BoolConditionEditor.tscn", "res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.gd", "res://addons/imjp94.yafsm/scenes/ContextMenu.tscn", "res://addons/imjp94.yafsm/scenes/ParametersPanel.gd", "res://addons/imjp94.yafsm/scenes/PathViewer.gd", "res://addons/imjp94.yafsm/scenes/transition_editors/TransitionLine.tscn", "res://addons/imjp94.yafsm/scenes/transition_editors/TransitionInspector.gd", "res://addons/imjp94.yafsm/scenes/transition_editors/TransitionEditor.tscn", "res://addons/imjp94.yafsm/scenes/transition_editors/TransitionLine.gd", "res://addons/imjp94.yafsm/scenes/transition_editors/TransitionEditor.gd", "res://addons/imjp94.yafsm/scenes/state_nodes/StateInspector.gd", "res://addons/imjp94.yafsm/scenes/state_nodes/StateNode.tscn", "res://addons/imjp94.yafsm/scenes/state_nodes/StateNode.gd", "res://addons/imjp94.yafsm/scenes/StateNodeContextMenu.tscn", "res://addons/imjp94.yafsm/scenes/StateMachineEditor.gd", "res://addons/imjp94.yafsm/scenes/flowchart/FlowChart.gd", "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.tscn", "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.tscn", "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLayer.gd", "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.gd", "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartGrid.gd", "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd", "res://addons/imjp94.yafsm/plugin.gd", "res://addons/imjp94.yafsm/assets/fonts/sans_serif.tres", "res://addons/imjp94.yafsm/assets/icons/compare_arrows-white-18dp.svg.import", "res://addons/imjp94.yafsm/assets/icons/state_machine_player_icon.png.import", "res://addons/imjp94.yafsm/assets/icons/close-white-18dp.svg.import", "res://addons/imjp94.yafsm/assets/icons/stack_player_icon.png", "res://addons/imjp94.yafsm/assets/icons/close-white-18dp.svg", "res://addons/imjp94.yafsm/assets/icons/add-white-18dp.svg", "res://addons/imjp94.yafsm/assets/icons/state_machine_icon.png.import", "res://addons/imjp94.yafsm/assets/icons/remove-white-18dp.svg.import", "res://addons/imjp94.yafsm/assets/icons/state_machine_icon.png", "res://addons/imjp94.yafsm/assets/icons/stack_player_icon.png.import", "res://addons/imjp94.yafsm/assets/icons/state_machine_player_icon.png", "res://addons/imjp94.yafsm/assets/icons/arrow_right-white-18dp.svg.import", "res://addons/imjp94.yafsm/assets/icons/arrow_right-white-18dp.svg", "res://addons/imjp94.yafsm/assets/icons/remove-white-18dp.svg", "res://addons/imjp94.yafsm/assets/icons/subdirectory_arrow_right-white-18dp.svg.import", "res://addons/imjp94.yafsm/assets/icons/add-white-18dp.svg.import", "res://addons/imjp94.yafsm/assets/icons/compare_arrows-white-18dp.svg", "res://addons/imjp94.yafsm/assets/icons/subdirectory_arrow_right-white-18dp.svg"],
|
||||
"dev": false,
|
||||
"exclude": [],
|
||||
"include": [],
|
||||
"install_root": "",
|
||||
"name": "gd-YAFSM",
|
||||
"on_updated": "",
|
||||
"plug_dir": "res://.plugged/gd-YAFSM",
|
||||
"tag": "",
|
||||
"url": "https://git::@github.com/imjp94/gd-YAFSM.git"
|
||||
},
|
||||
"gd-blender-3d-shortcuts": {
|
||||
"branch": "",
|
||||
"commit": "",
|
||||
"dest_files": ["res://addons/gd-blender-3d-shortcuts/Utils.gd", "res://addons/gd-blender-3d-shortcuts/plugin.cfg", "res://addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenuGroup.gd", "res://addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenuGroup.tscn", "res://addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.gd", "res://addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.tscn", "res://addons/gd-blender-3d-shortcuts/plugin.gd"],
|
||||
"dev": true,
|
||||
"exclude": [],
|
||||
"include": [],
|
||||
"install_root": "",
|
||||
"name": "gd-blender-3d-shortcuts",
|
||||
"on_updated": "",
|
||||
"plug_dir": "res://.plugged/gd-blender-3d-shortcuts",
|
||||
"tag": "",
|
||||
"url": "https://git::@github.com/imjp94/gd-blender-3d-shortcuts.git"
|
||||
},
|
||||
"loggie": {
|
||||
"branch": "",
|
||||
"commit": "",
|
||||
"dest_files": ["res://addons/loggie/tools/loggie_tools.gd", "res://addons/loggie/tools/loggie_system_specs.gd", "res://addons/loggie/tools/loggie_enums.gd", "res://addons/loggie/loggie_settings.gd", "res://addons/loggie/custom_settings.gd.example", "res://addons/loggie/plugin.cfg", "res://addons/loggie/plugin.gd", "res://addons/loggie/loggie_message.gd", "res://addons/loggie/loggie.gd", "res://addons/loggie/assets/icon.png", "res://addons/loggie/assets/icon.png.import", "res://addons/loggie/assets/logo.png", "res://addons/loggie/assets/logo.png.import"],
|
||||
"dev": false,
|
||||
"exclude": [],
|
||||
"include": ["addons/"],
|
||||
"install_root": "",
|
||||
"name": "loggie",
|
||||
"on_updated": "",
|
||||
"plug_dir": "res://.plugged/loggie",
|
||||
"tag": "",
|
||||
"url": "https://git::@github.com/Shiva-Shadowsong/loggie.git"
|
||||
}
|
||||
}
|
||||
|
|
1
.plugged/loggie
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 290d4fbe75b1103efab6fefab3dbf62b68022d84
|
7
addons/Asset_Drawer/AssetDrawerShortcut.tres
Normal file
|
@ -0,0 +1,7 @@
|
|||
[gd_resource type="InputEventKey" format=3 uid="uid://bafyb8y38ahfh"]
|
||||
|
||||
[resource]
|
||||
device = -1
|
||||
ctrl_pressed = true
|
||||
keycode = 32
|
||||
unicode = 32
|
117
addons/Asset_Drawer/FileSystem.gd
Normal file
|
@ -0,0 +1,117 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
## The root scene
|
||||
const ROOT: StringName = &"root"
|
||||
## Padding from the bottom when popped out
|
||||
const PADDING: int = 20
|
||||
## Padding from the bottom when not popped out
|
||||
const BOTTOM_PADDING: int = 60
|
||||
## Minimum height of the dock
|
||||
const MIN_HEIGHT: int = 50
|
||||
|
||||
## The file system
|
||||
var file_dock: FileSystemDock = null
|
||||
|
||||
var file_split_container: SplitContainer = null
|
||||
var file_tree: Tree = null
|
||||
var file_container: VBoxContainer = null
|
||||
var asset_drawer_shortcut: InputEventKey = InputEventKey.new()
|
||||
|
||||
## Toggle for when the file system is moved to bottom
|
||||
var files_bottom: bool = false
|
||||
var new_size: Vector2
|
||||
var initial_load: bool = false
|
||||
var showing: bool = false
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# Add tool button to move shelf to editor bottom
|
||||
add_tool_menu_item("Files to Bottom", files_to_bottom)
|
||||
|
||||
init_file_dock()
|
||||
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
files_to_bottom()
|
||||
|
||||
# Prevent file tree from being shrunk on load
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
file_split_container.split_offset = 175
|
||||
|
||||
# Get shortcut
|
||||
asset_drawer_shortcut = preload("res://addons/Asset_Drawer/AssetDrawerShortcut.tres") as InputEventKey
|
||||
|
||||
func init_file_dock() -> void:
|
||||
# Get our file system
|
||||
file_dock = EditorInterface.get_file_system_dock()
|
||||
file_split_container = file_dock.get_child(3) as SplitContainer
|
||||
file_tree = file_split_container.get_child(0) as Tree
|
||||
file_container = file_split_container.get_child(1) as VBoxContainer
|
||||
|
||||
#region show hide filesystem
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not files_bottom:
|
||||
return
|
||||
|
||||
if asset_drawer_shortcut.is_match(event) and event.is_pressed() and not event.is_echo():
|
||||
if showing:
|
||||
hide_bottom_panel()
|
||||
else:
|
||||
make_bottom_panel_item_visible(file_dock)
|
||||
|
||||
showing = not showing
|
||||
#endregion
|
||||
|
||||
func _exit_tree() -> void:
|
||||
remove_tool_menu_item("Files to Bottom")
|
||||
files_to_bottom()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
var window := file_dock.get_window()
|
||||
new_size = window.size
|
||||
|
||||
# Keeps the file system from being unusable in size
|
||||
if window.name == ROOT and not files_bottom:
|
||||
file_tree.size.y = new_size.y - PADDING
|
||||
file_container.size.y = new_size.y - PADDING
|
||||
return
|
||||
|
||||
# Adjust the size of the file system based on how far up
|
||||
# the drawer has been pulled
|
||||
if window.name == ROOT and files_bottom:
|
||||
var dock_container := file_dock.get_parent() as Control
|
||||
new_size = dock_container.size
|
||||
var editorsettings := EditorInterface.get_editor_settings()
|
||||
var fontsize: int = editorsettings.get_setting("interface/editor/main_font_size")
|
||||
var editorscale := EditorInterface.get_editor_scale()
|
||||
|
||||
file_tree.size.y = new_size.y - (fontsize * 2) - (BOTTOM_PADDING * editorscale)
|
||||
file_container.size.y = new_size.y - (fontsize * 2) - (BOTTOM_PADDING * editorscale)
|
||||
return
|
||||
|
||||
# Keeps our systems sized when popped out
|
||||
if window.name != ROOT and not files_bottom:
|
||||
window.min_size.y = MIN_HEIGHT
|
||||
file_tree.size.y = new_size.y - PADDING
|
||||
file_container.size.y = new_size.y - PADDING
|
||||
|
||||
# Centers window on first pop
|
||||
if not initial_load:
|
||||
initial_load = true
|
||||
var screenSize: Vector2 = DisplayServer.screen_get_size()
|
||||
window.position = screenSize / 2
|
||||
|
||||
|
||||
# Moves the files between the bottom panel and the original dock
|
||||
func files_to_bottom() -> void:
|
||||
if files_bottom:
|
||||
remove_control_from_bottom_panel(file_dock)
|
||||
add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_BR, file_dock)
|
||||
files_bottom = false
|
||||
return
|
||||
|
||||
init_file_dock()
|
||||
remove_control_from_docks(file_dock)
|
||||
add_control_to_bottom_panel(file_dock, "File System")
|
||||
files_bottom = true
|
21
addons/Asset_Drawer/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Michael McGuire
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
7
addons/Asset_Drawer/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="Asset Drawer"
|
||||
description="Converts the File dock to an Asset Drawer at the bottom of the editor."
|
||||
author="GlitchedCode"
|
||||
version=""
|
||||
script="FileSystem.gd"
|
188
addons/gd-blender-3d-shortcuts/Utils.gd
Normal file
|
@ -0,0 +1,188 @@
|
|||
static func apply_transform(nodes, transform, cache_global_transforms):
|
||||
var i = 0
|
||||
for node in nodes:
|
||||
var cache_global_transform = cache_global_transforms[i]
|
||||
node.global_transform.origin = cache_global_transform.origin
|
||||
node.global_transform.origin += cache_global_transform.basis.get_rotation_quaternion() * transform.origin
|
||||
node.global_transform.basis.x = cache_global_transform.basis * transform.basis.x
|
||||
node.global_transform.basis.y = cache_global_transform.basis * transform.basis.y
|
||||
node.global_transform.basis.z = cache_global_transform.basis * transform.basis.z
|
||||
i += 1
|
||||
|
||||
static func apply_global_transform(nodes, transform, cache_transforms):
|
||||
var i = 0
|
||||
for node in nodes:
|
||||
node.global_transform = transform * cache_transforms[i]
|
||||
i += 1
|
||||
|
||||
static func revert_transform(nodes, cache_global_transforms):
|
||||
var i = 0
|
||||
for node in nodes:
|
||||
node.global_transform = cache_global_transforms[i]
|
||||
i += 1
|
||||
|
||||
static func reset_translation(nodes):
|
||||
for node in nodes:
|
||||
node.transform.origin = Vector3.ZERO
|
||||
|
||||
static func reset_rotation(nodes):
|
||||
for node in nodes:
|
||||
var scale = node.transform.basis.get_scale()
|
||||
node.transform.basis = Basis().scaled(scale)
|
||||
|
||||
static func reset_scale(nodes):
|
||||
for node in nodes:
|
||||
var quat = node.transform.basis.get_rotation_quaternion()
|
||||
node.transform.basis = Basis(quat)
|
||||
|
||||
static func hide_nodes(nodes, is_hide=true):
|
||||
for node in nodes:
|
||||
node.visible = !is_hide
|
||||
|
||||
static func recursive_get_children(node):
|
||||
var children = node.get_children()
|
||||
if children.size() == 0:
|
||||
return []
|
||||
else:
|
||||
for child in children:
|
||||
children += recursive_get_children(child)
|
||||
return children
|
||||
|
||||
static func get_spatial_editor(base_control):
|
||||
var children = recursive_get_children(base_control)
|
||||
for child in children:
|
||||
if child.get_class() == "Node3DEditor":
|
||||
return child
|
||||
|
||||
static func get_spatial_editor_viewport_container(spatial_editor):
|
||||
var children = recursive_get_children(spatial_editor)
|
||||
for child in children:
|
||||
if child.get_class() == "Node3DEditorViewportContainer":
|
||||
return child
|
||||
|
||||
static func get_spatial_editor_viewports(spatial_editor_viewport):
|
||||
var children = recursive_get_children(spatial_editor_viewport)
|
||||
var spatial_editor_viewports = []
|
||||
for child in children:
|
||||
if child.get_class() == "Node3DEditorViewport":
|
||||
spatial_editor_viewports.append(child)
|
||||
return spatial_editor_viewports
|
||||
|
||||
static func get_spatial_editor_viewport_viewport(spatial_editor_viewport):
|
||||
var children = recursive_get_children(spatial_editor_viewport)
|
||||
for child in children:
|
||||
if child.get_class() == "SubViewport":
|
||||
return child
|
||||
|
||||
static func get_spatial_editor_viewport_control(spatial_editor_viewport):
|
||||
var children = recursive_get_children(spatial_editor_viewport)
|
||||
for child in children:
|
||||
if child.get_class() == "Control":
|
||||
return child
|
||||
|
||||
static func get_focused_spatial_editor_viewport(spatial_editor_viewports):
|
||||
for viewport in spatial_editor_viewports:
|
||||
var viewport_control = get_spatial_editor_viewport_control(viewport)
|
||||
if viewport_control.get_rect().has_point(viewport_control.get_local_mouse_position()):
|
||||
return viewport
|
||||
|
||||
static func get_snap_dialog(spatial_editor):
|
||||
var children = recursive_get_children(spatial_editor)
|
||||
for child in children:
|
||||
if child.get_class() == "ConfirmationDialog":
|
||||
if child.title == "Snap Settings":
|
||||
return child
|
||||
|
||||
static func get_snap_dialog_line_edits(snap_dialog):
|
||||
var line_edits = []
|
||||
for child in recursive_get_children(snap_dialog):
|
||||
if child.get_class() == "LineEdit":
|
||||
line_edits.append(child)
|
||||
return line_edits
|
||||
|
||||
static func get_spatial_editor_local_space_button(spatial_editor):
|
||||
var children = recursive_get_children(spatial_editor)
|
||||
for child in children:
|
||||
if child.get_class() == "Button":
|
||||
if child.shortcut:
|
||||
if child.shortcut.get_as_text() == OS.get_keycode_string(KEY_T):# TODO: Check if user has custom shortcut
|
||||
return child
|
||||
|
||||
static func get_spatial_editor_snap_button(spatial_editor):
|
||||
var children = recursive_get_children(spatial_editor)
|
||||
for child in children:
|
||||
if child.get_class() == "Button":
|
||||
if child.shortcut:
|
||||
if child.shortcut.get_as_text() == OS.get_keycode_string(KEY_Y):# TODO: Check if user has custom shortcut
|
||||
return child
|
||||
|
||||
static func project_on_plane(camera, screen_point, plane):
|
||||
var from = camera.project_ray_origin(screen_point)
|
||||
var dir = camera.project_ray_normal(screen_point)
|
||||
var intersection = plane.intersects_ray(from, dir)
|
||||
return intersection if intersection else Vector3.ZERO
|
||||
|
||||
static func transform_to_plane(t):
|
||||
var a = t.basis.x
|
||||
var b = t.basis.z
|
||||
var c = a + b
|
||||
var o = t.origin
|
||||
return Plane(a + o, b + o, c + o)
|
||||
|
||||
# Return new position when out of bounds
|
||||
static func infinite_rect(rect, from, to):
|
||||
# Clamp from position to rect first, so it won't hit current side
|
||||
from = Vector2(clamp(from.x, rect.position.x + 2, rect.size.x - 2), clamp(from.y, rect.position.y + 2, rect.size.y - 2))
|
||||
# Intersect with sides of rect
|
||||
var intersection
|
||||
# Top
|
||||
intersection = Geometry2D.segment_intersects_segment(rect.position, Vector2(rect.size.x, rect.position.y), from, to)
|
||||
if intersection:
|
||||
return intersection
|
||||
# Left
|
||||
intersection = Geometry2D.segment_intersects_segment(rect.position, Vector2(rect.position.x, rect.size.y), from, to)
|
||||
if intersection:
|
||||
return intersection
|
||||
# Right
|
||||
intersection = Geometry2D.segment_intersects_segment(rect.size, Vector2(rect.size.x, rect.position.y), from, to)
|
||||
if intersection:
|
||||
return intersection
|
||||
# Bottom
|
||||
intersection = Geometry2D.segment_intersects_segment(rect.size, Vector2(rect.position.x, rect.size.y), from, to)
|
||||
if intersection:
|
||||
return intersection
|
||||
return null
|
||||
|
||||
static func draw_axis(im, origin, axis, length, color):
|
||||
var from = origin + (-axis * length / 2)
|
||||
var to = origin + (axis * length / 2)
|
||||
im.surface_begin(Mesh.PRIMITIVE_LINES)
|
||||
im.surface_set_color(color)
|
||||
im.surface_add_vertex(from)
|
||||
im.surface_add_vertex(to)
|
||||
im.surface_end()
|
||||
|
||||
static func draw_dashed_line(canvas_item, from, to, color, width, dash_length = 5, cap_end = false, antialiased = false):
|
||||
# See https://github.com/juddrgledhill/godot-dashed-line/blob/master/line_harness.gd
|
||||
var length = (to - from).length()
|
||||
var normal = (to - from).normalized()
|
||||
var dash_step = normal * dash_length
|
||||
|
||||
if length < dash_length: #not long enough to dash
|
||||
canvas_item.draw_line(from, to, color, width, antialiased)
|
||||
return
|
||||
|
||||
else:
|
||||
var draw_flag = true
|
||||
var segment_start = from
|
||||
var steps = length/dash_length
|
||||
for start_length in range(0, steps + 1):
|
||||
var segment_end = segment_start + dash_step
|
||||
if draw_flag:
|
||||
canvas_item.draw_line(segment_start, segment_end, color, width, antialiased)
|
||||
|
||||
segment_start = segment_end
|
||||
draw_flag = !draw_flag
|
||||
|
||||
if cap_end:
|
||||
canvas_item.draw_line(segment_start, to, color, width, antialiased)
|
7
addons/gd-blender-3d-shortcuts/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="Blender 3D Shortcuts"
|
||||
description="Blender's 3D transforming shortcuts in Godot"
|
||||
author="imjp94"
|
||||
version="0.3.2"
|
||||
script="plugin.gd"
|
828
addons/gd-blender-3d-shortcuts/plugin.gd
Normal file
|
@ -0,0 +1,828 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const Utils = preload("Utils.gd")
|
||||
const PieMenuScn = preload("scenes/pie_menu/PieMenu.tscn")
|
||||
const PieMenuGroupScn = preload("scenes/pie_menu/PieMenuGroup.tscn")
|
||||
|
||||
const DEFAULT_LINE_COLOR = Color.WHITE
|
||||
# [name, value]
|
||||
const DEBUG_DRAW_OPTIONS = [
|
||||
["Normal", 0], ["Unshaded", 1], ["Lighting", 2], ["Overdraw", 3], ["Wireframe", 4],
|
||||
[
|
||||
"Advance",
|
||||
[
|
||||
["Shadows",
|
||||
[
|
||||
["Shadow Atlas", 9], ["Directional Shadow Atlas", 10], ["Directional Shadow Splits", 14]
|
||||
]
|
||||
],
|
||||
["Lights",
|
||||
[
|
||||
["Omni Lights Cluster", 20], ["Spot Lights Cluster", 21]
|
||||
]
|
||||
],
|
||||
["VoxelGI",
|
||||
[
|
||||
["VoxelGI Albedo", 6], ["VoxelGI Lighting", 7], ["VoxelGI Emission", 8]
|
||||
]
|
||||
],
|
||||
["SDFGI",
|
||||
[
|
||||
["SDFGI", 16], ["SDFGI Probes", 17], ["GI Buffer", 18]
|
||||
]
|
||||
],
|
||||
["Environment",
|
||||
[
|
||||
["SSAO", 12], ["SSIL", 13]
|
||||
]
|
||||
],
|
||||
["Decals",
|
||||
[
|
||||
["Decal Atlas", 15], ["Decal Cluster", 22]
|
||||
]
|
||||
],
|
||||
["Others",
|
||||
[
|
||||
["Normal Buffer", 5], ["Scene Luminance", 11], ["Disable LOD", 19], ["Cluster Reflection Probes", 23], ["Occluders", 24], ["Motion Vectors", 25]
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
|
||||
enum SESSION {
|
||||
TRANSLATE,
|
||||
ROTATE,
|
||||
SCALE,
|
||||
NONE
|
||||
}
|
||||
|
||||
var translate_snap_line_edit
|
||||
var rotate_snap_line_edit
|
||||
var scale_snap_line_edit
|
||||
var local_space_button
|
||||
var snap_button
|
||||
var overlay_control
|
||||
var spatial_editor_viewports
|
||||
var debug_draw_pie_menu
|
||||
var overlay_control_canvas_layer = CanvasLayer.new()
|
||||
|
||||
var overlay_label = Label.new()
|
||||
var axis_mesh_inst
|
||||
var axis_im = ImmediateMesh.new()
|
||||
var axis_im_material = StandardMaterial3D.new()
|
||||
|
||||
var current_session = SESSION.NONE
|
||||
var pivot_point = Vector3.ZERO
|
||||
var constraint_axis = Vector3.ONE
|
||||
var translate_snap = 1.0
|
||||
var rotate_snap = deg_to_rad(15.0)
|
||||
var scale_snap = 0.1
|
||||
var is_snapping = false
|
||||
var is_global = true
|
||||
var axis_length = 1000
|
||||
var precision_mode = false
|
||||
var precision_factor = 0.1
|
||||
|
||||
var _is_editing = false
|
||||
var _camera
|
||||
var _editing_transform = Transform3D.IDENTITY
|
||||
var _applying_transform = Transform3D.IDENTITY
|
||||
var _last_world_pos = Vector3.ZERO
|
||||
var _init_angle = NAN
|
||||
var _last_angle = 0
|
||||
var _last_center_offset = 0
|
||||
var _cummulative_center_offset = 0
|
||||
var _max_x = 0
|
||||
var _min_x = 0
|
||||
var _cache_global_transforms = []
|
||||
var _cache_transforms = [] # Nodes' local transform relative to pivot_point
|
||||
var _input_string = ""
|
||||
var _is_global_on_session = false
|
||||
var _is_warping_mouse = false
|
||||
var _is_pressing_right_mouse_button = false
|
||||
|
||||
|
||||
func _init():
|
||||
axis_im_material.flags_unshaded = true
|
||||
axis_im_material.vertex_color_use_as_albedo = true
|
||||
axis_im_material.flags_no_depth_test = true
|
||||
|
||||
overlay_label.set("custom_colors/font_color_shadow", Color.BLACK)
|
||||
|
||||
func _ready():
|
||||
var spatial_editor = Utils.get_spatial_editor(get_editor_interface().get_base_control())
|
||||
var snap_dialog = Utils.get_snap_dialog(spatial_editor)
|
||||
var snap_dialog_line_edits = Utils.get_snap_dialog_line_edits(snap_dialog)
|
||||
translate_snap_line_edit = snap_dialog_line_edits[0]
|
||||
rotate_snap_line_edit = snap_dialog_line_edits[1]
|
||||
scale_snap_line_edit = snap_dialog_line_edits[2]
|
||||
translate_snap_line_edit.connect("text_changed", _on_snap_value_changed.bind(SESSION.TRANSLATE))
|
||||
rotate_snap_line_edit.connect("text_changed", _on_snap_value_changed.bind(SESSION.ROTATE))
|
||||
scale_snap_line_edit.connect("text_changed", _on_snap_value_changed.bind(SESSION.SCALE))
|
||||
local_space_button = Utils.get_spatial_editor_local_space_button(spatial_editor)
|
||||
local_space_button.connect("toggled", _on_local_space_button_toggled)
|
||||
snap_button = Utils.get_spatial_editor_snap_button(spatial_editor)
|
||||
snap_button.connect("toggled", _on_snap_button_toggled)
|
||||
debug_draw_pie_menu = PieMenuGroupScn.instantiate()
|
||||
debug_draw_pie_menu.populate_menu(DEBUG_DRAW_OPTIONS, PieMenuScn.instantiate())
|
||||
debug_draw_pie_menu.theme_source_node = spatial_editor
|
||||
debug_draw_pie_menu.connect("item_focused", _on_PieMenu_item_focused)
|
||||
debug_draw_pie_menu.connect("item_selected", _on_PieMenu_item_selected)
|
||||
var spatial_editor_viewport_container = Utils.get_spatial_editor_viewport_container(spatial_editor)
|
||||
if spatial_editor_viewport_container:
|
||||
spatial_editor_viewports = Utils.get_spatial_editor_viewports(spatial_editor_viewport_container)
|
||||
sync_settings()
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventKey:
|
||||
if event.pressed and not event.echo:
|
||||
match event.keycode:
|
||||
KEY_Z:
|
||||
var focus = find_focused_control(get_tree().root)
|
||||
|
||||
if focus != null:
|
||||
if focus.get_parent_control() != null:
|
||||
# This may be slightly fragile if this name changes or the control gets placed another level deeper internally
|
||||
if "Node3DEditorViewport" in focus.get_parent_control().name:
|
||||
if debug_draw_pie_menu.visible:
|
||||
debug_draw_pie_menu.hide()
|
||||
get_viewport().set_input_as_handled()
|
||||
else:
|
||||
if not (event.ctrl_pressed or event.alt_pressed or event.shift_pressed) and current_session == SESSION.NONE:
|
||||
show_debug_draw_pie_menu()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# Hacky way to intercept default shortcut behavior when in session
|
||||
if current_session != SESSION.NONE:
|
||||
var event_text = event.as_text()
|
||||
if event_text.begins_with("Kp"):
|
||||
append_input_string(event_text.replace("Kp ", ""))
|
||||
get_viewport().set_input_as_handled()
|
||||
match event.keycode:
|
||||
KEY_Y:
|
||||
if event.shift_pressed:
|
||||
toggle_constraint_axis(Vector3.RIGHT + Vector3.BACK)
|
||||
else:
|
||||
toggle_constraint_axis(Vector3.UP)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
if current_session != SESSION.NONE and overlay_control:
|
||||
# Infinite mouse movement
|
||||
var rect = overlay_control.get_rect()
|
||||
var local_mouse_pos = overlay_control.get_local_mouse_position()
|
||||
if not rect.has_point(local_mouse_pos):
|
||||
var warp_pos = Utils.infinite_rect(rect, local_mouse_pos, -event.velocity.normalized() * rect.size.length())
|
||||
if warp_pos:
|
||||
Input.warp_mouse(overlay_control.global_position + warp_pos)
|
||||
_is_warping_mouse = true
|
||||
|
||||
func _on_snap_value_changed(text, session):
|
||||
match session:
|
||||
SESSION.TRANSLATE:
|
||||
translate_snap = text.to_float()
|
||||
SESSION.ROTATE:
|
||||
rotate_snap = deg_to_rad(text.to_float())
|
||||
SESSION.SCALE:
|
||||
scale_snap = text.to_float() / 100.0
|
||||
|
||||
func _on_PieMenu_item_focused(menu, index):
|
||||
var value = menu.buttons[index].get_meta("value", 0)
|
||||
if not (value is Array):
|
||||
switch_display_mode(value)
|
||||
|
||||
func _on_PieMenu_item_selected(menu, index):
|
||||
var value = menu.buttons[index].get_meta("value", 0)
|
||||
if not (value is Array):
|
||||
switch_display_mode(value)
|
||||
|
||||
func show_debug_draw_pie_menu():
|
||||
var spatial_editor_viewport = Utils.get_focused_spatial_editor_viewport(spatial_editor_viewports)
|
||||
overlay_control = Utils.get_spatial_editor_viewport_control(spatial_editor_viewport) if spatial_editor_viewport else null
|
||||
if not overlay_control:
|
||||
return false
|
||||
if overlay_control_canvas_layer.get_parent() != overlay_control:
|
||||
overlay_control.add_child(overlay_control_canvas_layer)
|
||||
if debug_draw_pie_menu.get_parent() != overlay_control_canvas_layer:
|
||||
overlay_control_canvas_layer.add_child(debug_draw_pie_menu)
|
||||
var viewport = Utils.get_spatial_editor_viewport_viewport(spatial_editor_viewport)
|
||||
|
||||
debug_draw_pie_menu.popup(overlay_control.get_global_mouse_position())
|
||||
return true
|
||||
|
||||
func _on_local_space_button_toggled(pressed):
|
||||
is_global = !pressed
|
||||
|
||||
func _on_snap_button_toggled(pressed):
|
||||
is_snapping = pressed
|
||||
|
||||
func _handles(object):
|
||||
if object is Node3D:
|
||||
_is_editing = get_editor_interface().get_selection().get_selected_nodes().size()
|
||||
return _is_editing
|
||||
elif object.get_class() == "MultiNodeEdit": # Explicitly handle MultiNodeEdit, otherwise, it will active when selected Resource
|
||||
_is_editing = get_editor_interface().get_selection().get_transformable_selected_nodes().size() > 0
|
||||
return _is_editing
|
||||
return false
|
||||
|
||||
func _edit(object):
|
||||
var scene_root = get_editor_interface().get_edited_scene_root()
|
||||
if scene_root:
|
||||
# Let editor free axis_mesh_inst as the scene closed,
|
||||
# then create new instance whenever needed
|
||||
if not is_instance_valid(axis_mesh_inst):
|
||||
axis_mesh_inst = MeshInstance3D.new()
|
||||
axis_mesh_inst.mesh = axis_im
|
||||
axis_mesh_inst.material_override = axis_im_material
|
||||
if axis_mesh_inst.get_parent() == null:
|
||||
scene_root.get_parent().add_child(axis_mesh_inst)
|
||||
else:
|
||||
if axis_mesh_inst.get_parent() != scene_root:
|
||||
axis_mesh_inst.get_parent().remove_child(axis_mesh_inst)
|
||||
scene_root.get_parent().add_child(axis_mesh_inst)
|
||||
|
||||
func find_focused_control(node):
|
||||
if node is Control and node.has_focus():
|
||||
return node
|
||||
|
||||
for child in node.get_children():
|
||||
var result = find_focused_control(child)
|
||||
|
||||
if result:
|
||||
return result
|
||||
|
||||
return null
|
||||
|
||||
func _forward_3d_gui_input(camera, event):
|
||||
var forward = false
|
||||
if current_session == SESSION.NONE:
|
||||
# solve conflict with free look
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
_is_pressing_right_mouse_button = event.is_pressed()
|
||||
if _is_editing:
|
||||
if event is InputEventKey:
|
||||
if event.pressed:
|
||||
match event.keycode:
|
||||
KEY_G:
|
||||
start_session(SESSION.TRANSLATE, camera, event)
|
||||
forward = true
|
||||
KEY_R:
|
||||
start_session(SESSION.ROTATE, camera, event)
|
||||
forward = true
|
||||
KEY_S:
|
||||
if not event.ctrl_pressed:
|
||||
# solve conflict with free look
|
||||
if not _is_pressing_right_mouse_button:
|
||||
start_session(SESSION.SCALE, camera, event)
|
||||
forward = true
|
||||
KEY_H:
|
||||
commit_hide_nodes()
|
||||
KEY_X:
|
||||
if event.shift_pressed:
|
||||
delete_selected_nodes()
|
||||
else:
|
||||
confirm_delete_selected_nodes()
|
||||
else:
|
||||
if event is InputEventKey:
|
||||
# Not sure why event.pressed always return false for numpad keys
|
||||
match event.keycode:
|
||||
KEY_KP_SUBTRACT:
|
||||
toggle_input_string_sign()
|
||||
return true
|
||||
KEY_KP_ENTER:
|
||||
commit_session()
|
||||
end_session()
|
||||
return true
|
||||
|
||||
if event.keycode == KEY_SHIFT:
|
||||
precision_mode = event.pressed
|
||||
forward = true
|
||||
|
||||
if event.pressed:
|
||||
var event_text = event.as_text()
|
||||
if append_input_string(event_text):
|
||||
return true
|
||||
match event.keycode:
|
||||
KEY_G:
|
||||
if current_session != SESSION.TRANSLATE:
|
||||
revert()
|
||||
clear_session()
|
||||
start_session(SESSION.TRANSLATE, camera, event)
|
||||
return true
|
||||
KEY_R:
|
||||
if current_session != SESSION.ROTATE:
|
||||
revert()
|
||||
clear_session()
|
||||
start_session(SESSION.ROTATE, camera, event)
|
||||
return true
|
||||
KEY_S:
|
||||
if not event.ctrl_pressed:
|
||||
if current_session != SESSION.SCALE:
|
||||
revert()
|
||||
clear_session()
|
||||
start_session(SESSION.SCALE, camera, event)
|
||||
return true
|
||||
KEY_X:
|
||||
if event.shift_pressed:
|
||||
toggle_constraint_axis(Vector3.UP + Vector3.BACK)
|
||||
else:
|
||||
toggle_constraint_axis(Vector3.RIGHT)
|
||||
return true
|
||||
KEY_Y:
|
||||
if event.shift_pressed:
|
||||
toggle_constraint_axis(Vector3.RIGHT + Vector3.BACK)
|
||||
else:
|
||||
toggle_constraint_axis(Vector3.UP)
|
||||
return true
|
||||
KEY_Z:
|
||||
if event.shift_pressed:
|
||||
toggle_constraint_axis(Vector3.RIGHT + Vector3.UP)
|
||||
else:
|
||||
toggle_constraint_axis(Vector3.BACK)
|
||||
return true
|
||||
KEY_MINUS:
|
||||
toggle_input_string_sign()
|
||||
return true
|
||||
KEY_BACKSPACE:
|
||||
trim_input_string()
|
||||
return true
|
||||
KEY_ENTER:
|
||||
commit_session()
|
||||
end_session()
|
||||
return true
|
||||
KEY_ESCAPE:
|
||||
revert()
|
||||
end_session()
|
||||
return true
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed:
|
||||
if event.button_index == 2:
|
||||
revert()
|
||||
end_session()
|
||||
return true
|
||||
else:
|
||||
commit_session()
|
||||
end_session()
|
||||
forward = true
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
match current_session:
|
||||
SESSION.TRANSLATE, SESSION.ROTATE, SESSION.SCALE:
|
||||
mouse_transform(event)
|
||||
update_overlays()
|
||||
forward = true
|
||||
|
||||
return forward
|
||||
|
||||
func _forward_3d_draw_over_viewport(overlay):
|
||||
if current_session == SESSION.NONE:
|
||||
if overlay_label.get_parent() != null:
|
||||
overlay_label.get_parent().remove_child(overlay_label)
|
||||
return
|
||||
|
||||
var editor_settings = get_editor_interface().get_editor_settings()
|
||||
var line_color = DEFAULT_LINE_COLOR
|
||||
if editor_settings.has_setting("editors/3d/selection_box_color"):
|
||||
line_color = editor_settings.get_setting("editors/3d/selection_box_color")
|
||||
var snapped = "snapped" if is_snapping else ""
|
||||
var global_or_local = "global" if is_global else "local"
|
||||
var along_axis = ""
|
||||
if not constraint_axis.is_equal_approx(Vector3.ONE):
|
||||
if constraint_axis.x > 0:
|
||||
along_axis = "X"
|
||||
if constraint_axis.y > 0:
|
||||
along_axis += ", Y" if along_axis.length() else "Y"
|
||||
if constraint_axis.z > 0:
|
||||
along_axis += ", Z" if along_axis.length() else "Z"
|
||||
if along_axis.length():
|
||||
along_axis = "along " + along_axis
|
||||
|
||||
if overlay_label.get_parent() == null:
|
||||
overlay_control.add_child(overlay_label)
|
||||
overlay_label.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
overlay_label.position += Vector2(8, -8)
|
||||
match current_session:
|
||||
SESSION.TRANSLATE:
|
||||
var translation = _applying_transform.origin
|
||||
overlay_label.text = ("Translate (%.3f, %.3f, %.3f) %s %s %s" % [translation.x, translation.y, translation.z, global_or_local, along_axis, snapped])
|
||||
SESSION.ROTATE:
|
||||
var rotation = _applying_transform.basis.get_euler()
|
||||
overlay_label.text = ("Rotate (%.3f, %.3f, %.3f) %s %s %s" % [rad_to_deg(rotation.x), rad_to_deg(rotation.y), rad_to_deg(rotation.z), global_or_local, along_axis, snapped])
|
||||
SESSION.SCALE:
|
||||
var scale = _applying_transform.basis.get_scale()
|
||||
overlay_label.text = ("Scale (%.3f, %.3f, %.3f) %s %s %s" % [scale.x, scale.y, scale.z, global_or_local, along_axis, snapped])
|
||||
if not _input_string.is_empty():
|
||||
overlay_label.text += "(%s)" % _input_string
|
||||
var is_pivot_point_behind_camera = _camera.is_position_behind(pivot_point)
|
||||
var screen_origin = overlay.size / 2.0 if is_pivot_point_behind_camera else _camera.unproject_position(pivot_point)
|
||||
Utils.draw_dashed_line(overlay, screen_origin, overlay.get_local_mouse_position(), line_color, 1, 5, true, true)
|
||||
|
||||
func text_transform(text):
|
||||
var input_value = text.to_float()
|
||||
match current_session:
|
||||
SESSION.TRANSLATE:
|
||||
_applying_transform.origin = constraint_axis * input_value
|
||||
SESSION.ROTATE:
|
||||
_applying_transform.basis = Basis().rotated((-_camera.global_transform.basis.z * constraint_axis).normalized(), deg_to_rad(input_value))
|
||||
SESSION.SCALE:
|
||||
if constraint_axis.x:
|
||||
_applying_transform.basis.x = Vector3.RIGHT * input_value
|
||||
if constraint_axis.y:
|
||||
_applying_transform.basis.y = Vector3.UP * input_value
|
||||
if constraint_axis.z:
|
||||
_applying_transform.basis.z = Vector3.BACK * input_value
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var t = _applying_transform
|
||||
if is_global or (constraint_axis.is_equal_approx(Vector3.ONE) and current_session == SESSION.TRANSLATE):
|
||||
t.origin += pivot_point
|
||||
Utils.apply_global_transform(nodes, t, _cache_transforms)
|
||||
else:
|
||||
Utils.apply_transform(nodes, t, _cache_global_transforms)
|
||||
|
||||
func mouse_transform(event):
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var is_single_node = nodes.size() == 1
|
||||
var node1 = nodes[0]
|
||||
var is_pivot_point_behind_camera = _camera.is_position_behind(pivot_point)
|
||||
if is_nan(_init_angle):
|
||||
var screen_origin = _camera.unproject_position(pivot_point)
|
||||
_init_angle = event.position.angle_to_point(screen_origin)
|
||||
# Translation offset
|
||||
var plane_transform = _camera.global_transform
|
||||
plane_transform.origin = pivot_point
|
||||
plane_transform.basis = plane_transform.basis.rotated(plane_transform.basis * Vector3.LEFT, deg_to_rad(90))
|
||||
if is_pivot_point_behind_camera:
|
||||
plane_transform.origin = _camera.global_transform.origin + -_camera.global_transform.basis.z * 10.0
|
||||
var plane = Utils.transform_to_plane(plane_transform)
|
||||
var axis_count = get_constraint_axis_count()
|
||||
if axis_count == 2:
|
||||
var normal = (Vector3.ONE - constraint_axis).normalized()
|
||||
if is_single_node and not is_global:
|
||||
normal = node1.global_transform.basis * normal
|
||||
var plane_dist = normal * plane_transform.origin
|
||||
plane = Plane(normal, plane_dist.x + plane_dist.y + plane_dist.z)
|
||||
var world_pos = Utils.project_on_plane(_camera, event.position, plane)
|
||||
if not is_global and is_single_node and axis_count < 3:
|
||||
var normalized_node1_basis = node1.global_transform.basis.scaled(Vector3.ONE / node1.global_transform.basis.get_scale())
|
||||
world_pos = world_pos * normalized_node1_basis
|
||||
if is_equal_approx(_last_world_pos.length(), 0):
|
||||
_last_world_pos = world_pos
|
||||
var offset = world_pos - _last_world_pos
|
||||
offset *= constraint_axis
|
||||
offset = offset.snapped(Vector3.ONE * 0.001)
|
||||
if _is_warping_mouse:
|
||||
offset = Vector3.ZERO
|
||||
# Rotation offset
|
||||
var screen_origin = _camera.unproject_position(pivot_point)
|
||||
if is_pivot_point_behind_camera:
|
||||
screen_origin = overlay_control.size / 2.0
|
||||
var angle = event.position.angle_to_point(screen_origin) - _init_angle
|
||||
var angle_offset = angle - _last_angle
|
||||
angle_offset = snapped(angle_offset, 0.001)
|
||||
# Scale offset
|
||||
if _max_x == 0:
|
||||
_max_x = event.position.x
|
||||
_min_x = _max_x - (_max_x - screen_origin.x) * 2
|
||||
var center_value = 2 * ((event.position.x - _min_x) / (_max_x - _min_x)) - 1
|
||||
if _last_center_offset == 0:
|
||||
_last_center_offset = center_value
|
||||
var center_offset = center_value - _last_center_offset
|
||||
center_offset = snapped(center_offset, 0.001)
|
||||
if _is_warping_mouse:
|
||||
center_offset = 0
|
||||
_cummulative_center_offset += center_offset
|
||||
if _input_string.is_empty():
|
||||
match current_session:
|
||||
SESSION.TRANSLATE:
|
||||
_editing_transform = _editing_transform.translated(offset)
|
||||
_applying_transform.origin = _editing_transform.origin
|
||||
if is_snapping:
|
||||
var snap = Vector3.ONE * (translate_snap if not precision_mode else translate_snap * precision_factor)
|
||||
_applying_transform.origin = _applying_transform.origin.snapped(snap)
|
||||
SESSION.ROTATE:
|
||||
var rotation_axis = (-_camera.global_transform.basis.z * constraint_axis).normalized()
|
||||
if not rotation_axis.is_equal_approx(Vector3.ZERO):
|
||||
_editing_transform.basis = _editing_transform.basis.rotated(rotation_axis, angle_offset)
|
||||
var quat = _editing_transform.basis.get_rotation_quaternion()
|
||||
if is_snapping:
|
||||
var snap = Vector3.ONE * (rotate_snap if not precision_mode else rotate_snap * precision_factor)
|
||||
quat.from_euler(quat.get_euler().snapped(snap))
|
||||
_applying_transform.basis = Basis(quat)
|
||||
SESSION.SCALE:
|
||||
if constraint_axis.x:
|
||||
_editing_transform.basis.x = Vector3.RIGHT * (1 + _cummulative_center_offset)
|
||||
if constraint_axis.y:
|
||||
_editing_transform.basis.y = Vector3.UP * (1 + _cummulative_center_offset)
|
||||
if constraint_axis.z:
|
||||
_editing_transform.basis.z = Vector3.BACK * (1 + _cummulative_center_offset)
|
||||
_applying_transform.basis = _editing_transform.basis
|
||||
if is_snapping:
|
||||
var snap = Vector3.ONE * (scale_snap if not precision_mode else scale_snap * precision_factor)
|
||||
_applying_transform.basis.x = _applying_transform.basis.x.snapped(snap)
|
||||
_applying_transform.basis.y = _applying_transform.basis.y.snapped(snap)
|
||||
_applying_transform.basis.z = _applying_transform.basis.z.snapped(snap)
|
||||
|
||||
var t = _applying_transform
|
||||
if is_global or (constraint_axis.is_equal_approx(Vector3.ONE) and current_session == SESSION.TRANSLATE):
|
||||
t.origin += pivot_point
|
||||
Utils.apply_global_transform(nodes, t, _cache_transforms)
|
||||
else:
|
||||
Utils.apply_transform(nodes, t, _cache_global_transforms)
|
||||
_last_world_pos = world_pos
|
||||
_last_center_offset = center_value
|
||||
_last_angle = angle
|
||||
_is_warping_mouse = false
|
||||
|
||||
func cache_selected_nodes_transforms():
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var inversed_pivot_transform = Transform3D().translated(pivot_point).affine_inverse()
|
||||
for i in nodes.size():
|
||||
var node = nodes[i]
|
||||
_cache_global_transforms.append(node.global_transform)
|
||||
_cache_transforms.append(inversed_pivot_transform * node.global_transform)
|
||||
|
||||
func update_pivot_point():
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var aabb = AABB()
|
||||
for i in nodes.size():
|
||||
var node = nodes[i]
|
||||
if i == 0:
|
||||
aabb.position = node.global_transform.origin
|
||||
aabb = aabb.expand(node.global_transform.origin)
|
||||
pivot_point = aabb.position + aabb.size / 2.0
|
||||
|
||||
func start_session(session, camera, event):
|
||||
if get_editor_interface().get_selection().get_transformable_selected_nodes().size() == 0:
|
||||
return
|
||||
current_session = session
|
||||
_camera = camera
|
||||
_is_global_on_session = is_global
|
||||
update_pivot_point()
|
||||
cache_selected_nodes_transforms()
|
||||
|
||||
if event.alt_pressed:
|
||||
commit_reset_transform()
|
||||
end_session()
|
||||
return
|
||||
|
||||
update_overlays()
|
||||
var spatial_editor_viewport = Utils.get_focused_spatial_editor_viewport(spatial_editor_viewports)
|
||||
overlay_control = Utils.get_spatial_editor_viewport_control(spatial_editor_viewport) if spatial_editor_viewport else null
|
||||
|
||||
func end_session():
|
||||
_is_editing = get_editor_interface().get_selection().get_transformable_selected_nodes().size() > 0
|
||||
# Manually set is_global to avoid triggering revert()
|
||||
if is_instance_valid(local_space_button):
|
||||
local_space_button.button_pressed = !_is_global_on_session
|
||||
is_global = _is_global_on_session
|
||||
clear_session()
|
||||
update_overlays()
|
||||
|
||||
func commit_session():
|
||||
var undo_redo = get_undo_redo()
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
Utils.revert_transform(nodes, _cache_global_transforms)
|
||||
undo_redo.create_action(SESSION.keys()[current_session].to_lower().capitalize())
|
||||
var t = _applying_transform
|
||||
if is_global or (constraint_axis.is_equal_approx(Vector3.ONE) and current_session == SESSION.TRANSLATE):
|
||||
t.origin += pivot_point
|
||||
undo_redo.add_do_method(Utils, "apply_global_transform", nodes, t, _cache_transforms)
|
||||
else:
|
||||
undo_redo.add_do_method(Utils, "apply_transform", nodes, t, _cache_global_transforms)
|
||||
undo_redo.add_undo_method(Utils, "revert_transform", nodes, _cache_global_transforms)
|
||||
undo_redo.commit_action()
|
||||
|
||||
func commit_reset_transform():
|
||||
var undo_redo = get_undo_redo()
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
match current_session:
|
||||
SESSION.TRANSLATE:
|
||||
undo_redo.create_action("Reset Translation")
|
||||
undo_redo.add_do_method(Utils, "reset_translation", nodes)
|
||||
undo_redo.add_undo_method(Utils, "revert_transform", nodes, _cache_global_transforms)
|
||||
undo_redo.commit_action()
|
||||
SESSION.ROTATE:
|
||||
undo_redo.create_action("Reset Rotation")
|
||||
undo_redo.add_do_method(Utils, "reset_rotation", nodes)
|
||||
undo_redo.add_undo_method(Utils, "revert_transform", nodes, _cache_global_transforms)
|
||||
undo_redo.commit_action()
|
||||
SESSION.SCALE:
|
||||
undo_redo.create_action("Reset Scale")
|
||||
undo_redo.add_do_method(Utils, "reset_scale", nodes)
|
||||
undo_redo.add_undo_method(Utils, "revert_transform", nodes, _cache_global_transforms)
|
||||
undo_redo.commit_action()
|
||||
current_session = SESSION.NONE
|
||||
|
||||
func commit_hide_nodes():
|
||||
var undo_redo = get_undo_redo()
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
undo_redo.create_action("Hide Nodes")
|
||||
undo_redo.add_do_method(Utils, "hide_nodes", nodes, true)
|
||||
undo_redo.add_undo_method(Utils, "hide_nodes", nodes, false)
|
||||
undo_redo.commit_action()
|
||||
|
||||
## Opens a popup dialog to confirm deletion of selected nodes.
|
||||
func confirm_delete_selected_nodes():
|
||||
var selected_nodes = get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected_nodes.is_empty():
|
||||
return
|
||||
|
||||
var editor_theme = get_editor_interface().get_base_control().theme
|
||||
var popup = ConfirmationDialog.new()
|
||||
popup.theme = editor_theme
|
||||
|
||||
# Setting dialog text dynamically depending on the selection to mimick Godot's normal behavior.
|
||||
popup.dialog_text = "Delete "
|
||||
var selection_size = selected_nodes.size()
|
||||
if selection_size == 1:
|
||||
popup.dialog_text += selected_nodes[0].get_name()
|
||||
elif selection_size > 1:
|
||||
popup.dialog_text += str(selection_size) + " nodes"
|
||||
for node in selected_nodes:
|
||||
if node.get_child_count() > 0:
|
||||
popup.dialog_text += " and children"
|
||||
break
|
||||
popup.dialog_text += "?"
|
||||
|
||||
add_child(popup)
|
||||
popup.popup_centered()
|
||||
popup.canceled.connect(popup.queue_free)
|
||||
popup.confirmed.connect(delete_selected_nodes)
|
||||
popup.confirmed.connect(popup.queue_free)
|
||||
|
||||
## Instantly deletes selected nodes and creates an undo history entry.
|
||||
func delete_selected_nodes():
|
||||
var undo_redo = get_undo_redo()
|
||||
|
||||
var selected_nodes = get_editor_interface().get_selection().get_selected_nodes()
|
||||
# Avoid creating an unnecessary history entry if no nodes are selected.
|
||||
if selected_nodes.is_empty():
|
||||
return
|
||||
|
||||
undo_redo.create_action("Delete Nodes", UndoRedo.MERGE_DISABLE)
|
||||
for node in selected_nodes:
|
||||
# We can't free nodes, they must be kept in memory for undo to work.
|
||||
# That's why we use remove_child instead and call UndoRedo.add_undo_reference() below.
|
||||
undo_redo.add_do_method(node.get_parent(), "remove_child", node)
|
||||
undo_redo.add_undo_method(node.get_parent(), "add_child", node, true)
|
||||
undo_redo.add_undo_method(node.get_parent(), "move_child", node, node.get_index())
|
||||
# Every node's owner must be set upon undoing, otherwise, it won't appear in the scene dock
|
||||
# and it'll be lost upon saving.
|
||||
undo_redo.add_undo_method(node, "set_owner", node.owner)
|
||||
for child in Utils.recursive_get_children(node):
|
||||
undo_redo.add_undo_method(child, "set_owner", node.owner)
|
||||
undo_redo.add_undo_reference(node)
|
||||
undo_redo.commit_action()
|
||||
|
||||
func revert():
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
Utils.revert_transform(nodes, _cache_global_transforms)
|
||||
_editing_transform = Transform3D.IDENTITY
|
||||
_applying_transform = Transform3D.IDENTITY
|
||||
_last_world_pos = Vector3.ZERO
|
||||
axis_im.clear_surfaces()
|
||||
|
||||
func clear_session():
|
||||
current_session = SESSION.NONE
|
||||
constraint_axis = Vector3.ONE
|
||||
pivot_point = Vector3.ZERO
|
||||
precision_mode = false
|
||||
_editing_transform = Transform3D.IDENTITY
|
||||
_applying_transform = Transform3D.IDENTITY
|
||||
_last_world_pos = Vector3.ZERO
|
||||
_init_angle = NAN
|
||||
_last_angle = 0
|
||||
_last_center_offset = 0
|
||||
_cummulative_center_offset = 0
|
||||
_max_x = 0
|
||||
_min_x = 0
|
||||
_cache_global_transforms = []
|
||||
_cache_transforms = []
|
||||
_input_string = ""
|
||||
_is_warping_mouse = false
|
||||
axis_im.clear_surfaces()
|
||||
|
||||
func sync_settings():
|
||||
if translate_snap_line_edit:
|
||||
translate_snap = translate_snap_line_edit.text.to_float()
|
||||
if rotate_snap_line_edit:
|
||||
rotate_snap = deg_to_rad(rotate_snap_line_edit.text.to_float())
|
||||
if scale_snap_line_edit:
|
||||
scale_snap = scale_snap_line_edit.text.to_float() / 100.0
|
||||
if local_space_button:
|
||||
is_global = !local_space_button.button_pressed
|
||||
if snap_button:
|
||||
is_snapping = snap_button.button_pressed
|
||||
|
||||
func switch_display_mode(debug_draw):
|
||||
var spatial_editor_viewport = Utils.get_focused_spatial_editor_viewport(spatial_editor_viewports)
|
||||
if spatial_editor_viewport:
|
||||
var viewport = Utils.get_spatial_editor_viewport_viewport(spatial_editor_viewport)
|
||||
viewport.debug_draw = debug_draw
|
||||
|
||||
# Repeatedly applying same axis will results in toggling is_global, just like pressing xx, yy or zz in blender
|
||||
func toggle_constraint_axis(axis):
|
||||
# Following order as below:
|
||||
# 1) Apply constraint on current mode
|
||||
# 2) Toggle mode
|
||||
# 3) Toggle mode again, and remove constraint
|
||||
if is_global == _is_global_on_session:
|
||||
if not constraint_axis.is_equal_approx(axis):
|
||||
# 1
|
||||
set_constraint_axis(axis)
|
||||
else:
|
||||
# 2
|
||||
set_is_global(!_is_global_on_session)
|
||||
else:
|
||||
if constraint_axis.is_equal_approx(axis):
|
||||
# 3
|
||||
set_is_global(_is_global_on_session)
|
||||
set_constraint_axis(Vector3.ONE)
|
||||
else:
|
||||
# Others situation
|
||||
set_constraint_axis(axis)
|
||||
|
||||
func toggle_input_string_sign():
|
||||
if _input_string.begins_with("-"):
|
||||
_input_string = _input_string.trim_prefix("-")
|
||||
else:
|
||||
_input_string = "-" + _input_string
|
||||
input_string_changed()
|
||||
|
||||
func trim_input_string():
|
||||
_input_string = _input_string.substr(0, _input_string.length() - 1)
|
||||
input_string_changed()
|
||||
|
||||
func append_input_string(text):
|
||||
text = "." if text == "Period" else text
|
||||
if text.is_valid_int() or text == ".":
|
||||
_input_string += text
|
||||
input_string_changed()
|
||||
return true
|
||||
|
||||
func input_string_changed():
|
||||
if not _input_string.is_empty():
|
||||
text_transform(_input_string)
|
||||
else:
|
||||
_applying_transform = Transform3D.IDENTITY
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
Utils.revert_transform(nodes, _cache_global_transforms)
|
||||
update_overlays()
|
||||
|
||||
func get_constraint_axis_count():
|
||||
var axis_count = 3
|
||||
if constraint_axis.x == 0:
|
||||
axis_count -= 1
|
||||
if constraint_axis.y == 0:
|
||||
axis_count -= 1
|
||||
if constraint_axis.z == 0:
|
||||
axis_count -= 1
|
||||
return axis_count
|
||||
|
||||
func set_constraint_axis(v):
|
||||
revert()
|
||||
if constraint_axis != v:
|
||||
constraint_axis = v
|
||||
draw_axises()
|
||||
else:
|
||||
constraint_axis = Vector3.ONE
|
||||
if not _input_string.is_empty():
|
||||
text_transform(_input_string)
|
||||
update_overlays()
|
||||
|
||||
func set_is_global(v):
|
||||
if is_global != v:
|
||||
if is_instance_valid(local_space_button):
|
||||
local_space_button.button_pressed = !v
|
||||
revert()
|
||||
is_global = v
|
||||
draw_axises()
|
||||
if not _input_string.is_empty():
|
||||
text_transform(_input_string)
|
||||
update_overlays()
|
||||
|
||||
func draw_axises():
|
||||
if not constraint_axis.is_equal_approx(Vector3.ONE):
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var axis_lines = []
|
||||
if constraint_axis.x > 0:
|
||||
axis_lines.append({"axis": Vector3.RIGHT, "color": Color.RED})
|
||||
if constraint_axis.y > 0:
|
||||
axis_lines.append({"axis": Vector3.UP, "color": Color.GREEN})
|
||||
if constraint_axis.z > 0:
|
||||
axis_lines.append({"axis": Vector3.BACK, "color": Color.BLUE})
|
||||
|
||||
for axis_line in axis_lines:
|
||||
var axis = axis_line.get("axis")
|
||||
var color = axis_line.get("color")
|
||||
if is_global:
|
||||
var is_pivot_point_behind_camera = _camera.is_position_behind(pivot_point)
|
||||
var axis_origin = _camera.global_transform.origin + -_camera.global_transform.basis.z * 10.0 if is_pivot_point_behind_camera else pivot_point
|
||||
Utils.draw_axis(axis_im, axis_origin, axis, axis_length, color)
|
||||
else:
|
||||
for node in nodes:
|
||||
Utils.draw_axis(axis_im, node.global_transform.origin, node.global_transform.basis * axis, axis_length, color)
|
159
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.gd
Normal file
|
@ -0,0 +1,159 @@
|
|||
@tool
|
||||
extends Control
|
||||
|
||||
signal item_selected(index)
|
||||
signal item_focused(index)
|
||||
signal item_cancelled()
|
||||
|
||||
const button_margin = 6
|
||||
|
||||
@export var items := [] : set = set_items
|
||||
@export var selected_index = -1 : set = set_selected_index
|
||||
@export var radius = 100.0 : set = set_radius
|
||||
|
||||
var buttons = []
|
||||
var pie_menus = []
|
||||
|
||||
var focused_index = -1
|
||||
var theme_source_node = self : set = set_theme_source_node
|
||||
var grow_with_max_button_width = false
|
||||
|
||||
|
||||
func _ready():
|
||||
set_items(items)
|
||||
set_selected_index(selected_index)
|
||||
set_radius(radius)
|
||||
hide()
|
||||
connect("visibility_changed", _on_visiblity_changed)
|
||||
|
||||
func _input(event):
|
||||
if visible:
|
||||
if event is InputEventKey:
|
||||
if event.pressed:
|
||||
match event.keycode:
|
||||
KEY_ESCAPE:
|
||||
cancel()
|
||||
if event is InputEventMouseMotion:
|
||||
focus_item()
|
||||
get_viewport().set_input_as_handled()
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed:
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
select_item(focused_index)
|
||||
get_viewport().set_input_as_handled()
|
||||
MOUSE_BUTTON_RIGHT:
|
||||
cancel()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_visiblity_changed():
|
||||
if not visible:
|
||||
if selected_index != focused_index: # Cancellation
|
||||
focused_index = selected_index
|
||||
|
||||
func cancel():
|
||||
hide()
|
||||
get_viewport().set_input_as_handled()
|
||||
emit_signal("item_cancelled")
|
||||
|
||||
func select_item(index):
|
||||
set_button_style(selected_index, "normal", "normal")
|
||||
selected_index = index
|
||||
focused_index = selected_index
|
||||
hide()
|
||||
emit_signal("item_selected", selected_index)
|
||||
|
||||
func focus_item():
|
||||
queue_redraw()
|
||||
var pos = get_global_mouse_position()
|
||||
var count = max(buttons.size(), 1)
|
||||
var angle_offset = 2 * PI / count
|
||||
var angle = pos.angle_to_point(global_position) + PI / 2 # -90 deg initial offset
|
||||
if angle < 0:
|
||||
angle += 2 * PI
|
||||
|
||||
var index = (angle / angle_offset)
|
||||
var decimal = index - floor(index)
|
||||
index = floor(index)
|
||||
if decimal >= 0.5:
|
||||
index += 1
|
||||
if index > buttons.size()-1:
|
||||
index = 0
|
||||
|
||||
set_button_style(focused_index, "normal", "normal")
|
||||
focused_index = index
|
||||
set_button_style(focused_index, "normal", "hover")
|
||||
set_button_style(selected_index, "normal", "focus")
|
||||
emit_signal("item_focused", focused_index)
|
||||
|
||||
func popup(pos):
|
||||
global_position = pos
|
||||
show()
|
||||
|
||||
func populate_menu():
|
||||
clear_menu()
|
||||
buttons = []
|
||||
for i in items.size():
|
||||
var item = items[i]
|
||||
var is_array = item is Array
|
||||
var name = item if not is_array else item[0]
|
||||
var value = null if not is_array else item[1]
|
||||
var button = Button.new()
|
||||
button.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
button.text = name
|
||||
if value != null:
|
||||
button.set_meta("value", value)
|
||||
buttons.append(button)
|
||||
set_button_style(i, "hover", "hover")
|
||||
set_button_style(i, "pressed", "pressed")
|
||||
set_button_style(i, "focus", "focus")
|
||||
set_button_style(i, "disabled", "disabled")
|
||||
set_button_style(i, "normal", "normal")
|
||||
add_child(button)
|
||||
align()
|
||||
|
||||
set_button_style(selected_index, "normal", "focus")
|
||||
|
||||
func align():
|
||||
var final_radius = radius
|
||||
if grow_with_max_button_width:
|
||||
var max_button_width = 0.0
|
||||
for button in buttons:
|
||||
max_button_width = max(max_button_width, button.size.x)
|
||||
final_radius = max(radius, max_button_width)
|
||||
var count = max(buttons.size(), 1)
|
||||
var angle_offset = 2 * PI / count
|
||||
var angle = PI / 2 # 90 deg initial offset
|
||||
for button in buttons:
|
||||
button.position = Vector2(final_radius, 0.0).rotated(angle) - (button.size / 2.0)
|
||||
angle += angle_offset
|
||||
|
||||
func clear_menu():
|
||||
for button in buttons:
|
||||
button.queue_free()
|
||||
|
||||
func set_button_style(index, name, source):
|
||||
if index < 0 or index > buttons.size() - 1:
|
||||
return
|
||||
|
||||
buttons[index].set("theme_override_styles/%s" % name, get_theme_stylebox(source, "Button"))
|
||||
|
||||
func set_items(v):
|
||||
items = v
|
||||
if is_inside_tree():
|
||||
populate_menu()
|
||||
|
||||
func set_selected_index(v):
|
||||
set_button_style(selected_index, "normal", "normal")
|
||||
selected_index = v
|
||||
set_button_style(selected_index, "normal", "focus")
|
||||
|
||||
func set_radius(v):
|
||||
radius = v
|
||||
align()
|
||||
|
||||
func set_theme_source_node(v):
|
||||
theme_source_node = v
|
||||
for pie_menu in pie_menus:
|
||||
if pie_menu:
|
||||
pie_menu.theme_source_node = theme_source_node
|
11
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.tscn
Normal file
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://bxummco35581e"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.gd" id="1"]
|
||||
|
||||
[node name="PieMenu" type="Control"]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1")
|
113
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenuGroup.gd
Normal file
|
@ -0,0 +1,113 @@
|
|||
@tool
|
||||
extends Control
|
||||
const PieMenuScn = preload("PieMenu.tscn")
|
||||
|
||||
signal item_focused(menu, index)
|
||||
signal item_selected(menu, index)
|
||||
signal item_cancelled(menu)
|
||||
|
||||
var root
|
||||
var page_index = [0]
|
||||
var theme_source_node = self : set = set_theme_source_node
|
||||
|
||||
|
||||
func _ready():
|
||||
hide()
|
||||
|
||||
func _on_item_cancelled(pie_menu):
|
||||
back()
|
||||
emit_signal("item_cancelled", pie_menu)
|
||||
|
||||
func _on_item_focused(index, pie_menu):
|
||||
var current_menu = get_current_menu()
|
||||
if current_menu == pie_menu:
|
||||
emit_signal("item_focused", current_menu, index)
|
||||
|
||||
func _on_item_selected(index):
|
||||
var last_menu = get_current_menu()
|
||||
page_index.append(index)
|
||||
var current_menu = get_current_menu()
|
||||
if current_menu:
|
||||
current_menu.selected_index = -1
|
||||
if current_menu.pie_menus.size() > 0: # Has next page
|
||||
current_menu.popup(global_position)
|
||||
else:
|
||||
# Final selection, revert page index
|
||||
if page_index.size() > 1:
|
||||
page_index.pop_back()
|
||||
last_menu = get_current_menu()
|
||||
page_index = [0]
|
||||
hide()
|
||||
emit_signal("item_selected", last_menu, index)
|
||||
|
||||
func popup(pos):
|
||||
global_position = pos
|
||||
var pie_menu = get_current_menu()
|
||||
pie_menu.popup(global_position)
|
||||
show()
|
||||
|
||||
func populate_menu(items, pie_menu):
|
||||
add_child(pie_menu)
|
||||
if not root:
|
||||
root = pie_menu
|
||||
root.connect("item_focused", _on_item_focused.bind(pie_menu))
|
||||
root.connect("item_selected", _on_item_selected)
|
||||
root.connect("item_cancelled", _on_item_cancelled.bind(pie_menu))
|
||||
|
||||
pie_menu.items = items
|
||||
|
||||
for i in items.size():
|
||||
var item = items[i]
|
||||
var is_array = item is Array
|
||||
# var name = item if not is_array else item[0]
|
||||
var value = null if not is_array else item[1]
|
||||
if value is Array:
|
||||
var new_pie_menu = PieMenuScn.instantiate()
|
||||
new_pie_menu.connect("item_focused", _on_item_focused.bind(new_pie_menu))
|
||||
new_pie_menu.connect("item_selected", _on_item_selected)
|
||||
new_pie_menu.connect("item_cancelled", _on_item_cancelled.bind(new_pie_menu))
|
||||
|
||||
populate_menu(value, new_pie_menu)
|
||||
pie_menu.pie_menus.append(new_pie_menu)
|
||||
else:
|
||||
pie_menu.pie_menus.append(null)
|
||||
return pie_menu
|
||||
|
||||
func clear_menu():
|
||||
if root:
|
||||
root.queue_free()
|
||||
|
||||
func back():
|
||||
var last_menu = get_current_menu()
|
||||
last_menu.hide()
|
||||
page_index.pop_back()
|
||||
if page_index.size() == 0:
|
||||
page_index = [0]
|
||||
hide()
|
||||
return
|
||||
else:
|
||||
var current_menu = get_current_menu()
|
||||
if current_menu:
|
||||
current_menu.popup(global_position)
|
||||
|
||||
func get_menu(indexes=[0]):
|
||||
var pie_menu = root
|
||||
for i in indexes.size():
|
||||
if i == 0:
|
||||
continue # root
|
||||
|
||||
var page = indexes[i]
|
||||
pie_menu = pie_menu.pie_menus[page]
|
||||
return pie_menu
|
||||
|
||||
func get_current_menu():
|
||||
return get_menu(page_index)
|
||||
|
||||
func set_theme_source_node(v):
|
||||
theme_source_node = v
|
||||
if not root:
|
||||
return
|
||||
|
||||
for pie_menu in root.pie_menus:
|
||||
if pie_menu:
|
||||
pie_menu.theme_source_node = theme_source_node
|
|
@ -0,0 +1,13 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://c4cfbaj52t05b"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenuGroup.gd" id="1"]
|
||||
|
||||
[node name="PieMenuGroup" type="Control"]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1")
|
81
addons/imjp94.yafsm/README.md
Normal file
|
@ -0,0 +1,81 @@
|
|||
# Documentation
|
||||
|
||||
## Classes
|
||||
|
||||
All of the class are located in `res://addons/imjp94.yafsm/src` but you can just preload `res://addons/imjp94.yafsm/YAFSM.gd` to import all class available:
|
||||
|
||||
```gdscript
|
||||
const YAFSM = preload("res://addons/imjp94.yafsm/YAFSM.gd")
|
||||
const StackPlayer = YAFSM.StackPlayer
|
||||
const StateMachinePlayer = YAFSM.StateMachinePlayer
|
||||
const StateMachine = YAFSM.StateMachine
|
||||
const State = YAFSM.State
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
- [StackPlayer](src/StackPlayer.gd) 
|
||||
> Manage stack of item, use push/pop function to set current item on top of stack
|
||||
- `current # Current item on top of stack`
|
||||
- `stack`
|
||||
- signals:
|
||||
- `pushed(to) # When item pushed to stack`
|
||||
- `popped(from) # When item popped from stack`
|
||||
- [StateMachinePlayer](src/StateMachinePlayer.gd)(extends StackPlayer) 
|
||||
> Manage state based on `StateMachine` and parameters inputted
|
||||
- `state_machine # StateMachine being played`
|
||||
- `active # Activeness of player`
|
||||
- `autostart # Automatically enter Entry state on ready if true`
|
||||
- `process_mode # ProcessMode of player`
|
||||
- signals:
|
||||
- `transited(from, to) # Transition of state`
|
||||
- `entered(to) # Entry of state machine(including nested), empty string equals to root`
|
||||
- `exited(from) # Exit of state machine(including nested, empty string equals to root`
|
||||
- `updated(state, delta) # Time to update(based on process_mode), up to user to handle any logic, for example, update movement of KinematicBody`
|
||||
|
||||
### Control
|
||||
|
||||
- [StackPlayerDebugger](src/debugger/StackPlayerDebugger.gd)
|
||||
> Visualize stack of parent StackPlayer on screen
|
||||
|
||||
### Reference
|
||||
|
||||
- [StateDirectory](src/StateDirectory.gd)
|
||||
> Convert state path to directory object for traversal, mainly used for nested state
|
||||
|
||||
### Resource
|
||||
|
||||
Relationship between all `Resource`s can be best represented as below:
|
||||
|
||||
```gdscript
|
||||
var state_machine = state_machine_player.state_machine
|
||||
var state = state_machine.states[state_name] # keyed by state name
|
||||
var transition = state_machine.transitions[from][to] # keyed by state name transition from/to
|
||||
var condition = transition.conditions[condition_name] # keyed by condition name
|
||||
```
|
||||
|
||||
> For normal usage, you really don't have to access any `Resource` during runtime as they only store static data that describe the state machine, accessing `StackPlayer`/`StateMachinePlayer` alone should be sufficient.
|
||||
|
||||
- [State](src/states/State.gd)
|
||||
> Resource that represent a state
|
||||
- `name`
|
||||
- [StateMachine](src/states/StateMachine.gd)(`extends State`) 
|
||||
> `StateMachine` is also a `State`, but mainly used as container of `State`s and `Transitions`s
|
||||
- `states`
|
||||
- `transitions`
|
||||
- [Transition](src/transitions/Transition.gd)
|
||||
> Describing connection from one state to another, all conditions must be fulfilled to transit to next state
|
||||
- `from`
|
||||
- `to`
|
||||
- `conditions`
|
||||
- [Condition](src/conditions/Condition.gd)
|
||||
> Empty condition with just a name, treated as trigger
|
||||
- `name`
|
||||
- [ValueCondition](src/conditions/ValueCondition.gd)(`extends Condition`)
|
||||
> Condition with value, fulfilled by comparing values based on comparation
|
||||
- `comparation`
|
||||
- `value`
|
||||
- [BooleanCondition](src/conditions/BooleanCondition.gd)(`extends ValueCondition`)
|
||||
- [IntegerCondition](src/conditions/IntegerCondition.gd)(`extends ValueCondition`)
|
||||
- [FloatCondition](src/conditions/FloatCondition.gd)(`extends ValueCondition`)
|
||||
- [StringCondition](src/conditions/StringCondition.gd)(`extends ValueCondition`)
|
20
addons/imjp94.yafsm/YAFSM.gd
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Node
|
||||
const StackPlayer = preload("src/StackPlayer.gd")
|
||||
const StateMachinePlayer = preload("src/StateMachinePlayer.gd")
|
||||
|
||||
# Reference
|
||||
const StateDirectory = preload("src/StateDirectory.gd")
|
||||
|
||||
# Resources
|
||||
# States
|
||||
const State = preload("src/states/State.gd")
|
||||
const StateMachine = preload("src/states/StateMachine.gd")
|
||||
# Transitions
|
||||
const Transition = preload("src/transitions/Transition.gd")
|
||||
# Conditions
|
||||
const Condition = preload("src/conditions/Condition.gd")
|
||||
const ValueCondition = preload("src/conditions/ValueCondition.gd")
|
||||
const BooleanCondition = preload("src/conditions/BooleanCondition.gd")
|
||||
const IntegerCondition = preload("src/conditions/IntegerCondition.gd")
|
||||
const FloatCondition = preload("src/conditions/FloatCondition.gd")
|
||||
const StringCondition = preload("src/conditions/StringCondition.gd")
|
5
addons/imjp94.yafsm/assets/fonts/sans_serif.tres
Normal file
|
@ -0,0 +1,5 @@
|
|||
[gd_resource type="SystemFont" format=3 uid="uid://dmcxm8gxsonbq"]
|
||||
|
||||
[resource]
|
||||
font_names = PackedStringArray("Sans-Serif")
|
||||
multichannel_signed_distance_field = true
|
1
addons/imjp94.yafsm/assets/icons/add-white-18dp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
After Width: | Height: | Size: 190 B |
37
addons/imjp94.yafsm/assets/icons/add-white-18dp.svg.import
Normal file
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dg8cmn5ubq6r5"
|
||||
path="res://.godot/imported/add-white-18dp.svg-06b50d941748dbfd6e0203dec68494ea.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/add-white-18dp.svg"
|
||||
dest_files=["res://.godot/imported/add-white-18dp.svg-06b50d941748dbfd6e0203dec68494ea.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M10 17l5-5-5-5v10z"/><path d="M0 24V0h24v24H0z" fill="none"/></svg>
|
After Width: | Height: | Size: 176 B |
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://yw43hcwiudst"
|
||||
path="res://.godot/imported/arrow_right-white-18dp.svg-10d349447e9bd513637eade1f10225f0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/arrow_right-white-18dp.svg"
|
||||
dest_files=["res://.godot/imported/arrow_right-white-18dp.svg-10d349447e9bd513637eade1f10225f0.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=4.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
1
addons/imjp94.yafsm/assets/icons/close-white-18dp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
After Width: | Height: | Size: 256 B |
37
addons/imjp94.yafsm/assets/icons/close-white-18dp.svg.import
Normal file
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://l78bjwo7shm"
|
||||
path="res://.godot/imported/close-white-18dp.svg-3d0e2341eb99a6dc45a6aecef969301b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/close-white-18dp.svg"
|
||||
dest_files=["res://.godot/imported/close-white-18dp.svg-3d0e2341eb99a6dc45a6aecef969301b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><g><rect fill="none" height="24" width="24" x="0"/></g><g><g><g><path d="M9.01,14H2v2h7.01v3L13,15l-3.99-4V14z M14.99,13v-3H22V8h-7.01V5L11,9L14.99,13z"/></g></g></g></svg>
|
After Width: | Height: | Size: 306 B |
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cnkaa2ky1f4jq"
|
||||
path="res://.godot/imported/compare_arrows-white-18dp.svg-7313ec3b54f05c948521b16e0efaaeed.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/compare_arrows-white-18dp.svg"
|
||||
dest_files=["res://.godot/imported/compare_arrows-white-18dp.svg-7313ec3b54f05c948521b16e0efaaeed.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
1
addons/imjp94.yafsm/assets/icons/remove-white-18dp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13H5v-2h14v2z"/></svg>
|
After Width: | Height: | Size: 172 B |
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://p2md5n42lcqj"
|
||||
path="res://.godot/imported/remove-white-18dp.svg-984af3406d3d64ea0f778da7f0f5a4c3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/remove-white-18dp.svg"
|
||||
dest_files=["res://.godot/imported/remove-white-18dp.svg-984af3406d3d64ea0f778da7f0f5a4c3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
BIN
addons/imjp94.yafsm/assets/icons/stack_player_icon.png
Normal file
After Width: | Height: | Size: 781 B |
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bcc8ni3mjf55j"
|
||||
path="res://.godot/imported/stack_player_icon.png-bf093c6193b73dc7a03c728b884edd0b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/stack_player_icon.png"
|
||||
dest_files=["res://.godot/imported/stack_player_icon.png-bf093c6193b73dc7a03c728b884edd0b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
BIN
addons/imjp94.yafsm/assets/icons/state_machine_icon.png
Normal file
After Width: | Height: | Size: 883 B |
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://quofx2t3tj1b"
|
||||
path="res://.godot/imported/state_machine_icon.png-9917b22df6299aea6994b92cacbcef16.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/state_machine_icon.png"
|
||||
dest_files=["res://.godot/imported/state_machine_icon.png-9917b22df6299aea6994b92cacbcef16.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
BIN
addons/imjp94.yafsm/assets/icons/state_machine_player_icon.png
Normal file
After Width: | Height: | Size: 947 B |
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://crcg0exl13kdd"
|
||||
path="res://.godot/imported/state_machine_player_icon.png-12d6c36cda302327e8c107292c578aa4.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/state_machine_player_icon.png"
|
||||
dest_files=["res://.godot/imported/state_machine_player_icon.png-12d6c36cda302327e8c107292c578aa4.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 15l-6 6-1.42-1.42L15.17 16H4V4h2v10h9.17l-3.59-3.58L13 9l6 6z"/></svg>
|
After Width: | Height: | Size: 222 B |
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b2coah58shtq1"
|
||||
path="res://.godot/imported/subdirectory_arrow_right-white-18dp.svg-09b2961410e6b2c0e48e0cf1138c3548.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/imjp94.yafsm/assets/icons/subdirectory_arrow_right-white-18dp.svg"
|
||||
dest_files=["res://.godot/imported/subdirectory_arrow_right-white-18dp.svg-09b2961410e6b2c0e48e0cf1138c3548.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=false
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
7
addons/imjp94.yafsm/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="gd-YAFSM"
|
||||
description="Yet Another Finite State Machine"
|
||||
author="imjp94"
|
||||
version="0.6.2"
|
||||
script="plugin.gd"
|
151
addons/imjp94.yafsm/plugin.gd
Normal file
|
@ -0,0 +1,151 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const StateMachineEditor = preload("scenes/StateMachineEditor.tscn")
|
||||
const TransitionInspector = preload("scenes/transition_editors/TransitionInspector.gd")
|
||||
const StateInspector = preload("scenes/state_nodes/StateInspector.gd")
|
||||
|
||||
const StackPlayerIcon = preload("assets/icons/stack_player_icon.png")
|
||||
const StateMachinePlayerIcon = preload("assets/icons/state_machine_player_icon.png")
|
||||
|
||||
var state_machine_editor = StateMachineEditor.instantiate()
|
||||
var transition_inspector = TransitionInspector.new()
|
||||
var state_inspector = StateInspector.new()
|
||||
|
||||
var focused_object: # Can be StateMachine/StateMachinePlayer
|
||||
set = set_focused_object
|
||||
var editor_selection
|
||||
|
||||
var _handled_and_ready_to_edit = false # forces _handles => _edit flow
|
||||
|
||||
func _enter_tree():
|
||||
editor_selection = get_editor_interface().get_selection()
|
||||
editor_selection.selection_changed.connect(_on_EditorSelection_selection_changed)
|
||||
var editor_base_control = get_editor_interface().get_base_control()
|
||||
add_custom_type("StackPlayer", "Node", StackPlayer, StackPlayerIcon)
|
||||
add_custom_type("StateMachinePlayer", "Node", StateMachinePlayer, StateMachinePlayerIcon)
|
||||
|
||||
state_machine_editor.undo_redo = get_undo_redo()
|
||||
state_machine_editor.selection_stylebox.bg_color = editor_base_control.get_theme_color("box_selection_fill_color", "Editor")
|
||||
state_machine_editor.selection_stylebox.border_color = editor_base_control.get_theme_color("box_selection_stroke_color", "Editor")
|
||||
state_machine_editor.zoom_minus.icon = editor_base_control.get_theme_icon("ZoomLess", "EditorIcons")
|
||||
state_machine_editor.zoom_reset.icon = editor_base_control.get_theme_icon("ZoomReset", "EditorIcons")
|
||||
state_machine_editor.zoom_plus.icon = editor_base_control.get_theme_icon("ZoomMore", "EditorIcons")
|
||||
state_machine_editor.snap_button.icon = editor_base_control.get_theme_icon("SnapGrid", "EditorIcons")
|
||||
state_machine_editor.condition_visibility.texture_pressed = editor_base_control.get_theme_icon("GuiVisibilityVisible", "EditorIcons")
|
||||
state_machine_editor.condition_visibility.texture_normal = editor_base_control.get_theme_icon("GuiVisibilityHidden", "EditorIcons")
|
||||
state_machine_editor.editor_accent_color = editor_base_control.get_theme_color("accent_color", "Editor")
|
||||
state_machine_editor.current_layer.editor_accent_color = state_machine_editor.editor_accent_color
|
||||
state_machine_editor.transition_arrow_icon = editor_base_control.get_theme_icon("TransitionImmediateBig", "EditorIcons")
|
||||
state_machine_editor.inspector_changed.connect(_on_inspector_changed)
|
||||
state_machine_editor.node_selected.connect(_on_StateMachineEditor_node_selected)
|
||||
state_machine_editor.node_deselected.connect(_on_StateMachineEditor_node_deselected)
|
||||
state_machine_editor.debug_mode_changed.connect(_on_StateMachineEditor_debug_mode_changed)
|
||||
# Force anti-alias for default font, so rotated text will looks smoother
|
||||
var font = editor_base_control.get_theme_font("main", "EditorFonts")
|
||||
# font.use_filter = true
|
||||
|
||||
transition_inspector.undo_redo = get_undo_redo()
|
||||
transition_inspector.transition_icon = editor_base_control.get_theme_icon("ToolConnect", "EditorIcons")
|
||||
add_inspector_plugin(transition_inspector)
|
||||
add_inspector_plugin(state_inspector)
|
||||
|
||||
func _exit_tree():
|
||||
remove_custom_type("StackPlayer")
|
||||
remove_custom_type("StateMachinePlayer")
|
||||
|
||||
remove_inspector_plugin(transition_inspector)
|
||||
remove_inspector_plugin(state_inspector)
|
||||
|
||||
if state_machine_editor:
|
||||
hide_state_machine_editor()
|
||||
state_machine_editor.queue_free()
|
||||
|
||||
func _handles(object):
|
||||
if object is StateMachine:
|
||||
_handled_and_ready_to_edit = true # this should not be necessary, but it seemingly is (Godot 4.0-rc1)
|
||||
return true # when return true from _handles, _edit can proceed.
|
||||
if object is StateMachinePlayer:
|
||||
if object.get_class() == "EditorDebuggerRemoteObject":
|
||||
set_focused_object(object)
|
||||
state_machine_editor.debug_mode = true
|
||||
return false
|
||||
return false
|
||||
|
||||
func _edit(object):
|
||||
if _handled_and_ready_to_edit: # Forces _handles => _edit flow. This should not be necessary, but it seemingly is (Godot 4.0-rc1)
|
||||
_handled_and_ready_to_edit = false
|
||||
set_focused_object(object)
|
||||
|
||||
func show_state_machine_editor():
|
||||
if focused_object and state_machine_editor:
|
||||
if not state_machine_editor.is_inside_tree():
|
||||
add_control_to_bottom_panel(state_machine_editor, "StateMachine")
|
||||
make_bottom_panel_item_visible(state_machine_editor)
|
||||
|
||||
func hide_state_machine_editor():
|
||||
if state_machine_editor.is_inside_tree():
|
||||
state_machine_editor.state_machine = null
|
||||
remove_control_from_bottom_panel(state_machine_editor)
|
||||
|
||||
func _on_EditorSelection_selection_changed():
|
||||
if editor_selection == null:
|
||||
return
|
||||
|
||||
var selected_nodes = editor_selection.get_selected_nodes()
|
||||
if selected_nodes.size() == 1:
|
||||
var selected_node = selected_nodes[0]
|
||||
if selected_node is StateMachinePlayer:
|
||||
set_focused_object(selected_node)
|
||||
return
|
||||
set_focused_object(null)
|
||||
|
||||
func _on_focused_object_changed(new_obj):
|
||||
if new_obj:
|
||||
# Must be shown first, otherwise StateMachineEditor can't execute ui action as it is not added to scene tree
|
||||
show_state_machine_editor()
|
||||
var state_machine
|
||||
if focused_object is StateMachinePlayer:
|
||||
if focused_object.get_class() == "EditorDebuggerRemoteObject":
|
||||
state_machine = focused_object.get("Members/state_machine")
|
||||
if state_machine == null:
|
||||
state_machine = focused_object.get("Members/StateMachinePlayer.gd/state_machine")
|
||||
else:
|
||||
state_machine = focused_object.state_machine
|
||||
state_machine_editor.state_machine_player = focused_object
|
||||
elif focused_object is StateMachine:
|
||||
state_machine = focused_object
|
||||
state_machine_editor.state_machine_player = null
|
||||
state_machine_editor.state_machine = state_machine
|
||||
else:
|
||||
hide_state_machine_editor()
|
||||
|
||||
func _on_inspector_changed(property):
|
||||
#get_editor_interface().get_inspector().refresh()
|
||||
notify_property_list_changed()
|
||||
|
||||
func _on_StateMachineEditor_node_selected(node):
|
||||
var to_inspect
|
||||
if "state" in node:
|
||||
if node.state is StateMachine: # Ignore, inspect state machine will trigger edit()
|
||||
return
|
||||
to_inspect = node.state
|
||||
elif "transition" in node:
|
||||
to_inspect = node.transition
|
||||
get_editor_interface().inspect_object(to_inspect)
|
||||
|
||||
func _on_StateMachineEditor_node_deselected(node):
|
||||
# editor_selection.remove_node(node)
|
||||
get_editor_interface().inspect_object(state_machine_editor.state_machine)
|
||||
|
||||
func _on_StateMachineEditor_debug_mode_changed(new_debug_mode):
|
||||
if not new_debug_mode:
|
||||
state_machine_editor.debug_mode = false
|
||||
state_machine_editor.state_machine_player = null
|
||||
set_focused_object(null)
|
||||
hide_state_machine_editor()
|
||||
|
||||
func set_focused_object(obj):
|
||||
if focused_object != obj:
|
||||
focused_object = obj
|
||||
_on_focused_object_changed(obj)
|
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
103
addons/imjp94.yafsm/scripts/Utils.gd
Normal file
|
@ -0,0 +1,103 @@
|
|||
# Position Popup near to its target while within window, solution from ColorPickerButton source code(https://github.com/godotengine/godot/blob/6d8c14f849376905e1577f9fc3f9512bcffb1e3c/scene/gui/color_picker.cpp#L878)
|
||||
static func popup_on_target(popup: Popup, target: Control):
|
||||
popup.reset_size()
|
||||
var usable_rect = Rect2(Vector2.ZERO, DisplayServer.window_get_size_with_decorations())
|
||||
var cp_rect = Rect2(Vector2.ZERO, popup.get_size())
|
||||
for i in 4:
|
||||
if i > 1:
|
||||
cp_rect.position.y = target.global_position.y - cp_rect.size.y
|
||||
else:
|
||||
cp_rect.position.y = target.global_position.y + target.get_size().y
|
||||
|
||||
if i & 1:
|
||||
cp_rect.position.x = target.global_position.x
|
||||
else:
|
||||
cp_rect.position.x = target.global_position.x - max(0, cp_rect.size.x - target.get_size().x)
|
||||
|
||||
if usable_rect.encloses(cp_rect):
|
||||
break
|
||||
var main_window_position = DisplayServer.window_get_position()
|
||||
var popup_position = main_window_position + Vector2i(cp_rect.position) # make it work in multi-screen setups
|
||||
popup.set_position(popup_position)
|
||||
popup.popup()
|
||||
|
||||
static func get_complementary_color(color):
|
||||
var r = max(color.r, max(color.b, color.g)) + min(color.r, min(color.b, color.g)) - color.r
|
||||
var g = max(color.r, max(color.b, color.g)) + min(color.r, min(color.b, color.g)) - color.g
|
||||
var b = max(color.r, max(color.b, color.g)) + min(color.r, min(color.b, color.g)) - color.b
|
||||
return Color(r, g, b)
|
||||
|
||||
class CohenSutherland:
|
||||
const INSIDE = 0 # 0000
|
||||
const LEFT = 1 # 0001
|
||||
const RIGHT = 2 # 0010
|
||||
const BOTTOM = 4 # 0100
|
||||
const TOP = 8 # 1000
|
||||
|
||||
# Compute bit code for a point(x, y) using the clip
|
||||
static func compute_code(x, y, x_min, y_min, x_max, y_max):
|
||||
var code = INSIDE # initialised as being inside of clip window
|
||||
if x < x_min: # to the left of clip window
|
||||
code |= LEFT
|
||||
elif x > x_max: # to the right of clip window
|
||||
code |= RIGHT
|
||||
|
||||
if y < y_min: # below the clip window
|
||||
code |= BOTTOM
|
||||
elif y > y_max: # above the clip window
|
||||
code |= TOP
|
||||
|
||||
return code
|
||||
|
||||
# Cohen-Sutherland clipping algorithm clips a line from
|
||||
# P0 = (x0, y0) to P1 = (x1, y1) against a rectangle with
|
||||
# diagonal from start(x_min, y_min) to end(x_max, y_max)
|
||||
static func line_intersect_rectangle(from, to, rect):
|
||||
var x_min = rect.position.x
|
||||
var y_min = rect.position.y
|
||||
var x_max = rect.end.x
|
||||
var y_max = rect.end.y
|
||||
|
||||
var code0 = compute_code(from.x, from.y, x_min, y_min, x_max, y_max)
|
||||
var code1 = compute_code(to.x, to.y, x_min, y_min, x_max, y_max)
|
||||
|
||||
var i = 0
|
||||
while true:
|
||||
i += 1
|
||||
if !(code0 | code1): # bitwise OR 0, both points inside window
|
||||
return true
|
||||
elif code0 & code1: # Bitwise AND not 0, both points share an outside zone
|
||||
return false
|
||||
else:
|
||||
# Failed both test, so calculate line segment to clip
|
||||
# from outside point to intersection with clip edge
|
||||
var x
|
||||
var y
|
||||
var code_out = max(code0, code1) # Pick the one outside window
|
||||
|
||||
# Find intersection points
|
||||
# slope = (y1 - y0) / (x1 - x0)
|
||||
# x = x0 + (1 / slope) * (ym - y0), where ym is y_mix/y_max
|
||||
# y = y0 + slope * (xm - x0), where xm is x_min/x_max
|
||||
if code_out & TOP: # Point above clip window
|
||||
x = from.x + (to.x - from.x) * (y_max - from.y) / (to.y - from.y)
|
||||
y = y_max
|
||||
elif code_out & BOTTOM: # Point below clip window
|
||||
x = from.x + (to.x - from.x) * (y_min - from.y) / (to.y - from.y)
|
||||
y = y_min
|
||||
elif code_out & RIGHT: # Point is to the right of clip window
|
||||
y = from.y + (to.y - from.y) * (x_max - from.x) / (to.x - from.x)
|
||||
x = x_max
|
||||
elif code_out & LEFT: # Point is to the left of clip window
|
||||
y = from.y + (to.y - from.y) * (x_min - from.x) / (to.x - from.x)
|
||||
x = x_min
|
||||
|
||||
# Now move outside point to intersection point to clip and ready for next pass
|
||||
if code_out == code0:
|
||||
from.x = x
|
||||
from.y = y
|
||||
code0 = compute_code(from.x, from.y, x_min, y_min, x_max, y_max)
|
||||
else:
|
||||
to.x = x
|
||||
to.y = y
|
||||
code1 = compute_code(to.x ,to.y, x_min, y_min, x_max, y_max)
|
88
addons/imjp94.yafsm/src/StackPlayer.gd
Normal file
|
@ -0,0 +1,88 @@
|
|||
@tool
|
||||
class_name StackPlayer extends Node
|
||||
|
||||
signal pushed(to) # When item pushed to stack
|
||||
signal popped(from) # When item popped from stack
|
||||
|
||||
# Enum to specify how reseting state stack should trigger event(transit, push, pop etc.)
|
||||
enum ResetEventTrigger {
|
||||
NONE = -1, # No event
|
||||
ALL = 0, # All removed state will emit event
|
||||
LAST_TO_DEST = 1 # Only last state and destination will emit event
|
||||
}
|
||||
|
||||
var current: # Current item on top of stack
|
||||
get = get_current
|
||||
var stack:
|
||||
set = _set_stack,
|
||||
get = _get_stack
|
||||
|
||||
var _stack
|
||||
|
||||
|
||||
func _init():
|
||||
_stack = []
|
||||
|
||||
# Push an item to the top of stack
|
||||
func push(to):
|
||||
var from = get_current()
|
||||
_stack.push_back(to)
|
||||
_on_pushed(from, to)
|
||||
emit_signal("pushed", to)
|
||||
|
||||
# Remove the current item on top of stack
|
||||
func pop():
|
||||
var to = get_previous()
|
||||
var from = _stack.pop_back()
|
||||
_on_popped(from, to)
|
||||
emit_signal("popped", from)
|
||||
|
||||
# Called when item pushed
|
||||
func _on_pushed(from, to):
|
||||
pass
|
||||
|
||||
# Called when item popped
|
||||
func _on_popped(from, to):
|
||||
pass
|
||||
|
||||
# Reset stack to given index, -1 to clear all item by default
|
||||
# Use ResetEventTrigger to define how _on_popped should be called
|
||||
func reset(to=-1, event=ResetEventTrigger.ALL):
|
||||
assert(to > -2 and to < _stack.size(), "Reset to index out of bounds")
|
||||
var last_index = _stack.size() - 1
|
||||
var first_state = ""
|
||||
var num_to_pop = last_index - to
|
||||
|
||||
if num_to_pop > 0:
|
||||
for i in range(num_to_pop):
|
||||
first_state = get_current() if i == 0 else first_state
|
||||
match event:
|
||||
ResetEventTrigger.LAST_TO_DEST:
|
||||
_stack.pop_back()
|
||||
if i == num_to_pop - 1:
|
||||
_stack.push_back(first_state)
|
||||
pop()
|
||||
ResetEventTrigger.ALL:
|
||||
pop()
|
||||
_:
|
||||
_stack.pop_back()
|
||||
elif num_to_pop == 0:
|
||||
match event:
|
||||
ResetEventTrigger.NONE:
|
||||
_stack.pop_back()
|
||||
_:
|
||||
pop()
|
||||
|
||||
func _set_stack(val):
|
||||
push_warning("Attempting to edit read-only state stack directly. " \
|
||||
+ "Control state machine from setting parameters or call update() instead")
|
||||
|
||||
# Get duplicate of the stack being played
|
||||
func _get_stack():
|
||||
return _stack.duplicate()
|
||||
|
||||
func get_current():
|
||||
return _stack.back() if not _stack.is_empty() else null
|
||||
|
||||
func get_previous():
|
||||
return _stack[_stack.size() - 2] if _stack.size() > 1 else null
|
94
addons/imjp94.yafsm/src/StateDirectory.gd
Normal file
|
@ -0,0 +1,94 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const State = preload("states/State.gd")
|
||||
|
||||
var path
|
||||
var current:
|
||||
get = get_current
|
||||
var base:
|
||||
get = get_base
|
||||
var end:
|
||||
get = get_end
|
||||
|
||||
var _current_index = 0
|
||||
var _dirs = [""] # Empty string equals to root
|
||||
|
||||
|
||||
func _init(p):
|
||||
path = p
|
||||
_dirs += Array(p.split("/"))
|
||||
|
||||
# Move to next level and return state if exists, else null
|
||||
func next():
|
||||
if has_next():
|
||||
_current_index += 1
|
||||
return get_current_end()
|
||||
|
||||
return null
|
||||
|
||||
# Move to previous level and return state if exists, else null
|
||||
func back():
|
||||
if has_back():
|
||||
_current_index -= 1
|
||||
return get_current_end()
|
||||
|
||||
return null
|
||||
|
||||
# Move to specified index and return state
|
||||
func goto(index):
|
||||
assert(index > -1 and index < _dirs.size())
|
||||
_current_index = index
|
||||
return get_current_end()
|
||||
|
||||
# Check if directory has next level
|
||||
func has_next():
|
||||
return _current_index < _dirs.size() - 1
|
||||
|
||||
# Check if directory has previous level
|
||||
func has_back():
|
||||
return _current_index > 0
|
||||
|
||||
# Get current full path
|
||||
func get_current():
|
||||
# In Godot 4.x the end parameter of Array.slice() is EXCLUSIVE!
|
||||
# https://docs.godotengine.org/en/latest/classes/class_array.html#class-array-method-slice
|
||||
var packed_string_array: PackedStringArray = PackedStringArray(_dirs.slice(get_base_index(), _current_index+1))
|
||||
return "/".join(packed_string_array)
|
||||
|
||||
# Get current end state name of path
|
||||
func get_current_end():
|
||||
var current_path = get_current()
|
||||
return current_path.right(current_path.length()-1 - current_path.rfind("/"))
|
||||
|
||||
# Get index of base state
|
||||
func get_base_index():
|
||||
return 1 # Root(empty string) at index 0, base at index 1
|
||||
|
||||
# Get level index of end state
|
||||
func get_end_index():
|
||||
return _dirs.size() - 1
|
||||
|
||||
# Get base state name
|
||||
func get_base():
|
||||
return _dirs[get_base_index()]
|
||||
|
||||
# Get end state name
|
||||
func get_end():
|
||||
return _dirs[get_end_index()]
|
||||
|
||||
# Get arrays of directories
|
||||
func get_dirs():
|
||||
return _dirs.duplicate()
|
||||
|
||||
# Check if it is Entry state
|
||||
func is_entry():
|
||||
return get_end() == State.ENTRY_STATE
|
||||
|
||||
# Check if it is Exit state
|
||||
func is_exit():
|
||||
return get_end() == State.EXIT_STATE
|
||||
|
||||
# Check if it is nested. ("Base" is not nested, "Base/NextState" is nested)
|
||||
func is_nested():
|
||||
return _dirs.size() > 2 # Root(empty string) & base taken 2 place
|
378
addons/imjp94.yafsm/src/StateMachinePlayer.gd
Normal file
|
@ -0,0 +1,378 @@
|
|||
@tool
|
||||
class_name StateMachinePlayer extends StackPlayer
|
||||
|
||||
|
||||
signal transited(from, to) # Transition of state
|
||||
signal entered(to) # Entry of state machine(including nested), empty string equals to root
|
||||
signal exited(from) # Exit of state machine(including nested, empty string equals to root
|
||||
signal updated(state, delta) # Time to update(based on process_mode), up to user to handle any logic, for example, update movement of KinematicBody
|
||||
|
||||
# Enum to define how state machine should be updated
|
||||
enum UpdateProcessMode {
|
||||
PHYSICS,
|
||||
IDLE,
|
||||
MANUAL
|
||||
}
|
||||
|
||||
@export var state_machine: StateMachine # StateMachine being played
|
||||
@export var active: = true: # Activeness of player
|
||||
set = set_active
|
||||
@export var autostart: = true # Automatically enter Entry state on ready if true
|
||||
@export var update_process_mode: UpdateProcessMode = UpdateProcessMode.IDLE: # ProcessMode of player
|
||||
set = set_update_process_mode
|
||||
|
||||
var _is_started = false
|
||||
var _parameters # Parameters to be passed to condition
|
||||
var _local_parameters
|
||||
var _is_update_locked = true
|
||||
var _was_transited = false # If last transition was successful
|
||||
var _is_param_edited = false
|
||||
|
||||
|
||||
func _init():
|
||||
super._init()
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
_parameters = {}
|
||||
_local_parameters = {}
|
||||
_was_transited = true # Trigger _transit on _ready
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var _errors: Array[String] = []
|
||||
|
||||
if state_machine:
|
||||
if not state_machine.has_entry():
|
||||
_errors.append("The StateMachine provided does not have an Entry node.\nPlease create one to it works properly.")
|
||||
else:
|
||||
_errors.append("StateMachinePlayer needs a StateMachine to run.\nPlease create a StateMachine resource to it.")
|
||||
|
||||
return PackedStringArray(_errors)
|
||||
|
||||
func _ready():
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
set_process(false)
|
||||
set_physics_process(false)
|
||||
call_deferred("_initiate") # Make sure connection of signals can be done in _ready to receive all signal callback
|
||||
|
||||
func _initiate():
|
||||
if autostart:
|
||||
start()
|
||||
_on_active_changed()
|
||||
_on_update_process_mode_changed()
|
||||
|
||||
func _process(delta):
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
_update_start()
|
||||
update(delta)
|
||||
_update_end()
|
||||
|
||||
func _physics_process(delta):
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
_update_start()
|
||||
update(delta)
|
||||
_update_end()
|
||||
|
||||
# Only get called in 2 condition, _parameters edited or last transition was successful
|
||||
func _transit():
|
||||
if not active:
|
||||
return
|
||||
# Attempt to transit if parameter edited or last transition was successful
|
||||
if not _is_param_edited and not _was_transited:
|
||||
return
|
||||
|
||||
var from = get_current()
|
||||
var local_params = _local_parameters.get(path_backward(from), {})
|
||||
var next_state = state_machine.transit(get_current(), _parameters, local_params)
|
||||
if next_state:
|
||||
if stack.has(next_state):
|
||||
reset(stack.find(next_state))
|
||||
else:
|
||||
push(next_state)
|
||||
var to = next_state
|
||||
_was_transited = next_state != null and next_state != ""
|
||||
_is_param_edited = false
|
||||
_flush_trigger(_parameters)
|
||||
_flush_trigger(_local_parameters, true)
|
||||
|
||||
if _was_transited:
|
||||
_on_state_changed(from, to)
|
||||
|
||||
func _on_state_changed(from, to):
|
||||
match to:
|
||||
State.ENTRY_STATE:
|
||||
emit_signal("entered", "")
|
||||
State.EXIT_STATE:
|
||||
set_active(false) # Disable on exit
|
||||
emit_signal("exited", "")
|
||||
|
||||
if to.ends_with(State.ENTRY_STATE) and to.length() > State.ENTRY_STATE.length():
|
||||
# Nexted Entry state
|
||||
var state = path_backward(get_current())
|
||||
emit_signal("entered", state)
|
||||
elif to.ends_with(State.EXIT_STATE) and to.length() > State.EXIT_STATE.length():
|
||||
# Nested Exit state, clear "local" params
|
||||
var state = path_backward(get_current())
|
||||
clear_param(state, false) # Clearing params internally, do not update
|
||||
emit_signal("exited", state)
|
||||
|
||||
emit_signal("transited", from, to)
|
||||
|
||||
# Called internally if process_mode is PHYSICS/IDLE to unlock update()
|
||||
func _update_start():
|
||||
_is_update_locked = false
|
||||
|
||||
# Called internally if process_mode is PHYSICS/IDLE to lock update() from external call
|
||||
func _update_end():
|
||||
_is_update_locked = true
|
||||
|
||||
# Called after update() which is dependant on process_mode, override to process current state
|
||||
func _on_updated(state, delta):
|
||||
pass
|
||||
|
||||
func _on_update_process_mode_changed():
|
||||
if not active:
|
||||
return
|
||||
|
||||
match update_process_mode:
|
||||
UpdateProcessMode.PHYSICS:
|
||||
set_physics_process(true)
|
||||
set_process(false)
|
||||
UpdateProcessMode.IDLE:
|
||||
set_physics_process(false)
|
||||
set_process(true)
|
||||
UpdateProcessMode.MANUAL:
|
||||
set_physics_process(false)
|
||||
set_process(false)
|
||||
|
||||
func _on_active_changed():
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
if active:
|
||||
_on_update_process_mode_changed()
|
||||
_transit()
|
||||
else:
|
||||
set_physics_process(false)
|
||||
set_process(false)
|
||||
|
||||
# Remove all trigger(param with null value) from provided params, only get called after _transit
|
||||
# Trigger another call of _flush_trigger on first layer of dictionary if nested is true
|
||||
func _flush_trigger(params, nested=false):
|
||||
for param_key in params.keys():
|
||||
var value = params[param_key]
|
||||
if nested and value is Dictionary:
|
||||
_flush_trigger(value)
|
||||
if value == null: # Param with null as value is treated as trigger
|
||||
params.erase(param_key)
|
||||
|
||||
func reset(to=-1, event=ResetEventTrigger.LAST_TO_DEST):
|
||||
super.reset(to, event)
|
||||
_was_transited = true # Make sure to call _transit on next update
|
||||
|
||||
# Manually start the player, automatically called if autostart is true
|
||||
func start():
|
||||
assert(state_machine != null, "A StateMachine resource is required to start this StateMachinePlayer.")
|
||||
assert(state_machine.has_entry(), "The StateMachine provided does not have an Entry node.")
|
||||
push(State.ENTRY_STATE)
|
||||
emit_signal("entered", "")
|
||||
_was_transited = true
|
||||
_is_started = true
|
||||
|
||||
# Restart player
|
||||
func restart(is_active=true, preserve_params=false):
|
||||
reset()
|
||||
set_active(is_active)
|
||||
if not preserve_params:
|
||||
clear_param("", false)
|
||||
start()
|
||||
|
||||
# Update player to, first initiate transition, then call _on_updated, finally emit "update" signal, delta will be given based on process_mode.
|
||||
# Can only be called manually if process_mode is MANUAL, otherwise, assertion error will be raised.
|
||||
# *delta provided will be reflected in signal updated(state, delta)
|
||||
func update(delta=get_physics_process_delta_time()):
|
||||
if not active:
|
||||
return
|
||||
if update_process_mode != UpdateProcessMode.MANUAL:
|
||||
assert(not _is_update_locked, "Attempting to update manually with ProcessMode %s" % UpdateProcessMode.keys()[update_process_mode])
|
||||
|
||||
_transit()
|
||||
var current_state = get_current()
|
||||
_on_updated(current_state, delta)
|
||||
emit_signal("updated", current_state, delta)
|
||||
if update_process_mode == UpdateProcessMode.MANUAL:
|
||||
# Make sure to auto advance even in MANUAL mode
|
||||
if _was_transited:
|
||||
call_deferred("update")
|
||||
|
||||
# Set trigger to be tested with condition, then trigger _transit on next update,
|
||||
# automatically call update() if process_mode set to MANUAL and auto_update true
|
||||
# Nested trigger can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
|
||||
func set_trigger(name, auto_update=true):
|
||||
set_param(name, null, auto_update)
|
||||
|
||||
func set_nested_trigger(path, name, auto_update=true):
|
||||
set_nested_param(path, name, null, auto_update)
|
||||
|
||||
# Set param(null value treated as trigger) to be tested with condition, then trigger _transit on next update,
|
||||
# automatically call update() if process_mode set to MANUAL and auto_update true
|
||||
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
|
||||
func set_param(name, value, auto_update=true):
|
||||
var path = ""
|
||||
if "/" in name:
|
||||
path = path_backward(name)
|
||||
name = path_end_dir(name)
|
||||
set_nested_param(path, name, value, auto_update)
|
||||
|
||||
func set_nested_param(path, name, value, auto_update=true):
|
||||
if path.is_empty():
|
||||
_parameters[name] = value
|
||||
else:
|
||||
var local_params = _local_parameters.get(path)
|
||||
if local_params is Dictionary:
|
||||
local_params[name] = value
|
||||
else:
|
||||
local_params = {}
|
||||
local_params[name] = value
|
||||
_local_parameters[path] = local_params
|
||||
_on_param_edited(auto_update)
|
||||
|
||||
# Remove param, then trigger _transit on next update,
|
||||
# automatically call update() if process_mode set to MANUAL and auto_update true
|
||||
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
|
||||
func erase_param(name, auto_update=true):
|
||||
var path = ""
|
||||
if "/" in name:
|
||||
path = path_backward(name)
|
||||
name = path_end_dir(name)
|
||||
return erase_nested_param(path, name, auto_update)
|
||||
|
||||
func erase_nested_param(path, name, auto_update=true):
|
||||
var result = false
|
||||
if path.is_empty():
|
||||
result = _parameters.erase(name)
|
||||
else:
|
||||
result = _local_parameters.get(path, {}).erase(name)
|
||||
_on_param_edited(auto_update)
|
||||
return result
|
||||
|
||||
# Clear params from specified path, empty string to clear all, then trigger _transit on next update,
|
||||
# automatically call update() if process_mode set to MANUAL and auto_update true
|
||||
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
|
||||
func clear_param(path="", auto_update=true):
|
||||
if path.is_empty():
|
||||
_parameters.clear()
|
||||
else:
|
||||
_local_parameters.get(path, {}).clear()
|
||||
# Clear nested params
|
||||
for param_key in _local_parameters.keys():
|
||||
if param_key.begins_with(path):
|
||||
_local_parameters.erase(param_key)
|
||||
|
||||
# Called when param edited, automatically call update() if process_mode set to MANUAL and auto_update true
|
||||
func _on_param_edited(auto_update=true):
|
||||
_is_param_edited = true
|
||||
if update_process_mode == UpdateProcessMode.MANUAL and auto_update and _is_started:
|
||||
update()
|
||||
|
||||
# Get value of param
|
||||
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
|
||||
func get_param(name, default=null):
|
||||
var path = ""
|
||||
if "/" in name:
|
||||
path = path_backward(name)
|
||||
name = path_end_dir(name)
|
||||
return get_nested_param(path, name, default)
|
||||
|
||||
func get_nested_param(path, name, default=null):
|
||||
if path.is_empty():
|
||||
return _parameters.get(name, default)
|
||||
else:
|
||||
var local_params = _local_parameters.get(path, {})
|
||||
return local_params.get(name, default)
|
||||
|
||||
# Get duplicate of whole parameter dictionary
|
||||
func get_params():
|
||||
return _parameters.duplicate()
|
||||
|
||||
# Return true if param exists
|
||||
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
|
||||
func has_param(name):
|
||||
var path = ""
|
||||
if "/" in name:
|
||||
path = path_backward(name)
|
||||
name = path_end_dir(name)
|
||||
return has_nested_param(path, name)
|
||||
|
||||
func has_nested_param(path, name):
|
||||
if path.is_empty():
|
||||
return name in _parameters
|
||||
else:
|
||||
var local_params = _local_parameters.get(path, {})
|
||||
return name in local_params
|
||||
|
||||
# Return if player started
|
||||
func is_entered():
|
||||
return State.ENTRY_STATE in stack
|
||||
|
||||
# Return if player ended
|
||||
func is_exited():
|
||||
return get_current() == State.EXIT_STATE
|
||||
|
||||
func set_active(v):
|
||||
if active != v:
|
||||
if v:
|
||||
if is_exited():
|
||||
push_warning("Attempting to make exited StateMachinePlayer active, call reset() then set_active() instead")
|
||||
return
|
||||
active = v
|
||||
_on_active_changed()
|
||||
|
||||
func set_update_process_mode(mode):
|
||||
if update_process_mode != mode:
|
||||
update_process_mode = mode
|
||||
_on_update_process_mode_changed()
|
||||
|
||||
func get_current():
|
||||
var v = super.get_current()
|
||||
return v if v else ""
|
||||
|
||||
func get_previous():
|
||||
var v = super.get_previous()
|
||||
return v if v else ""
|
||||
|
||||
# Convert node path to state path that can be used to query state with StateMachine.get_state.
|
||||
# Node path, "root/path/to/state", equals to State path, "path/to/state"
|
||||
static func node_path_to_state_path(node_path):
|
||||
var p = node_path.replace("root", "")
|
||||
if p.begins_with("/"):
|
||||
p = p.substr(1)
|
||||
return p
|
||||
|
||||
# Convert state path to node path that can be used for query node in scene tree.
|
||||
# State path, "path/to/state", equals to Node path, "root/path/to/state"
|
||||
static func state_path_to_node_path(state_path):
|
||||
var path = state_path
|
||||
if path.is_empty():
|
||||
path = "root"
|
||||
else:
|
||||
path = str("root/", path)
|
||||
return path
|
||||
|
||||
# Return parent path, "path/to/state" return "path/to"
|
||||
static func path_backward(path):
|
||||
return path.substr(0, path.rfind("/"))
|
||||
|
||||
# Return end directory of path, "path/to/state" returns "state"
|
||||
static func path_end_dir(path):
|
||||
# In Godot 4.x the old behaviour of String.right() can be achieved with
|
||||
# a negative length. Check the docs:
|
||||
# https://docs.godotengine.org/en/stable/classes/class_string.html#class-string-method-right
|
||||
return path.right(path.length()-1 - path.rfind("/"))
|
22
addons/imjp94.yafsm/src/conditions/BooleanCondition.gd
Normal file
|
@ -0,0 +1,22 @@
|
|||
@tool
|
||||
extends ValueCondition
|
||||
class_name BooleanCondition
|
||||
|
||||
@export var value: bool:
|
||||
set = set_value,
|
||||
get = get_value
|
||||
|
||||
|
||||
func set_value(v):
|
||||
if value != v:
|
||||
value = v
|
||||
emit_signal("value_changed", v)
|
||||
emit_signal("display_string_changed", display_string())
|
||||
|
||||
func get_value():
|
||||
return value
|
||||
|
||||
func compare(v):
|
||||
if typeof(v) != TYPE_BOOL:
|
||||
return false
|
||||
return super.compare(v)
|
23
addons/imjp94.yafsm/src/conditions/Condition.gd
Normal file
|
@ -0,0 +1,23 @@
|
|||
@tool
|
||||
extends Resource
|
||||
class_name Condition
|
||||
|
||||
signal name_changed(old, new)
|
||||
signal display_string_changed(new)
|
||||
|
||||
@export var name: = "": # Name of condition, unique to Transition
|
||||
set = set_name
|
||||
|
||||
|
||||
func _init(p_name=""):
|
||||
name = p_name
|
||||
|
||||
func set_name(n):
|
||||
if name != n:
|
||||
var old = name
|
||||
name = n
|
||||
emit_signal("name_changed", old, n)
|
||||
emit_signal("display_string_changed", display_string())
|
||||
|
||||
func display_string():
|
||||
return name
|
25
addons/imjp94.yafsm/src/conditions/FloatCondition.gd
Normal file
|
@ -0,0 +1,25 @@
|
|||
@tool
|
||||
extends ValueCondition
|
||||
class_name FloatCondition
|
||||
|
||||
@export var value: float:
|
||||
set = set_value,
|
||||
get = get_value
|
||||
|
||||
|
||||
func set_value(v):
|
||||
if not is_equal_approx(value, v):
|
||||
value = v
|
||||
emit_signal("value_changed", v)
|
||||
emit_signal("display_string_changed", display_string())
|
||||
|
||||
func get_value():
|
||||
return value
|
||||
|
||||
func get_value_string():
|
||||
return str(snapped(value, 0.01)).pad_decimals(2)
|
||||
|
||||
func compare(v):
|
||||
if typeof(v) != TYPE_FLOAT:
|
||||
return false
|
||||
return super.compare(v)
|
23
addons/imjp94.yafsm/src/conditions/IntegerCondition.gd
Normal file
|
@ -0,0 +1,23 @@
|
|||
@tool
|
||||
extends ValueCondition
|
||||
class_name IntegerCondition
|
||||
|
||||
|
||||
@export var value: int:
|
||||
set = set_value,
|
||||
get = get_value
|
||||
|
||||
|
||||
func set_value(v):
|
||||
if value != v:
|
||||
value = v
|
||||
emit_signal("value_changed", v)
|
||||
emit_signal("display_string_changed", display_string())
|
||||
|
||||
func get_value():
|
||||
return value
|
||||
|
||||
func compare(v):
|
||||
if typeof(v) != TYPE_INT:
|
||||
return false
|
||||
return super.compare(v)
|
25
addons/imjp94.yafsm/src/conditions/StringCondition.gd
Normal file
|
@ -0,0 +1,25 @@
|
|||
@tool
|
||||
extends ValueCondition
|
||||
class_name StringCondition
|
||||
|
||||
@export var value: String:
|
||||
set = set_value,
|
||||
get = get_value
|
||||
|
||||
|
||||
func set_value(v):
|
||||
if value != v:
|
||||
value = v
|
||||
emit_signal("value_changed", v)
|
||||
emit_signal("display_string_changed", display_string())
|
||||
|
||||
func get_value():
|
||||
return value
|
||||
|
||||
func get_value_string():
|
||||
return "\"%s\"" % value
|
||||
|
||||
func compare(v):
|
||||
if typeof(v) != TYPE_STRING:
|
||||
return false
|
||||
return super.compare(v)
|
73
addons/imjp94.yafsm/src/conditions/ValueCondition.gd
Normal file
|
@ -0,0 +1,73 @@
|
|||
@tool
|
||||
extends Condition
|
||||
class_name ValueCondition
|
||||
|
||||
signal comparation_changed(new_comparation) # Comparation hanged
|
||||
signal value_changed(new_value) # Value changed
|
||||
|
||||
# Enum to define how to compare value
|
||||
enum Comparation {
|
||||
EQUAL,
|
||||
INEQUAL,
|
||||
GREATER,
|
||||
LESSER,
|
||||
GREATER_OR_EQUAL,
|
||||
LESSER_OR_EQUAL
|
||||
}
|
||||
# Comparation symbols arranged in order as enum Comparation
|
||||
const COMPARATION_SYMBOLS = [
|
||||
"==",
|
||||
"!=",
|
||||
">",
|
||||
"<",
|
||||
"≥",
|
||||
"≤"
|
||||
]
|
||||
|
||||
@export var comparation: Comparation = Comparation.EQUAL:
|
||||
set = set_comparation
|
||||
|
||||
func _init(p_name="", p_comparation=Comparation.EQUAL):
|
||||
super._init(p_name)
|
||||
comparation = p_comparation
|
||||
|
||||
func set_comparation(c):
|
||||
if comparation != c:
|
||||
comparation = c
|
||||
emit_signal("comparation_changed", c)
|
||||
emit_signal("display_string_changed", display_string())
|
||||
|
||||
# To be overrided by child class and emit value_changed signal
|
||||
func set_value(v):
|
||||
pass
|
||||
|
||||
# To be overrided by child class, as it is impossible to export(Variant)
|
||||
func get_value():
|
||||
pass
|
||||
|
||||
# To be used in _to_string()
|
||||
func get_value_string():
|
||||
return get_value()
|
||||
|
||||
# Compare value against this condition, return true if succeeded
|
||||
func compare(v):
|
||||
if v == null:
|
||||
return false
|
||||
|
||||
match comparation:
|
||||
Comparation.EQUAL:
|
||||
return v == get_value()
|
||||
Comparation.INEQUAL:
|
||||
return v != get_value()
|
||||
Comparation.GREATER:
|
||||
return v > get_value()
|
||||
Comparation.LESSER:
|
||||
return v < get_value()
|
||||
Comparation.GREATER_OR_EQUAL:
|
||||
return v >= get_value()
|
||||
Comparation.LESSER_OR_EQUAL:
|
||||
return v <= get_value()
|
||||
|
||||
# Return human readable display string, for example, "condition_name == True"
|
||||
func display_string():
|
||||
return "%s %s %s" % [super.display_string(), COMPARATION_SYMBOLS[comparation], get_value_string()]
|
11
addons/imjp94.yafsm/src/debugger/StackItem.tscn
Normal file
|
@ -0,0 +1,11 @@
|
|||
[gd_scene format=3 uid="uid://b3b5ivtjmka6b"]
|
||||
|
||||
[node name="StackItem" type="PanelContainer"]
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
offset_right = 36.0
|
||||
offset_bottom = 26.0
|
||||
text = "Item"
|
50
addons/imjp94.yafsm/src/debugger/StackPlayerDebugger.gd
Normal file
|
@ -0,0 +1,50 @@
|
|||
@tool
|
||||
extends Control
|
||||
const StackPlayer = preload("../StackPlayer.gd")
|
||||
const StackItem = preload("StackItem.tscn")
|
||||
|
||||
@onready var Stack = $MarginContainer/Stack
|
||||
|
||||
|
||||
func _get_configuration_warning():
|
||||
if not (get_parent() is StackPlayer):
|
||||
return "Debugger must be child of StackPlayer"
|
||||
return ""
|
||||
|
||||
func _ready():
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
get_parent().pushed.connect(_on_StackPlayer_pushed)
|
||||
get_parent().popped.connect(_on_StackPlayer_popped)
|
||||
sync_stack()
|
||||
|
||||
# Override to handle custom object presentation
|
||||
func _on_set_label(label, obj):
|
||||
label.text = obj
|
||||
|
||||
func _on_StackPlayer_pushed(to):
|
||||
var stack_item = StackItem.instantiate()
|
||||
_on_set_label(stack_item.get_node("Label"), to)
|
||||
Stack.add_child(stack_item)
|
||||
Stack.move_child(stack_item, 0)
|
||||
|
||||
func _on_StackPlayer_popped(from):
|
||||
# Sync whole stack instead of just popping top item, as ResetEventTrigger passed to reset() may be varied
|
||||
sync_stack()
|
||||
|
||||
func sync_stack():
|
||||
var diff = Stack.get_child_count() - get_parent().stack.size()
|
||||
for i in abs(diff):
|
||||
if diff < 0:
|
||||
var stack_item = StackItem.instantiate()
|
||||
Stack.add_child(stack_item)
|
||||
else:
|
||||
var child = Stack.get_child(0)
|
||||
Stack.remove_child(child)
|
||||
child.queue_free()
|
||||
var stack = get_parent().stack
|
||||
for i in stack.size():
|
||||
var obj = stack[stack.size()-1 - i] # Descending order, to list from bottom to top in VBoxContainer
|
||||
var child = Stack.get_child(i)
|
||||
_on_set_label(child.get_node("Label"), obj)
|
27
addons/imjp94.yafsm/src/debugger/StackPlayerDebugger.tscn
Normal file
|
@ -0,0 +1,27 @@
|
|||
[gd_scene load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/imjp94.yafsm/src/debugger/StackPlayerDebugger.gd" type="Script" id=1]
|
||||
|
||||
[node name="StackPlayerDebugger" type="Control"]
|
||||
modulate = Color( 1, 1, 1, 0.498039 )
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource( 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Stack" type="VBoxContainer" parent="MarginContainer"]
|
||||
margin_top = 600.0
|
||||
margin_bottom = 600.0
|
||||
mouse_filter = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 8
|
39
addons/imjp94.yafsm/src/states/State.gd
Normal file
|
@ -0,0 +1,39 @@
|
|||
@tool
|
||||
extends Resource
|
||||
class_name State
|
||||
|
||||
signal name_changed(new_name)
|
||||
|
||||
# Reserved state name for Entry/Exit
|
||||
const ENTRY_STATE = "Entry"
|
||||
const EXIT_STATE = "Exit"
|
||||
|
||||
const META_GRAPH_OFFSET = "graph_offset" # Meta key for graph_offset
|
||||
|
||||
@export var name: = "": # Name of state, unique within StateMachine
|
||||
set = set_name
|
||||
|
||||
var graph_offset: # Position in FlowChart stored as meta, for editor only
|
||||
set = set_graph_offset,
|
||||
get = get_graph_offset
|
||||
|
||||
|
||||
func _init(p_name=""):
|
||||
name = p_name
|
||||
|
||||
func is_entry():
|
||||
return name == ENTRY_STATE
|
||||
|
||||
func is_exit():
|
||||
return name == EXIT_STATE
|
||||
|
||||
func set_graph_offset(offset):
|
||||
set_meta(META_GRAPH_OFFSET, offset)
|
||||
|
||||
func get_graph_offset():
|
||||
return get_meta(META_GRAPH_OFFSET) if has_meta(META_GRAPH_OFFSET) else Vector2.ZERO
|
||||
|
||||
func set_name(n):
|
||||
if name != n:
|
||||
name = n
|
||||
emit_signal("name_changed", name)
|
230
addons/imjp94.yafsm/src/states/StateMachine.gd
Normal file
|
@ -0,0 +1,230 @@
|
|||
@tool
|
||||
@icon("../../assets/icons/state_machine_icon.png")
|
||||
extends State
|
||||
class_name StateMachine
|
||||
|
||||
signal transition_added(transition) # Transition added
|
||||
signal transition_removed(to_state) # Transition removed
|
||||
|
||||
@export var states: Dictionary: # States within this StateMachine, keyed by State.name
|
||||
get = get_states,
|
||||
set = set_states
|
||||
@export var transitions: Dictionary: # Transitions from this state, keyed by Transition.to
|
||||
get = get_transitions,
|
||||
set = set_transitions
|
||||
|
||||
var _states
|
||||
var _transitions
|
||||
|
||||
|
||||
func _init(p_name="", p_transitions={}, p_states={}):
|
||||
super._init(p_name)
|
||||
_transitions = p_transitions
|
||||
_states = p_states
|
||||
|
||||
# Attempt to transit with global/local parameters, where local_params override params
|
||||
func transit(current_state, params={}, local_params={}):
|
||||
var nested_states = current_state.split("/")
|
||||
var is_nested = nested_states.size() > 1
|
||||
var end_state_machine = self
|
||||
var base_path = ""
|
||||
for i in nested_states.size() - 1: # Ignore last one, to get its parent StateMachine
|
||||
var state = nested_states[i]
|
||||
# Construct absolute base path
|
||||
base_path = join_path(base_path, [state])
|
||||
if end_state_machine != self:
|
||||
end_state_machine = end_state_machine.states[state]
|
||||
else:
|
||||
end_state_machine = _states[state] # First level state
|
||||
|
||||
# Nested StateMachine in Exit state
|
||||
if is_nested:
|
||||
var is_nested_exit = nested_states[nested_states.size()-1] == State.EXIT_STATE
|
||||
if is_nested_exit:
|
||||
# Normalize path to transit again with parent of end_state_machine
|
||||
var end_state_machine_parent_path = ""
|
||||
for i in nested_states.size() - 2: # Ignore last two state(which is end_state_machine/end_state)
|
||||
end_state_machine_parent_path = join_path(end_state_machine_parent_path, [nested_states[i]])
|
||||
var end_state_machine_parent = get_state(end_state_machine_parent_path)
|
||||
var normalized_current_state = end_state_machine.name
|
||||
var next_state = end_state_machine_parent.transit(normalized_current_state, params)
|
||||
if next_state:
|
||||
# Construct next state into absolute path
|
||||
next_state = join_path(end_state_machine_parent_path, [next_state])
|
||||
return next_state
|
||||
|
||||
# Transit with current running nested state machine
|
||||
var from_transitions = end_state_machine.transitions.get(nested_states[nested_states.size()-1])
|
||||
if from_transitions:
|
||||
var from_transitions_array = from_transitions.values()
|
||||
from_transitions_array.sort_custom(func(a, b): Transition.sort(a, b))
|
||||
|
||||
for transition in from_transitions_array:
|
||||
var next_state = transition.transit(params, local_params)
|
||||
if next_state:
|
||||
if "states" in end_state_machine.states[next_state]:
|
||||
# Next state is a StateMachine, return entry state of the state machine in absolute path
|
||||
next_state = join_path(base_path, [next_state, State.ENTRY_STATE])
|
||||
else:
|
||||
# Construct next state into absolute path
|
||||
next_state = join_path(base_path, [next_state])
|
||||
return next_state
|
||||
return null
|
||||
|
||||
# Get state from absolute path, for exmaple, "path/to/state" (root == empty string)
|
||||
# *It is impossible to get parent state machine with path like "../sibling", as StateMachine is not structed as a Tree
|
||||
func get_state(path):
|
||||
var state
|
||||
if path.is_empty():
|
||||
state = self
|
||||
else:
|
||||
var nested_states = path.split("/")
|
||||
for i in nested_states.size():
|
||||
var dir = nested_states[i]
|
||||
if state:
|
||||
state = state.states[dir]
|
||||
else:
|
||||
state = _states[dir] # First level state
|
||||
return state
|
||||
|
||||
# Add state, state name must be unique within this StateMachine, return state added if succeed else return null
|
||||
func add_state(state):
|
||||
if not state:
|
||||
return null
|
||||
if state.name in _states:
|
||||
return null
|
||||
|
||||
_states[state.name] = state
|
||||
return state
|
||||
|
||||
# Remove state by its name
|
||||
func remove_state(state):
|
||||
return _states.erase(state)
|
||||
|
||||
# Change existing state key in states(Dictionary), return true if success
|
||||
func change_state_name(from, to):
|
||||
if not (from in _states) or to in _states:
|
||||
return false
|
||||
|
||||
for state_key in _states.keys():
|
||||
var state = _states[state_key]
|
||||
var is_name_changing_state = state_key == from
|
||||
if is_name_changing_state:
|
||||
state.name = to
|
||||
_states[to] = state
|
||||
_states.erase(from)
|
||||
for from_key in _transitions.keys():
|
||||
var from_transitions = _transitions[from_key]
|
||||
if from_key == from:
|
||||
_transitions.erase(from)
|
||||
_transitions[to] = from_transitions
|
||||
for to_key in from_transitions.keys():
|
||||
var transition = from_transitions[to_key]
|
||||
if transition.from == from:
|
||||
transition.from = to
|
||||
elif transition.to == from:
|
||||
transition.to = to
|
||||
if not is_name_changing_state:
|
||||
# Transitions to name changed state needs to be updated
|
||||
from_transitions.erase(from)
|
||||
from_transitions[to] = transition
|
||||
return true
|
||||
|
||||
# Add transition, Transition.from must be equal to this state's name and Transition.to not added yet
|
||||
func add_transition(transition):
|
||||
if transition.from == "" or transition.to == "":
|
||||
push_warning("Transition missing from/to (%s/%s)" % [transition.from, transition.to])
|
||||
return
|
||||
|
||||
var from_transitions
|
||||
if transition.from in _transitions:
|
||||
from_transitions = _transitions[transition.from]
|
||||
else:
|
||||
from_transitions = {}
|
||||
_transitions[transition.from] = from_transitions
|
||||
|
||||
from_transitions[transition.to] = transition
|
||||
emit_signal("transition_added", transition)
|
||||
|
||||
# Remove transition with Transition.to(name of state transiting to)
|
||||
func remove_transition(from_state, to_state):
|
||||
var from_transitions = _transitions.get(from_state)
|
||||
if from_transitions:
|
||||
if to_state in from_transitions:
|
||||
from_transitions.erase(to_state)
|
||||
if from_transitions.is_empty():
|
||||
_transitions.erase(from_state)
|
||||
emit_signal("transition_removed", from_state, to_state)
|
||||
|
||||
func get_entries():
|
||||
return _transitions[State.ENTRY_STATE].values()
|
||||
|
||||
func get_exits():
|
||||
return _transitions[State.EXIT_STATE].values()
|
||||
|
||||
func has_entry():
|
||||
return State.ENTRY_STATE in _states
|
||||
|
||||
func has_exit():
|
||||
return State.EXIT_STATE in _states
|
||||
|
||||
# Get duplicate of states dictionary
|
||||
func get_states():
|
||||
return _states.duplicate()
|
||||
|
||||
func set_states(val):
|
||||
_states = val
|
||||
|
||||
# Get duplicate of transitions dictionary
|
||||
func get_transitions():
|
||||
return _transitions.duplicate()
|
||||
|
||||
func set_transitions(val):
|
||||
_transitions = val
|
||||
|
||||
static func join_path(base, dirs):
|
||||
var path = base
|
||||
for dir in dirs:
|
||||
if path.is_empty():
|
||||
path = dir
|
||||
else:
|
||||
path = str(path, "/", dir)
|
||||
return path
|
||||
|
||||
# Validate state machine resource to identify and fix error
|
||||
static func validate(state_machine):
|
||||
var validated = false
|
||||
for from_key in state_machine.transitions.keys():
|
||||
# Non-existing state found in StateMachine.transitions
|
||||
# See https://github.com/imjp94/gd-YAFSM/issues/6
|
||||
if not (from_key in state_machine.states):
|
||||
validated = true
|
||||
push_warning("gd-YAFSM ValidationError: Non-existing state(%s) found in transition" % from_key)
|
||||
state_machine.transitions.erase(from_key)
|
||||
continue
|
||||
|
||||
var from_transition = state_machine.transitions[from_key]
|
||||
for to_key in from_transition.keys():
|
||||
# Non-existing state found in StateMachine.transitions
|
||||
# See https://github.com/imjp94/gd-YAFSM/issues/6
|
||||
if not (to_key in state_machine.states):
|
||||
validated = true
|
||||
push_warning("gd-YAFSM ValidationError: Non-existing state(%s) found in transition(%s -> %s)" % [to_key, from_key, to_key])
|
||||
from_transition.erase(to_key)
|
||||
continue
|
||||
|
||||
# Mismatch of StateMachine.transitions with Transition.to
|
||||
# See https://github.com/imjp94/gd-YAFSM/issues/6
|
||||
var to_transition = from_transition[to_key]
|
||||
if to_key != to_transition.to:
|
||||
validated = true
|
||||
push_warning("gd-YAFSM ValidationError: Mismatch of StateMachine.transitions key(%s) with Transition.to(%s)" % [to_key, to_transition.to])
|
||||
to_transition.to = to_key
|
||||
|
||||
# Self connecting transition
|
||||
# See https://github.com/imjp94/gd-YAFSM/issues/5
|
||||
if to_transition.from == to_transition.to:
|
||||
validated = true
|
||||
push_warning("gd-YAFSM ValidationError: Self connecting transition(%s -> %s)" % [to_transition.from, to_transition.to])
|
||||
from_transition.erase(to_key)
|
||||
return validated
|
98
addons/imjp94.yafsm/src/transitions/Transition.gd
Normal file
|
@ -0,0 +1,98 @@
|
|||
@tool
|
||||
extends Resource
|
||||
class_name Transition
|
||||
|
||||
signal condition_added(condition)
|
||||
signal condition_removed(condition)
|
||||
|
||||
@export var from: String # Name of state transiting from
|
||||
@export var to: String # Name of state transiting to
|
||||
@export var conditions: Dictionary: # Conditions to transit successfuly, keyed by Condition.name
|
||||
set = set_conditions,
|
||||
get = get_conditions
|
||||
@export var priority: = 0 # Higher the number, higher the priority
|
||||
|
||||
var _conditions
|
||||
|
||||
|
||||
func _init(p_from="", p_to="", p_conditions={}):
|
||||
from = p_from
|
||||
to = p_to
|
||||
_conditions = p_conditions
|
||||
|
||||
# Attempt to transit with parameters given, return name of next state if succeeded else null
|
||||
func transit(params={}, local_params={}):
|
||||
var can_transit = _conditions.size() > 0
|
||||
for condition in _conditions.values():
|
||||
var has_param = params.has(condition.name)
|
||||
var has_local_param = local_params.has(condition.name)
|
||||
if has_param or has_local_param:
|
||||
# local_params > params
|
||||
var value = local_params.get(condition.name) if has_local_param else params.get(condition.name)
|
||||
if value == null: # null value is treated as trigger
|
||||
can_transit = can_transit and true
|
||||
else:
|
||||
if "value" in condition:
|
||||
can_transit = can_transit and condition.compare(value)
|
||||
else:
|
||||
can_transit = false
|
||||
if can_transit or _conditions.size() == 0:
|
||||
return to
|
||||
return null
|
||||
|
||||
# Add condition, return true if succeeded
|
||||
func add_condition(condition):
|
||||
if condition.name in _conditions:
|
||||
return false
|
||||
|
||||
_conditions[condition.name] = condition
|
||||
emit_signal("condition_added", condition)
|
||||
return true
|
||||
|
||||
# Remove condition by name of condition
|
||||
func remove_condition(name):
|
||||
var condition = _conditions.get(name)
|
||||
if condition:
|
||||
_conditions.erase(name)
|
||||
emit_signal("condition_removed", condition)
|
||||
return true
|
||||
return false
|
||||
|
||||
# Change condition name, return true if succeeded
|
||||
func change_condition_name(from, to):
|
||||
if not (from in _conditions) or to in _conditions:
|
||||
return false
|
||||
|
||||
var condition = _conditions[from]
|
||||
condition.name = to
|
||||
_conditions.erase(from)
|
||||
_conditions[to] = condition
|
||||
return true
|
||||
|
||||
func get_unique_name(name):
|
||||
var new_name = name
|
||||
var i = 1
|
||||
while new_name in _conditions:
|
||||
new_name = name + str(i)
|
||||
i += 1
|
||||
return new_name
|
||||
|
||||
func equals(obj):
|
||||
if obj == null:
|
||||
return false
|
||||
if not ("from" in obj and "to" in obj):
|
||||
return false
|
||||
|
||||
return from == obj.from and to == obj.to
|
||||
|
||||
# Get duplicate of conditions dictionary
|
||||
func get_conditions():
|
||||
return _conditions.duplicate()
|
||||
|
||||
func set_conditions(val):
|
||||
_conditions = val
|
||||
|
||||
static func sort(a, b):
|
||||
if a.priority > b.priority:
|
||||
return true
|
||||
return false
|
BIN
addons/loggie/assets/icon.png
Normal file
After Width: | Height: | Size: 16 KiB |
34
addons/loggie/assets/icon.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://2fr6et0qni2y"
|
||||
path="res://.godot/imported/icon.png-57313fc4d67f18c33a83a3388ad36531.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/loggie/assets/icon.png"
|
||||
dest_files=["res://.godot/imported/icon.png-57313fc4d67f18c33a83a3388ad36531.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
BIN
addons/loggie/assets/logo.png
Normal file
After Width: | Height: | Size: 58 KiB |
34
addons/loggie/assets/logo.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://duuj0ibyreygv"
|
||||
path="res://.godot/imported/logo.png-2771abf7e361d7f10d5859133bc43562.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/loggie/assets/logo.png"
|
||||
dest_files=["res://.godot/imported/logo.png-2771abf7e361d7f10d5859133bc43562.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
53
addons/loggie/custom_settings.gd.example
Normal file
|
@ -0,0 +1,53 @@
|
|||
class_name CustomLoggieSettings extends LoggieSettings
|
||||
|
||||
func load():
|
||||
# Omit settings from here to have them use their default value instead.
|
||||
# Otherwise, directly set the value of the setting to your liking.
|
||||
# Any variable in [LoggieSettings] is a valid target to alter.
|
||||
# You could also have them loaded here in some custom way, for example, from a .json or .ini file.
|
||||
# See the documentation of the [LoggieSettings] class to see all available options and their descriptions.
|
||||
|
||||
self.terminal_mode = LoggieEnums.TerminalMode.BBCODE
|
||||
self.log_level = LoggieEnums.LogLevel.INFO
|
||||
self.show_loggie_specs = LoggieEnums.ShowLoggieSpecsMode.ESSENTIAL
|
||||
self.show_system_specs = true
|
||||
self.enforce_optimal_settings_in_release_build = true
|
||||
|
||||
self.output_message_domain = true
|
||||
self.print_errors_to_console = true
|
||||
self.print_warnings_to_console = true
|
||||
self.use_print_debug_for_debug_msg = true
|
||||
self.derive_and_show_class_names = true
|
||||
self.nameless_class_name_proxy = LoggieEnums.NamelessClassExtensionNameProxy.BASE_TYPE
|
||||
self.output_timestamps = true
|
||||
self.timestamps_use_utc = true
|
||||
|
||||
self.format_timestamp = "[{day}.{month}.{year} {hour}:{minute}:{second}]"
|
||||
self.format_info_msg = "{msg}"
|
||||
self.format_notice_msg = "[b][color=cyan][NOTICE]:[/color][/b] {msg}"
|
||||
self.format_warning_msg = "[b][color=orange][WARN]:[/color][/b] {msg}"
|
||||
self.format_error_msg = "[b][color=red][ERROR]:[/color][/b] {msg}"
|
||||
self.format_debug_msg = "[b][color=pink][DEBUG]:[/color][/b] {msg}"
|
||||
self.format_header = "[b][i]{msg}[/i][/b]"
|
||||
self.format_domain_prefix = "[b]({domain})[/b] {msg}"
|
||||
|
||||
self.h_separator_symbol = "-"
|
||||
self.box_characters_mode = LoggieEnums.BoxCharactersMode.COMPATIBLE
|
||||
|
||||
self.box_symbols_compatible = {
|
||||
"top_left" : "-",
|
||||
"top_right" : "-",
|
||||
"bottom_left" : "-",
|
||||
"bottom_right" : "-",
|
||||
"h_line" : "-",
|
||||
"v_line" : ":",
|
||||
}
|
||||
|
||||
self.box_symbols_pretty = {
|
||||
"top_left" : "┌",
|
||||
"top_right" : "┐",
|
||||
"bottom_left" : "└",
|
||||
"bottom_right" : "┘",
|
||||
"h_line" : "─",
|
||||
"v_line" : "│",
|
||||
}
|
167
addons/loggie/loggie.gd
Normal file
|
@ -0,0 +1,167 @@
|
|||
@tool
|
||||
|
||||
## Loggie is a basic logging utility for those who need common minor improvements and helpers around the basic [method print], [method print_rich]
|
||||
## and other default Godot printing functions. Loggie creates instances of [LoggieMsg], which are a wrapper around a string that needs to manipulated,
|
||||
## then uses them to properly format, arrange and present them in the console and .log files. Loggie uses the default Godot logging mechanism under the hood.
|
||||
extends Node
|
||||
|
||||
## Stores a string describing the current version of Loggie.
|
||||
const VERSION : String = "v1.5"
|
||||
|
||||
## Emitted any time Loggie attempts to log a message.
|
||||
## Useful for capturing the messages that pass through Loggie.
|
||||
## [br][param msg] is the message Loggie attempted to log (before any preprocessing).
|
||||
## [br][param preprocessed_content] is what the string content of that message contained after the preprocessing step,
|
||||
## which is what ultimately gets logged.
|
||||
## [br][param result] describes the final result of the attempt to log that message.
|
||||
signal log_attempted(msg : LoggieMsg, preprocessed_content : String, result : LoggieEnums.LogAttemptResult)
|
||||
|
||||
## A reference to the settings of this Loggie. Read more about [LoggieSettings].
|
||||
var settings : LoggieSettings
|
||||
|
||||
## Holds a mapping between all registered domains (string keys) and bool values representing whether
|
||||
## those domains are currently enabled. Enable domains with [method set_domain_enabled].
|
||||
## You can then place [LoggieMsg] messages into a domain by calling [method LoggieMsg.domain].
|
||||
## Messages belonging to a disabled domain will never be outputted.
|
||||
var domains : Dictionary = {}
|
||||
|
||||
## Holds a mapping between script paths and the names of the classes defined in those scripts.
|
||||
var class_names : Dictionary = {}
|
||||
|
||||
func _init() -> void:
|
||||
var uses_original_settings_file = true
|
||||
var default_settings_path = get_script().get_path().get_base_dir().path_join("loggie_settings.gd")
|
||||
var custom_settings_path = get_script().get_path().get_base_dir().path_join("custom_settings.gd")
|
||||
|
||||
if self.settings == null:
|
||||
if custom_settings_path != null and custom_settings_path != "" and ResourceLoader.exists(custom_settings_path):
|
||||
var loaded_successfully = load_settings_from_path(custom_settings_path)
|
||||
if loaded_successfully:
|
||||
uses_original_settings_file = false
|
||||
|
||||
if uses_original_settings_file:
|
||||
var _settings = ResourceLoader.load(default_settings_path)
|
||||
if _settings != null:
|
||||
self.settings = _settings.new()
|
||||
self.settings.load()
|
||||
else:
|
||||
push_error("Loggie loaded neither a custom nor a default settings file. This will break the plugin. Make sure that a valid loggie_settings.gd is in the same directory where loggie.gd is.")
|
||||
return
|
||||
|
||||
if self.settings.enforce_optimal_settings_in_release_build == true and is_in_production():
|
||||
self.settings.terminal_mode = LoggieEnums.TerminalMode.PLAIN
|
||||
self.settings.box_characters_mode = LoggieEnums.BoxCharactersMode.COMPATIBLE
|
||||
|
||||
# Already cache the name of the singleton found at loggie's script path.
|
||||
class_names[self.get_script().resource_path] = LoggieSettings.loggie_singleton_name
|
||||
|
||||
# Prepopulate class data from ProjectSettings to avoid needing to read files.
|
||||
if self.settings.derive_and_show_class_names == true and OS.has_feature("debug"):
|
||||
for class_data: Dictionary in ProjectSettings.get_global_class_list():
|
||||
class_names[class_data.path] = class_data.class
|
||||
|
||||
for autoload_setting: String in ProjectSettings.get_property_list().map(func(prop): return prop.name).filter(func(prop): return prop.begins_with("autoload/") and ProjectSettings.has_setting(prop)):
|
||||
var autoload_class: String = autoload_setting.trim_prefix("autoload/")
|
||||
var class_path: String = ProjectSettings.get_setting(autoload_setting)
|
||||
class_path = class_path.trim_prefix("*")
|
||||
if not class_names.has(class_path):
|
||||
class_names[class_path] = autoload_class
|
||||
|
||||
# Don't print Loggie boot messages if Loggie is running only from the editor.
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
if self.settings.show_loggie_specs != LoggieEnums.ShowLoggieSpecsMode.DISABLED:
|
||||
msg("👀 Loggie {version} booted.".format({"version" : self.VERSION})).color(Color.ORANGE).header().nl().info()
|
||||
var loggie_specs_msg = LoggieSystemSpecsMsg.new().use_logger(self)
|
||||
loggie_specs_msg.add(msg("|\t Using Custom Settings File: ").bold(), !uses_original_settings_file).nl().add("|\t ").hseparator(35).nl()
|
||||
|
||||
match self.settings.show_loggie_specs:
|
||||
LoggieEnums.ShowLoggieSpecsMode.ESSENTIAL:
|
||||
loggie_specs_msg.embed_essential_logger_specs()
|
||||
LoggieEnums.ShowLoggieSpecsMode.ADVANCED:
|
||||
loggie_specs_msg.embed_advanced_logger_specs()
|
||||
|
||||
loggie_specs_msg.preprocessed(false).info()
|
||||
|
||||
if self.settings.show_system_specs:
|
||||
var system_specs_msg = LoggieSystemSpecsMsg.new().use_logger(self)
|
||||
system_specs_msg.embed_specs().preprocessed(false).info()
|
||||
|
||||
## Attempts to instantiate a LoggieSettings object from the script at the given [param path].
|
||||
## Returns true if successful, otherwise false and prints an error.
|
||||
func load_settings_from_path(path : String) -> bool:
|
||||
var settings_resource = ResourceLoader.load(path)
|
||||
var settings_instance
|
||||
|
||||
if settings_resource != null:
|
||||
settings_instance = settings_resource.new()
|
||||
|
||||
if (settings_instance is LoggieSettings):
|
||||
self.settings = settings_instance
|
||||
self.settings.load()
|
||||
return true
|
||||
else:
|
||||
push_error("Unable to instantiate a LoggieSettings object from the script at path {path}. Check that loggie.gd -> custom_settings_path is pointing to a valid .gd script that contains the class definition of a class that either extends LoggieSettings, or is LoggieSettings.".format({"path": path}))
|
||||
return false
|
||||
|
||||
## Checks if Loggie is running in production (release) mode of the game.
|
||||
## While it is, every [LoggieMsg] will have plain output.
|
||||
## Uses a sensible default check for most projects, but
|
||||
## you can rewrite this function to your needs if necessary.
|
||||
func is_in_production() -> bool:
|
||||
return OS.has_feature("release")
|
||||
|
||||
## Sets whether the domain with the given name is enabled.
|
||||
func set_domain_enabled(domain_name : String, enabled : bool) -> void:
|
||||
domains[domain_name] = enabled
|
||||
|
||||
## Checks whether the domain with the given name is enabled.
|
||||
## The domain name "" (empty string) is the default one for all newly created messages,
|
||||
## and is designed to always be enabled.
|
||||
func is_domain_enabled(domain_name : String) -> bool:
|
||||
if domain_name == "":
|
||||
return true
|
||||
|
||||
if domains.has(domain_name) and domains[domain_name] == true:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
## Creates a new [LoggieMsg] out of the given [param msg] and extra arguments (by converting them to strings and concatenating them to the msg).
|
||||
## You may continue to modify the [LoggieMsg] with additional functions from that class, then when you are ready to output it, use methods like:
|
||||
## [method LoggieMsg.info], [method LoggieMsg.warn], etc.
|
||||
func msg(message = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> LoggieMsg:
|
||||
var loggieMsg = LoggieMsg.new(message, arg1, arg2, arg3, arg4, arg5)
|
||||
loggieMsg.use_logger(self)
|
||||
return loggieMsg
|
||||
|
||||
## A shortcut method that instantly creates a [LoggieMsg] with the given arguments and outputs it at the info level.
|
||||
## Can be used when you have no intention of customizing a LoggieMsg in any way using helper methods.
|
||||
## For customization, use [method msg] instead.
|
||||
func info(message = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> LoggieMsg:
|
||||
return msg(message, arg1, arg2, arg3, arg4, arg5).info()
|
||||
|
||||
## A shortcut method that instantly creates a [LoggieMsg] with the given arguments and outputs it at the warn level.
|
||||
## Can be used when you have no intention of customizing a LoggieMsg in any way using helper methods.
|
||||
## For customization, use [method msg] instead.
|
||||
func warn(message = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> LoggieMsg:
|
||||
return msg(message, arg1, arg2, arg3, arg4, arg5).warn()
|
||||
|
||||
## A shortcut method that instantly creates a [LoggieMsg] with the given arguments and outputs it at the error level.
|
||||
## Can be used when you have no intention of customizing a LoggieMsg in any way using helper methods.
|
||||
## For customization, use [method msg] instead.
|
||||
func error(message = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> LoggieMsg:
|
||||
return msg(message, arg1, arg2, arg3, arg4, arg5).error()
|
||||
|
||||
## A shortcut method that instantly creates a [LoggieMsg] with the given arguments and outputs it at the debug level.
|
||||
## Can be used when you have no intention of customizing a LoggieMsg in any way using helper methods.
|
||||
## For customization, use [method msg] instead.
|
||||
func debug(message = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> LoggieMsg:
|
||||
return msg(message, arg1, arg2, arg3, arg4, arg5).debug()
|
||||
|
||||
## A shortcut method that instantly creates a [LoggieMsg] with the given arguments and outputs it at the notice level.
|
||||
## Can be used when you have no intention of customizing a LoggieMsg in any way using helper methods.
|
||||
## For customization, use [method msg] instead.
|
||||
func notice(message = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> LoggieMsg:
|
||||
return msg(message, arg1, arg2, arg3, arg4, arg5).notice()
|
344
addons/loggie/loggie_message.gd
Normal file
|
@ -0,0 +1,344 @@
|
|||
@tool
|
||||
|
||||
## LoggieMsg represents a mutable object that holds an array of strings ([member content]) [i](referred to as 'content segments')[/i], and
|
||||
## a bunch of helper methods that make it easy to manipulate these segments and chain together additions and changes to them.
|
||||
## [br][br]For example:
|
||||
## [codeblock]
|
||||
### Prints: "Hello world!" at the INFO debug level.
|
||||
##var msg = LoggieMsg.new("Hello world").color(Color.RED).suffix("!").info()
|
||||
##[/codeblock]
|
||||
## [br] You can also use [method Loggie.msg] to quickly construct a message.
|
||||
## [br] Example of usage:
|
||||
## [codeblock]Loggie.msg("Hello world").color(Color("#ffffff")).suffix("!").info()[/codeblock]
|
||||
class_name LoggieMsg extends RefCounted
|
||||
|
||||
## The full content of this message. By calling various helper methods in this class, this content is further altered.
|
||||
## The content is an array of strings which represents segments of the message which are ultimately appended together
|
||||
## to form the final message. You can start a new segment by calling [method msg] on this class.
|
||||
## You can then output the whole message with methods like [method info], [method debug], etc.
|
||||
var content : Array = [""]
|
||||
|
||||
## The segment of [member content] that is currently being edited.
|
||||
var current_segment_index : int = 0
|
||||
|
||||
## The (key string) domain this message belongs to.
|
||||
## "" is the default domain which is always enabled.
|
||||
## If this message attempts to be outputted, but belongs to a disabled domain, it will not be outputted.
|
||||
## You can change which domains are enabled in Loggie at any time with [Loggie.set_domain_enabled].
|
||||
## This is useful for creating blocks of debugging output that you can simply turn off/on with a boolean when you actually need them.
|
||||
var domain_name : String = ""
|
||||
|
||||
## Whether this message should be preprocessed and modified during [method output].
|
||||
var preprocess : bool = true
|
||||
|
||||
## Stores a reference to the logger that generated this message, from which we need to read settings and other data.
|
||||
## This variable should be set with [method use_logger] before an attempt is made to log this message out.
|
||||
var _logger : Variant
|
||||
|
||||
func _init(message = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> void:
|
||||
self.content[current_segment_index] = LoggieTools.concatenate_msg_and_args(message, arg1, arg2, arg3, arg4, arg5)
|
||||
|
||||
## Returns a reference to the logger object that created this message.
|
||||
func get_logger() -> Variant:
|
||||
return self._logger
|
||||
|
||||
## Sets this message to use the given [param logger] as the logger from which it will be reading
|
||||
## settings. The given logger should be of class [Loggie] or an extension of it.
|
||||
func use_logger(logger_to_use : Variant) -> LoggieMsg:
|
||||
self._logger = logger_to_use
|
||||
return self
|
||||
|
||||
## Outputs the given string [param msg] at the given output [param level] to the standard output using either [method print_rich] or [method print].
|
||||
## The domain from which the message is considered to be coming can be provided via [param target_domain].
|
||||
## The classification of the message can be provided via [param msg_type], as certain types need extra handling and treatment.
|
||||
## It also does a number of changes to the given [param msg] based on various Loggie settings.
|
||||
## Designed to be called internally. You should consider using [method info], [method error], [method warn], [method notice], [method debug] instead.
|
||||
func output(level : LoggieEnums.LogLevel, message : String, target_domain : String = "", msg_type : LoggieEnums.MsgType = LoggieEnums.MsgType.STANDARD) -> void:
|
||||
var loggie = get_logger()
|
||||
|
||||
if loggie == null:
|
||||
push_error("Attempt to log output with an invalid _logger. Make sure to call LoggieMsg.use_logger to set the appropriate logger before working with the message.")
|
||||
return
|
||||
|
||||
if loggie.settings == null:
|
||||
push_error("Attempt to use a _logger with invalid settings.")
|
||||
return
|
||||
|
||||
# We don't output the message if the settings dictate that messages of that level shouldn't be outputted.
|
||||
if level > loggie.settings.log_level:
|
||||
loggie.log_attempted.emit(self, message, LoggieEnums.LogAttemptResult.LOG_LEVEL_INSUFFICIENT)
|
||||
return
|
||||
|
||||
# We don't output the message if the domain from which it comes is not enabled.
|
||||
if not loggie.is_domain_enabled(target_domain):
|
||||
loggie.log_attempted.emit(self, message, LoggieEnums.LogAttemptResult.DOMAIN_DISABLED)
|
||||
return
|
||||
|
||||
# Apply the matching formatting to the message based on the log level.
|
||||
match level:
|
||||
LoggieEnums.LogLevel.ERROR:
|
||||
message = loggie.settings.format_error_msg.format({"msg": message})
|
||||
LoggieEnums.LogLevel.WARN:
|
||||
message = loggie.settings.format_warning_msg.format({"msg": message})
|
||||
LoggieEnums.LogLevel.NOTICE:
|
||||
message = loggie.settings.format_notice_msg.format({"msg": message})
|
||||
LoggieEnums.LogLevel.INFO:
|
||||
message = loggie.settings.format_info_msg.format({"msg": message})
|
||||
LoggieEnums.LogLevel.DEBUG:
|
||||
message = loggie.settings.format_debug_msg.format({"msg": message})
|
||||
|
||||
# Enter the preprocess tep unless it is disabled.
|
||||
if self.preprocess:
|
||||
# We append the name of the domain if that setting is enabled.
|
||||
if !target_domain.is_empty() and loggie.settings.output_message_domain == true:
|
||||
message = loggie.settings.format_domain_prefix.format({"domain" : target_domain, "msg" : message})
|
||||
|
||||
# We prepend the name of the class that called the function which resulted in this output being generated
|
||||
# (if Loggie settings are configured to do so).
|
||||
if loggie.settings.derive_and_show_class_names == true and OS.has_feature("debug"):
|
||||
var stack_frame : Dictionary = LoggieTools.get_current_stack_frame_data()
|
||||
var _class_name : String
|
||||
|
||||
var scriptPath = stack_frame.source
|
||||
if loggie.class_names.has(scriptPath):
|
||||
_class_name = loggie.class_names[scriptPath]
|
||||
else:
|
||||
_class_name = LoggieTools.get_class_name_from_script(scriptPath, loggie.settings.nameless_class_name_proxy)
|
||||
loggie.class_names[scriptPath] = _class_name
|
||||
|
||||
if _class_name != "":
|
||||
message = "[b]({class_name})[/b] {msg}".format({
|
||||
"class_name" : _class_name,
|
||||
"msg" : message
|
||||
})
|
||||
|
||||
# We prepend a timestamp to the message (if Loggie settings are configured to do so).
|
||||
if loggie.settings.output_timestamps == true:
|
||||
var format_dict : Dictionary = Time.get_datetime_dict_from_system(loggie.settings.timestamps_use_utc)
|
||||
for field in ["month", "day", "hour", "minute", "second"]:
|
||||
format_dict[field] = "%02d" % format_dict[field]
|
||||
message = "{formatted_time} {msg}".format({
|
||||
"formatted_time" : loggie.settings.format_timestamp.format(format_dict),
|
||||
"msg" : message
|
||||
})
|
||||
|
||||
# Prepare the preprocessed message to be output in the terminal mode of choice.
|
||||
message = LoggieTools.get_terminal_ready_string(message, loggie.settings.terminal_mode)
|
||||
|
||||
# Output the preprocessed message.
|
||||
match loggie.settings.terminal_mode:
|
||||
LoggieEnums.TerminalMode.ANSI, LoggieEnums.TerminalMode.BBCODE:
|
||||
print_rich(message)
|
||||
LoggieEnums.TerminalMode.PLAIN, _:
|
||||
print(message)
|
||||
|
||||
# Dump a non-preprocessed terminal-ready version of the message in additional ways if that has been configured.
|
||||
if msg_type == LoggieEnums.MsgType.ERROR and loggie.settings.print_errors_to_console:
|
||||
push_error(LoggieTools.get_terminal_ready_string(self.string(), LoggieEnums.TerminalMode.PLAIN))
|
||||
if msg_type == LoggieEnums.MsgType.WARNING and loggie.settings.print_warnings_to_console:
|
||||
push_warning(LoggieTools.get_terminal_ready_string(self.string(), LoggieEnums.TerminalMode.PLAIN))
|
||||
if msg_type == LoggieEnums.MsgType.DEBUG and loggie.settings.use_print_debug_for_debug_msg:
|
||||
print_debug(LoggieTools.get_terminal_ready_string(self.string(), loggie.settings.terminal_mode))
|
||||
|
||||
loggie.log_attempted.emit(self, message, LoggieEnums.LogAttemptResult.SUCCESS)
|
||||
|
||||
## Outputs this message from Loggie as an Error type message.
|
||||
## The [Loggie.settings.log_level] must be equal to or higher to the ERROR level for this to work.
|
||||
func error() -> LoggieMsg:
|
||||
output(LoggieEnums.LogLevel.ERROR, self.string(), self.domain_name, LoggieEnums.MsgType.ERROR)
|
||||
return self
|
||||
|
||||
## Outputs this message from Loggie as an Warning type message.
|
||||
## The [Loggie.settings.log_level] must be equal to or higher to the WARN level for this to work.
|
||||
func warn() -> LoggieMsg:
|
||||
output(LoggieEnums.LogLevel.WARN, self.string(), self.domain_name, LoggieEnums.MsgType.WARNING)
|
||||
return self
|
||||
|
||||
## Outputs this message from Loggie as an Notice type message.
|
||||
## The [Loggie.settings.log_level] must be equal to or higher to the NOTICE level for this to work.
|
||||
func notice() -> LoggieMsg:
|
||||
output(LoggieEnums.LogLevel.NOTICE, self.string(), self.domain_name)
|
||||
return self
|
||||
|
||||
## Outputs this message from Loggie as an Info type message.
|
||||
## The [Loggie.settings.log_level] must be equal to or higher to the INFO level for this to work.
|
||||
func info() -> LoggieMsg:
|
||||
output(LoggieEnums.LogLevel.INFO, self.string(), self.domain_name)
|
||||
return self
|
||||
|
||||
## Outputs this message from Loggie as a Debug type message.
|
||||
## The [Loggie.settings.log_level] must be equal to or higher to the DEBUG level for this to work.
|
||||
func debug() -> LoggieMsg:
|
||||
output(LoggieEnums.LogLevel.DEBUG, self.string(), self.domain_name, LoggieEnums.MsgType.DEBUG)
|
||||
return self
|
||||
|
||||
## Returns the string content of this message.
|
||||
## If [param segment] is provided, it should be an integer indicating which segment of the message to return.
|
||||
## If its value is -1, all segments are concatenated together and returned.
|
||||
func string(segment : int = -1) -> String:
|
||||
if segment == -1:
|
||||
return "".join(self.content)
|
||||
else:
|
||||
if segment < self.content.size():
|
||||
return self.content[segment]
|
||||
else:
|
||||
push_error("Attempt to access a non-existent segment of a LoggieMsg. Make sure to use a valid segment index.")
|
||||
return ""
|
||||
|
||||
## Converts the current content of this message to an ANSI compatible form.
|
||||
func to_ANSI() -> LoggieMsg:
|
||||
var new_content : Array = []
|
||||
for segment in self.content:
|
||||
new_content.append(LoggieTools.rich_to_ANSI(segment))
|
||||
self.content = new_content
|
||||
return self
|
||||
|
||||
## Strips all the BBCode in the current content of this message.
|
||||
func strip_BBCode() -> LoggieMsg:
|
||||
var new_content : Array = []
|
||||
for segment in self.content:
|
||||
new_content.append(LoggieTools.remove_BBCode(segment))
|
||||
self.content = new_content
|
||||
return self
|
||||
|
||||
## Wraps the content of the current segment of this message in the given color.
|
||||
## The [param color] can be provided as a [Color], a recognized Godot color name (String, e.g. "red"), or a color hex code (String, e.g. "#ff0000").
|
||||
func color(_color : Variant) -> LoggieMsg:
|
||||
if _color is Color:
|
||||
_color = _color.to_html()
|
||||
|
||||
self.content[current_segment_index] = "[color={colorstr}]{msg}[/color]".format({
|
||||
"colorstr": _color,
|
||||
"msg": self.content[current_segment_index]
|
||||
})
|
||||
|
||||
return self
|
||||
|
||||
## Stylizes the current segment of this message to be bold.
|
||||
func bold() -> LoggieMsg:
|
||||
self.content[current_segment_index] = "[b]{msg}[/b]".format({"msg": self.content[current_segment_index]})
|
||||
return self
|
||||
|
||||
## Stylizes the current segment of this message to be italic.
|
||||
func italic() -> LoggieMsg:
|
||||
self.content[current_segment_index] = "[i]{msg}[/i]".format({"msg": self.content[current_segment_index]})
|
||||
return self
|
||||
|
||||
## Stylizes the current segment of this message as a header.
|
||||
func header() -> LoggieMsg:
|
||||
var loggie = get_logger()
|
||||
self.content[current_segment_index] = loggie.settings.format_header.format({"msg": self.content[current_segment_index]})
|
||||
return self
|
||||
|
||||
## Constructs a decorative box with the given horizontal padding around the current segment
|
||||
## of this message. Messages containing a box are not going to be preprocessed, so they are best
|
||||
## used only as a special header or decoration.
|
||||
func box(h_padding : int = 4):
|
||||
var loggie = get_logger()
|
||||
var stripped_content = LoggieTools.remove_BBCode(self.content[current_segment_index]).strip_edges(true, true)
|
||||
var content_length = stripped_content.length()
|
||||
var h_fill_length = content_length + (h_padding * 2)
|
||||
var box_character_source = loggie.settings.box_symbols_compatible if loggie.settings.box_characters_mode == LoggieEnums.BoxCharactersMode.COMPATIBLE else loggie.settings.box_symbols_pretty
|
||||
|
||||
var top_row_design = "{top_left_corner}{h_fill}{top_right_corner}".format({
|
||||
"top_left_corner" : box_character_source.top_left,
|
||||
"h_fill" : box_character_source.h_line.repeat(h_fill_length),
|
||||
"top_right_corner" : box_character_source.top_right
|
||||
})
|
||||
|
||||
var middle_row_design = "{vert_line}{padding}{content}{space_fill}{padding}{vert_line}".format({
|
||||
"vert_line" : box_character_source.v_line,
|
||||
"content" : self.content[current_segment_index],
|
||||
"padding" : " ".repeat(h_padding),
|
||||
"space_fill" : " ".repeat(h_fill_length - stripped_content.length() - h_padding*2)
|
||||
})
|
||||
|
||||
var bottom_row_design = "{bottom_left_corner}{h_fill}{bottom_right_corner}".format({
|
||||
"bottom_left_corner" : box_character_source.bottom_left,
|
||||
"h_fill" : box_character_source.h_line.repeat(h_fill_length),
|
||||
"bottom_right_corner" : box_character_source.bottom_right
|
||||
})
|
||||
|
||||
self.content[current_segment_index] = "{top_row}\n{middle_row}\n{bottom_row}\n".format({
|
||||
"top_row" : top_row_design,
|
||||
"middle_row" : middle_row_design,
|
||||
"bottom_row" : bottom_row_design
|
||||
})
|
||||
|
||||
self.preprocessed(false)
|
||||
|
||||
return self
|
||||
|
||||
## Appends additional content to this message at the end of the current content and its stylings.
|
||||
## This does not create a new message segment, just appends to the current one.
|
||||
func add(message : Variant = null, arg1 : Variant = null, arg2 : Variant = null, arg3 : Variant = null, arg4 : Variant = null, arg5 : Variant = null) -> LoggieMsg:
|
||||
self.content[current_segment_index] = self.content[current_segment_index] + LoggieTools.concatenate_msg_and_args(message, arg1, arg2, arg3, arg4, arg5)
|
||||
return self
|
||||
|
||||
## Adds a specified amount of newlines to the end of the current segment of this message.
|
||||
func nl(amount : int = 1) -> LoggieMsg:
|
||||
self.content[current_segment_index] += "\n".repeat(amount)
|
||||
return self
|
||||
|
||||
## Adds a specified amount of spaces to the end of the current segment of this message.
|
||||
func space(amount : int = 1) -> LoggieMsg:
|
||||
self.content[current_segment_index] += " ".repeat(amount)
|
||||
return self
|
||||
|
||||
## Adds a specified amount of tabs to the end of the current segment of this message.
|
||||
func tab(amount : int = 1) -> LoggieMsg:
|
||||
self.content[current_segment_index] += "\t".repeat(amount)
|
||||
return self
|
||||
|
||||
## Sets this message to belong to the domain with the given name.
|
||||
## If it attempts to be outputted, but the domain is disabled, it won't be outputted.
|
||||
func domain(_domain_name : String) -> LoggieMsg:
|
||||
self.domain_name = _domain_name
|
||||
return self
|
||||
|
||||
## Prepends the given prefix string to the start of the message (first segment) with the provided separator.
|
||||
func prefix(str_prefix : String, separator : String = "") -> LoggieMsg:
|
||||
self.content[0] = "{prefix}{separator}{content}".format({
|
||||
"prefix" : str_prefix,
|
||||
"separator" : separator,
|
||||
"content" : self.content[0]
|
||||
})
|
||||
return self
|
||||
|
||||
## Appends the given suffix string to the end of the message (last segment) with the provided separator.
|
||||
func suffix(str_suffix : String, separator : String = "") -> LoggieMsg:
|
||||
self.content[self.content.size() - 1] = "{content}{separator}{suffix}".format({
|
||||
"suffix" : str_suffix,
|
||||
"separator" : separator,
|
||||
"content" : self.content[self.content.size() - 1]
|
||||
})
|
||||
return self
|
||||
|
||||
## Appends a horizontal separator with the given length to the current segment of this message.
|
||||
## If [param alternative_symbol] is provided, it should be a String, and it will be used as the symbol for the separator instead of the default one.
|
||||
func hseparator(size : int = 16, alternative_symbol : Variant = null) -> LoggieMsg:
|
||||
var loggie = get_logger()
|
||||
var symbol = loggie.settings.h_separator_symbol if alternative_symbol == null else str(alternative_symbol)
|
||||
self.content[current_segment_index] = self.content[current_segment_index] + (symbol.repeat(size))
|
||||
return self
|
||||
|
||||
## Ends the current segment of the message and starts a new one.
|
||||
func endseg() -> LoggieMsg:
|
||||
self.content.push_back("")
|
||||
self.current_segment_index = self.content.size() - 1
|
||||
return self
|
||||
|
||||
## Creates a new segment in this message and sets its content to the given message.
|
||||
## Acts as a shortcut for calling [method endseg] + [method add].
|
||||
func msg(message = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null) -> LoggieMsg:
|
||||
self.endseg()
|
||||
self.content[current_segment_index] = LoggieTools.concatenate_msg_and_args(message, arg1, arg2, arg3, arg4, arg5)
|
||||
return self
|
||||
|
||||
## Sets whether this message should be preprocessed and potentially modified with prefixes and suffixes during [method output].
|
||||
## If turned off, while outputting this message, Loggie will skip the steps where it appends the messaage domain, class name, timestamp, etc.
|
||||
## Whether preprocess is set to true doesn't affect the final conversion from RICH to ANSI or PLAIN, which always happens
|
||||
## under some circumstances that are based on other settings.
|
||||
func preprocessed(shouldPreprocess : bool) -> LoggieMsg:
|
||||
self.preprocess = shouldPreprocess
|
||||
return self
|
404
addons/loggie/loggie_settings.gd
Normal file
|
@ -0,0 +1,404 @@
|
|||
@tool
|
||||
|
||||
## Defines a set of variables through which all the relevant settings of Loggie can have their
|
||||
## values set, read and documented. An instance of this class is found in [member Loggie.settings], and that's where Loggie
|
||||
## ultimately reads from when it's asking for the value of a setting. For user convenience, settings are (by default) exported
|
||||
## as custom Godot project settings and are loaded from there into these variables during [method load], however,
|
||||
## you can extend or overwrite this class' [method load] method to define a different way of loading these settings if you prefer.
|
||||
## [i](e.g. loading from a config.ini file, or a .json file, etc.)[/i].[br][br]
|
||||
##
|
||||
## Loggie calls [method load] on this class during its [method _ready] function.
|
||||
class_name LoggieSettings extends Resource
|
||||
|
||||
## The name that will be used for the singleton referring to Loggie.
|
||||
## [br][br][i][b]Note:[/b] You may change this to something you're more used to, such as "log" or "logger".[/i]
|
||||
## When doing so, make sure to either do it while the Plugin is enabled, then disable and re-enable the plugin,
|
||||
## or that you manually clear out the previously created autoload (should be called "Loggie") in Project Settings -> Autoloads.
|
||||
static var loggie_singleton_name = "Loggie"
|
||||
|
||||
## The dictionary which is used to grab the defaults and other values associated with each setting
|
||||
## relevant to Loggie, particularly important for the default way of loading [LoggieSettings] and
|
||||
## setting up Godot Project Settings related to Loggie.
|
||||
const project_settings = {
|
||||
"remove_settings_if_plugin_disabled" = {
|
||||
"path": "loggie/general/remove_settings_if_plugin_disabled",
|
||||
"default_value" : true,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "Choose whether you want Loggie project settings to be wiped from ProjectSettings if the Loggie plugin is disabled.",
|
||||
},
|
||||
"terminal_mode" = {
|
||||
"path": "loggie/general/terminal_mode",
|
||||
"default_value" : LoggieEnums.TerminalMode.BBCODE,
|
||||
"type" : TYPE_INT,
|
||||
"hint" : PROPERTY_HINT_ENUM,
|
||||
"hint_string" : "Plain:0,ANSI:1,BBCode:2",
|
||||
"doc" : "Choose the terminal for which loggie should preprocess the output so that it displays as intended.[br][br]Use BBCode for Godot console.[br]Use ANSI for Powershell, Bash, etc.[br]Use PLAIN for log files.",
|
||||
},
|
||||
"log_level" = {
|
||||
"path": "loggie/general/log_level",
|
||||
"default_value" : LoggieEnums.LogLevel.INFO,
|
||||
"type" : TYPE_INT,
|
||||
"hint" : PROPERTY_HINT_ENUM,
|
||||
"hint_string" : "Error:0,Warn:1,Notice:2,Info:3,Debug:4",
|
||||
"doc" : "Choose the level of messages which should be displayed. Loggie displays all messages that are outputted at the currently set level (or any lower level).",
|
||||
},
|
||||
"show_system_specs" = {
|
||||
"path": "loggie/general/show_system_specs",
|
||||
"default_value" : true,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "Should Loggie log the system and device specs of the user as soon as it is booted?",
|
||||
},
|
||||
"show_loggie_specs" = {
|
||||
"path": "loggie/general/show_loggie_specs",
|
||||
"default_value" : LoggieEnums.ShowLoggieSpecsMode.ESSENTIAL,
|
||||
"type" : TYPE_INT,
|
||||
"hint" : PROPERTY_HINT_ENUM,
|
||||
"hint_string" : "Disabled:0,Essential:1,Advanced:2",
|
||||
"doc" : "Defines which way Loggie should print its own specs when it is booted.",
|
||||
},
|
||||
"enforce_optimal_settings_in_release_build" = {
|
||||
"path": "loggie/general/enforce_optimal_settings_in_release_build",
|
||||
"default_value" : true,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "Should Loggie enforce certain settings to automatically change to optimal values in production/release builds?",
|
||||
},
|
||||
"output_timestamps" = {
|
||||
"path": "loggie/timestamps/output_timestamps",
|
||||
"default_value" : false,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "Should Loggie output a timestamp prefix with each message, showing the exact moment when that log line was produced?",
|
||||
},
|
||||
"timestamps_use_utc" = {
|
||||
"path": "loggie/timestamps/timestamps_use_utc",
|
||||
"default_value" : true,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "If 'Output Timestamps' is true, should those timestamps use the UTC time. If not, local system time is used instead.",
|
||||
},
|
||||
"output_message_domain" = {
|
||||
"path": "loggie/preprocessing/output_message_domain",
|
||||
"default_value" : false,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "If true, logged messages will have the domain they are coming from prepended to them.",
|
||||
},
|
||||
"output_errors_to_console" = {
|
||||
"path": "loggie/preprocessing/output_errors_also_to_console",
|
||||
"default_value" : true,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "If true, errors printed by Loggie will also be visible through an additional print in the main output.",
|
||||
},
|
||||
"output_warnings_to_console" = {
|
||||
"path": "loggie/preprocessing/output_warnings_also_to_console",
|
||||
"default_value" : true,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "If true, warnings printed by Loggie will also be visible through an additional print in the main output.",
|
||||
},
|
||||
"use_print_debug_for_debug_msgs" = {
|
||||
"path": "loggie/preprocessing/use_print_debug_for_debug_msgs",
|
||||
"default_value" : false,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "If true, 'debug' level messages outputted by Loggie will be printed using Godot's 'print_debug' function, which is more verbose.",
|
||||
},
|
||||
"derive_and_display_class_names_from_scripts" = {
|
||||
"path": "loggie/preprocessing/derive_and_display_class_names_from_scripts",
|
||||
"default_value" : false,
|
||||
"type" : TYPE_BOOL,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "If true, Loggie will attempt to find out the name of the main class from which the log line is coming and append it in front of the message.",
|
||||
},
|
||||
"nameless_class_name_proxy" = {
|
||||
"path": "loggie/preprocessing/nameless_class_name_proxy",
|
||||
"default_value" : LoggieEnums.NamelessClassExtensionNameProxy.BASE_TYPE,
|
||||
"type" : TYPE_INT,
|
||||
"hint" : PROPERTY_HINT_ENUM,
|
||||
"hint_string" : "Nothing:0,ScriptName:1,BaseType:2",
|
||||
"doc" : "If 'Derive and Display Class Names From Scripts' is enabled, and a script doesn't have a 'class_name', which text should we use as a substitute?",
|
||||
},
|
||||
"format_timestamp" = {
|
||||
"path": "loggie/formats/timestamp",
|
||||
"default_value" : "[{day}.{month}.{year} {hour}:{minute}:{second}]",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The format used for timestamps which are prepended to the message when output_timestamps is enabled.",
|
||||
},
|
||||
"format_debug_msg" = {
|
||||
"path": "loggie/formats/debug_message",
|
||||
"default_value" : "[b][color=pink][DEBUG]:[/color][/b] {msg}",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The format used for debug messages.",
|
||||
},
|
||||
"format_info_msg" = {
|
||||
"path": "loggie/formats/info_message",
|
||||
"default_value" : "{msg}",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The format used for info messages.",
|
||||
},
|
||||
"format_notice_msg" = {
|
||||
"path": "loggie/formats/notice_message",
|
||||
"default_value" : "[b][color=cyan][NOTICE]:[/color][/b] {msg}",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The format used for notice messages.",
|
||||
},
|
||||
"format_warning_msg" = {
|
||||
"path": "loggie/formats/warning_message",
|
||||
"default_value" : "[b][color=orange][WARN]:[/color][/b] {msg}",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The format used for warning messages.",
|
||||
},
|
||||
"format_error_msg" = {
|
||||
"path": "loggie/formats/error_message",
|
||||
"default_value" : "[b][color=red][ERROR]:[/color][/b] {msg}",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The format used for error messages.",
|
||||
},
|
||||
"format_domain_prefix" = {
|
||||
"path": "loggie/formats/domain_prefix",
|
||||
"default_value" : "[b]({domain})[/b] {msg}",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The format used for domain prefixes.",
|
||||
},
|
||||
"format_header" = {
|
||||
"path": "loggie/formats/header",
|
||||
"default_value" : "[b][i]{msg}[/i][/b]",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The format used for headers.",
|
||||
},
|
||||
"h_separator_symbol" = {
|
||||
"path": "loggie/formats/h_separator_symbol",
|
||||
"default_value" : "-",
|
||||
"type" : TYPE_STRING,
|
||||
"hint" : PROPERTY_HINT_NONE,
|
||||
"hint_string" : "",
|
||||
"doc" : "The symbol used for the horizontal separator.",
|
||||
},
|
||||
"box_characters_mode" = {
|
||||
"path": "loggie/formats/box_characters_mode",
|
||||
"default_value" : LoggieEnums.BoxCharactersMode.COMPATIBLE,
|
||||
"type" : TYPE_INT,
|
||||
"hint" : PROPERTY_HINT_ENUM,
|
||||
"hint_string" : "Compatible:0,Pretty:1",
|
||||
"doc" : "There are two sets of box characters defined in LoggieSettings - one set contains prettier characters that produce a nicer looking box, but may not render correctly in the context of various terminals. The other set contains characters that produce a less pretty box, but are compatible with being shown in most terminals.",
|
||||
},
|
||||
}
|
||||
|
||||
## The current terminal mode of Loggie.
|
||||
## Terminal mode determines whether BBCode, ANSI or some other type of
|
||||
## formatting is used to convey text effects, such as bold, italic, colors, etc.
|
||||
## [br][br]BBCode is compatible with the Godot console.
|
||||
## [br]ANSI is compatible with consoles like Powershell and Windows CMD.
|
||||
## [br]PLAIN is used to strip any effects and use plain text instead, which is good for saving raw logs into log files.
|
||||
var terminal_mode : LoggieEnums.TerminalMode
|
||||
|
||||
## The current log level of Loggie.
|
||||
## It determines which types of messages are allowed to be logged.
|
||||
## Set this using [method setLogLevel].
|
||||
var log_level : LoggieEnums.LogLevel
|
||||
|
||||
## Whether or not Loggie should log the loggie specs on ready.
|
||||
var show_loggie_specs : LoggieEnums.ShowLoggieSpecsMode
|
||||
|
||||
## Whether or not Loggie should log the system specs on ready.
|
||||
var show_system_specs : bool
|
||||
|
||||
## If true, the domain from which the [LoggieMsg] is coming will be prepended to the output.
|
||||
## If the domain is default (empty), nothing will change.
|
||||
var output_message_domain : bool
|
||||
|
||||
## Whether to, in addition to logging errors with [method push_error],
|
||||
## Loggie should also print the error as a message in the standard output.
|
||||
var print_errors_to_console : bool
|
||||
|
||||
## Whether to, in addition to logging errors with [method push_warning],
|
||||
## Loggie should also print the error as a message in the standard output.
|
||||
var print_warnings_to_console : bool
|
||||
|
||||
## If true, instead of [method print], [method print_debug] will be
|
||||
## used when printing messages with [method LoggieMsg.debug].
|
||||
var use_print_debug_for_debug_msg : bool
|
||||
|
||||
## Whether Loggie should use the scripts from which it is being called to
|
||||
## figure out a class name for the class that called a loggie function,
|
||||
## and display it at the start of each output message.
|
||||
## This only works in debug builds because it uses [method @GDScript.get_stack].
|
||||
## See that method's documentation to see why that can't be used in release builds.
|
||||
var derive_and_show_class_names : bool
|
||||
|
||||
## Defines which text will be used as a substitute for the 'class_name' of scripts that do not have a 'class_name'.
|
||||
## Relevant only if [member derive_and_show_class_names] is enabled.
|
||||
var nameless_class_name_proxy : LoggieEnums.NamelessClassExtensionNameProxy
|
||||
|
||||
## Whether Loggie should prepend a timestamp to each output message.
|
||||
var output_timestamps : bool
|
||||
|
||||
## Whether the outputted timestamps (if [member output_timestamps] is enabled) use UTC or local machine time.
|
||||
var timestamps_use_utc : bool
|
||||
|
||||
## Whether Loggie should enforce optimal values for certain settings when in a Release/Production build.
|
||||
## [br]If true, Loggie will enforce:
|
||||
## [br] * [member terminal_mode] to [member LoggieEnums.TerminalMode.PLAIN]
|
||||
## [br] * [member box_characters_mode] to [member LoggieEnums.BoxCharactersMode.COMPATIBLE]
|
||||
var enforce_optimal_settings_in_release_build : bool
|
||||
|
||||
# ----------------------------------------------- #
|
||||
#region Formats for prints
|
||||
# ----------------------------------------------- #
|
||||
# As per the `print_rich` documentation, supported colors are: black, red, green, yellow, blue, magenta, pink, purple, cyan, white, orange, gray.
|
||||
# Any other color will be displayed in the Godot console or an ANSI based console, but the color tag (in case of BBCode) won't be properly stripped
|
||||
# when written to the .log file, resulting in BBCode visible in .log files.
|
||||
|
||||
## The format used to decorate a message as a header when using [method LoggieMsg.header].[br]
|
||||
## The [param {msg}] is a variable that will be replaced with the contents of the message.[br]
|
||||
var format_header = "[b][i]{msg}[/i][/b]"
|
||||
|
||||
## The format used when appending a domain to a message.[br]
|
||||
## See: [member output_message_domain]
|
||||
## The [param {msg}] is a variable that will be replaced with the contents of the message.[br]
|
||||
## The [param {domain}] is a variable that will be replaced with the domain key.[br]
|
||||
## You can customize this in your ProjectSettings, or custom_settings.gd (if using it).[br]
|
||||
var format_domain_prefix = "[b]({domain})[/b] {msg}"
|
||||
|
||||
## The format used when outputting error messages.[br]
|
||||
## The [param {msg}] is a variable that will be replaced with the contents of the message.[br]
|
||||
## You can customize this in your ProjectSettings, or custom_settings.gd (if using it).[br]
|
||||
var format_error_msg = "[b][color=red][ERROR]:[/color][/b] {msg}"
|
||||
|
||||
## The format used when outputting warning messages.[br]
|
||||
## The [param {msg}] is a variable that will be replaced with the contents of the message.[br]
|
||||
## You can customize this in your ProjectSettings, or custom_settings.gd (if using it).[br]
|
||||
var format_warning_msg = "[b][color=orange][WARN]:[/color][/b] {msg}"
|
||||
|
||||
## The format used when outputting notice messages.[br]
|
||||
## The [param {msg}] is a variable that will be replaced with the contents of the message.[br]
|
||||
## You can customize this in your ProjectSettings, or custom_settings.gd (if using it).[br]
|
||||
var format_notice_msg = "[b][color=cyan][NOTICE]:[/color][/b] {msg}"
|
||||
|
||||
## The format used when outputting info messages.[br]
|
||||
## The [param {msg}] is a variable that will be replaced with the contents of the message.[br]
|
||||
## You can customize this in your ProjectSettings, or custom_settings.gd (if using it).[br]
|
||||
var format_info_msg = "{msg}"
|
||||
|
||||
## The format used when outputting debug messages.[br]
|
||||
## The [param {msg}] is a variable that will be replaced with the contents of the message.[br]
|
||||
## You can customize this in your ProjectSettings, or custom_settings.gd (if using it).[br]
|
||||
var format_debug_msg = "[b][color=pink][DEBUG]:[/color][/b] {msg}"
|
||||
|
||||
## The format used for timestamps which are prepended to the message when [member output_timestamps] is enabled.[br]
|
||||
## The variables [param {day}], [param {month}], [param {year}], [param {hour}], [param {minute}], [param {second}], [param {weekday}], and [param {dst}] are supported.
|
||||
## You can customize this in your ProjectSettings, or custom_settings.gd (if using it).[br]
|
||||
var format_timestamp = "[{day}.{month}.{year} {hour}:{minute}:{second}]"
|
||||
|
||||
## The symbol which will be used for the HSeparator.
|
||||
var h_separator_symbol = "-"
|
||||
|
||||
## The mode used for drawing boxes.
|
||||
var box_characters_mode : LoggieEnums.BoxCharactersMode
|
||||
|
||||
## The symbols which will be used to construct a box decoration that will properly
|
||||
## display on any kind of terminal or text reader.
|
||||
## For a prettier but potentially incompatible box, use [member box_symbols_pretty] instead.
|
||||
var box_symbols_compatible = {
|
||||
# ANSI and .log compatible box characters:
|
||||
"top_left" : "-",
|
||||
"top_right" : "-",
|
||||
"bottom_left" : "-",
|
||||
"bottom_right" : "-",
|
||||
"h_line" : "-",
|
||||
"v_line" : ":",
|
||||
}
|
||||
|
||||
## The symbols which will be used to construct pretty box decoration.
|
||||
## These may not be compatible with some terminals or text readers.
|
||||
## Use the [member box_symbols_compatible] instead as an alternative.
|
||||
var box_symbols_pretty = {
|
||||
"top_left" : "┌",
|
||||
"top_right" : "┐",
|
||||
"bottom_left" : "└",
|
||||
"bottom_right" : "┘",
|
||||
"h_line" : "─",
|
||||
"v_line" : "│",
|
||||
}
|
||||
|
||||
#endregion
|
||||
# ----------------------------------------------- #
|
||||
|
||||
## Loads the initial (default) values for all of the LoggieSettings variables.
|
||||
## (By default, loads them from ProjectSettings (if any modifications there exist),
|
||||
## or looks in [LoggieEditorPlugin..project_settings] for default values).
|
||||
## [br][br]Extend this class and override this function to write your own logic for
|
||||
## how loggie should obtain these settings if you have a need for a different approach.
|
||||
func load():
|
||||
terminal_mode = ProjectSettings.get_setting(project_settings.terminal_mode.path, project_settings.terminal_mode.default_value)
|
||||
log_level = ProjectSettings.get_setting(project_settings.log_level.path, project_settings.log_level.default_value)
|
||||
show_loggie_specs = ProjectSettings.get_setting(project_settings.show_loggie_specs.path, project_settings.show_loggie_specs.default_value)
|
||||
show_system_specs = ProjectSettings.get_setting(project_settings.show_system_specs.path, project_settings.show_system_specs.default_value)
|
||||
output_timestamps = ProjectSettings.get_setting(project_settings.output_timestamps.path, project_settings.output_timestamps.default_value)
|
||||
timestamps_use_utc = ProjectSettings.get_setting(project_settings.timestamps_use_utc.path, project_settings.timestamps_use_utc.default_value)
|
||||
enforce_optimal_settings_in_release_build = ProjectSettings.get_setting(project_settings.enforce_optimal_settings_in_release_build.path, project_settings.enforce_optimal_settings_in_release_build.default_value)
|
||||
|
||||
print_errors_to_console = ProjectSettings.get_setting(project_settings.output_errors_to_console.path, project_settings.output_errors_to_console.default_value)
|
||||
print_warnings_to_console = ProjectSettings.get_setting(project_settings.output_warnings_to_console.path, project_settings.output_warnings_to_console.default_value)
|
||||
use_print_debug_for_debug_msg = ProjectSettings.get_setting(project_settings.use_print_debug_for_debug_msgs.path, project_settings.use_print_debug_for_debug_msgs.default_value)
|
||||
|
||||
output_message_domain = ProjectSettings.get_setting(project_settings.output_message_domain.path, project_settings.output_message_domain.default_value)
|
||||
derive_and_show_class_names = ProjectSettings.get_setting(project_settings.derive_and_display_class_names_from_scripts.path, project_settings.derive_and_display_class_names_from_scripts.default_value)
|
||||
|
||||
nameless_class_name_proxy = ProjectSettings.get_setting(project_settings.nameless_class_name_proxy.path, project_settings.nameless_class_name_proxy.default_value)
|
||||
box_characters_mode = ProjectSettings.get_setting(project_settings.box_characters_mode.path, project_settings.box_characters_mode.default_value)
|
||||
|
||||
format_timestamp = ProjectSettings.get_setting(project_settings.format_timestamp.path, project_settings.format_timestamp.default_value)
|
||||
format_info_msg = ProjectSettings.get_setting(project_settings.format_info_msg.path, project_settings.format_info_msg.default_value)
|
||||
format_notice_msg = ProjectSettings.get_setting(project_settings.format_notice_msg.path, project_settings.format_notice_msg.default_value)
|
||||
format_warning_msg = ProjectSettings.get_setting(project_settings.format_warning_msg.path, project_settings.format_warning_msg.default_value)
|
||||
format_error_msg = ProjectSettings.get_setting(project_settings.format_error_msg.path, project_settings.format_error_msg.default_value)
|
||||
format_debug_msg = ProjectSettings.get_setting(project_settings.format_debug_msg.path, project_settings.format_debug_msg.default_value)
|
||||
h_separator_symbol = ProjectSettings.get_setting(project_settings.h_separator_symbol.path, project_settings.h_separator_symbol.default_value)
|
||||
|
||||
## Returns a dictionary where the indices are names of relevant variables in the LoggieSettings class,
|
||||
## and the values are their current values.
|
||||
func to_dict() -> Dictionary:
|
||||
var dict = {}
|
||||
var included = [
|
||||
"terminal_mode", "log_level", "show_loggie_specs", "show_system_specs", "enforce_optimal_settings_in_release_build",
|
||||
"output_message_domain", "print_errors_to_console", "print_warnings_to_console",
|
||||
"use_print_debug_for_debug_msg", "derive_and_show_class_names", "nameless_class_name_proxy",
|
||||
"output_timestamps", "timestamps_use_utc", "format_header", "format_domain_prefix", "format_error_msg",
|
||||
"format_warning_msg", "format_notice_msg", "format_info_msg", "format_debug_msg", "format_timestamp",
|
||||
"h_separator_symbol", "box_characters_mode", "box_symbols_compatible", "box_symbols_pretty",
|
||||
]
|
||||
|
||||
for var_name in included:
|
||||
dict[var_name] = get(var_name)
|
||||
return dict
|
7
addons/loggie/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="Loggie"
|
||||
description="Simple functional stylish logger for your basic logging needs."
|
||||
author="Shiva Shadowsong"
|
||||
version="1.5"
|
||||
script="plugin.gd"
|
47
addons/loggie/plugin.gd
Normal file
|
@ -0,0 +1,47 @@
|
|||
@tool
|
||||
class_name LoggieEditorPlugin extends EditorPlugin
|
||||
|
||||
func _enter_tree():
|
||||
add_autoload_singleton(LoggieSettings.loggie_singleton_name, "res://addons/loggie/loggie.gd")
|
||||
add_loggie_project_settings()
|
||||
|
||||
func _enable_plugin() -> void:
|
||||
add_loggie_project_settings()
|
||||
|
||||
func _disable_plugin() -> void:
|
||||
var wipe_setting_exists = ProjectSettings.has_setting(LoggieSettings.project_settings.remove_settings_if_plugin_disabled.path)
|
||||
if (not wipe_setting_exists) or (wipe_setting_exists and ProjectSettings.get_setting(LoggieSettings.project_settings.remove_settings_if_plugin_disabled.path, true)):
|
||||
push_warning("The Loggie plugin is being disabled, and all of its ProjectSettings are erased from Godot. If you wish to prevent this behavior, look for the 'Project Settings -> Loggie -> General -> Remove Settings if Plugin Disabled' option while the plugin is enabled.")
|
||||
remove_loggie_project_setings()
|
||||
else:
|
||||
push_warning("The Loggie plugin is being disabled, but its ProjectSettings have been prevented from being removed from Godot. If you wish to allow that behavior, look for the 'Project Settings -> Loggie -> General -> Remove Settings if Plugin Disabled' option while the plugin is enabled.")
|
||||
remove_autoload_singleton(LoggieSettings.loggie_singleton_name)
|
||||
|
||||
## Adds new Loggie related ProjectSettings to Godot.
|
||||
func add_loggie_project_settings():
|
||||
for setting in LoggieSettings.project_settings.values():
|
||||
add_project_setting(setting["path"], setting["default_value"], setting["type"], setting["hint"], setting["hint_string"], setting["doc"])
|
||||
|
||||
## Removes Loggie related ProjectSettings from Godot.
|
||||
func remove_loggie_project_setings():
|
||||
for setting in LoggieSettings.project_settings.values():
|
||||
ProjectSettings.set_setting(setting["path"], null)
|
||||
|
||||
var error: int = ProjectSettings.save()
|
||||
if error != OK:
|
||||
push_error("Loggie - Encountered error %d while saving project settings." % error)
|
||||
|
||||
## Adds a new project setting to Godot.
|
||||
## TODO: Figure out how to also add the documentation to the ProjectSetting so that it shows up
|
||||
## in the Godot Editor tooltip when the setting is hovered over.
|
||||
func add_project_setting(setting_name: String, default_value : Variant, value_type: int, type_hint: int = PROPERTY_HINT_NONE, hint_string: String = "", documentation : String = ""):
|
||||
if !ProjectSettings.has_setting(setting_name):
|
||||
ProjectSettings.set_setting(setting_name, default_value)
|
||||
|
||||
ProjectSettings.set_initial_value(setting_name, default_value)
|
||||
ProjectSettings.add_property_info({ "name": setting_name, "type": value_type, "hint": type_hint, "hint_string": hint_string})
|
||||
ProjectSettings.set_as_basic(setting_name, true)
|
||||
|
||||
var error: int = ProjectSettings.save()
|
||||
if error:
|
||||
push_error("Loggie - Encountered error %d while saving project settings." % error)
|