Initial commit.
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
local_certs
|
||||||
|
auto_https disable_redirects
|
||||||
|
ocsp_stapling off
|
||||||
|
}
|
||||||
|
|
||||||
|
:2480 {
|
||||||
|
bind 0.0.0.0
|
||||||
|
encode
|
||||||
|
file_server {
|
||||||
|
root build/web
|
||||||
|
index JackIt.html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
31.125.232.239:2443 {
|
||||||
|
bind 0.0.0.0
|
||||||
|
encode
|
||||||
|
file_server {
|
||||||
|
root build/web
|
||||||
|
index JackIt.html
|
||||||
|
}
|
||||||
|
tls internal {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
local_certs
|
||||||
|
auto_https disable_redirects
|
||||||
|
ocsp_stapling off
|
||||||
|
}
|
||||||
|
|
||||||
|
31.125.232.239:2480 {
|
||||||
|
bind 0.0.0.0
|
||||||
|
encode
|
||||||
|
file_server {
|
||||||
|
root build/web
|
||||||
|
index JackIt.html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
31.125.232.239:2443 {
|
||||||
|
bind 0.0.0.0
|
||||||
|
encode
|
||||||
|
file_server {
|
||||||
|
root build/web
|
||||||
|
index JackIt.html
|
||||||
|
}
|
||||||
|
tls internal {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
@tool
|
||||||
|
extends EditorExportPlugin
|
||||||
|
|
||||||
|
var export_path: String = ""
|
||||||
|
|
||||||
|
func _get_name() -> String:
|
||||||
|
return "tnhujftdbnhkjdftrgh"
|
||||||
|
|
||||||
|
func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void:
|
||||||
|
export_path = path.left(path.rfind("/"))
|
||||||
|
print("Project is being exported to %s" % export_path)
|
||||||
|
|
||||||
|
func _export_end() -> void:
|
||||||
|
var dir: DirAccess = DirAccess.open(export_path)
|
||||||
|
if dir:
|
||||||
|
for file in dir.get_files():
|
||||||
|
print("Exported file: %s" % file)
|
||||||
|
if file.ends_with(".js"):
|
||||||
|
hack_js_file(export_path + "/" + file)
|
||||||
|
|
||||||
|
func hack_js_file(file: String) -> void:
|
||||||
|
print("Checking exported JS file %s" % file)
|
||||||
|
var file_access: FileAccess = FileAccess.open(file,FileAccess.READ_WRITE)
|
||||||
|
if file_access == null:
|
||||||
|
printerr("Failed to open file %s, code: %s" % [file, FileAccess.get_open_error()])
|
||||||
|
return
|
||||||
|
var contents: String = file_access.get_as_text()
|
||||||
|
var regex: RegEx = RegEx.create_from_string("if \\(!Features.isSecureContext\\(\\)\\) \\{(\n[^\n]*){2}")
|
||||||
|
var match: RegExMatch = regex.search(contents)
|
||||||
|
if match and match.get_start() != -1:
|
||||||
|
print("Found target string! Modifying...")
|
||||||
|
contents = regex.sub(contents, "/* I fucked this file to enable HTTP support, look at me */")
|
||||||
|
file_access.resize(0)
|
||||||
|
file_access.store_string(contents)
|
|
@ -0,0 +1,7 @@
|
||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="GodotWebHTTPHack"
|
||||||
|
description=""
|
||||||
|
author=""
|
||||||
|
version=""
|
||||||
|
script="plugin.gd"
|
|
@ -0,0 +1,14 @@
|
||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
const ExportPlugin = preload("res://addons/GodotWebHTTPHack/export_plugin.gd")
|
||||||
|
var export_plugin = ExportPlugin.new()
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
# Initialization of the plugin goes here.
|
||||||
|
add_export_plugin(export_plugin)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
# Clean-up of the plugin goes here.
|
||||||
|
remove_export_plugin(export_plugin)
|
|
@ -0,0 +1,8 @@
|
||||||
|
@tool
|
||||||
|
class_name Cables extends EditorPlugin
|
||||||
|
|
||||||
|
func _enter_tree():
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
pass
|
|
@ -0,0 +1 @@
|
||||||
|
uid://2mslx0hvbkff
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="Layer_1"
|
||||||
|
data-name="Layer 1"
|
||||||
|
version="1.1"
|
||||||
|
sodipodi:docname="cable-icon.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||||
|
inkscape:export-filename="..\..\..\cables-addon-logo.png"
|
||||||
|
inkscape:export-xdpi="122.88"
|
||||||
|
inkscape:export-ydpi="122.88"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview3"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.10125"
|
||||||
|
inkscape:cx="400"
|
||||||
|
inkscape:cy="400"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1057"
|
||||||
|
inkscape:window-x="1912"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_1" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<style
|
||||||
|
id="style1">.cls-1{fill:none;stroke:#020202;stroke-miterlimit:10;stroke-width:1.91px;}</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#cccccc;stroke-miterlimit:10;-inkscape-stroke:none"
|
||||||
|
d="m 15.820313,0.54492187 c -2.625846,10e-9 -4.775391,2.14954523 -4.775391,4.77539063 V 18.679687 c 0,1.593606 -1.2716287,2.865235 -2.8652345,2.865235 -1.5936058,0 -2.8652344,-1.271629 -2.8652344,-2.865235 V 10.089844 H 3.4042969 v 8.589843 c 0,2.625846 2.1495452,4.775391 4.7753906,4.775391 2.6258455,0 4.7753905,-2.149545 4.7753905,-4.775391 V 5.3203125 c 0,-1.5936058 1.271629,-2.8652344 2.865235,-2.8652344 1.593605,0 2.865234,1.2716286 2.865234,2.8652344 v 8.5898435 h 1.910156 V 5.3203125 c 0,-2.6258454 -2.149545,-4.77539062 -4.77539,-4.77539063 z"
|
||||||
|
id="path3" />
|
||||||
|
<g
|
||||||
|
id="g2"
|
||||||
|
style="fill:#ffff00">
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#ffff00;stroke-miterlimit:10;-inkscape-stroke:none"
|
||||||
|
d="M 1.4941406,0.54492187 V 6.2753906 H 7.2246094 V 0.54492187 Z M 3.4042969,2.4550781 H 5.3144531 V 4.3652344 H 3.4042969 Z"
|
||||||
|
id="rect1" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#ffff00;stroke-miterlimit:10;-inkscape-stroke:none"
|
||||||
|
d="m 22.494609,23.455391 v -5.730469 h -5.730468 v 5.730469 z m -1.910156,-1.910157 h -1.910156 v -1.910156 h 1.910156 z"
|
||||||
|
id="rect2" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g1"
|
||||||
|
style="fill:#00ffff">
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#00ffff;stroke-miterlimit:10;-inkscape-stroke:none"
|
||||||
|
d="m 0.54492187,4.3652344 v 3.8144531 c 10e-9,1.5709826 1.29425173,2.8652345 2.86523443,2.8652345 h 1.9101562 c 1.5709826,0 2.8652344,-1.2942519 2.8652344,-2.8652345 V 4.3652344 Z M 2.4550781,6.2753906 h 3.8203125 v 1.9042969 c 0,0.538743 -0.4163351,0.9550781 -0.9550781,0.9550781 H 3.4101563 c -0.5387431,0 -0.9550782,-0.4163351 -0.9550782,-0.9550781 z"
|
||||||
|
id="path1" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#00ffff;stroke-miterlimit:10;-inkscape-stroke:none"
|
||||||
|
d="m 23.455547,19.634922 v -3.814453 c 0,-1.570983 -1.294252,-2.865235 -2.865234,-2.865235 h -1.910157 c -1.570982,0 -2.865234,1.294252 -2.865234,2.865235 v 3.814453 z m -1.910156,-1.910156 h -3.820313 v -1.904297 c 0,-0.538743 0.416335,-0.955078 0.955078,-0.955078 h 1.910157 c 0.538743,0 0.955078,0.416335 0.955078,0.955078 z"
|
||||||
|
id="path2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,37 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cww3x2h528pv7"
|
||||||
|
path="res://.godot/imported/cable-icon.svg-f3b569967f2871c2fb436853cfcc9402.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/cables/icons/cable-icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/cable-icon.svg-f3b569967f2871c2fb436853cfcc9402.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
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 327.68 327.68"
|
||||||
|
id="_04_In_alt_"
|
||||||
|
data-name="04 In (alt)"
|
||||||
|
version="1.1"
|
||||||
|
sodipodi:docname="consumer-icon.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.10125"
|
||||||
|
inkscape:cx="307.37798"
|
||||||
|
inkscape:cy="288.30874"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1057"
|
||||||
|
inkscape:window-x="1912"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="_04_In_alt_" />
|
||||||
|
<path
|
||||||
|
id="Path_6"
|
||||||
|
data-name="Path 6"
|
||||||
|
d="M 279.56389,10.91285 H 145.75967 a 19.11489,19.11489 0 0 0 0,38.229777 H 260.449 V 278.52129 H 145.75967 a 19.114889,19.114889 0 0 0 0,38.22977 h 133.80422 a 19.109513,19.109513 0 0 0 19.11489,-19.11489 V 30.027739 A 19.109513,19.109513 0 0 0 279.56389,10.91285 Z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
style="fill:#f9f9f9;stroke-width:0.597341" />
|
||||||
|
<path
|
||||||
|
id="Path_7"
|
||||||
|
style="fill:#f9f9f9;stroke-width:0.815834"
|
||||||
|
d="M 106.54875 82.9175 L 106.54875 142.50125 L 54.62875 142.50125 L 54.62875 182.22375 L 106.54875 182.22375 L 106.54875 241.80625 L 243.815 162.3625 L 106.54875 82.9175 z " />
|
||||||
|
<path
|
||||||
|
id="rect1"
|
||||||
|
style="fill:#f9f9f9;stroke-width:0.515543"
|
||||||
|
d="m 32.417935,142.5359 c -17.182295,0 -31.085835,8.89302 -31.085835,19.86125 0,10.96823 13.90354,19.86125 31.085835,19.86125 h 53.76273 v -39.7225 z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,37 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cdeod6hfxbyry"
|
||||||
|
path="res://.godot/imported/consumer-icon.svg-92a77bb7120fb045c57bf86e818fd775.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/cables/icons/consumer-icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/consumer-icon.svg-92a77bb7120fb045c57bf86e818fd775.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
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 327.68 327.68"
|
||||||
|
id="_04_In_alt_"
|
||||||
|
data-name="04 In (alt)"
|
||||||
|
version="1.1"
|
||||||
|
sodipodi:docname="producer-icon.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.10125"
|
||||||
|
inkscape:cx="163.90465"
|
||||||
|
inkscape:cy="366.40182"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1057"
|
||||||
|
inkscape:window-x="1912"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="_04_In_alt_" />
|
||||||
|
<path
|
||||||
|
id="Path_6"
|
||||||
|
data-name="Path 6"
|
||||||
|
d="m 290.16776,4.5201155 h -137.1755 a 19.596501,19.596501 0 0 0 0,39.1930015 h 117.579 V 278.87111 h -117.579 a 19.596499,19.596499 0 0 0 0,39.19299 h 137.1755 a 19.590988,19.590988 0 0 0 19.59649,-19.5965 V 24.116615 A 19.590988,19.590988 0 0 0 290.16776,4.5201155 Z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
style="fill:#f9f9f9;stroke-width:0.61239" />
|
||||||
|
<path
|
||||||
|
id="Path_7"
|
||||||
|
style="fill:#f9f9f9;stroke-width:0.818193"
|
||||||
|
d="M 156.68374,70.937376 40.10012,165.01569 156.68374,259.09401 v -70.55874 h 114.7164 v -47.03916 h -114.7164 z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,37 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b1cwxnbxhl3v5"
|
||||||
|
path="res://.godot/imported/producer-icon.svg-be5a0b4c1a9db245c9812cb27a6a40d8.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/cables/icons/producer-icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/producer-icon.svg-be5a0b4c1a9db245c9812cb27a6a40d8.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
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
|
@ -0,0 +1,7 @@
|
||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="Cables"
|
||||||
|
description="Higher-order construct to bridge the signal gap between scenes"
|
||||||
|
author="Obsidize Games"
|
||||||
|
version="2.3.0"
|
||||||
|
script="cables.gd"
|
|
@ -0,0 +1,131 @@
|
||||||
|
## A resource-based signal type.
|
||||||
|
##
|
||||||
|
## Bridges the gap between scene assets, and fixes the issue of
|
||||||
|
## signal connections getting broken when rearranging scenes.
|
||||||
|
@icon("res://addons/cables/icons/cable-icon.svg")
|
||||||
|
class_name Cable extends Resource
|
||||||
|
|
||||||
|
## Special value to indicate [method void_notify] was called.
|
||||||
|
## [br][br]
|
||||||
|
## Needed because [code]null[/code] can't be used for void events, otherwise
|
||||||
|
## the calls [code]notify(null)[/code] and [code]void_notify()[/code] would be ambiguous.
|
||||||
|
const VOID_EVENT := {}
|
||||||
|
|
||||||
|
## Utility to check if the value was produced from a [method void_notify] call.
|
||||||
|
static func is_void_event(value: Variant) -> bool:
|
||||||
|
return typeof(value) == typeof(VOID_EVENT) and value == VOID_EVENT
|
||||||
|
|
||||||
|
var _current_value: Variant = null
|
||||||
|
var _did_notify_once: bool = false
|
||||||
|
|
||||||
|
# Core signal event that this cable wraps.
|
||||||
|
# This should not be used directly - instead, use the
|
||||||
|
# `link()` / `unlink()` methods.
|
||||||
|
signal _value_updated(value: Variant)
|
||||||
|
|
||||||
|
## When [code]true[/code], will re-emit the last value update
|
||||||
|
## to all [code]Callable[/code]s registered via [method link].
|
||||||
|
## [br][br]
|
||||||
|
## This solves the issue of late registration where
|
||||||
|
## the cable value has already settled before some consumer
|
||||||
|
## is ready to start watching for changes.
|
||||||
|
@export var replay_on_link := false
|
||||||
|
|
||||||
|
## When [code]true[/code], will print all interactions with this cable to the console
|
||||||
|
@export var debug_trace := false
|
||||||
|
|
||||||
|
## Optional description indicating intended use of
|
||||||
|
## this particular cable resource.
|
||||||
|
## [br][br]
|
||||||
|
## This is for documentation purposes only, to
|
||||||
|
## keep your game organized as the number of cable resources grows.
|
||||||
|
@export_multiline var debug_description := ""
|
||||||
|
|
||||||
|
## [code]True[/code] if this Cable has emitted at least one value.
|
||||||
|
## [br]
|
||||||
|
## This will help determine when the value should be replayed.
|
||||||
|
var did_notify_once: bool:
|
||||||
|
get: return _did_notify_once
|
||||||
|
|
||||||
|
## The last emitted value.
|
||||||
|
## [br][br]
|
||||||
|
## This can be compared against the value received from [method link] where
|
||||||
|
## this would be the old value, and the value in the [method link] callable
|
||||||
|
## would be the new value.
|
||||||
|
var current_value: Variant:
|
||||||
|
get: return _current_value
|
||||||
|
set(value): notify(value)
|
||||||
|
|
||||||
|
## Print contextual information for this cable to the console.
|
||||||
|
## [br]
|
||||||
|
## Does nothing when [member debug_trace] is [code]false[/code]
|
||||||
|
func debug_log(message: String) -> void:
|
||||||
|
if debug_trace: print("[%s] %s" % [resource_path.get_file(), message])
|
||||||
|
|
||||||
|
## Convenience for obtaining the current value or a fallback,
|
||||||
|
## depending on the state of this cable.
|
||||||
|
func get_value_or_default(default_value: Variant) -> Variant:
|
||||||
|
if did_notify_once and not Cable.is_void_event(current_value):
|
||||||
|
return current_value
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
## Produce a new value to be passed on to any [code]Callable[/code]s registered
|
||||||
|
## via [method link], as well as any registered [CableValueConsumer] instances.
|
||||||
|
func notify(value: Variant) -> void:
|
||||||
|
_value_updated.emit(value)
|
||||||
|
_current_value = value
|
||||||
|
_did_notify_once = true
|
||||||
|
|
||||||
|
## Produce a special [constant Cable.VOID_EVENT] value to to any registered listeners.
|
||||||
|
## [br][br]
|
||||||
|
## This should be used if the cable is intended to be strictly an event producer,
|
||||||
|
## and does not need to supply any specific value change
|
||||||
|
## (e.g. for a [b]player death[/b] event).
|
||||||
|
func void_notify() -> void:
|
||||||
|
debug_log("void_notify()")
|
||||||
|
notify(VOID_EVENT)
|
||||||
|
|
||||||
|
## Registers the given [code]Callable[/code] to this cable.
|
||||||
|
## [br][br]
|
||||||
|
## The given [code]Callable[/code] will be updated with the latest value
|
||||||
|
## whenever [method notify] or [method void_notify] is called on this Cable.
|
||||||
|
## [br][br]
|
||||||
|
## If this Cable has emitted at least one value, and is set up to
|
||||||
|
## replay values, the given [code]Callable[/code] will be called immediately
|
||||||
|
## with the latest value.
|
||||||
|
## [br][br]
|
||||||
|
## Does nothing if the given callable is already connected to this Cable.
|
||||||
|
## [br][br]
|
||||||
|
## Returns a [code]Callable[/code] that, when called, will deregister
|
||||||
|
## the originally passed in [code]Callable[/code] function.
|
||||||
|
func link(callable: Callable) -> Callable:
|
||||||
|
if not _value_updated.is_connected(callable):
|
||||||
|
debug_log("link")
|
||||||
|
_value_updated.connect(callable)
|
||||||
|
|
||||||
|
if replay_on_link and did_notify_once:
|
||||||
|
debug_log("replay_on_link")
|
||||||
|
callable.call(current_value)
|
||||||
|
|
||||||
|
var unlink_action := func(): unlink(callable)
|
||||||
|
|
||||||
|
return unlink_action
|
||||||
|
|
||||||
|
## Deregisters the given [code]Callable[/code] from this cable.
|
||||||
|
## [br]
|
||||||
|
## Does nothing if the [code]Callable[/code] is not connected.
|
||||||
|
func unlink(callable: Callable) -> void:
|
||||||
|
if _value_updated.is_connected(callable):
|
||||||
|
debug_log("unlink")
|
||||||
|
_value_updated.disconnect(callable)
|
||||||
|
|
||||||
|
## Links the given [code]Callable[/code], and will automatically unlink it
|
||||||
|
## when the given [NodeWithLifetime] instance is about to be
|
||||||
|
## deleted (i.e. when a [b]NOTIFICATION_PREDELETE[/b] event is received on it).
|
||||||
|
## [br][br]
|
||||||
|
## This is a more optimal alternative to [b]tree_entered[/b] / [b]tree_exiting[/b] events,
|
||||||
|
## which may fire multiple times if a node is reparented one or more times.
|
||||||
|
func link_until_destroyed(node: NodeWithLifetime, callable: Callable) -> void:
|
||||||
|
debug_log("link_until_destroyed(%s)" % node.name)
|
||||||
|
var unlink_action := link(callable)
|
||||||
|
node.node_destroyed.connect(unlink_action)
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bu8qa53bmqorh
|
|
@ -0,0 +1,17 @@
|
||||||
|
## Thin wrapper around [CallableSink] that adds some
|
||||||
|
## functionality specific to [Cable]s.
|
||||||
|
class_name CableLinkGroup extends CallableSink
|
||||||
|
|
||||||
|
## Convenience to reduce boilerplate of [method with_lifetime]
|
||||||
|
## by inferring [method NodeWithLifetime.from] while creating the new link group.
|
||||||
|
static func with_lifetime_of(node: Node) -> CableLinkGroup:
|
||||||
|
return CableLinkGroup.with_lifetime(NodeWithLifetime.from(node))
|
||||||
|
|
||||||
|
## Creates a new [CableLinkGroup], and connects its
|
||||||
|
## [method call_each_and_clear] method as a callable to the given
|
||||||
|
## [param lifetime]'s [signal NodeWithLifetime.node_destroyed] signal.
|
||||||
|
static func with_lifetime(lifetime: NodeWithLifetime) -> CableLinkGroup:
|
||||||
|
var result := CableLinkGroup.new()
|
||||||
|
var unlink_all := func(): result.call_each_and_clear()
|
||||||
|
lifetime.node_destroyed.connect(unlink_all)
|
||||||
|
return result
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b1smiv6qwydhb
|
|
@ -0,0 +1,50 @@
|
||||||
|
## Plug-and-play transformer for Cables that localizes
|
||||||
|
## cable signals into local ones that can be bound to in the editor.
|
||||||
|
##
|
||||||
|
## NOTE: Using this node and subscribing to its signals may reintroduce
|
||||||
|
## the problem that [member Cable.replay_on_link] solves. If this is
|
||||||
|
## the case, it is recommended to bind to the target [Cable](s) in code
|
||||||
|
## via [method CableLinkGroup.with_lifetime_of] like so:
|
||||||
|
## [br]
|
||||||
|
## [codeblock]
|
||||||
|
## func _ready() -> void:
|
||||||
|
## CableLinkGroup.with_lifetime_of(self).append([
|
||||||
|
## cable1.link(_on_cable1_value),
|
||||||
|
## cable2.link(_on_cable2_value),
|
||||||
|
## ])
|
||||||
|
##
|
||||||
|
## [/codeblock]
|
||||||
|
@tool
|
||||||
|
@icon("res://addons/cables/icons/consumer-icon.svg")
|
||||||
|
class_name CableValueConsumer extends NodeWithLifetime
|
||||||
|
|
||||||
|
## Emits when the [member input] cable emits a value that is not [constant Cable.VOID_EVENT]
|
||||||
|
signal value_updated(value: Variant)
|
||||||
|
|
||||||
|
## Emits when the [member input] cable emits a [constant Cable.VOID_EVENT]
|
||||||
|
signal void_update()
|
||||||
|
|
||||||
|
## Emits when either a value update or void update occurs
|
||||||
|
signal any_update()
|
||||||
|
|
||||||
|
## The Cable to forward events from
|
||||||
|
@export var input: Cable
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
input.debug_log("Consumer<%s> link_until_destroyed()" % str(self))
|
||||||
|
input.link_until_destroyed(self, _on_cable_value_update)
|
||||||
|
|
||||||
|
func _on_cable_value_update(value: Variant) -> void:
|
||||||
|
any_update.emit()
|
||||||
|
if Cable.is_void_event(value):
|
||||||
|
input.debug_log("Consumer<%s> void_update()" % str(self))
|
||||||
|
void_update.emit()
|
||||||
|
else:
|
||||||
|
input.debug_log("Consumer<%s> value_updated(%s)" % [str(self), str(value)])
|
||||||
|
value_updated.emit(value)
|
||||||
|
|
||||||
|
func _get_configuration_warnings() -> PackedStringArray:
|
||||||
|
var warnings := PackedStringArray()
|
||||||
|
if input == null:
|
||||||
|
warnings.append("Input cable not assigned")
|
||||||
|
return warnings
|
|
@ -0,0 +1 @@
|
||||||
|
uid://duwq1cmdr5cie
|
|
@ -0,0 +1,25 @@
|
||||||
|
## Plug-and-play transformer for Cables that forwards
|
||||||
|
## local send calls onto the target [member output] [Cable].
|
||||||
|
@tool
|
||||||
|
@icon("res://addons/cables/icons/producer-icon.svg")
|
||||||
|
class_name CableValueProducer extends NodeWithLifetime
|
||||||
|
|
||||||
|
## The [Cable] to forward local [method send_value_update] and
|
||||||
|
## [method send_void_update] calls to.
|
||||||
|
@export var output: Cable
|
||||||
|
|
||||||
|
## Emits on the assigned [member output] [Cable] with the given [param value].
|
||||||
|
func send_value_update(value: Variant) -> void:
|
||||||
|
output.debug_log("Producer<%s> send_value_update(%s)" % [str(self), str(value)])
|
||||||
|
output.notify(value)
|
||||||
|
|
||||||
|
## Emits a [constant Cable.VOID_EVENT] on the assigned [member output] [Cable].
|
||||||
|
func send_void_update() -> void:
|
||||||
|
output.debug_log("Producer<%s> send_void_update()" % str(self))
|
||||||
|
output.void_notify()
|
||||||
|
|
||||||
|
func _get_configuration_warnings() -> PackedStringArray:
|
||||||
|
var warnings := PackedStringArray()
|
||||||
|
if output == null:
|
||||||
|
warnings.append("Output cable not assigned")
|
||||||
|
return warnings
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ooqrpwvnhwer
|
|
@ -0,0 +1,53 @@
|
||||||
|
## Generic utility to aggregate [code]Callable[/code]s.
|
||||||
|
class_name CallableSink extends RefCounted
|
||||||
|
|
||||||
|
var _actions: Array[Callable] = []
|
||||||
|
|
||||||
|
## Appends the given array of [code]Callable[/code]s to this group,
|
||||||
|
## while leaving any already-registered [code]Callable[/code]s in-tact.
|
||||||
|
## [br][br]
|
||||||
|
## This can be chained multiple times without causing any deregistration.
|
||||||
|
## [br][br]
|
||||||
|
## If you would like to instead [i]overwrite[/i] the currently stored
|
||||||
|
## [code]Callable[/code]s in this group, use [method collect] instead.
|
||||||
|
func append(unlink_actions: Array[Callable]) -> CallableSink:
|
||||||
|
if typeof(unlink_actions) == TYPE_ARRAY:
|
||||||
|
_actions.append_array(unlink_actions)
|
||||||
|
return self
|
||||||
|
|
||||||
|
## Similar to [method append], but returns a [code]Callable[/code]
|
||||||
|
## instead of a [code]self[/code] reference.
|
||||||
|
## [br][br]
|
||||||
|
## Useful in some edge cases where you may want to aggregate groups of groups
|
||||||
|
## like so:
|
||||||
|
## [br]
|
||||||
|
## [codeblock]
|
||||||
|
## var deregister_all := CallableSink.new().aggregate([
|
||||||
|
## CallableSink.new().aggregate(deregister_group_1),
|
||||||
|
## CallableSink.new().aggregate(deregister_group_2),
|
||||||
|
## CallableSink.new().aggregate(deregister_group_3),
|
||||||
|
## ])
|
||||||
|
## [/codeblock]
|
||||||
|
func aggregate(unlink_actions: Array[Callable]) -> Callable:
|
||||||
|
append(unlink_actions)
|
||||||
|
return call_each_and_clear
|
||||||
|
|
||||||
|
## Similar to [method aggregate], but deregisters any currently
|
||||||
|
## registered [code]Callable[/code]s via [method call_each_and_clear]
|
||||||
|
## before appending the given [param unlink_actions] to this group.
|
||||||
|
## [br][br]
|
||||||
|
## Call this when you intend to re-use an existing [CallableSink]
|
||||||
|
## to perform several "clean slate" hookups.
|
||||||
|
func collect(unlink_actions: Array[Callable]) -> Callable:
|
||||||
|
call_each_and_clear()
|
||||||
|
return aggregate(unlink_actions)
|
||||||
|
|
||||||
|
## Calls each currently registered [code]Callable[/code]
|
||||||
|
func call_each() -> void:
|
||||||
|
for c in _actions: if c is Callable: c.call()
|
||||||
|
|
||||||
|
## Similar to [method call_each], but clears the [code]Callable[/code] cache
|
||||||
|
## after all currently registered [code]Callable[/code]s have run.
|
||||||
|
func call_each_and_clear() -> void:
|
||||||
|
call_each()
|
||||||
|
_actions.clear()
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d4kxs1rngqv4q
|
|
@ -0,0 +1,42 @@
|
||||||
|
## Base class for Cable nodes that are interested in the [signal node_destroyed] event,
|
||||||
|
## i.e. when this node receives a [b]NOTIFICATION_PREDELETE[/b] message.
|
||||||
|
class_name NodeWithLifetime extends Node
|
||||||
|
|
||||||
|
## Emits when a [b]NOTIFICATION_PREDELETE[/b] notification is intercepted.
|
||||||
|
signal node_destroyed()
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_PREDELETE:
|
||||||
|
node_destroyed.emit()
|
||||||
|
|
||||||
|
## Translates the given [param node] into a [NodeWithLifetime] reference.
|
||||||
|
## [br][br]
|
||||||
|
## This returned instance will reflect the lifecycle events of the given [param node],
|
||||||
|
## i.e. when the returned instance is about to be deleted, it is inferred
|
||||||
|
## that the origin [param node] will also be deleted.
|
||||||
|
## [br][br]
|
||||||
|
## If the given [param node] is already a [NodeWithLifetime] instance
|
||||||
|
## it will be returned as-is.
|
||||||
|
## [br][br]
|
||||||
|
## If the given [param node] has a child node which is a [NodeWithLifetime] instance,
|
||||||
|
## that child node will be returned.
|
||||||
|
## [br][br]
|
||||||
|
## If neither of the above cases are met, this will create a new [NodeWithLifetime]
|
||||||
|
## instance and append it as a child of the given [param node].
|
||||||
|
static func from(node: Node) -> NodeWithLifetime:
|
||||||
|
if node is NodeWithLifetime:
|
||||||
|
return node as NodeWithLifetime
|
||||||
|
|
||||||
|
var result: NodeWithLifetime = null
|
||||||
|
|
||||||
|
for c in node.get_children():
|
||||||
|
if c is NodeWithLifetime:
|
||||||
|
result = c as NodeWithLifetime
|
||||||
|
break
|
||||||
|
|
||||||
|
if result == null:
|
||||||
|
result = NodeWithLifetime.new()
|
||||||
|
node.add_child(result)
|
||||||
|
result.name = "%s_LifetimeContext" % node.name
|
||||||
|
|
||||||
|
return result
|
|
@ -0,0 +1 @@
|
||||||
|
uid://citfmy45cuhr7
|
|
@ -0,0 +1,38 @@
|
||||||
|
## Special type of producer that sends a void event in response to button interaction,
|
||||||
|
## based on this node's configuration.
|
||||||
|
## This node type should always be added as a direct child of a Button node type.
|
||||||
|
@tool
|
||||||
|
@icon("res://addons/cables/icons/producer-icon.svg")
|
||||||
|
class_name CableButtonEventProducer extends CableValueProducer
|
||||||
|
|
||||||
|
enum TriggerType {
|
||||||
|
PRESSED,
|
||||||
|
BUTTON_DOWN,
|
||||||
|
BUTTON_UP
|
||||||
|
}
|
||||||
|
|
||||||
|
@export var trigger_type: TriggerType = TriggerType.PRESSED
|
||||||
|
|
||||||
|
var _button: Button = null
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var p := get_parent()
|
||||||
|
|
||||||
|
if p is Button:
|
||||||
|
_button = p as Button
|
||||||
|
|
||||||
|
if _button == null or Engine.is_editor_hint():
|
||||||
|
return
|
||||||
|
|
||||||
|
match trigger_type:
|
||||||
|
TriggerType.PRESSED: _button.pressed.connect(send_void_update)
|
||||||
|
TriggerType.BUTTON_DOWN: _button.button_down.connect(send_void_update)
|
||||||
|
TriggerType.BUTTON_UP: _button.button_up.connect(send_void_update)
|
||||||
|
_: push_warning("Unknown trigger type detected: %s" % str(trigger_type))
|
||||||
|
|
||||||
|
func _get_configuration_warnings() -> PackedStringArray:
|
||||||
|
var warnings := super._get_configuration_warnings()
|
||||||
|
var p := get_parent()
|
||||||
|
if not (p is Button):
|
||||||
|
warnings.append("Parent node must be a Button")
|
||||||
|
return warnings
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cermu1whlnehb
|
|
@ -0,0 +1,37 @@
|
||||||
|
## Special type of producer that sends a void event in response to an input action,
|
||||||
|
## based on this node's configuration.
|
||||||
|
## This node type should always be added as a direct child of a Button node type.
|
||||||
|
@tool
|
||||||
|
@icon("res://addons/cables/icons/producer-icon.svg")
|
||||||
|
class_name CableInputEventProducer extends CableValueProducer
|
||||||
|
|
||||||
|
enum TriggerType {
|
||||||
|
JUST_PRESSED,
|
||||||
|
JUST_RELEASED,
|
||||||
|
HELD_DOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
@export var action: StringName = ""
|
||||||
|
@export var trigger_type: TriggerType = TriggerType.JUST_PRESSED
|
||||||
|
|
||||||
|
var is_target_event_type: Callable = func(): return false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
match trigger_type:
|
||||||
|
TriggerType.JUST_PRESSED: is_target_event_type = _is_just_pressed
|
||||||
|
TriggerType.JUST_RELEASED: is_target_event_type = _is_just_released
|
||||||
|
TriggerType.HELD_DOWN: is_target_event_type = _is_held_down
|
||||||
|
_: push_warning("Unknown trigger type: %s" % str(trigger_type))
|
||||||
|
|
||||||
|
func _is_just_pressed() -> bool:
|
||||||
|
return Input.is_action_just_pressed(action)
|
||||||
|
|
||||||
|
func _is_just_released() -> bool:
|
||||||
|
return Input.is_action_just_released(action)
|
||||||
|
|
||||||
|
func _is_held_down() -> bool:
|
||||||
|
return Input.is_action_pressed(action)
|
||||||
|
|
||||||
|
func _unhandled_input(_event: InputEvent) -> void:
|
||||||
|
var is_required_action: bool = is_target_event_type.call()
|
||||||
|
if is_required_action: send_void_update()
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cmxbdgq2s2gp1
|
|
@ -0,0 +1,53 @@
|
||||||
|
## Special type of producer that sends a node reference.
|
||||||
|
##
|
||||||
|
## This class will track the lifetime of the referenced node,
|
||||||
|
## and emit a "cleared" event automatically to avoid reference-after-frees.
|
||||||
|
@tool
|
||||||
|
@icon("res://addons/cables/icons/producer-icon.svg")
|
||||||
|
class_name CableNodeValueProducer extends CableValueProducer
|
||||||
|
|
||||||
|
## Optional - the node to be broadcast on the assigned [member output] Cable.
|
||||||
|
## Can also be updated at runtime via [method send_node_value_update].
|
||||||
|
@export var node_value: Node = null
|
||||||
|
|
||||||
|
## Indicates that the [member node_value] should be broadcast on the given
|
||||||
|
## [member output] as soon as it becomes ready. This should be [code]true[/code]
|
||||||
|
## in most cases, especially when [member node_value] is set in the editor and not at runtime.
|
||||||
|
@export var notify_on_node_value_ready := true
|
||||||
|
|
||||||
|
## Indicates that the value on the given [member output] should be cleared when
|
||||||
|
## [i]this[/i] node is destroyed (NOT the [member node_value] node).
|
||||||
|
## [br][br]
|
||||||
|
## This node should generally be a child or sibling of the [member node_value] in
|
||||||
|
## most cases, so the destruction of this node will infer the destruction
|
||||||
|
## of the target.
|
||||||
|
## [br][br]
|
||||||
|
## If this inferred behavior is [i]not[/i] the case, set this value to [code]false[/code]
|
||||||
|
## and call [method send_node_value_clear] manually instead when you want
|
||||||
|
## the cable link to be removed.
|
||||||
|
@export var clear_on_destroy := true
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if node_value and notify_on_node_value_ready:
|
||||||
|
if node_value.is_node_ready():
|
||||||
|
send_node_value()
|
||||||
|
else:
|
||||||
|
node_value.ready.connect(send_node_value)
|
||||||
|
|
||||||
|
if clear_on_destroy:
|
||||||
|
node_destroyed.connect(send_node_value_clear)
|
||||||
|
|
||||||
|
## Broadcasts [param node] on the currently assigned [member output].
|
||||||
|
func send_node_value_update(node: Node) -> void:
|
||||||
|
node_value = node
|
||||||
|
send_value_update(node)
|
||||||
|
|
||||||
|
## Broadcasts the current [member node_value] value on the given [member output].
|
||||||
|
## Triggered automatically when [member notify_on_node_value_ready] is [code]true[/code].
|
||||||
|
func send_node_value() -> void:
|
||||||
|
send_node_value_update(node_value)
|
||||||
|
|
||||||
|
## Broadcasts [code]null[/code] on the currently assigned [member output].
|
||||||
|
## Triggered automatically when [member clear_on_destroy] is [code]true[/code].
|
||||||
|
func send_node_value_clear() -> void:
|
||||||
|
send_node_value_update(null)
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bx76g3c6nnl1d
|
|
@ -0,0 +1,22 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 deep Entertainment
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
## Class inheriting HttpRouter for handling file serving requests
|
||||||
|
##
|
||||||
|
## NOTE: This class mainly handles behind the scenes stuff.
|
||||||
|
class_name HttpFileRouter
|
||||||
|
extends HttpRouter
|
||||||
|
|
||||||
|
## Full path to the folder which will be exposed to web
|
||||||
|
var path: String = ""
|
||||||
|
|
||||||
|
## Relative path to the index page, which will be served when a request is made to "/" (server root)
|
||||||
|
var index_page: String = "index.html"
|
||||||
|
|
||||||
|
## Relative path to the fallback page which will be served if the requested file was not found
|
||||||
|
var fallback_page: String = ""
|
||||||
|
|
||||||
|
## An ordered list of extensions that will be checked
|
||||||
|
## if no file extension is provided by the request
|
||||||
|
var extensions: PackedStringArray = ["html"]
|
||||||
|
|
||||||
|
## A list of extensions that will be excluded if requested
|
||||||
|
var exclude_extensions: PackedStringArray = []
|
||||||
|
|
||||||
|
## Creates an HttpFileRouter intance
|
||||||
|
## [br]
|
||||||
|
## [br][param path] - Full path to the folder which will be exposed to web.
|
||||||
|
## [br][param options] - Optional Dictionary of options which can be configured:
|
||||||
|
## [br] - [param fallback_page]: Full path to the fallback page which will be served if the requested file was not found
|
||||||
|
## [br] - [param extensions]: A list of extensions that will be checked if no file extension is provided by the request
|
||||||
|
## [br] - [param exclude_extensions]: A list of extensions that will be excluded if requested
|
||||||
|
func _init(
|
||||||
|
path: String,
|
||||||
|
options: Dictionary = {
|
||||||
|
index_page = index_page,
|
||||||
|
fallback_page = fallback_page,
|
||||||
|
extensions = extensions,
|
||||||
|
exclude_extensions = exclude_extensions,
|
||||||
|
}
|
||||||
|
) -> void:
|
||||||
|
self.path = path
|
||||||
|
self.index_page = options.get("index_page", "")
|
||||||
|
self.fallback_page = options.get("fallback_page", "")
|
||||||
|
self.extensions = options.get("extensions", [])
|
||||||
|
self.exclude_extensions = options.get("exclude_extensions", [])
|
||||||
|
|
||||||
|
## Handle a GET request
|
||||||
|
## [br]
|
||||||
|
## [br][param request] - The request from the client
|
||||||
|
## [br][param response] - The response to send to the clinet
|
||||||
|
func handle_get(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
var serving_path: String = path + request.path
|
||||||
|
var file_exists: bool = _file_exists(serving_path)
|
||||||
|
|
||||||
|
if request.path == "/" and not file_exists:
|
||||||
|
if index_page.length() > 0:
|
||||||
|
serving_path = path + "/" + index_page
|
||||||
|
file_exists = _file_exists(serving_path)
|
||||||
|
|
||||||
|
if request.path.get_extension() == "" and not file_exists:
|
||||||
|
for extension in extensions:
|
||||||
|
serving_path = path + request.path + "." + extension
|
||||||
|
file_exists = _file_exists(serving_path)
|
||||||
|
if file_exists:
|
||||||
|
break
|
||||||
|
|
||||||
|
# GDScript must be excluded, unless it is used as a preprocessor (php-like)
|
||||||
|
if (file_exists and not serving_path.get_extension() in ["gd"] + Array(exclude_extensions)):
|
||||||
|
response.send_raw(
|
||||||
|
200,
|
||||||
|
_serve_file(serving_path),
|
||||||
|
_get_mime(serving_path.get_extension())
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if fallback_page.length() > 0:
|
||||||
|
serving_path = path + "/" + fallback_page
|
||||||
|
response.send_raw(200 if index_page == fallback_page else 404, _serve_file(serving_path), _get_mime(fallback_page.get_extension()))
|
||||||
|
else:
|
||||||
|
response.send_raw(404)
|
||||||
|
|
||||||
|
# Reads a file as text
|
||||||
|
#
|
||||||
|
# #### Parameters
|
||||||
|
# - file_path: Full path to the file
|
||||||
|
func _serve_file(file_path: String) -> PackedByteArray:
|
||||||
|
var content: PackedByteArray = []
|
||||||
|
var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
|
||||||
|
var error = file.get_open_error()
|
||||||
|
if error:
|
||||||
|
content = ("Couldn't serve file, ERROR = %s" % error).to_ascii_buffer()
|
||||||
|
else:
|
||||||
|
content = file.get_buffer(file.get_length())
|
||||||
|
file.close()
|
||||||
|
return content
|
||||||
|
|
||||||
|
# Check if a file exists
|
||||||
|
#
|
||||||
|
# #### Parameters
|
||||||
|
# - file_path: Full path to the file
|
||||||
|
func _file_exists(file_path: String) -> bool:
|
||||||
|
return FileAccess.file_exists(file_path)
|
||||||
|
|
||||||
|
# Get the full MIME type of a file from its extension
|
||||||
|
#
|
||||||
|
# #### Parameters
|
||||||
|
# - file_extension: Extension of the file to be served
|
||||||
|
func _get_mime(file_extension: String) -> String:
|
||||||
|
var type: String = "application"
|
||||||
|
var subtype : String = "octet-stream"
|
||||||
|
match file_extension:
|
||||||
|
# Web files
|
||||||
|
"css","html","csv","js","mjs":
|
||||||
|
type = "text"
|
||||||
|
subtype = "javascript" if file_extension in ["js","mjs"] else file_extension
|
||||||
|
"php":
|
||||||
|
subtype = "x-httpd-php"
|
||||||
|
"ttf","woff","woff2":
|
||||||
|
type = "font"
|
||||||
|
subtype = file_extension
|
||||||
|
# Image
|
||||||
|
"png","bmp","gif","png","webp":
|
||||||
|
type = "image"
|
||||||
|
subtype = file_extension
|
||||||
|
"jpeg","jpg":
|
||||||
|
type = "image"
|
||||||
|
subtype = "jpg"
|
||||||
|
"tiff", "tif":
|
||||||
|
type = "image"
|
||||||
|
subtype = "jpg"
|
||||||
|
"svg":
|
||||||
|
type = "image"
|
||||||
|
subtype = "svg+xml"
|
||||||
|
"ico":
|
||||||
|
type = "image"
|
||||||
|
subtype = "vnd.microsoft.icon"
|
||||||
|
# Documents
|
||||||
|
"doc":
|
||||||
|
subtype = "msword"
|
||||||
|
"docx":
|
||||||
|
subtype = "vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
"7z":
|
||||||
|
subtype = "x-7x-compressed"
|
||||||
|
"gz":
|
||||||
|
subtype = "gzip"
|
||||||
|
"tar":
|
||||||
|
subtype = "application/x-tar"
|
||||||
|
"json","pdf","zip":
|
||||||
|
subtype = file_extension
|
||||||
|
"txt":
|
||||||
|
type = "text"
|
||||||
|
subtype = "plain"
|
||||||
|
"ppt":
|
||||||
|
subtype = "vnd.ms-powerpoint"
|
||||||
|
# Audio
|
||||||
|
"midi","mp3","wav":
|
||||||
|
type = "audio"
|
||||||
|
subtype = file_extension
|
||||||
|
"mp4","mpeg","webm":
|
||||||
|
type = "audio"
|
||||||
|
subtype = file_extension
|
||||||
|
"oga","ogg":
|
||||||
|
type = "audio"
|
||||||
|
subtype = "ogg"
|
||||||
|
"mpkg":
|
||||||
|
subtype = "vnd.apple.installer+xml"
|
||||||
|
# Video
|
||||||
|
"ogv":
|
||||||
|
type = "video"
|
||||||
|
subtype = "ogg"
|
||||||
|
"avi":
|
||||||
|
type = "video"
|
||||||
|
subtype = "x-msvideo"
|
||||||
|
"ogx":
|
||||||
|
subtype = "ogg"
|
||||||
|
return type + "/" + subtype
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cm340ld75sy8v
|
|
@ -0,0 +1,53 @@
|
||||||
|
## An HTTP request received by the server
|
||||||
|
class_name HttpRequest
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## A dictionary of the headers of the request
|
||||||
|
var headers: Dictionary
|
||||||
|
|
||||||
|
## The received raw body
|
||||||
|
var body: String
|
||||||
|
|
||||||
|
## A match object of the regular expression that matches the path
|
||||||
|
var query_match: RegExMatch
|
||||||
|
|
||||||
|
## The path that matches the router path
|
||||||
|
var path: String
|
||||||
|
|
||||||
|
## The method
|
||||||
|
var method: String
|
||||||
|
|
||||||
|
## A dictionary of request (aka. routing) parameters
|
||||||
|
var parameters: Dictionary
|
||||||
|
|
||||||
|
## A dictionary of request query parameters
|
||||||
|
var query: Dictionary
|
||||||
|
|
||||||
|
## Returns the body object based on the raw body and the content type of the request
|
||||||
|
func get_body_parsed() -> Variant:
|
||||||
|
var content_type: String = ""
|
||||||
|
|
||||||
|
if(headers.has("content-type")):
|
||||||
|
content_type = headers["content-type"]
|
||||||
|
elif(headers.has("Content-Type")):
|
||||||
|
content_type = headers["Content-Type"]
|
||||||
|
|
||||||
|
if(content_type == "application/json"):
|
||||||
|
return JSON.parse_string(body)
|
||||||
|
|
||||||
|
if(content_type == "application/x-www-form-urlencoded"):
|
||||||
|
var data = {}
|
||||||
|
|
||||||
|
for body_part in body.split("&"):
|
||||||
|
var key_and_value = body_part.split("=")
|
||||||
|
data[key_and_value[0]] = key_and_value[1]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Not supported contenty type parsing... for now
|
||||||
|
return null
|
||||||
|
|
||||||
|
## Override `str()` method, automatically called in `print()` function
|
||||||
|
func _to_string() -> String:
|
||||||
|
return JSON.stringify({headers=headers, method=method, path=path})
|
|
@ -0,0 +1 @@
|
||||||
|
uid://3wm0jxm2ena7
|
|
@ -0,0 +1,174 @@
|
||||||
|
## A response object useful to send out responses
|
||||||
|
class_name HttpResponse
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## The client currently talking to the server
|
||||||
|
var client: StreamPeer
|
||||||
|
|
||||||
|
## The server identifier to use on responses [GodotTPD]
|
||||||
|
var server_identifier: String = "GodotTPD"
|
||||||
|
|
||||||
|
## A dictionary of headers
|
||||||
|
## [br] Headers can be set using the `set(name, value)` function
|
||||||
|
var headers: Dictionary = {}
|
||||||
|
|
||||||
|
## An array of cookies
|
||||||
|
## [br] Cookies can be set using the `cookie(name, value, options)` function
|
||||||
|
## [br] Cookies will be automatically sent via "Set-Cookie" headers to clients
|
||||||
|
var cookies: Array = []
|
||||||
|
|
||||||
|
## Origins allowed to call this resource
|
||||||
|
var access_control_origin = "*"
|
||||||
|
|
||||||
|
## Comma separed methods for the access control
|
||||||
|
var access_control_allowed_methods = "POST, GET, OPTIONS"
|
||||||
|
|
||||||
|
## Comma separed headers for the access control
|
||||||
|
var access_control_allowed_headers = "content-type"
|
||||||
|
|
||||||
|
## Send out a raw (Bytes) response to the client
|
||||||
|
## [br] Useful to send files faster or raw data which will be converted by the client
|
||||||
|
## [br][param status] - The HTTP Status code to send
|
||||||
|
## [br][param data] - The body data to send
|
||||||
|
## [br][param content_type] - The type of content to send.
|
||||||
|
func send_raw(status_code: int, data: PackedByteArray = PackedByteArray([]), content_type: String = "application/octet-stream") -> void:
|
||||||
|
client.put_data(("HTTP/1.1 %d %s\r\n" % [status_code, _match_status_code(status_code)]).to_ascii_buffer())
|
||||||
|
client.put_data(("Server: %s\r\n" % server_identifier).to_ascii_buffer())
|
||||||
|
for header in headers.keys():
|
||||||
|
client.put_data(("%s: %s\r\n" % [header, headers[header]]).to_ascii_buffer())
|
||||||
|
for cookie in cookies:
|
||||||
|
client.put_data(("Set-Cookie: %s\r\n" % cookie).to_ascii_buffer())
|
||||||
|
client.put_data(("Content-Length: %d\r\n" % data.size()).to_ascii_buffer())
|
||||||
|
client.put_data("Connection: close\r\n".to_ascii_buffer())
|
||||||
|
client.put_data(("Access-Control-Allow-Origin: %s\r\n" % access_control_origin).to_ascii_buffer())
|
||||||
|
client.put_data(("Access-Control-Allow-Methods: %s\r\n" % access_control_allowed_methods).to_ascii_buffer())
|
||||||
|
client.put_data(("Access-Control-Allow-Headers: %s\r\n" % access_control_allowed_headers).to_ascii_buffer())
|
||||||
|
client.put_data(("Content-Type: %s\r\n\r\n" % content_type).to_ascii_buffer())
|
||||||
|
client.put_data(data)
|
||||||
|
|
||||||
|
## Send out a response to the client
|
||||||
|
## [br]
|
||||||
|
## [br][param status_code] - The HTTP status code to send
|
||||||
|
## [br][param data] - The body to send
|
||||||
|
## [br][param content_type] - The type of the content to send
|
||||||
|
func send(status_code: int, data: String = "", content_type = "text/html") -> void:
|
||||||
|
send_raw(status_code, data.to_ascii_buffer(), content_type)
|
||||||
|
|
||||||
|
## Send out a JSON response to the client
|
||||||
|
## [br] This function will internally call the [method send]
|
||||||
|
## [br]
|
||||||
|
## [br][param status_code] - The HTTP status code to send
|
||||||
|
## [br][param data] - The body to send
|
||||||
|
func json(status_code: int, data) -> void:
|
||||||
|
send(status_code, JSON.stringify(data), "application/json")
|
||||||
|
|
||||||
|
|
||||||
|
## Sets the response’s header "field" to "value"
|
||||||
|
## [br]
|
||||||
|
## [br][param field] - The name of the header. i.e. [code]Accept-Type[/code]
|
||||||
|
## [br][param value] - The value of this header. i.e. [code]application/json[/code]
|
||||||
|
func set(field: StringName, value: Variant) -> void:
|
||||||
|
headers[field] = value
|
||||||
|
|
||||||
|
|
||||||
|
## Sets cookie "name" to "value"
|
||||||
|
## [br]
|
||||||
|
## [br][param name] - The name of the cookie. i.e. [code]user-id[/code]
|
||||||
|
## [br][param value] - The value of this cookie. i.e. [code]abcdef[/code]
|
||||||
|
## [br][param options] - A Dictionary of [url=https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes]cookie attributes[/url]
|
||||||
|
## for this specific cokkie in the [code]{ "secure" : "true"}[/code] format.
|
||||||
|
func cookie(name: String, value: String, options: Dictionary = {}) -> void:
|
||||||
|
var cookie: String = name+"="+value
|
||||||
|
if options.has("domain"): cookie+="; Domain="+options["domain"]
|
||||||
|
if options.has("max-age"): cookie+="; Max-Age="+options["max-age"]
|
||||||
|
if options.has("expires"): cookie+="; Expires="+options["expires"]
|
||||||
|
if options.has("path"): cookie+="; Path="+options["path"]
|
||||||
|
if options.has("secure"): cookie+="; Secure="+options["secure"]
|
||||||
|
if options.has("httpOnly"): cookie+="; HttpOnly="+options["httpOnly"]
|
||||||
|
if options.has("sameSite"):
|
||||||
|
match (options["sameSite"]):
|
||||||
|
true: cookie += "; SameSite=Strict"
|
||||||
|
"lax": cookie += "; SameSite=Lax"
|
||||||
|
"strict": cookie += "; SameSite=Strict"
|
||||||
|
"none": cookie += "; SameSite=None"
|
||||||
|
_: pass
|
||||||
|
cookies.append(cookie)
|
||||||
|
|
||||||
|
|
||||||
|
## Automatically matches a "status_code" to an RFC 7231 compliant "status_text"
|
||||||
|
## [br]
|
||||||
|
## [br][param code] - The HTTP Status code to be matched
|
||||||
|
## [br]Returns: the matched [code]status_text[/code]
|
||||||
|
func _match_status_code(code: int) -> String:
|
||||||
|
var text: String = "OK"
|
||||||
|
match(code):
|
||||||
|
# 1xx - Informational Responses
|
||||||
|
100: text="Continue"
|
||||||
|
101: text="Switching protocols"
|
||||||
|
102: text="Processing"
|
||||||
|
103: text="Early Hints"
|
||||||
|
# 2xx - Successful Responses
|
||||||
|
200: text="OK"
|
||||||
|
201: text="Created"
|
||||||
|
202: text="Accepted"
|
||||||
|
203: text="Non-Authoritative Information"
|
||||||
|
204: text="No Content"
|
||||||
|
205: text="Reset Content"
|
||||||
|
206: text="Partial Content"
|
||||||
|
207: text="Multi-Status"
|
||||||
|
208: text="Already Reported"
|
||||||
|
226: text="IM Used"
|
||||||
|
# 3xx - Redirection Messages
|
||||||
|
300: text="Multiple Choices"
|
||||||
|
301: text="Moved Permanently"
|
||||||
|
302: text="Found (Previously 'Moved Temporarily')"
|
||||||
|
303: text="See Other"
|
||||||
|
304: text="Not Modified"
|
||||||
|
305: text="Use Proxy"
|
||||||
|
306: text="Switch Proxy"
|
||||||
|
307: text="Temporary Redirect"
|
||||||
|
308: text="Permanent Redirect"
|
||||||
|
# 4xx - Client Error Responses
|
||||||
|
400: text="Bad Request"
|
||||||
|
401: text="Unauthorized"
|
||||||
|
402: text="Payment Required"
|
||||||
|
403: text="Forbidden"
|
||||||
|
404: text="Not Found"
|
||||||
|
405: text="Method Not Allowed"
|
||||||
|
406: text="Not Acceptable"
|
||||||
|
407: text="Proxy Authentication Required"
|
||||||
|
408: text="Request Timeout"
|
||||||
|
409: text="Conflict"
|
||||||
|
410: text="Gone"
|
||||||
|
411: text="Length Required"
|
||||||
|
412: text="Precondition Failed"
|
||||||
|
413: text="Payload Too Large"
|
||||||
|
414: text="URI Too Long"
|
||||||
|
415: text="Unsupported Media Type"
|
||||||
|
416: text="Range Not Satisfiable"
|
||||||
|
417: text="Expectation Failed"
|
||||||
|
418: text="I'm a Teapot"
|
||||||
|
421: text="Misdirected Request"
|
||||||
|
422: text="Unprocessable Entity"
|
||||||
|
423: text="Locked"
|
||||||
|
424: text="Failed Dependency"
|
||||||
|
425: text="Too Early"
|
||||||
|
426: text="Upgrade Required"
|
||||||
|
428: text="Precondition Required"
|
||||||
|
429: text="Too Many Requests"
|
||||||
|
431: text="Request Header Fields Too Large"
|
||||||
|
451: text="Unavailable For Legal Reasons"
|
||||||
|
# 5xx - Server Error Responses
|
||||||
|
500: text="Internal Server Error"
|
||||||
|
501: text="Not Implemented"
|
||||||
|
502: text="Bad Gateway"
|
||||||
|
503: text="Service Unavailable"
|
||||||
|
504: text="Gateway Timeout"
|
||||||
|
505: text="HTTP Version Not Supported"
|
||||||
|
506: text="Variant Also Negotiates"
|
||||||
|
507: text="Insufficient Storage"
|
||||||
|
508: text="Loop Detected"
|
||||||
|
510: text="Not Extended"
|
||||||
|
511: text="Network Authentication Required"
|
||||||
|
return text
|
|
@ -0,0 +1 @@
|
||||||
|
uid://01jj3dfsmp1f
|
|
@ -0,0 +1,77 @@
|
||||||
|
## A base class for all HTTP routers
|
||||||
|
##
|
||||||
|
## This router handles all the requests that the client sends to the server.
|
||||||
|
## [br]NOTE: This class is meant to be expanded upon instead of used directly.
|
||||||
|
## [br]Usage:
|
||||||
|
## [codeblock]
|
||||||
|
## class_name MyCustomRouter
|
||||||
|
## extends HttpRouter
|
||||||
|
##
|
||||||
|
## func handle_get(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
## response.send(200, "Hello World")
|
||||||
|
## [/codeblock]
|
||||||
|
class_name HttpRouter
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## Handle a GET request
|
||||||
|
## [br]
|
||||||
|
## [br][param request] - The request from the client
|
||||||
|
## [br][param response] - The node to send the response back to the client
|
||||||
|
@warning_ignore("unused_parameter")
|
||||||
|
func handle_get(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
response.send(405, "GET not allowed")
|
||||||
|
|
||||||
|
|
||||||
|
## Handle a POST request
|
||||||
|
## [br]
|
||||||
|
## [br][param request] - The request from the client
|
||||||
|
## [br][param response] - The node to send the response back to the client
|
||||||
|
@warning_ignore("unused_parameter")
|
||||||
|
func handle_post(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
response.send(405, "POST not allowed")
|
||||||
|
|
||||||
|
|
||||||
|
## Handle a HEAD request
|
||||||
|
## [br]
|
||||||
|
## [br][param request] - The request from the client
|
||||||
|
## [br][param response] - The node to send the response back to the client
|
||||||
|
@warning_ignore("unused_parameter")
|
||||||
|
func handle_head(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
response.send(405, "HEAD not allowed")
|
||||||
|
|
||||||
|
|
||||||
|
## Handle a PUT request
|
||||||
|
## [br]
|
||||||
|
## [br][param request] - The request from the client
|
||||||
|
## [br][param response] - The node to send the response back to the client
|
||||||
|
@warning_ignore("unused_parameter")
|
||||||
|
func handle_put(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
response.send(405, "PUT not allowed")
|
||||||
|
|
||||||
|
|
||||||
|
## Handle a PATCH request
|
||||||
|
## [br]
|
||||||
|
## [br][param request] - The request from the client
|
||||||
|
## [br][param response] - The node to send the response back to the client
|
||||||
|
@warning_ignore("unused_parameter")
|
||||||
|
func handle_patch(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
response.send(405, "PATCH not allowed")
|
||||||
|
|
||||||
|
|
||||||
|
## Handle a DELETE request
|
||||||
|
## [br]
|
||||||
|
## [br][param request] - The request from the client
|
||||||
|
## [br][param response] - The node to send the response back to the client
|
||||||
|
@warning_ignore("unused_parameter")
|
||||||
|
func handle_delete(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
response.send(405, "DELETE not allowed")
|
||||||
|
|
||||||
|
|
||||||
|
## Handle an OPTIONS request
|
||||||
|
## [br]
|
||||||
|
## [br][param request] - The request from the client
|
||||||
|
## [br][param response] - The node to send the response back to the client
|
||||||
|
@warning_ignore("unused_parameter")
|
||||||
|
func handle_options(request: HttpRequest, response: HttpResponse) -> void:
|
||||||
|
response.send(405, "OPTIONS not allowed")
|
|
@ -0,0 +1 @@
|
||||||
|
uid://1oi0xg0ly4pi
|
|
@ -0,0 +1,311 @@
|
||||||
|
## A routable HTTP server for Godot
|
||||||
|
##
|
||||||
|
## Provides a web server with routes for specific endpoints
|
||||||
|
## [br]Example usage:
|
||||||
|
## [codeblock]
|
||||||
|
## var server := HttpServer.new()
|
||||||
|
## server.register_router("/", MyExampleRouter.new())
|
||||||
|
## add_child(server)
|
||||||
|
## server.start()
|
||||||
|
## [/codeblock]
|
||||||
|
|
||||||
|
class_name HttpServer
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
## The ip address to bind the server to. Use * for all IP addresses [*]
|
||||||
|
@export var bind_address: String = "*"
|
||||||
|
|
||||||
|
## The port to bind the server to. [8080]
|
||||||
|
@export var port: int = 8080
|
||||||
|
|
||||||
|
## The server identifier to use when responding to requests [GodotTPD]
|
||||||
|
@export var server_identifier: String = "GodotTPD"
|
||||||
|
|
||||||
|
# If `HttpRequest`s and `HttpResponse`s should be logged
|
||||||
|
@export var _logging: bool = false
|
||||||
|
|
||||||
|
# The TCP server instance used
|
||||||
|
var _server: TCPServer
|
||||||
|
|
||||||
|
# An array of StraemPeerTCP objects who are currently talking to the server
|
||||||
|
var _clients: Array
|
||||||
|
|
||||||
|
# A list of HttpRequest routers who could handle a request
|
||||||
|
var _routers: Array = []
|
||||||
|
|
||||||
|
# A regex identifiying the method line
|
||||||
|
var _method_regex: RegEx = RegEx.new()
|
||||||
|
|
||||||
|
# A regex for header lines
|
||||||
|
var _header_regex: RegEx = RegEx.new()
|
||||||
|
|
||||||
|
# The base path used in a project to serve files
|
||||||
|
var _local_base_path: String = "res://src"
|
||||||
|
|
||||||
|
# list of host allowed to call the server
|
||||||
|
var _allowed_origins: PackedStringArray = []
|
||||||
|
|
||||||
|
# Comma separed methods for the access control
|
||||||
|
var _access_control_allowed_methods = "POST, GET, OPTIONS"
|
||||||
|
|
||||||
|
# Comma separed headers for the access control
|
||||||
|
var _access_control_allowed_headers = "content-type"
|
||||||
|
|
||||||
|
# Compile the required regex
|
||||||
|
func _init(_logging: bool = false):
|
||||||
|
self._logging = _logging
|
||||||
|
set_process(false)
|
||||||
|
_method_regex.compile("^(?<method>GET|POST|HEAD|PUT|PATCH|DELETE|OPTIONS) (?<path>[^ ]+) HTTP/1.1$")
|
||||||
|
_header_regex.compile("^(?<key>[\\w-]+): (?<value>(.*))$")
|
||||||
|
|
||||||
|
# Print a debug message in console, if the debug mode is enabled
|
||||||
|
#
|
||||||
|
# #### Parameters
|
||||||
|
# - message: The message to be printed (only in debug mode)
|
||||||
|
func _print_debug(message: String) -> void:
|
||||||
|
var time = Time.get_datetime_dict_from_system()
|
||||||
|
var time_return = "%02d-%02d-%02d %02d:%02d:%02d" % [time.year, time.month, time.day, time.hour, time.minute, time.second]
|
||||||
|
print_debug("[SERVER] ",time_return," >> ", message)
|
||||||
|
|
||||||
|
## Register a new router to handle a specific path
|
||||||
|
## [br]
|
||||||
|
## [br][param path] - The path the router will handle.
|
||||||
|
## Supports a regular expression and the group matches will be available in HttpRequest.query_match.
|
||||||
|
## [br][param router] - The router which will handle the request
|
||||||
|
func register_router(path: String, router: HttpRouter):
|
||||||
|
var path_regex = RegEx.new()
|
||||||
|
var params: Array = []
|
||||||
|
if path.left(0) == "^":
|
||||||
|
path_regex.compile(path)
|
||||||
|
else:
|
||||||
|
var regexp: Array = _path_to_regexp(path, router is HttpFileRouter)
|
||||||
|
path_regex.compile(regexp[0])
|
||||||
|
params = regexp[1]
|
||||||
|
_routers.push_back({
|
||||||
|
"path": path_regex,
|
||||||
|
"params": params,
|
||||||
|
"router": router
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
## Handle possibly incoming requests
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _server:
|
||||||
|
while _server.is_connection_available():
|
||||||
|
var new_client = _server.take_connection()
|
||||||
|
if new_client:
|
||||||
|
self._clients.append(new_client)
|
||||||
|
for client in self._clients:
|
||||||
|
client.poll()
|
||||||
|
if client.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
||||||
|
var bytes = client.get_available_bytes()
|
||||||
|
if bytes > 0:
|
||||||
|
var request_string = client.get_utf8_string(bytes)
|
||||||
|
self._handle_request(client, request_string)
|
||||||
|
_remove_disconnected_clients()
|
||||||
|
|
||||||
|
|
||||||
|
func _remove_disconnected_clients():
|
||||||
|
var valid_statuses = [StreamPeerTCP.STATUS_CONNECTED, StreamPeerTCP.STATUS_CONNECTING]
|
||||||
|
self._clients = self._clients.filter(
|
||||||
|
func(c: StreamPeerTCP): return valid_statuses.has(c.get_status())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Start the server
|
||||||
|
func start():
|
||||||
|
set_process(true)
|
||||||
|
self._server = TCPServer.new()
|
||||||
|
var err: int = self._server.listen(self.port, self.bind_address)
|
||||||
|
match err:
|
||||||
|
22:
|
||||||
|
_print_debug("Could not bind to port %d, already in use" % [self.port])
|
||||||
|
stop()
|
||||||
|
_:
|
||||||
|
_print_debug("HTTP Server listening on http://%s:%s" % [self.bind_address, self.port])
|
||||||
|
|
||||||
|
|
||||||
|
## Stop the server and disconnect all clients
|
||||||
|
func stop():
|
||||||
|
for client in self._clients:
|
||||||
|
client.disconnect_from_host()
|
||||||
|
self._clients.clear()
|
||||||
|
self._server.stop()
|
||||||
|
set_process(false)
|
||||||
|
_print_debug("Server stopped.")
|
||||||
|
|
||||||
|
|
||||||
|
# Interpret a request string and perform the request
|
||||||
|
#
|
||||||
|
# #### Parameters
|
||||||
|
# - client: The client that send the request
|
||||||
|
# - request: The received request as a String
|
||||||
|
func _handle_request(client: StreamPeer, request_string: String):
|
||||||
|
var request = HttpRequest.new()
|
||||||
|
for line in request_string.split("\r\n"):
|
||||||
|
var method_matches = _method_regex.search(line)
|
||||||
|
var header_matches = _header_regex.search(line)
|
||||||
|
if method_matches:
|
||||||
|
request.method = method_matches.get_string("method")
|
||||||
|
var request_path: String = method_matches.get_string("path")
|
||||||
|
# Check if request_path contains "?" character, could be a query parameter
|
||||||
|
if not "?" in request_path:
|
||||||
|
request.path = request_path
|
||||||
|
else:
|
||||||
|
var path_query: PackedStringArray = request_path.split("?")
|
||||||
|
request.path = path_query[0]
|
||||||
|
request.query = _extract_query_params(path_query[1])
|
||||||
|
request.headers = {}
|
||||||
|
request.body = ""
|
||||||
|
elif header_matches:
|
||||||
|
request.headers[header_matches.get_string("key")] = \
|
||||||
|
header_matches.get_string("value")
|
||||||
|
else:
|
||||||
|
request.body += line
|
||||||
|
self._perform_current_request(client, request)
|
||||||
|
|
||||||
|
|
||||||
|
# Handle a specific request and send it to a router
|
||||||
|
# If no router matches, send a 404
|
||||||
|
#
|
||||||
|
# #### Parameters
|
||||||
|
# - client: The client that send the request
|
||||||
|
# - request_info: A dictionary with information about the request
|
||||||
|
# - method: The method of the request (e.g. GET, POST)
|
||||||
|
# - path: The requested path
|
||||||
|
# - headers: A dictionary of headers of the request
|
||||||
|
# - body: The raw body of the request
|
||||||
|
func _perform_current_request(client: StreamPeer, request: HttpRequest):
|
||||||
|
_print_debug("HTTP Request: " + str(request))
|
||||||
|
var found = false
|
||||||
|
var is_allowed_origin = false
|
||||||
|
var response = HttpResponse.new()
|
||||||
|
var fetch_mode = ""
|
||||||
|
var origin = ""
|
||||||
|
response.client = client
|
||||||
|
response.server_identifier = server_identifier
|
||||||
|
|
||||||
|
if request.headers.has("Sec-Fetch-Mode"):
|
||||||
|
fetch_mode = request.headers["Sec-Fetch-Mode"]
|
||||||
|
elif request.headers.has("sec-fetch-mode"):
|
||||||
|
fetch_mode = request.headers["sec-fetch-mode"]
|
||||||
|
|
||||||
|
if request.headers.has("Origin"):
|
||||||
|
origin = request.headers["Origin"]
|
||||||
|
elif request.headers.has("origin"):
|
||||||
|
origin = request.headers["origin"]
|
||||||
|
|
||||||
|
if _allowed_origins.has(origin):
|
||||||
|
is_allowed_origin = true
|
||||||
|
response.access_control_origin = origin
|
||||||
|
|
||||||
|
response.access_control_allowed_methods = _access_control_allowed_methods
|
||||||
|
response.access_control_allowed_headers = _access_control_allowed_headers
|
||||||
|
|
||||||
|
for router in self._routers:
|
||||||
|
var matches = router.path.search(request.path)
|
||||||
|
if matches:
|
||||||
|
request.query_match = matches
|
||||||
|
if request.query_match.get_string("subpath"):
|
||||||
|
request.path = request.query_match.get_string("subpath")
|
||||||
|
if router.params.size() > 0:
|
||||||
|
for parameter in router.params:
|
||||||
|
request.parameters[parameter] = request.query_match.get_string(parameter)
|
||||||
|
match request.method:
|
||||||
|
"GET":
|
||||||
|
found = true
|
||||||
|
router.router.handle_get(request, response)
|
||||||
|
"POST":
|
||||||
|
found = true
|
||||||
|
router.router.handle_post(request, response)
|
||||||
|
"HEAD":
|
||||||
|
found = true
|
||||||
|
router.router.handle_head(request, response)
|
||||||
|
"PUT":
|
||||||
|
found = true
|
||||||
|
router.router.handle_put(request, response)
|
||||||
|
"PATCH":
|
||||||
|
found = true
|
||||||
|
router.router.handle_patch(request, response)
|
||||||
|
"DELETE":
|
||||||
|
found = true
|
||||||
|
router.router.handle_delete(request, response)
|
||||||
|
"OPTIONS":
|
||||||
|
if _allowed_origins.size() > 0 && fetch_mode == "cors":
|
||||||
|
if is_allowed_origin:
|
||||||
|
response.send(204)
|
||||||
|
else:
|
||||||
|
response.send(400, "%s is not present in the allowed origins" % origin)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
found = true
|
||||||
|
router.router.handle_options(request, response)
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
response.send(404, "Not found")
|
||||||
|
|
||||||
|
|
||||||
|
# Converts a URL path to @regexp RegExp, providing a mechanism to fetch groups from the expression
|
||||||
|
# indexing each parameter by name in the @params array
|
||||||
|
#
|
||||||
|
# #### Parameters
|
||||||
|
# - path: The path of the HttpRequest
|
||||||
|
# - should_match_subfolder: (dafult [false]) if subfolders should be matched and grouped,
|
||||||
|
# used for HttpFileRouter
|
||||||
|
#
|
||||||
|
# Returns: A 2D array, containing a @regexp String and Dictionary of @params
|
||||||
|
# [0] = @regexp --> the output expression as a String, to be compiled in RegExp
|
||||||
|
# [1] = @params --> an Array of parameters, indexed by names
|
||||||
|
# ex. "/user/:id" --> "^/user/(?<id>([^/#?]+?))[/#?]?$"
|
||||||
|
func _path_to_regexp(path: String, should_match_subfolders: bool = false) -> Array:
|
||||||
|
var regexp: String = "^"
|
||||||
|
var params: Array = []
|
||||||
|
var fragments: Array = path.split("/")
|
||||||
|
fragments.pop_front()
|
||||||
|
for fragment in fragments:
|
||||||
|
if fragment.left(1) == ":":
|
||||||
|
fragment = fragment.lstrip(":")
|
||||||
|
regexp += "/(?<%s>([^/#?]+?))" % fragment
|
||||||
|
params.append(fragment)
|
||||||
|
else:
|
||||||
|
regexp += "/" + fragment
|
||||||
|
regexp += "[/#?]?$" if not should_match_subfolders else "(?<subpath>$|/.*)"
|
||||||
|
return [regexp, params]
|
||||||
|
|
||||||
|
|
||||||
|
## Enable CORS (Cross-origin resource sharing) which only allows requests from the specified servers
|
||||||
|
## [br]
|
||||||
|
## [br][param allowed_origins] - The origins that are allowed to be accessed from this server
|
||||||
|
## [br][param access_control_allowed_methods] - The methods that are allowed to be used
|
||||||
|
## [br][param access_control_allowed_headers] - The headers that are allowed to be sent
|
||||||
|
func enable_cors(allowed_origins: PackedStringArray, access_control_allowed_methods : String = "POST, GET, OPTIONS", access_control_allowed_headers : String = "content-type"):
|
||||||
|
_allowed_origins = allowed_origins
|
||||||
|
_access_control_allowed_methods = access_control_allowed_methods
|
||||||
|
_access_control_allowed_headers = access_control_allowed_headers
|
||||||
|
|
||||||
|
|
||||||
|
# Extracts query parameters from a String query,
|
||||||
|
# building a Query Dictionary of param:value pairs
|
||||||
|
#
|
||||||
|
# #### Parameters
|
||||||
|
# - query_string: the query string, extracted from the HttpRequest.path
|
||||||
|
#
|
||||||
|
# Returns: A Dictionary of param:value pairs
|
||||||
|
func _extract_query_params(query_string: String) -> Dictionary:
|
||||||
|
var query: Dictionary = {}
|
||||||
|
if query_string == "":
|
||||||
|
return query
|
||||||
|
var parameters: Array = query_string.split("&")
|
||||||
|
for param in parameters:
|
||||||
|
if not "=" in param:
|
||||||
|
continue
|
||||||
|
var kv : Array = param.split("=")
|
||||||
|
var value: String = kv[1]
|
||||||
|
if value.is_valid_int():
|
||||||
|
query[kv[0]] = value.to_int()
|
||||||
|
elif value.is_valid_float():
|
||||||
|
query[kv[0]] = value.to_float()
|
||||||
|
else:
|
||||||
|
query[kv[0]] = value
|
||||||
|
return query
|
|
@ -0,0 +1 @@
|
||||||
|
uid://hq1yuo5k5f1
|
|
@ -0,0 +1,7 @@
|
||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="godottpd"
|
||||||
|
description="Web server for Godot"
|
||||||
|
author="deep Entertainment"
|
||||||
|
version="0.1.0"
|
||||||
|
script="plugin.gd"
|
|
@ -0,0 +1,4 @@
|
||||||
|
# A routable HTTP server for Godot
|
||||||
|
# We don't really need to initialize anything here
|
||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cbmhp4aolepl3
|
|
@ -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`)
|
|
@ -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")
|
|
@ -0,0 +1,5 @@
|
||||||
|
[gd_resource type="SystemFont" format=3 uid="uid://dmcxm8gxsonbq"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
font_names = PackedStringArray("Sans-Serif")
|
||||||
|
multichannel_signed_distance_field = true
|
|
@ -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 |
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
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
|
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
|
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
|
|
@ -0,0 +1,7 @@
|
||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="gd-YAFSM"
|
||||||
|
description="Yet Another Finite State Machine"
|
||||||
|
author="imjp94"
|
||||||
|
version="0.6.2"
|
||||||
|
script="plugin.gd"
|
|
@ -0,0 +1,150 @@
|
||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
const YAFSM = preload("YAFSM.gd")
|
||||||
|
const StackPlayer = YAFSM.StackPlayer
|
||||||
|
const StateMachinePlayer = YAFSM.StateMachinePlayer
|
||||||
|
|
||||||
|
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.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:
|
||||||
|
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")
|
||||||
|
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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,746 @@
|
||||||
|
@tool
|
||||||
|
extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChart.gd"
|
||||||
|
const StateMachinePlayer = preload("../src/StateMachinePlayer.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
|
||||||
|
|
||||||
|
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")
|
||||||
|
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()
|
||||||
|
state_machine_player.state_machine = new_state_machine
|
||||||
|
set_state_machine(new_state_machine)
|
||||||
|
create_new_state_machine_container.visible = false
|
||||||
|
check_has_entry()
|
||||||
|
emit_signal("inspector_changed", "state_machine")
|
||||||
|
|
||||||
|
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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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"
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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")
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|