diff --git a/source/addons/StairsCharacter/LICENSE b/source/addons/StairsCharacter/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/source/addons/StairsCharacter/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to diff --git a/source/addons/StairsCharacter/README.md b/source/addons/StairsCharacter/README.md new file mode 100644 index 0000000..60ccc0e --- /dev/null +++ b/source/addons/StairsCharacter/README.md @@ -0,0 +1,52 @@ +# Stairs Character 3D + +A simple class based on Godot's default CharacterBody3D with very simple stair stepping ability. +Just call "move_and_stair_step()" instead of "move_and_slide()". + +Only tested with cylinder colliders. Works best with "0.01" collider margin. + +There are a couple signals you can connect to: +- on_stair_step (any step, up or down) +- on_stair_step_down +- on_stair_step_up + +Example usage: +```gdscript + +extends StairsCharacter3D + + +const SPEED = 5.0 +const JUMP_VELOCITY = 4.5 + +# Get the gravity from the project settings to be synced with RigidBody nodes. +var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") + + +func _physics_process(delta: float) -> void: + # Add the gravity. + if not is_on_floor(): + velocity.y -= gravity * delta + + # Handle jump. + if Input.is_action_just_pressed("ui_accept") and is_on_floor(): + velocity.y = JUMP_VELOCITY + + # Get the input direction and handle the movement/deceleration. + # As good practice, you should replace UI actions with custom gameplay actions. + var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() + if direction: + velocity.x = direction.x * SPEED + velocity.z = direction.z * SPEED + else: + velocity.x = move_toward(velocity.x, 0, SPEED) + velocity.z = move_toward(velocity.z, 0, SPEED) + + # call move_and_stair_step instead of default move_and_slide + move_and_stair_step() +``` + +# Note +This plugin is basically just [Andricraft's GDScript Stairs Character](https://github.com/Andicraft/stairs-character) translated into C++. +I made it as a learning exercise for GDExtension. diff --git a/source/addons/StairsCharacter/StairsCharacter.gdextension b/source/addons/StairsCharacter/StairsCharacter.gdextension new file mode 100644 index 0000000..8fbf7bd --- /dev/null +++ b/source/addons/StairsCharacter/StairsCharacter.gdextension @@ -0,0 +1,22 @@ +[configuration] + +entry_symbol = "gdextension_init" +compatibility_minimum = 4.3 + +[libraries] + +linux.debug.x86_64 = "bin/libStairsCharacter.linux.debug.x86_64.so" +linux.release.x86_64 = "bin/libStairsCharacter.linux.release.x86_64.so" +linux.debug.arm64 = "bin/libStairsCharacter.linux.debug.arm64.so" +linux.release.arm64 = "bin/libStairsCharacter.linux.release.arm64.so" +linux.debug.rv64 = "bin/libStairsCharacter.linux.debug.rv64.so" +linux.release.rv64 = "bin/libStairsCharacter.linux.release.rv64.so" +windows.debug.x86_64 = "bin/libStairsCharacter.windows.debug.x86_64.dll" +windows.release.x86_64 = "bin/libStairsCharacter.windows.release.x86_64.dll" +macos.debug = "bin/libStairsCharacter.macos.debug.framework" +macos.release = "bin/libStairsCharacter.macos.release.framework" +android.debug.arm64 = "bin/libStairsCharacter.android.debug.arm64.so" +android.release.arm64 = "bin/libStairsCharacter.android.release.arm64.so" + +[icons] +StairsCharacter3D = "stairs_character.svg" \ No newline at end of file diff --git a/source/addons/StairsCharacter/StairsCharacter.gdextension.uid b/source/addons/StairsCharacter/StairsCharacter.gdextension.uid new file mode 100644 index 0000000..033ec11 --- /dev/null +++ b/source/addons/StairsCharacter/StairsCharacter.gdextension.uid @@ -0,0 +1 @@ +uid://tkege8xp3mnb diff --git a/source/addons/StairsCharacter/bin/libStairsCharacter.android.debug.arm64.so b/source/addons/StairsCharacter/bin/libStairsCharacter.android.debug.arm64.so new file mode 100644 index 0000000..866e6f0 Binary files /dev/null and b/source/addons/StairsCharacter/bin/libStairsCharacter.android.debug.arm64.so differ diff --git a/source/addons/StairsCharacter/bin/libStairsCharacter.android.release.arm64.so b/source/addons/StairsCharacter/bin/libStairsCharacter.android.release.arm64.so new file mode 100644 index 0000000..28615e1 Binary files /dev/null and b/source/addons/StairsCharacter/bin/libStairsCharacter.android.release.arm64.so differ diff --git a/source/addons/StairsCharacter/bin/libStairsCharacter.linux.debug.x86_64.so b/source/addons/StairsCharacter/bin/libStairsCharacter.linux.debug.x86_64.so new file mode 100644 index 0000000..525819b Binary files /dev/null and b/source/addons/StairsCharacter/bin/libStairsCharacter.linux.debug.x86_64.so differ diff --git a/source/addons/StairsCharacter/bin/libStairsCharacter.linux.release.x86_64.so b/source/addons/StairsCharacter/bin/libStairsCharacter.linux.release.x86_64.so new file mode 100644 index 0000000..6cdd73c Binary files /dev/null and b/source/addons/StairsCharacter/bin/libStairsCharacter.linux.release.x86_64.so differ diff --git a/source/addons/StairsCharacter/bin/libStairsCharacter.windows.debug.x86_64.dll b/source/addons/StairsCharacter/bin/libStairsCharacter.windows.debug.x86_64.dll new file mode 100644 index 0000000..dd770b4 Binary files /dev/null and b/source/addons/StairsCharacter/bin/libStairsCharacter.windows.debug.x86_64.dll differ diff --git a/source/addons/StairsCharacter/bin/libStairsCharacter.windows.release.x86_64.dll b/source/addons/StairsCharacter/bin/libStairsCharacter.windows.release.x86_64.dll new file mode 100644 index 0000000..004d999 Binary files /dev/null and b/source/addons/StairsCharacter/bin/libStairsCharacter.windows.release.x86_64.dll differ diff --git a/source/addons/StairsCharacter/stairs_character.svg b/source/addons/StairsCharacter/stairs_character.svg new file mode 100644 index 0000000..ac65e2c --- /dev/null +++ b/source/addons/StairsCharacter/stairs_character.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/addons/StairsCharacter/stairs_character.svg.import b/source/addons/StairsCharacter/stairs_character.svg.import new file mode 100644 index 0000000..5f39c85 --- /dev/null +++ b/source/addons/StairsCharacter/stairs_character.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://f8rpbcuiicd7" +path="res://.godot/imported/stairs_character.svg-a9660f3f5b9588e29c90e9a9aaadbbda.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/StairsCharacter/stairs_character.svg" +dest_files=["res://.godot/imported/stairs_character.svg-a9660f3f5b9588e29c90e9a9aaadbbda.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/ColourPicker.gd b/source/addons/Todo_Manager/ColourPicker.gd new file mode 100644 index 0000000..39a3f9a --- /dev/null +++ b/source/addons/Todo_Manager/ColourPicker.gd @@ -0,0 +1,17 @@ +@tool +extends HBoxContainer + +var colour : Color +var title : String: + set = set_title +var index : int + +@onready var colour_picker := $TODOColourPickerButton + +func _ready() -> void: + $TODOColourPickerButton.color = colour + $Label.text = title + +func set_title(value: String) -> void: + title = value + $Label.text = value diff --git a/source/addons/Todo_Manager/ColourPicker.gd.uid b/source/addons/Todo_Manager/ColourPicker.gd.uid new file mode 100644 index 0000000..8dd9d9d --- /dev/null +++ b/source/addons/Todo_Manager/ColourPicker.gd.uid @@ -0,0 +1 @@ +uid://dd033y6smf6jh diff --git a/source/addons/Todo_Manager/Current.gd b/source/addons/Todo_Manager/Current.gd new file mode 100644 index 0000000..544f59f --- /dev/null +++ b/source/addons/Todo_Manager/Current.gd @@ -0,0 +1,44 @@ +@tool +extends Control + +signal tree_built # used for debugging + +const Todo := preload("res://addons/Todo_Manager/todo_class.gd") +const TodoItem := preload("res://addons/Todo_Manager/todoItem_class.gd") + +var _sort_alphabetical := true + +@onready var tree := $Panel/Tree as Tree + +func build_tree(todo_item : TodoItem, patterns : Array, cased_patterns : Array[String]) -> void: + tree.clear() + var root := tree.create_item() + root.set_text(0, "Scripts") + var script := tree.create_item(root) + script.set_text(0, todo_item.get_short_path() + " -------") + script.set_metadata(0, todo_item) + for todo in todo_item.todos: + var item := tree.create_item(script) + var content_header : String = todo.content + if "\n" in todo.content: + content_header = content_header.split("\n")[0] + "..." + item.set_text(0, "(%0) - %1".format([todo.line_number, content_header], "%_")) + item.set_tooltip_text(0, todo.content) + item.set_metadata(0, todo) + for i in range(0, len(cased_patterns)): + if cased_patterns[i] == todo.pattern: + item.set_custom_color(0, patterns[i][1]) + emit_signal("tree_built") + + +func sort_alphabetical(a, b) -> bool: + if a.script_path > b.script_path: + return true + else: + return false + +func sort_backwards(a, b) -> bool: + if a.script_path < b.script_path: + return true + else: + return false diff --git a/source/addons/Todo_Manager/Current.gd.uid b/source/addons/Todo_Manager/Current.gd.uid new file mode 100644 index 0000000..211c440 --- /dev/null +++ b/source/addons/Todo_Manager/Current.gd.uid @@ -0,0 +1 @@ +uid://dp4hevwsquax6 diff --git a/source/addons/Todo_Manager/Dock.gd b/source/addons/Todo_Manager/Dock.gd new file mode 100644 index 0000000..058819c --- /dev/null +++ b/source/addons/Todo_Manager/Dock.gd @@ -0,0 +1,336 @@ +@tool +extends Control + +#signal tree_built # used for debugging +enum { CASE_INSENSITIVE, CASE_SENSITIVE } + +const Project := preload("res://addons/Todo_Manager/Project.gd") +const Current := preload("res://addons/Todo_Manager/Current.gd") + +const Todo := preload("res://addons/Todo_Manager/todo_class.gd") +const TodoItem := preload("res://addons/Todo_Manager/todoItem_class.gd") +const ColourPicker := preload("res://addons/Todo_Manager/UI/ColourPicker.tscn") +const PatternEdit := preload("res://addons/Todo_Manager/PatternEdit.gd") +const PatternEditScene := preload("res://addons/Todo_Manager/UI/PatternEdit.tscn") +const ToggleButton := preload("res://addons/Todo_Manager/UI/ToggleButton.tscn") + +# Pattern array format - [regex, colour, case_sensitivity, enabled] +const DEFAULT_PATTERNS := [["\\bTODO\\b", Color("96f1ad"), CASE_INSENSITIVE, true], ["\\bHACK\\b", Color("d5bc70"), CASE_INSENSITIVE, true], ["\\bFIXME\\b", Color("d57070"), CASE_INSENSITIVE, true]] +const DEFAULT_SCRIPT_COLOUR := Color("ccced3") +const DEFAULT_SCRIPT_NAME := false +const DEFAULT_SORT := true + +var plugin : EditorPlugin + +var todo_items : Array + +var script_colour := Color("ccced3") +var ignore_paths : Array[String] = [] +var full_path := false +var auto_refresh := true +var builtin_enabled := false +var _sort_alphabetical := true + +var patterns := [["\\bTODO\\b", Color("96f1ad"), CASE_INSENSITIVE, true], ["\\bHACK\\b", Color("d5bc70"), CASE_INSENSITIVE, true], ["\\bFIXME\\b", Color("d57070"), CASE_INSENSITIVE, true]] + +@onready var tabs := $VBoxContainer/TabContainer as TabContainer +@onready var project := $VBoxContainer/TabContainer/Project as Project +@onready var current := $VBoxContainer/TabContainer/Current as Current +@onready var project_tree := $VBoxContainer/TabContainer/Project/Panel/Tree as Tree +@onready var project_toggles_container := $VBoxContainer/TabContainer/Project/TogglesScrollContainer/VBoxContainer as VBoxContainer +@onready var current_tree := $VBoxContainer/TabContainer/Current/Panel/Tree as Tree +@onready var current_toggles_container := $VBoxContainer/TabContainer/Current/TogglesScrollContainer/VBoxContainer as VBoxContainer +@onready var settings_panel := $VBoxContainer/TabContainer/Settings as Panel +@onready var colours_container := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer3/Colours as VBoxContainer +@onready var pattern_container := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns as VBoxContainer +@onready var ignore_textbox := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/IgnorePaths/TextEdit as LineEdit +@onready var auto_refresh_button := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/RefreshCheckButton as CheckButton + +func _ready() -> void: + load_config() + populate_settings() + + +func build_tree() -> void: + if tabs: + add_toggle_buttons() + var filtered_patterns = patterns.filter(func (p): return p[3] == true) + match tabs.current_tab: + 0: + project.build_tree(todo_items, ignore_paths, filtered_patterns, plugin.cased_patterns, _sort_alphabetical, full_path) + create_config_file() + 1: + current.build_tree(get_active_script(), filtered_patterns, plugin.cased_patterns) + create_config_file() + 2: + pass + _: + pass + + +func get_active_script() -> TodoItem: + var current_script : Script = plugin.get_editor_interface().get_script_editor().get_current_script() + if current_script: + var script_path = current_script.resource_path + for todo_item in todo_items: + if todo_item.script_path == script_path: + return todo_item + + # nothing found + var todo_item := TodoItem.new(script_path, []) + return todo_item + else: + # not a script + var todo_item := TodoItem.new("res://Documentation", []) + return todo_item + + +func go_to_script(script_path: String, line_number : int = 0) -> void: + if plugin.get_editor_interface().get_editor_settings().get_setting("text_editor/external/use_external_editor"): + var exec_path = plugin.get_editor_interface().get_editor_settings().get_setting("text_editor/external/exec_path") + var args := get_exec_flags(exec_path, script_path, line_number) + OS.execute(exec_path, args) + else: + var script := load(script_path) + plugin.get_editor_interface().edit_resource(script) + plugin.get_editor_interface().get_script_editor().goto_line(line_number - 1) + + +func get_exec_flags(editor_path : String, script_path : String, line_number : int) -> PackedStringArray: + var args : PackedStringArray + var script_global_path = ProjectSettings.globalize_path(script_path) + + if editor_path.ends_with("code.cmd") or editor_path.ends_with("code"): ## VS Code + args.append(ProjectSettings.globalize_path("res://")) + args.append("--goto") + args.append(script_global_path + ":" + str(line_number)) + + elif editor_path.ends_with("rider64.exe") or editor_path.ends_with("rider"): ## Rider + args.append("--line") + args.append(str(line_number)) + args.append(script_global_path) + + else: ## Atom / Sublime + args.append(script_global_path + ":" + str(line_number)) + + return args + + +func sort_alphabetical(a, b) -> bool: + if a.script_path > b.script_path: + return true + else: + return false + + +func sort_backwards(a, b) -> bool: + if a.script_path < b.script_path: + return true + else: + return false + + +func populate_settings() -> void: + for i in patterns.size(): + ## Create Colour Pickers + var colour_picker: Node = ColourPicker.instantiate() + colour_picker.colour = patterns[i][1] + colour_picker.title = patterns[i][0] + colour_picker.index = i + colours_container.add_child(colour_picker) + colour_picker.colour_picker.color_changed.connect(change_colour.bind(i)) + + ## Create Patterns + var pattern_edit: PatternEdit = PatternEditScene.instantiate() + pattern_edit.text = patterns[i][0] + pattern_edit.index = i + pattern_container.add_child(pattern_edit) + pattern_edit.line_edit.text_changed.connect(change_pattern.bind(i, + colour_picker)) + pattern_edit.remove_button.pressed.connect(remove_pattern.bind(i, + pattern_edit, colour_picker)) + pattern_edit.case_checkbox.button_pressed = patterns[i][2] + pattern_edit.case_checkbox.toggled.connect(case_sensitive_pattern.bind(i)) + + var pattern_button := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns/AddPatternButton + $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns.move_child(pattern_button, 0) + + # path filtering + var ignore_paths_field := ignore_textbox + if not ignore_paths_field.is_connected("text_changed", _on_ignore_paths_changed): + ignore_paths_field.connect("text_changed", _on_ignore_paths_changed) + var ignore_paths_text := "" + for path in ignore_paths: + ignore_paths_text += path + ", " + ignore_paths_text = ignore_paths_text.trim_suffix(", ") + ignore_paths_field.text = ignore_paths_text + + auto_refresh_button.button_pressed = auto_refresh + + +func rebuild_settings() -> void: + for node in colours_container.get_children(): + node.queue_free() + for node in pattern_container.get_children(): + if node is Button: + continue + node.queue_free() + populate_settings() + + +func add_toggle_buttons() -> void: + for toggle in project_toggles_container.get_children(): + toggle.queue_free() + for toggle in current_toggles_container.get_children(): + toggle.queue_free() + for pattern in patterns: + var toggle : Button = ToggleButton.instantiate() + toggle.text = pattern[0].replace('\\b', '') + toggle.button_pressed = pattern[3] + toggle.connect("toggled", toggle_pattern_enabled.bind(pattern)) + if tabs.current_tab == 0: + project_toggles_container.add_child(toggle) + elif tabs.current_tab == 1: + current_toggles_container.add_child(toggle) + +#### CONFIG FILE #### +func create_config_file() -> void: + var config = ConfigFile.new() + + config.set_value("scripts", "full_path", full_path) + config.set_value("scripts", "sort_alphabetical", _sort_alphabetical) + config.set_value("scripts", "script_colour", script_colour) + config.set_value("scripts", "ignore_paths", ignore_paths) + + config.set_value("patterns", "patterns", patterns) + + config.set_value("config", "auto_refresh", auto_refresh) + config.set_value("config", "builtin_enabled", builtin_enabled) + + var err = config.save("res://addons/Todo_Manager/todo.cfg") + + +func load_config() -> void: + var config := ConfigFile.new() + if config.load("res://addons/Todo_Manager/todo.cfg") == OK: + full_path = config.get_value("scripts", "full_path", DEFAULT_SCRIPT_NAME) + _sort_alphabetical = config.get_value("scripts", "sort_alphabetical", DEFAULT_SORT) + script_colour = config.get_value("scripts", "script_colour", DEFAULT_SCRIPT_COLOUR) + ignore_paths = config.get_value("scripts", "ignore_paths", [] as Array[String]) + patterns = config.get_value("patterns", "patterns", DEFAULT_PATTERNS) + fix_missing_values(patterns) + auto_refresh = config.get_value("config", "auto_refresh", true) + builtin_enabled = config.get_value("config", "builtin_enabled", false) + else: + create_config_file() + + +func fix_missing_values(patterns: Array) -> void: + for pattern in patterns: + if pattern.size() == 2: + pattern.append(CASE_INSENSITIVE) + if pattern.size() == 3: + pattern.append(true) + + +#### Events #### +func _on_SettingsButton_toggled(button_pressed: bool) -> void: + settings_panel.visible = button_pressed + if button_pressed == false: + create_config_file() +# plugin.find_tokens_from_path(plugin.script_cache) + if auto_refresh: + plugin.rescan_files(true) + +func _on_Tree_item_activated() -> void: + var item : TreeItem + match tabs.current_tab: + 0: + item = project_tree.get_selected() + 1: + item = current_tree.get_selected() + if item.get_metadata(0) is Todo: + var todo : Todo = item.get_metadata(0) + call_deferred("go_to_script", todo.script_path, todo.line_number) + else: + var todo_item = item.get_metadata(0) + call_deferred("go_to_script", todo_item.script_path) + +func _on_FullPathCheckBox_toggled(button_pressed: bool) -> void: + full_path = button_pressed + +func _on_ScriptColourPickerButton_color_changed(color: Color) -> void: + script_colour = color + +func _on_RescanButton_pressed() -> void: + plugin.rescan_files(true) + +func change_colour(colour: Color, index: int) -> void: + patterns[index][1] = colour + +func change_pattern(value: String, index: int, this_colour: Node) -> void: + patterns[index][0] = value + this_colour.title = value + plugin.rescan_files(true) + +func remove_pattern(index: int, this: Node, this_colour: Node) -> void: + patterns.remove_at(index) + this.queue_free() + this_colour.queue_free() + plugin.rescan_files(true) + +func toggle_pattern_enabled(active: bool, pattern: Array) -> void: + pattern[3] = active + plugin.rescan_files(true) + +func case_sensitive_pattern(active: bool, index: int) -> void: + if active: + patterns[index][2] = CASE_SENSITIVE + else: + patterns[index][2] = CASE_INSENSITIVE + plugin.rescan_files(true) + +func _on_DefaultButton_pressed() -> void: + patterns = DEFAULT_PATTERNS.duplicate(true) + _sort_alphabetical = DEFAULT_SORT + script_colour = DEFAULT_SCRIPT_COLOUR + full_path = DEFAULT_SCRIPT_NAME + rebuild_settings() + plugin.rescan_files(true) + +func _on_AlphSortCheckBox_toggled(button_pressed: bool) -> void: + _sort_alphabetical = button_pressed + plugin.rescan_files(true) + +func _on_AddPatternButton_pressed() -> void: + patterns.append(["\\bplaceholder\\b", Color.WHITE, CASE_INSENSITIVE, true]) + rebuild_settings() + +func _on_RefreshCheckButton_toggled(button_pressed: bool) -> void: + auto_refresh = button_pressed + +func _on_Timer_timeout() -> void: + plugin.refresh_lock = false + +func _on_ignore_paths_changed(new_text: String) -> void: + var text = ignore_textbox.text + var split: Array = text.split(',') + ignore_paths.clear() + for elem in split: + if elem == " " || elem == "": + continue + ignore_paths.push_front(elem.lstrip(' ').rstrip(' ')) + # validate so no empty string slips through (all paths ignored) + var i := 0 + for path in ignore_paths: + if (path == "" || path == " "): + ignore_paths.remove_at(i) + i += 1 + plugin.rescan_files(true) + +func _on_TabContainer_tab_changed(tab: int) -> void: + build_tree() + +func _on_BuiltInCheckButton_toggled(button_pressed: bool) -> void: + builtin_enabled = button_pressed + plugin.rescan_files(true) diff --git a/source/addons/Todo_Manager/Dock.gd.uid b/source/addons/Todo_Manager/Dock.gd.uid new file mode 100644 index 0000000..77de93e --- /dev/null +++ b/source/addons/Todo_Manager/Dock.gd.uid @@ -0,0 +1 @@ +uid://csgl0vtsudgxh diff --git a/source/addons/Todo_Manager/PatternEdit.gd b/source/addons/Todo_Manager/PatternEdit.gd new file mode 100644 index 0000000..4e610af --- /dev/null +++ b/source/addons/Todo_Manager/PatternEdit.gd @@ -0,0 +1,21 @@ +@tool +extends HBoxContainer + + +var text : String : set = set_text +var disabled : bool +var index : int + +@onready var line_edit := $LineEdit as LineEdit +@onready var remove_button := $RemoveButton as Button +@onready var case_checkbox := %CaseSensativeCheckbox as CheckBox + +func _ready() -> void: + line_edit.text = text + remove_button.disabled = disabled + + +func set_text(value: String) -> void: + text = value + if line_edit: + line_edit.text = value diff --git a/source/addons/Todo_Manager/PatternEdit.gd.uid b/source/addons/Todo_Manager/PatternEdit.gd.uid new file mode 100644 index 0000000..bf28101 --- /dev/null +++ b/source/addons/Todo_Manager/PatternEdit.gd.uid @@ -0,0 +1 @@ +uid://betg01sk02eyy diff --git a/source/addons/Todo_Manager/Project.gd b/source/addons/Todo_Manager/Project.gd new file mode 100644 index 0000000..95b9d1b --- /dev/null +++ b/source/addons/Todo_Manager/Project.gd @@ -0,0 +1,73 @@ +@tool +extends Control + +signal tree_built # used for debugging + +const Todo := preload("res://addons/Todo_Manager/todo_class.gd") + +var _sort_alphabetical := true +var _full_path := false + +@onready var tree := $Panel/Tree as Tree + +func build_tree(todo_items : Array, ignore_paths : Array, patterns : Array, cased_patterns: Array[String], sort_alphabetical : bool, full_path : bool) -> void: + _full_path = full_path + tree.clear() + if sort_alphabetical: + todo_items.sort_custom(Callable(self, "sort_alphabetical")) + else: + todo_items.sort_custom(Callable(self, "sort_backwards")) + var root := tree.create_item() + root.set_text(0, "Scripts") + for todo_item in todo_items: + var ignore := false + for ignore_path in ignore_paths: + var script_path : String = todo_item.script_path + if script_path.begins_with(ignore_path) or script_path.begins_with("res://" + ignore_path) or script_path.begins_with("res:///" + ignore_path): + ignore = true + break + if ignore: + continue + var script := tree.create_item(root) + if full_path: + script.set_text(0, todo_item.script_path + " -------") + else: + script.set_text(0, todo_item.get_short_path() + " -------") + script.set_metadata(0, todo_item) + for todo in todo_item.todos: + var item := tree.create_item(script) + var content_header : String = todo.content + if "\n" in todo.content: + content_header = content_header.split("\n")[0] + "..." + item.set_text(0, "(%0) - %1".format([todo.line_number, content_header], "%_")) + item.set_tooltip_text(0, todo.content) + item.set_metadata(0, todo) + for i in range(0, len(cased_patterns)): + if cased_patterns[i] == todo.pattern: + item.set_custom_color(0, patterns[i][1]) + emit_signal("tree_built") + + +func sort_alphabetical(a, b) -> bool: + if _full_path: + if a.script_path < b.script_path: + return true + else: + return false + else: + if a.get_short_path() < b.get_short_path(): + return true + else: + return false + +func sort_backwards(a, b) -> bool: + if _full_path: + if a.script_path > b.script_path: + return true + else: + return false + else: + if a.get_short_path() > b.get_short_path(): + return true + else: + return false diff --git a/source/addons/Todo_Manager/Project.gd.uid b/source/addons/Todo_Manager/Project.gd.uid new file mode 100644 index 0000000..3ecd69a --- /dev/null +++ b/source/addons/Todo_Manager/Project.gd.uid @@ -0,0 +1 @@ +uid://bnyawhvwojm5g diff --git a/source/addons/Todo_Manager/Test.gd b/source/addons/Todo_Manager/Test.gd new file mode 100644 index 0000000..a24ed3d --- /dev/null +++ b/source/addons/Todo_Manager/Test.gd @@ -0,0 +1,4 @@ +class_name Test extends RefCounted + +var a: String +var b: bool diff --git a/source/addons/Todo_Manager/Test.gd.uid b/source/addons/Todo_Manager/Test.gd.uid new file mode 100644 index 0000000..b172560 --- /dev/null +++ b/source/addons/Todo_Manager/Test.gd.uid @@ -0,0 +1 @@ +uid://den1uk6816qrb diff --git a/source/addons/Todo_Manager/UI/ColourPicker.tscn b/source/addons/Todo_Manager/UI/ColourPicker.tscn new file mode 100644 index 0000000..d70381d --- /dev/null +++ b/source/addons/Todo_Manager/UI/ColourPicker.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=2 format=3 uid="uid://bie1xn8v1kd66"] + +[ext_resource type="Script" uid="uid://dd033y6smf6jh" path="res://addons/Todo_Manager/ColourPicker.gd" id="1"] + +[node name="TODOColour" type="HBoxContainer"] +offset_right = 105.0 +offset_bottom = 31.0 +script = ExtResource("1") +metadata/_edit_use_custom_anchors = false + +[node name="Label" type="Label" parent="."] +layout_mode = 2 + +[node name="TODOColourPickerButton" type="ColorPickerButton" parent="."] +custom_minimum_size = Vector2(40, 0) +layout_mode = 2 +size_flags_horizontal = 10 diff --git a/source/addons/Todo_Manager/UI/Dock.tscn b/source/addons/Todo_Manager/UI/Dock.tscn new file mode 100644 index 0000000..944ac75 --- /dev/null +++ b/source/addons/Todo_Manager/UI/Dock.tscn @@ -0,0 +1,365 @@ +[gd_scene load_steps=6 format=3 uid="uid://b6k0dtftankcx"] + +[ext_resource type="Script" uid="uid://csgl0vtsudgxh" path="res://addons/Todo_Manager/Dock.gd" id="1"] +[ext_resource type="Script" uid="uid://bnyawhvwojm5g" path="res://addons/Todo_Manager/Project.gd" id="2"] +[ext_resource type="Script" uid="uid://dp4hevwsquax6" path="res://addons/Todo_Manager/Current.gd" id="3"] + +[sub_resource type="ButtonGroup" id="ButtonGroup_kqxcu"] + +[sub_resource type="ButtonGroup" id="ButtonGroup_kltg3"] + +[node name="Dock" type="Control"] +custom_minimum_size = Vector2(0, 200) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 4.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_layout_mode = 1 + +[node name="Header" type="HBoxContainer" parent="VBoxContainer"] +visible = false +layout_mode = 2 + +[node name="HeaderLeft" type="HBoxContainer" parent="VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Title" type="Label" parent="VBoxContainer/Header/HeaderLeft"] +layout_mode = 2 +text = "Todo Dock:" + +[node name="HeaderRight" type="HBoxContainer" parent="VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +alignment = 2 + +[node name="SettingsButton" type="Button" parent="VBoxContainer/Header/HeaderRight"] +visible = false +layout_mode = 2 +toggle_mode = true +text = "Settings" + +[node name="TabContainer" type="TabContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +current_tab = 0 + +[node name="Project" type="Control" parent="VBoxContainer/TabContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("2") +metadata/_tab_index = 0 + +[node name="Panel" type="Panel" parent="VBoxContainer/TabContainer/Project"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -106.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Tree" type="Tree" parent="VBoxContainer/TabContainer/Project/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +hide_root = true + +[node name="TogglesScrollContainer" type="ScrollContainer" parent="VBoxContainer/TabContainer/Project"] +layout_mode = 1 +anchors_preset = 11 +anchor_left = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -102.0 +grow_horizontal = 0 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Project/TogglesScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 6 + +[node name="Current" type="Control" parent="VBoxContainer/TabContainer"] +visible = false +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("3") +metadata/_tab_index = 1 + +[node name="Panel" type="Panel" parent="VBoxContainer/TabContainer/Current"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -106.0 + +[node name="Tree" type="Tree" parent="VBoxContainer/TabContainer/Current/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +hide_folding = true +hide_root = true + +[node name="TogglesScrollContainer" type="ScrollContainer" parent="VBoxContainer/TabContainer/Current"] +layout_mode = 1 +anchors_preset = 11 +anchor_left = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -102.0 +grow_horizontal = 0 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Current/TogglesScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 6 + +[node name="Settings" type="Panel" parent="VBoxContainer/TabContainer"] +visible = false +layout_mode = 2 +metadata/_tab_index = 2 + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/TabContainer/Settings"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Scripts" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Scripts"] +layout_mode = 2 +text = "Scripts:" + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Scripts"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 5 + +[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer"] +layout_mode = 2 + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2"] +layout_mode = 2 + +[node name="Scripts" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2"] +layout_mode = 2 + +[node name="ScriptName" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptName"] +layout_mode = 2 +text = "Script Name:" + +[node name="FullPathCheckBox" type="CheckBox" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptName"] +layout_mode = 2 +button_group = SubResource("ButtonGroup_kqxcu") +text = "Full path" + +[node name="ShortNameCheckBox" type="CheckBox" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptName"] +layout_mode = 2 +button_pressed = true +button_group = SubResource("ButtonGroup_kqxcu") +text = "Short name" + +[node name="ScriptSort" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptSort"] +layout_mode = 2 +text = "Sort Order:" + +[node name="AlphSortCheckBox" type="CheckBox" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptSort"] +layout_mode = 2 +button_pressed = true +button_group = SubResource("ButtonGroup_kltg3") +text = "Alphabetical" + +[node name="RAlphSortCheckBox" type="CheckBox" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptSort"] +layout_mode = 2 +button_group = SubResource("ButtonGroup_kltg3") +text = "Reverse Alphabetical" + +[node name="ScriptColour" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptColour"] +layout_mode = 2 +text = "Script Colour:" + +[node name="ScriptColourPickerButton" type="ColorPickerButton" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptColour"] +custom_minimum_size = Vector2(40, 0) +layout_mode = 2 +color = Color(0.8, 0.807843, 0.827451, 1) + +[node name="IgnorePaths" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/IgnorePaths"] +layout_mode = 2 +text = "Ignore Paths:" + +[node name="TextEdit" type="LineEdit" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/IgnorePaths"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +expand_to_text_length = true + +[node name="Label3" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/IgnorePaths"] +layout_mode = 2 +text = "(Separated by commas)" + +[node name="TODOColours" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/TODOColours"] +layout_mode = 2 +text = "TODO Colours:" + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/TODOColours"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer3"] +layout_mode = 2 + +[node name="Colours" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer3"] +layout_mode = 2 + +[node name="Patterns" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Patterns"] +layout_mode = 2 +text = "Patterns:" + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Patterns"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer4" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4"] +layout_mode = 2 + +[node name="Patterns" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="AddPatternButton" type="Button" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns"] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Add" + +[node name="Config" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Config"] +layout_mode = 2 +text = "Config:" + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Config"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer5" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5"] +layout_mode = 2 + +[node name="Patterns" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5"] +layout_mode = 2 + +[node name="RefreshCheckButton" type="CheckButton" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns"] +layout_mode = 2 +size_flags_horizontal = 0 +button_pressed = true +text = "Auto Refresh" + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns"] +layout_mode = 2 + +[node name="BuiltInCheckButton" type="CheckButton" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/HBoxContainer"] +layout_mode = 2 +text = "Scan Built-in Scripts" + +[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/HBoxContainer"] +layout_mode = 2 + +[node name="DefaultButton" type="Button" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns"] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Reset to default" + +[node name="Timer" type="Timer" parent="."] +one_shot = true + +[node name="RescanButton" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -102.0 +offset_top = 3.0 +offset_bottom = 34.0 +grow_horizontal = 0 +text = "Rescan Files" +flat = true + +[connection signal="toggled" from="VBoxContainer/Header/HeaderRight/SettingsButton" to="." method="_on_SettingsButton_toggled"] +[connection signal="tab_changed" from="VBoxContainer/TabContainer" to="." method="_on_TabContainer_tab_changed"] +[connection signal="item_activated" from="VBoxContainer/TabContainer/Project/Panel/Tree" to="." method="_on_Tree_item_activated"] +[connection signal="item_activated" from="VBoxContainer/TabContainer/Current/Panel/Tree" to="." method="_on_Tree_item_activated"] +[connection signal="toggled" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptName/FullPathCheckBox" to="." method="_on_FullPathCheckBox_toggled"] +[connection signal="toggled" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptSort/AlphSortCheckBox" to="." method="_on_AlphSortCheckBox_toggled"] +[connection signal="color_changed" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptColour/ScriptColourPickerButton" to="." method="_on_ScriptColourPickerButton_color_changed"] +[connection signal="pressed" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns/AddPatternButton" to="." method="_on_AddPatternButton_pressed"] +[connection signal="toggled" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/RefreshCheckButton" to="." method="_on_RefreshCheckButton_toggled"] +[connection signal="toggled" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/HBoxContainer/BuiltInCheckButton" to="." method="_on_BuiltInCheckButton_toggled"] +[connection signal="pressed" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/DefaultButton" to="." method="_on_DefaultButton_pressed"] +[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"] +[connection signal="pressed" from="RescanButton" to="." method="_on_RescanButton_pressed"] diff --git a/source/addons/Todo_Manager/UI/FlatToggleButton.tres b/source/addons/Todo_Manager/UI/FlatToggleButton.tres new file mode 100644 index 0000000..70238d1 --- /dev/null +++ b/source/addons/Todo_Manager/UI/FlatToggleButton.tres @@ -0,0 +1,21 @@ +[gd_resource type="Theme" load_steps=4 format=3 uid="uid://cignxcqjqfst0"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0amm0"] +bg_color = Color(1, 1, 1, 0.0784314) + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_juds7"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_elllf"] +bg_color = Color(0.12549, 0.156863, 0.192157, 1) +border_width_bottom = 2 +border_color = Color(0.439216, 0.729412, 0.980392, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[resource] +Button/colors/font_pressed_color = Color(0.439216, 0.729412, 0.980392, 1) +Button/styles/hover = SubResource("StyleBoxFlat_0amm0") +Button/styles/normal = SubResource("StyleBoxEmpty_juds7") +Button/styles/pressed = SubResource("StyleBoxFlat_elllf") diff --git a/source/addons/Todo_Manager/UI/PatternEdit.tscn b/source/addons/Todo_Manager/UI/PatternEdit.tscn new file mode 100644 index 0000000..6ab0c6a --- /dev/null +++ b/source/addons/Todo_Manager/UI/PatternEdit.tscn @@ -0,0 +1,26 @@ +[gd_scene load_steps=2 format=3 uid="uid://bx11sel2q5wli"] + +[ext_resource type="Script" uid="uid://betg01sk02eyy" path="res://addons/Todo_Manager/PatternEdit.gd" id="1_mujq6"] + +[node name="Pattern" type="HBoxContainer"] +script = ExtResource("1_mujq6") + +[node name="LineEdit" type="LineEdit" parent="."] +layout_mode = 2 +size_flags_horizontal = 0 +expand_to_text_length = true + +[node name="RemoveButton" type="Button" parent="."] +layout_mode = 2 +text = "-" + +[node name="MarginContainer" type="MarginContainer" parent="."] +custom_minimum_size = Vector2(20, 0) +layout_mode = 2 +size_flags_horizontal = 0 + +[node name="CaseSensativeCheckbox" type="CheckBox" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 0 +text = "Case Sensitive" diff --git a/source/addons/Todo_Manager/UI/ToggleButton.tscn b/source/addons/Todo_Manager/UI/ToggleButton.tscn new file mode 100644 index 0000000..c5c7609 --- /dev/null +++ b/source/addons/Todo_Manager/UI/ToggleButton.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=5 format=3 uid="uid://bq0tjjng7hdvt"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_5887s"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rtgj1"] +bg_color = Color(1, 1, 1, 0.0705882) +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="StyleBoxFlat_5887s"] +bg_color = Color(0.12549, 0.156863, 0.192157, 1) +border_width_bottom = 2 +border_color = Color(0.439216, 0.729412, 0.980392, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_f41nr"] + +[node name="Button" type="Button"] +theme_override_styles/focus = SubResource("StyleBoxEmpty_5887s") +theme_override_styles/hover = SubResource("StyleBoxFlat_rtgj1") +theme_override_styles/pressed = SubResource("StyleBoxFlat_5887s") +theme_override_styles/normal = SubResource("StyleBoxEmpty_f41nr") +toggle_mode = true +text = "TODO" diff --git a/source/addons/Todo_Manager/doc/example.gd b/source/addons/Todo_Manager/doc/example.gd new file mode 100644 index 0000000..859bbba --- /dev/null +++ b/source/addons/Todo_Manager/doc/example.gd @@ -0,0 +1,7 @@ +extends Node + +# TODO: this is a TODO +# HACK: this is a HACK +# FIXME: this is a FIXME +# TODO this works too +#Hack any format will do diff --git a/source/addons/Todo_Manager/doc/example.gd.uid b/source/addons/Todo_Manager/doc/example.gd.uid new file mode 100644 index 0000000..2f2ae58 --- /dev/null +++ b/source/addons/Todo_Manager/doc/example.gd.uid @@ -0,0 +1 @@ +uid://dotx67t7w7hhk diff --git a/source/addons/Todo_Manager/doc/images/Instruct1.png b/source/addons/Todo_Manager/doc/images/Instruct1.png new file mode 100644 index 0000000..99a8db0 Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/Instruct1.png differ diff --git a/source/addons/Todo_Manager/doc/images/Instruct1.png.import b/source/addons/Todo_Manager/doc/images/Instruct1.png.import new file mode 100644 index 0000000..c9dfc43 --- /dev/null +++ b/source/addons/Todo_Manager/doc/images/Instruct1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c7v1fdudy4quq" +path="res://.godot/imported/Instruct1.png-698c6faa3ef3ac4960807faa3e028bd6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Todo_Manager/doc/images/Instruct1.png" +dest_files=["res://.godot/imported/Instruct1.png-698c6faa3ef3ac4960807faa3e028bd6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/doc/images/Instruct2.png b/source/addons/Todo_Manager/doc/images/Instruct2.png new file mode 100644 index 0000000..83798f9 Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/Instruct2.png differ diff --git a/source/addons/Todo_Manager/doc/images/Instruct2.png.import b/source/addons/Todo_Manager/doc/images/Instruct2.png.import new file mode 100644 index 0000000..99ba4d7 --- /dev/null +++ b/source/addons/Todo_Manager/doc/images/Instruct2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b80nmr6b06t21" +path="res://.godot/imported/Instruct2.png-4e8664e49d00a397bbd539f7dee976ff.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Todo_Manager/doc/images/Instruct2.png" +dest_files=["res://.godot/imported/Instruct2.png-4e8664e49d00a397bbd539f7dee976ff.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/doc/images/Instruct3.png b/source/addons/Todo_Manager/doc/images/Instruct3.png new file mode 100644 index 0000000..283c604 Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/Instruct3.png differ diff --git a/source/addons/Todo_Manager/doc/images/Instruct3.png.import b/source/addons/Todo_Manager/doc/images/Instruct3.png.import new file mode 100644 index 0000000..99fa3cc --- /dev/null +++ b/source/addons/Todo_Manager/doc/images/Instruct3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bmpb4ldb5xhm2" +path="res://.godot/imported/Instruct3.png-3cc62ed99bf29d90b803cb8eb40881e9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Todo_Manager/doc/images/Instruct3.png" +dest_files=["res://.godot/imported/Instruct3.png-3cc62ed99bf29d90b803cb8eb40881e9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/doc/images/Instruct4.png b/source/addons/Todo_Manager/doc/images/Instruct4.png new file mode 100644 index 0000000..66ef1c9 Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/Instruct4.png differ diff --git a/source/addons/Todo_Manager/doc/images/Instruct4.png.import b/source/addons/Todo_Manager/doc/images/Instruct4.png.import new file mode 100644 index 0000000..161ee81 --- /dev/null +++ b/source/addons/Todo_Manager/doc/images/Instruct4.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dtsgcaefwbblx" +path="res://.godot/imported/Instruct4.png-bf5aa1cffc066175cecf9281b0774809.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Todo_Manager/doc/images/Instruct4.png" +dest_files=["res://.godot/imported/Instruct4.png-bf5aa1cffc066175cecf9281b0774809.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/doc/images/Instruct5.png b/source/addons/Todo_Manager/doc/images/Instruct5.png new file mode 100644 index 0000000..d418faf Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/Instruct5.png differ diff --git a/source/addons/Todo_Manager/doc/images/Instruct5.png.import b/source/addons/Todo_Manager/doc/images/Instruct5.png.import new file mode 100644 index 0000000..62ddc49 --- /dev/null +++ b/source/addons/Todo_Manager/doc/images/Instruct5.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bq57uhyrdewef" +path="res://.godot/imported/Instruct5.png-001538ed8b5682dcf232de08035aab38.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Todo_Manager/doc/images/Instruct5.png" +dest_files=["res://.godot/imported/Instruct5.png-001538ed8b5682dcf232de08035aab38.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/doc/images/TODO_Manager_Logo.png b/source/addons/Todo_Manager/doc/images/TODO_Manager_Logo.png new file mode 100644 index 0000000..6d19fee Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/TODO_Manager_Logo.png differ diff --git a/source/addons/Todo_Manager/doc/images/TODO_Manager_Logo.png.import b/source/addons/Todo_Manager/doc/images/TODO_Manager_Logo.png.import new file mode 100644 index 0000000..2e05b81 --- /dev/null +++ b/source/addons/Todo_Manager/doc/images/TODO_Manager_Logo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cnjwqakk07q5g" +path="res://.godot/imported/TODO_Manager_Logo.png-e07d7ec75201c66b732ef87ec1bece15.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Todo_Manager/doc/images/TODO_Manager_Logo.png" +dest_files=["res://.godot/imported/TODO_Manager_Logo.png-e07d7ec75201c66b732ef87ec1bece15.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/doc/images/TodoExternal.gif b/source/addons/Todo_Manager/doc/images/TodoExternal.gif new file mode 100644 index 0000000..25d0850 Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/TodoExternal.gif differ diff --git a/source/addons/Todo_Manager/doc/images/example1.png b/source/addons/Todo_Manager/doc/images/example1.png new file mode 100644 index 0000000..786bd15 Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/example1.png differ diff --git a/source/addons/Todo_Manager/doc/images/example1.png.import b/source/addons/Todo_Manager/doc/images/example1.png.import new file mode 100644 index 0000000..d466d9d --- /dev/null +++ b/source/addons/Todo_Manager/doc/images/example1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dm4238rk6sken" +path="res://.godot/imported/example1.png-6386c332ca46e1e62ea061b956a901cd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Todo_Manager/doc/images/example1.png" +dest_files=["res://.godot/imported/example1.png-6386c332ca46e1e62ea061b956a901cd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/doc/images/example2.png b/source/addons/Todo_Manager/doc/images/example2.png new file mode 100644 index 0000000..256d754 Binary files /dev/null and b/source/addons/Todo_Manager/doc/images/example2.png differ diff --git a/source/addons/Todo_Manager/doc/images/example2.png.import b/source/addons/Todo_Manager/doc/images/example2.png.import new file mode 100644 index 0000000..e5ff3f7 --- /dev/null +++ b/source/addons/Todo_Manager/doc/images/example2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4gc0l6pibggy" +path="res://.godot/imported/example2.png-2e3a8f9cd1e178daf22b83dc0513f37a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Todo_Manager/doc/images/example2.png" +dest_files=["res://.godot/imported/example2.png-2e3a8f9cd1e178daf22b83dc0513f37a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/Todo_Manager/plugin.cfg b/source/addons/Todo_Manager/plugin.cfg new file mode 100644 index 0000000..a546ffc --- /dev/null +++ b/source/addons/Todo_Manager/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Todo Manager" +description="Dock for managing TODO messages and other notes in your code." +author="Peter de Vroom" +version="2.5.0" +script="plugin.gd" diff --git a/source/addons/Todo_Manager/plugin.gd b/source/addons/Todo_Manager/plugin.gd new file mode 100644 index 0000000..09e7885 --- /dev/null +++ b/source/addons/Todo_Manager/plugin.gd @@ -0,0 +1,295 @@ +@tool +extends EditorPlugin + +const DockScene := preload("res://addons/Todo_Manager/UI/Dock.tscn") +const Dock := preload("res://addons/Todo_Manager/Dock.gd") +const Todo := preload("res://addons/Todo_Manager/todo_class.gd") +const TodoItem := preload("res://addons/Todo_Manager/todoItem_class.gd") + +var _dockUI : Dock + +class TodoCacheValue: + var todos: Array + var last_modified_time: int + + func _init(todos: Array, last_modified_time: int): + self.todos = todos + self.last_modified_time = last_modified_time + +var todo_cache : Dictionary # { key: script_path, value: TodoCacheValue } +var remove_queue : Array +var combined_pattern : String +var cased_patterns : Array[String] + +var refresh_lock := false # makes sure _on_filesystem_changed only triggers once + + +func _enter_tree() -> void: + _dockUI = DockScene.instantiate() as Control + add_control_to_bottom_panel(_dockUI, "TODO") + get_editor_interface().get_resource_filesystem().connect("filesystem_changed", + _on_filesystem_changed) + get_editor_interface().get_file_system_dock().connect("file_removed", queue_remove) + get_editor_interface().get_script_editor().connect("editor_script_changed", + _on_active_script_changed) + _dockUI.plugin = self + + var filtered_patterns = _dockUI.patterns.filter(func (p): return p[3] == true) + if filtered_patterns.size() > 0: + combined_pattern = combine_patterns(filtered_patterns) + find_tokens_from_path(find_scripts()) + _dockUI.build_tree() + + +func _exit_tree() -> void: + _dockUI.create_config_file() + remove_control_from_bottom_panel(_dockUI) + _dockUI.free() + + +func queue_remove(file: String): + for i in _dockUI.todo_items.size() - 1: + if _dockUI.todo_items[i].script_path == file: + _dockUI.todo_items.remove_at(i) + + +func find_tokens_from_path(scripts: Array[String]) -> void: + for script_path in scripts: + var file := FileAccess.open(script_path, FileAccess.READ) + var contents := file.get_as_text() + if script_path.ends_with(".tscn"): + handle_built_in_scripts(contents, script_path) + else: + find_tokens(contents, script_path) + + +func handle_built_in_scripts(contents: String, resource_path: String): + var s := contents.split("sub_resource type=\"GDScript\"") + if s.size() <= 1: + return + for i in range(1, s.size()): + var script_components := s[i].split("script/source") + var script_name = script_components[0].substr(5, 14) + find_tokens(script_components[1], resource_path + "::" + script_name) + + +func find_tokens(text: String, script_path: String) -> void: + var cached_todos = get_cached_todos(script_path) + if cached_todos.size() != 0: +# var i := 0 +# for todo_item in _dockUI.todo_items: +# if todo_item.script_path == script_path: +# _dockUI.todo_items.remove_at(i) +# i += 1 + var todo_item := TodoItem.new(script_path, cached_todos) + _dockUI.todo_items.append(todo_item) + else: + var regex = RegEx.new() + # if regex.compile("#\\s*\\bTODO\\b.*|#\\s*\\bHACK\\b.*") == OK: + if regex.compile(combined_pattern) == OK: + var result : Array[RegExMatch] = regex.search_all(text) + if result.is_empty(): + for i in _dockUI.todo_items.size(): + if _dockUI.todo_items[i].script_path == script_path: + _dockUI.todo_items.remove_at(i) + return # No tokens found + var match_found : bool + var i := 0 + for todo_item in _dockUI.todo_items: + if todo_item.script_path == script_path: + match_found = true + var updated_todo_item := update_todo_item(todo_item, result, text, script_path) + _dockUI.todo_items.remove_at(i) + _dockUI.todo_items.insert(i, updated_todo_item) + break + i += 1 + if !match_found: + _dockUI.todo_items.append(create_todo_item(result, text, script_path)) + + +func create_todo_item(regex_results: Array[RegExMatch], text: String, script_path: String) -> TodoItem: + var todo_item = TodoItem.new(script_path, []) + todo_item.script_path = script_path + var last_line_number := 0 + var lines := text.split("\n") + for r in regex_results: + var new_todo : Todo = create_todo(r.get_string(), script_path) + new_todo.line_number = get_line_number(r.get_string(), text, last_line_number) + # GD Multiline comment + var trailing_line := new_todo.line_number + var should_break = false + while trailing_line < lines.size() and lines[trailing_line].dedent().begins_with("#"): + for other_r in regex_results: + if lines[trailing_line] in other_r.get_string(): + should_break = true + break + if should_break: + break + + new_todo.content += "\n" + lines[trailing_line] + trailing_line += 1 + + last_line_number = new_todo.line_number + todo_item.todos.append(new_todo) + cache_todos(todo_item.todos, script_path) + return todo_item + + +func update_todo_item(todo_item: TodoItem, regex_results: Array[RegExMatch], text: String, script_path: String) -> TodoItem: + todo_item.todos.clear() + var lines := text.split("\n") + for r in regex_results: + var new_todo : Todo = create_todo(r.get_string(), script_path) + new_todo.line_number = get_line_number(r.get_string(), text) + # GD Multiline comment + var trailing_line := new_todo.line_number + var should_break = false + while trailing_line < lines.size() and lines[trailing_line].dedent().begins_with("#"): + for other_r in regex_results: + if lines[trailing_line] in other_r.get_string(): + should_break = true + break + if should_break: + break + + new_todo.content += "\n" + lines[trailing_line] + trailing_line += 1 + todo_item.todos.append(new_todo) + return todo_item + + +func get_line_number(what: String, from: String, start := 0) -> int: + what = what.split('\n')[0] # Match first line of multiline C# comments + var temp_array := from.split('\n') + var lines := Array(temp_array) + var line_number# = lines.find(what) + 1 + for i in range(start, lines.size()): + if what in lines[i]: + line_number = i + 1 # +1 to account of 0-based array vs 1-based line numbers + break + else: + line_number = 0 # This is an error + return line_number + + +func _on_filesystem_changed() -> void: + if !refresh_lock: + if _dockUI.auto_refresh: + refresh_lock = true + _dockUI.get_node("Timer").start() + rescan_files(false) + + +func find_scripts() -> Array[String]: + var scripts : Array[String] + var directory_queue : Array[String] + var dir := DirAccess.open("res://") + if dir.get_open_error() == OK: + get_dir_contents(dir, scripts, directory_queue) + else: + printerr("TODO_Manager: There was an error during find_scripts()") + + while not directory_queue.is_empty(): + if dir.change_dir(directory_queue[0]) == OK: + get_dir_contents(dir, scripts, directory_queue) + else: + printerr("TODO_Manager: There was an error at: " + directory_queue[0]) + directory_queue.pop_front() + + return scripts + + +func cache_todos(todos: Array, script_path: String) -> void: + var last_modified_time = FileAccess.get_modified_time(script_path) + todo_cache[script_path] = TodoCacheValue.new(todos, last_modified_time) + + +func get_cached_todos(script_path: String) -> Array: + if todo_cache.has(script_path) and !script_path.contains("tscn::"): + var cached_value: TodoCacheValue = todo_cache[script_path] + if cached_value.last_modified_time == FileAccess.get_modified_time(script_path): + + return cached_value.todos + return [] + +func get_dir_contents(dir: DirAccess, scripts: Array[String], directory_queue: Array[String]) -> void: + dir.include_navigational = false + dir.include_hidden = false + if dir_has_gdignore(dir): + return + dir.list_dir_begin() + var file_name : String = dir.get_next() + + while file_name != "": + if dir.current_is_dir(): + if file_name.begins_with('.'): # Skip folders which should never have scripts + pass + else: + directory_queue.append(dir.get_current_dir().path_join(file_name)) + else: + if file_name.ends_with(".gd") or file_name.ends_with(".cs") \ + or file_name.ends_with(".c") or file_name.ends_with(".cpp") or file_name.ends_with(".h") \ + or ((file_name.ends_with(".tscn") and _dockUI.builtin_enabled)): + scripts.append(dir.get_current_dir().path_join(file_name)) + file_name = dir.get_next() + dir.list_dir_end() + +func dir_has_gdignore(dir: DirAccess) -> bool: + var files = dir.get_files() + return files.has(".gdignore") + +func rescan_files(clear_cache: bool) -> void: + _dockUI.todo_items.clear() + if clear_cache: + todo_cache.clear() + var filtered_patterns = _dockUI.patterns.filter(func (p): return p[3] == true) + + if filtered_patterns.size() > 0: + combined_pattern = combine_patterns(filtered_patterns) + find_tokens_from_path(find_scripts()) + _dockUI.build_tree() + + +func combine_patterns(patterns: Array) -> String: + # Case Sensitivity + cased_patterns = [] + for pattern in patterns: + if pattern[2] == _dockUI.CASE_INSENSITIVE: + cased_patterns.append(pattern[0].insert(0, "((?i)") + ")") + else: + cased_patterns.append("(" + pattern[0] + ")") + + + var pattern_string := "((\\/\\*)|(#|\\/\\/))\\s*(" + for i in range(patterns.size()): + if i == 0: + pattern_string += cased_patterns[i] + else: + pattern_string += "|" + cased_patterns[i] + pattern_string += ")(?(2)[\\s\\S]*?\\*\\/|.*)" + return pattern_string + + +func create_todo(todo_string: String, script_path: String) -> Todo: + var todo := Todo.new() + var regex = RegEx.new() + for pattern in cased_patterns: + if regex.compile(pattern) == OK: + var result : RegExMatch = regex.search(todo_string) + if result: + todo.pattern = pattern + todo.title = result.strings[0] + else: + continue + else: + printerr("Error compiling " + pattern) + + todo.content = todo_string + todo.script_path = script_path + return todo + + +func _on_active_script_changed(script) -> void: + if _dockUI: + if _dockUI.tabs.current_tab == 1: + _dockUI.build_tree() diff --git a/source/addons/Todo_Manager/plugin.gd.uid b/source/addons/Todo_Manager/plugin.gd.uid new file mode 100644 index 0000000..080b98e --- /dev/null +++ b/source/addons/Todo_Manager/plugin.gd.uid @@ -0,0 +1 @@ +uid://cgmj81roq24r8 diff --git a/source/addons/Todo_Manager/todo.cfg b/source/addons/Todo_Manager/todo.cfg new file mode 100644 index 0000000..0c85363 --- /dev/null +++ b/source/addons/Todo_Manager/todo.cfg @@ -0,0 +1,15 @@ +[scripts] + +full_path=false +sort_alphabetical=true +script_colour=Color(0.8, 0.80784315, 0.827451, 1) +ignore_paths=Array[String](["res://addons/"]) + +[patterns] + +patterns=[["\\bTODO\\b", Color(0.5882353, 0.94509804, 0.6784314, 1), 0, true], ["\\bHACK\\b", Color(0.8352941, 0.7372549, 0.4392157, 1), 0, true], ["\\bFIXME\\b", Color(0.8352941, 0.4392157, 0.4392157, 1), 0, true]] + +[config] + +auto_refresh=true +builtin_enabled=false diff --git a/source/addons/Todo_Manager/todoItem_class.gd b/source/addons/Todo_Manager/todoItem_class.gd new file mode 100644 index 0000000..9bcb000 --- /dev/null +++ b/source/addons/Todo_Manager/todoItem_class.gd @@ -0,0 +1,18 @@ +@tool +extends RefCounted + +var script_path : String +var todos : Array + +func _init(script_path: String, todos: Array): + self.script_path = script_path + self.todos = todos + +func get_short_path() -> String: + var temp_array := script_path.rsplit('/', false, 1) + var short_path : String + if not temp_array.size() > 1: + short_path = "(!)" + temp_array[0] + else: + short_path = temp_array[1] + return short_path diff --git a/source/addons/Todo_Manager/todoItem_class.gd.uid b/source/addons/Todo_Manager/todoItem_class.gd.uid new file mode 100644 index 0000000..2088299 --- /dev/null +++ b/source/addons/Todo_Manager/todoItem_class.gd.uid @@ -0,0 +1 @@ +uid://yao1gny5ka0a diff --git a/source/addons/Todo_Manager/todo_class.gd b/source/addons/Todo_Manager/todo_class.gd new file mode 100644 index 0000000..00b894e --- /dev/null +++ b/source/addons/Todo_Manager/todo_class.gd @@ -0,0 +1,8 @@ +@tool +extends RefCounted + +var pattern : String +var title : String +var content : String +var script_path : String +var line_number : int diff --git a/source/addons/Todo_Manager/todo_class.gd.uid b/source/addons/Todo_Manager/todo_class.gd.uid new file mode 100644 index 0000000..2ca5826 --- /dev/null +++ b/source/addons/Todo_Manager/todo_class.gd.uid @@ -0,0 +1 @@ +uid://cvxelosp5shwf diff --git a/source/addons/debug_draw_3d/libs/~libdd3d.windows.editor.x86_64.dll b/source/addons/debug_draw_3d/libs/~libdd3d.windows.editor.x86_64.dll deleted file mode 100644 index 497a0a7..0000000 Binary files a/source/addons/debug_draw_3d/libs/~libdd3d.windows.editor.x86_64.dll and /dev/null differ diff --git a/source/addons/godot_3d_cursor/3d_cursor.gd b/source/addons/godot_3d_cursor/3d_cursor.gd new file mode 100644 index 0000000..bb2e341 --- /dev/null +++ b/source/addons/godot_3d_cursor/3d_cursor.gd @@ -0,0 +1,44 @@ +@tool +class_name Cursor3D +extends Marker3D + +## The size of the 3D Cursor within your scene +@export var size_scale: float = 1.0 + +@export_group("Label Settings") +## This setting decides whether the label with the text '3D Cursor' should +## be displayed +@export var show_label: bool = true +## This setting decides whether the label should scale with the selected size +## of the 3D Cursor. +@export var scale_affect_label: bool = false + +# The standard scale of the 3D Cursor. This size is chosen because of the +# size of the .png used for the cursor. Please don't touch (private var) +var _scale: float = 0.25 + +## The sprite of the 3D Cursor +@onready var sprite_3d: Sprite3D = $Sprite3D +## The label of the 3D Cursor +@onready var label_3d: Label3D = $Sprite3D/Label3D + + +func _process(delta: float) -> void: + if not Engine.is_editor_hint(): + hide() + + # No manual user input allowed on rotation and scale; + # Reset any user input to 0 or 1 respectively + rotation = Vector3.ZERO + scale = Vector3.ONE + + # Show the label if desired + label_3d.visible = show_label + + # Set the scale of the 3D Cursor + sprite_3d.scale = Vector3(_scale * size_scale, _scale * size_scale, _scale * size_scale) + if scale_affect_label: + label_3d.scale = Vector3.ONE * 4 + else: + var label_scale = 1 / (_scale * size_scale) + label_3d.scale = Vector3(label_scale, label_scale, label_scale) diff --git a/source/addons/godot_3d_cursor/3d_cursor.gd.uid b/source/addons/godot_3d_cursor/3d_cursor.gd.uid new file mode 100644 index 0000000..6e8890b --- /dev/null +++ b/source/addons/godot_3d_cursor/3d_cursor.gd.uid @@ -0,0 +1 @@ +uid://dt04kfvdc21nb diff --git a/source/addons/godot_3d_cursor/3d_cursor.png b/source/addons/godot_3d_cursor/3d_cursor.png new file mode 100644 index 0000000..412a006 Binary files /dev/null and b/source/addons/godot_3d_cursor/3d_cursor.png differ diff --git a/source/addons/godot_3d_cursor/3d_cursor.png.import b/source/addons/godot_3d_cursor/3d_cursor.png.import new file mode 100644 index 0000000..5f0f5cb --- /dev/null +++ b/source/addons/godot_3d_cursor/3d_cursor.png.import @@ -0,0 +1,41 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdqg3y11tiold" +path.s3tc="res://.godot/imported/3d_cursor.png-3f509e9f2ec38702e6618d6c6cc63f4d.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/godot_3d_cursor/3d_cursor.png" +dest_files=["res://.godot/imported/3d_cursor.png-3f509e9f2ec38702e6618d6c6cc63f4d.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=0 diff --git a/source/addons/godot_3d_cursor/3d_cursor.tscn b/source/addons/godot_3d_cursor/3d_cursor.tscn new file mode 100644 index 0000000..04f720f --- /dev/null +++ b/source/addons/godot_3d_cursor/3d_cursor.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=3 format=3 uid="uid://dfpatff4d5okj"] + +[ext_resource type="Texture2D" uid="uid://brmxsn2cjdqw4" path="res://addons/godot_3d_cursor/3d_cursor.png" id="2_mnq4l"] +[ext_resource type="Script" uid="uid://dt04kfvdc21nb" path="res://addons/godot_3d_cursor/3d_cursor.gd" id="2_stis0"] + +[node name="3DCursor" type="Marker3D"] +script = ExtResource("2_stis0") + +[node name="Sprite3D" type="Sprite3D" parent="."] +transform = Transform3D(0.25, 0, 0, 0, 0.25, 0, 0, 0, 0.25, 0, 0, 0) +billboard = 1 +no_depth_test = true +texture = ExtResource("2_mnq4l") + +[node name="Label3D" type="Label3D" parent="Sprite3D"] +transform = Transform3D(4, 0, 0, 0, 4, 0, 0, 0, 4, 0, 2.4, 0) +billboard = 1 +no_depth_test = true +text = "3D Cursor" diff --git a/source/addons/godot_3d_cursor/pie_menu.gd b/source/addons/godot_3d_cursor/pie_menu.gd new file mode 100644 index 0000000..75781f3 --- /dev/null +++ b/source/addons/godot_3d_cursor/pie_menu.gd @@ -0,0 +1,228 @@ +@tool +class_name PieMenu +extends Control + +## Emitted when the "3D Cursor to Origin" command is invoked through the [PieMenu] +signal cursor_to_origin_pressed +## Emitted when the "3D Cursor to Selected Object(s)" command is invoked through the [PieMenu] +signal cursor_to_selected_objects_pressed +## Emitted when the "Selected Object to 3D Cursor" command is invoked through the [PieMenu] +signal selected_object_to_cursor_pressed +## Emitted when the "Remove 3D Cursor from Scene" command is invoked through the [PieMenu] +signal remove_cursor_from_scene_pressed +## Emitted when the "Toggle 3D Cursor" command is invoked through the [PieMenu] +signal toggle_cursor_pressed + +## The dimmed color for the selection indicator that is used if no button is hovered +const _dimmed_selection_indicator_color: Color = Color("8c8c8cFF") + +## The value at which the buttons start to animate/slide if the menu is shown +var slide_start: int = 0 +## The position the buttons animate/slide to +var slide_end: int = 100 +## The radius of the menu. The buttons are aligned around an invisible circle +## and this is the corresponding radius. +var menu_radius: int = slide_start +## The buttons that are "loaded" +var buttons: Array[Button] = [] + +var _hovered_button: Button +var _show_menu_echo: bool = false + +@onready var selection_indicator: Sprite2D = $SelectionIndicator +@onready var toggle_3d_cursor: Button = $Toggle3DCursor + + +func _process(delta: float) -> void: + #var viewport_height: int = get_viewport().size.y + #var viewport_width: int = get_viewport().size.x + + # If the menu is shown animate the buttons + if visible: + menu_radius = lerp(menu_radius, slide_end, 20 * delta) + + # Reset the button positions when the menu is hidden + if not visible: + menu_radius = slide_start + _show_menu_echo = true + + _align_buttons() + + # Load all children from the pie menu + var children = get_children() + # Get all the children that are buttons if there are no new button return + if children.filter(_is_button) == buttons: + return + + # If there are new buttons repopulate the buttons list and display them + buttons.clear() + for button: Button in children.filter(_is_button): + buttons.append(button) + + _align_buttons() + + +func _input(event: InputEvent) -> void: + if not is_visible_in_tree(): + return + + # If the [PieMenu] is opened the selection indicator will rotate according + # to the mouse position. If the user hovers over a button the indicator + # will change its color to white and a more dimmed color otherwise + if event is InputEventMouseMotion: + # Calculate the angle of the mouse position to the x axis + var rot: float = rad_to_deg(get_local_mouse_position().angle_to(Vector2.RIGHT)) - 45 + # Apply the rotation to the indicator + selection_indicator.rotation_degrees = -rot + + # If the key used to open the [PieMenu] is held down while selecting hovering + # over a button the user can invoke the buttons action by releasing the + # key (s). This does not work if the key was released prior to hovering + # over a button. This functionality is similar to the one in Blender + if not event is InputEventKey: + return + + if not event.keycode == KEY_S: + return + + if event.is_released() and _hovered_button == null: + _show_menu_echo = false + return + + if _show_menu_echo and event.is_released(): + _hovered_button.pressed.emit() + + +## This method should be used in conjuncton with a [Array.filter] method. +## It checks whether a node inherits from Button +func _is_button(child: Node) -> bool: + return child is Button + + +## This method aligns the available buttons in a circular menu by using +## some [sin] and [cos] magic +func _align_buttons() -> void: + var button_count: int = len(buttons) + for i in range(button_count): + var button: Button = buttons[i] + var theta: float = (i / float(button_count)) * TAU + var x: float = (menu_radius * cos(theta)) + var y: float = (menu_radius * sin(theta)) - button.size.y / 2.0 + x = x - button.size.x if x < 0 else x + button.position = Vector2(x, y) + + +## Connected to the corresponding UI button this method acts as a repeater +## by emitting the corresponding signal classes can listen to via a [PieMenu] +## instance +func _on_3d_cursor_to_origin_pressed() -> void: + hide() + cursor_to_origin_pressed.emit() + +## Executes when the "3D Cursor to Origin" button is hovered +func _on_3d_cursor_to_origin_mouse_entered() -> void: + _hovered_button = $"3DCursorToOrigin" + _on_mouse_entered_button() + +## Executes when the "3D Cursor to Origin" button is no longer hovered +func _on_3d_cursor_to_origin_mouse_exited() -> void: + _hovered_button = null + _on_mouse_exited_button() + + +## Connected to the corresponding UI button this method acts as a repeater +## by emitting the corresponding signal classes can listen to via a [PieMenu] +## instance +func _on_3d_cursor_to_selected_objects_pressed() -> void: + hide() + cursor_to_selected_objects_pressed.emit() + +## Executes when the "3D Cursor to Selected Object(s)" button is hovered +func _on_3d_cursor_to_selected_objects_mouse_entered() -> void: + _hovered_button = $"3DCursorToSelectedObjects" + _on_mouse_entered_button() + +## Executes when the "3D Cursor to Selected Object(s)" button is no longer hovered +func _on_3d_cursor_to_selected_objects_mouse_exited() -> void: + _hovered_button = null + _on_mouse_exited_button() + + +## Connected to the corresponding UI button this method acts as a repeater +## by emitting the corresponding signal classes can listen to via a [PieMenu] +## instance +func _on_selected_object_to_3d_cursor_pressed() -> void: + hide() + selected_object_to_cursor_pressed.emit() + +## Executes when the "Selected Object to 3D Cursor" button is hovered +func _on_selected_object_to_3d_cursor_mouse_entered() -> void: + _hovered_button = $SelectedObjectTo3DCursor + _on_mouse_entered_button() + +## Executes when the "Selected Object to 3D Cursor" button is no longer hovered +func _on_selected_object_to_3d_cursor_mouse_exited() -> void: + _hovered_button = null + _on_mouse_exited_button() + + +## Connected to the corresponding UI button this method acts as a repeater +## by emitting the corresponding signal classes can listen to via a [PieMenu] +## instance +func _on_remove_3d_cursor_from_scene_pressed() -> void: + hide() + remove_cursor_from_scene_pressed.emit() + +## Executes when the "Remove 3D Cursor" button is hovered +func _on_remove_3d_cursor_from_scene_mouse_entered() -> void: + _hovered_button = $Remove3DCursorFromScene + _on_mouse_entered_button() + +## Executes when the "Remove 3D Cursor" button is no longer hovered +func _on_remove_3d_cursor_from_scene_mouse_exited() -> void: + _hovered_button = null + _on_mouse_exited_button() + + +## Connected to the corresponding UI button this method acts as a repeater +## by emitting the corresponding signal classes can listen to via a [PieMenu] +## instance +func _on_toggle_3d_cursor_pressed() -> void: + hide() + toggle_cursor_pressed.emit() + +## Executes when the "Disable/Enable 3D Cursor" button is hovered +func _on_toggle_3d_cursor_mouse_entered() -> void: + _hovered_button = toggle_3d_cursor + _on_mouse_entered_button() + +## Executes when the "Disable/Enable 3D Cursor" button is hovered +func _on_toggle_3d_cursor_mouse_exited() -> void: + _hovered_button = null + _on_mouse_exited_button() + + +## This method is executed by every button of the [PieMenu]. It brightens +## the color of the selection indicator when a button is hovered. +func _on_mouse_entered_button() -> void: + selection_indicator.modulate = Color.WHITE + +## This method is executed by every button of the [PieMenu]. It dims the color +## of the selection indicator when a button is no longer hovered +func _on_mouse_exited_button() -> void: + selection_indicator.modulate = _dimmed_selection_indicator_color + + +## This method is a little helper that is used to prevent some quirky behaviour +## with the consumption of events. It checks whether the user clicked on a +## button rather than the space around it +func hit_any_button() -> bool: + var mouse_position: Vector2 = get_global_mouse_position() + for button in buttons: + if button.get_global_rect().has_point(mouse_position): + return true + return false + + +func change_toggle_label(visible: bool) -> void: + toggle_3d_cursor.text = ("Disable" if visible else "Enable") + " 3D Cursor" diff --git a/source/addons/godot_3d_cursor/pie_menu.gd.uid b/source/addons/godot_3d_cursor/pie_menu.gd.uid new file mode 100644 index 0000000..16df97f --- /dev/null +++ b/source/addons/godot_3d_cursor/pie_menu.gd.uid @@ -0,0 +1 @@ +uid://dul31m8bwxlxm diff --git a/source/addons/godot_3d_cursor/pie_menu.tscn b/source/addons/godot_3d_cursor/pie_menu.tscn new file mode 100644 index 0000000..27d3ae5 --- /dev/null +++ b/source/addons/godot_3d_cursor/pie_menu.tscn @@ -0,0 +1,119 @@ +[gd_scene load_steps=5 format=3 uid="uid://igrlue2n5478"] + +[ext_resource type="Script" uid="uid://dul31m8bwxlxm" path="res://addons/godot_3d_cursor/pie_menu.gd" id="1_a7cko"] +[ext_resource type="Texture2D" uid="uid://bhift5k0v2tga" path="res://addons/godot_3d_cursor/selection_ring.png" id="2_k5qvu"] +[ext_resource type="Texture2D" uid="uid://bngw0w7d1nrgv" path="res://addons/godot_3d_cursor/selection_indicator.png" id="3_euipm"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s5fna"] +bg_color = Color(0.145098, 0.145098, 0.145098, 0.258824) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[node name="PieMenu" type="Control"] +layout_mode = 3 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +script = ExtResource("1_a7cko") + +[node name="Panel" type="Panel" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -347.5 +offset_top = -145.0 +offset_right = 379.5 +offset_bottom = 145.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_s5fna") + +[node name="3DCursorToSelectedObjects" type="Button" parent="."] +layout_mode = 0 +offset_left = 100.0 +offset_top = -15.5 +offset_right = 347.0 +offset_bottom = 15.5 +mouse_default_cursor_shape = 2 +text = "3D Cursor to Selected Object(s)" + +[node name="Remove3DCursorFromScene" type="Button" parent="."] +layout_mode = 0 +offset_left = 30.9017 +offset_top = 79.6054 +offset_right = 273.902 +offset_bottom = 110.606 +mouse_default_cursor_shape = 2 +text = "Remove 3D Cursor from Scene" + +[node name="SelectedObjectTo3DCursor" type="Button" parent="."] +layout_mode = 0 +offset_left = -309.902 +offset_top = 43.2785 +offset_right = -80.9017 +offset_bottom = 74.2785 +mouse_default_cursor_shape = 2 +text = "Selected Object to 3D Cursor" + +[node name="3DCursorToOrigin" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -237.902 +offset_top = -74.2785 +offset_right = -80.9017 +offset_bottom = -43.2785 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 0 +mouse_default_cursor_shape = 2 +text = "3D Cursor to Origin" + +[node name="Toggle3DCursor" type="Button" parent="."] +layout_mode = 0 +offset_left = 30.9017 +offset_top = -110.606 +offset_right = 176.902 +offset_bottom = -79.6055 +mouse_default_cursor_shape = 2 +text = "Disable 3D Cursor" + +[node name="SelectionRing" type="Sprite2D" parent="."] +modulate = Color(0.148438, 0.148438, 0.148438, 1) +scale = Vector2(0.2, 0.2) +texture = ExtResource("2_k5qvu") + +[node name="SelectionIndicator" type="Sprite2D" parent="."] +modulate = Color(0.550781, 0.550781, 0.550781, 1) +scale = Vector2(0.2, 0.2) +texture = ExtResource("3_euipm") + +[connection signal="mouse_entered" from="3DCursorToSelectedObjects" to="." method="_on_3d_cursor_to_selected_objects_mouse_entered"] +[connection signal="mouse_exited" from="3DCursorToSelectedObjects" to="." method="_on_3d_cursor_to_selected_objects_mouse_exited"] +[connection signal="pressed" from="3DCursorToSelectedObjects" to="." method="_on_3d_cursor_to_selected_objects_pressed"] +[connection signal="mouse_entered" from="Remove3DCursorFromScene" to="." method="_on_remove_3d_cursor_from_scene_mouse_entered"] +[connection signal="mouse_exited" from="Remove3DCursorFromScene" to="." method="_on_remove_3d_cursor_from_scene_mouse_exited"] +[connection signal="pressed" from="Remove3DCursorFromScene" to="." method="_on_remove_3d_cursor_from_scene_pressed"] +[connection signal="mouse_entered" from="SelectedObjectTo3DCursor" to="." method="_on_selected_object_to_3d_cursor_mouse_entered"] +[connection signal="mouse_exited" from="SelectedObjectTo3DCursor" to="." method="_on_selected_object_to_3d_cursor_mouse_exited"] +[connection signal="pressed" from="SelectedObjectTo3DCursor" to="." method="_on_selected_object_to_3d_cursor_pressed"] +[connection signal="mouse_entered" from="3DCursorToOrigin" to="." method="_on_3d_cursor_to_origin_mouse_entered"] +[connection signal="mouse_exited" from="3DCursorToOrigin" to="." method="_on_3d_cursor_to_origin_mouse_exited"] +[connection signal="pressed" from="3DCursorToOrigin" to="." method="_on_3d_cursor_to_origin_pressed"] +[connection signal="mouse_entered" from="Toggle3DCursor" to="." method="_on_toggle_3d_cursor_mouse_entered"] +[connection signal="mouse_exited" from="Toggle3DCursor" to="." method="_on_toggle_3d_cursor_mouse_exited"] +[connection signal="pressed" from="Toggle3DCursor" to="." method="_on_toggle_3d_cursor_pressed"] diff --git a/source/addons/godot_3d_cursor/plugin.cfg b/source/addons/godot_3d_cursor/plugin.cfg new file mode 100644 index 0000000..83e5030 --- /dev/null +++ b/source/addons/godot_3d_cursor/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Godot 3D Cursor" +description="This plugin for the Godot Engine implements a 3D cursor, offering functionality similar to Blender’s 3D cursor.\n\nNote: Due to certain engine limitations, you must switch to a tab other than 3D (e.g., 2D, Script, or AssetLib) and back to 3D once after enabling this plugin, and also upon engine startup, to activate the plugin correctly.\n\nUsage: You can set the 3D cursor's position by using Shift + Right Click.\n\nVersion 1.2: The Pie Menu / Undo-Redo Update\n\nIn this update, I’ve added a Pie Menu, similar to Blender’s, which can be invoked with Shift + S. This menu displays commands found in the Command Palette (Ctrl + Shift + P) and offers faster access to desired functionality.\n\nAnother major feature in this version is the Undo-Redo functionality. This should be straightforward, except for the ‘Remove 3D Cursor from Scene’ command, which is intentionally not undoable, as it frees the cursor object itself." +author="Marco" +version="1.3.2" +script="plugin.gd" diff --git a/source/addons/godot_3d_cursor/plugin.gd b/source/addons/godot_3d_cursor/plugin.gd new file mode 100644 index 0000000..af40d6d --- /dev/null +++ b/source/addons/godot_3d_cursor/plugin.gd @@ -0,0 +1,488 @@ +@tool +extends EditorPlugin + + +## This variable indicates whether the active tab is 3D +var is_in_3d_tab: bool = false +## The position of the mouse used to raycast into the 3D world +var mouse_position: Vector2 +## The camera used for raycasting to calculate the position +## for the 3D cursor +var temp_camera: Camera3D +## The Editor Viewport used to get the mouse position +var editor_viewport: SubViewport +## The camera that displays what the user sees in the 3D editor tab +var editor_camera: Camera3D +## The root node of the active scene +var edited_scene_root: Node +## The scene used to instantiate the 3D Cursor +var cursor_scene: PackedScene +## The instance of the 3D Cursor +var cursor: Cursor3D +## The scene used to instantiate the pie menu for the 3D Cursor +var pie_menu_scene: PackedScene +## The instance of the pie menu for the 3D Cursor +var pie_menu: PieMenu +## A reference to the [EditorCommandPalette] singleton used to add +## some useful actions to the command palette such as '3D Cursor to Origin' +## or '3D Cursor to selected object' like in Blender +var command_palette: EditorCommandPalette +## The InputEvent holding the MouseButton event to trigger the +## set position function of the 3D Cursor +var input_event_set_3d_cursor: InputEventMouseButton +var input_event_show_pie_menu: InputEventKey +## The boolean that ensures the _recover_cursor function is executed once +var cursor_set: bool = false +## The instance of the Undo Redo class +var undo_redo: EditorUndoRedoManager + + +func _enter_tree() -> void: + # Register the switching of tabs in the editor. We only want the + # 3D Cursor functionality within the 3D tab + connect("main_screen_changed", _on_main_scene_changed) + # We want to place newly added Nodes that inherit [Node3D] at + # the location of the 3D Cursor. Therefore we listen to the + # node_added event + get_tree().connect("node_added", _on_node_added) + + # Loading the 3D Cursor scene for later instancing + cursor_scene = preload("res://addons/godot_3d_cursor/3d_cursor.tscn") + pie_menu_scene = preload("res://addons/godot_3d_cursor/pie_menu.tscn") + + command_palette = EditorInterface.get_command_palette() + # Adding the previously mentioned actions + command_palette.add_command("3D Cursor to Origin", "3D Cursor/3D Cursor to Origin", _3d_cursor_to_origin) + command_palette.add_command("3D Cursor to Selected Object", "3D Cursor/3D Cursor to Selected Object", _3d_cursor_to_selected_objects) + command_palette.add_command("Selected Object to 3D Cursor", "3D Cursor/Selected Object to 3D Cursor", _selected_object_to_3d_cursor) + # Adding the remove 3D Cursor in Scene action + command_palette.add_command("Remove 3D Cursor from Scene", "3D Cursor/Remove 3D Cursor from Scene", _remove_3d_cursor_from_scene) + command_palette.add_command("Toggle 3D Cursor", "3D Cursor/Toggle 3D Cursor", _toggle_3d_cursor) + + editor_viewport = EditorInterface.get_editor_viewport_3d() + editor_camera = editor_viewport.get_camera_3d() + + # Get the reference to the UndoRedo instance of the editor + undo_redo = get_undo_redo() + + # Instantiating the pie menu for the 3D Cursor commands + pie_menu = pie_menu_scene.instantiate() + pie_menu.hide() + # Connecting the button events from the pie menu to the corresponding function + pie_menu.connect("cursor_to_origin_pressed", _3d_cursor_to_origin) + pie_menu.connect("cursor_to_selected_objects_pressed", _3d_cursor_to_selected_objects) + pie_menu.connect("selected_object_to_cursor_pressed", _selected_object_to_3d_cursor) + pie_menu.connect("remove_cursor_from_scene_pressed", _remove_3d_cursor_from_scene) + pie_menu.connect("toggle_cursor_pressed", _toggle_3d_cursor) + add_child(pie_menu) + + + # Setting up the InputMap so that we can set the 3D Cursor + # by Shift + Right Click + if not InputMap.has_action("3d_cursor_set_location"): + InputMap.add_action("3d_cursor_set_location") + input_event_set_3d_cursor = InputEventMouseButton.new() + input_event_set_3d_cursor.button_index = MOUSE_BUTTON_RIGHT + InputMap.action_add_event("3d_cursor_set_location", input_event_set_3d_cursor) + + # Adding the action that shows the pie menu for the 3D Cursor commands. + if not InputMap.has_action("3d_cursor_show_pie_menu"): + InputMap.add_action("3d_cursor_show_pie_menu") + input_event_show_pie_menu = InputEventKey.new() + input_event_show_pie_menu.keycode = KEY_S + InputMap.action_add_event("3d_cursor_show_pie_menu", input_event_show_pie_menu) + + + +func _exit_tree() -> void: + # Removing listeners + disconnect("main_screen_changed", _on_main_scene_changed) + get_tree().disconnect("node_added", _on_node_added) + + pie_menu.disconnect("cursor_to_origin_pressed", _3d_cursor_to_origin) + pie_menu.disconnect("cursor_to_selected_objects_pressed", _3d_cursor_to_selected_objects) + pie_menu.disconnect("selected_object_to_cursor_pressed", _selected_object_to_3d_cursor) + pie_menu.disconnect("remove_cursor_from_scene_pressed", _remove_3d_cursor_from_scene) + pie_menu.disconnect("toggle_cursor_pressed", _toggle_3d_cursor) + + # Removing the actions from the [EditorCommandPalette] + command_palette.remove_command("3D Cursor/3D Cursor to Origin") + command_palette.remove_command("3D Cursor/3D Cursor to Selected Object") + command_palette.remove_command("3D Cursor/Selected Object to 3D Cursor") + command_palette.remove_command("3D Cursor/Remove 3D Cursor from Scene") + command_palette.remove_command("3D Cursor/Toggle 3D Cursor") + command_palette = null + + # Removing the '3D Cursor set Location' action from the InputMap + if InputMap.has_action("3d_cursor_set_location"): + InputMap.action_erase_event("3d_cursor_set_location", input_event_set_3d_cursor) + InputMap.erase_action("3d_cursor_set_location") + + # Removing the 'Show Pie Menu' action from the InputMap + if InputMap.has_action("3d_cursor_show_pie_menu"): + InputMap.action_erase_event("3d_cursor_show_pie_menu", input_event_show_pie_menu) + InputMap.erase_action("3d_cursor_show_pie_menu") + + # Removing and freeing the helper objects + if temp_camera != null and editor_viewport != null: + editor_viewport.remove_child(temp_camera) + temp_camera.queue_free() + + # Deleting the 3D Cursor + if cursor != null: + cursor.queue_free() + cursor_scene = null + + # Deleting the pie menu + if pie_menu != null: + pie_menu.queue_free() + pie_menu_scene = null + + +func _process(delta: float) -> void: + # Only allow setting the 3D Cursors location in 3D tab + if not is_in_3d_tab: + return + + # If the action is not yet set up: return + if not InputMap.has_action("3d_cursor_set_location"): + return + + # Set the location of the 3D Cursor + if Input.is_key_pressed(KEY_SHIFT) and Input.is_action_just_pressed("3d_cursor_set_location"): + mouse_position = editor_viewport.get_mouse_position() + _get_selection() + + if cursor == null or not cursor.is_inside_tree(): + return + + if Input.is_key_pressed(KEY_SHIFT) and Input.is_action_just_pressed("3d_cursor_show_pie_menu"): + pie_menu.visible = not pie_menu.visible + _set_visibility_toggle_label() + + +func _input(event: InputEvent) -> void: + if event.is_released(): + return + + if not pie_menu.visible: + return + + if pie_menu.hit_any_button(): + return + + if event is InputEventKey and event.keycode == KEY_S and event.is_echo(): + return + + if event is InputEventKey or event is InputEventMouseButton: + pie_menu.hide() + # CAUTION: Do not mess with this statement! It can render your editor + # responseless. If it happens remove the plugin and restart the engine. + editor_viewport.set_input_as_handled() + + +## Checks whether the current active tab is named '3D' +## returns true if so, otherwise false +func _on_main_scene_changed(screen_name: String) -> void: + is_in_3d_tab = screen_name == "3D" + + +## Connected to the node_added event of the get_tree() +func _on_node_added(node: Node) -> void: + if not _cursor_available(): + return + if EditorInterface.get_edited_scene_root() != cursor.owner: + return + if node.name == cursor.name: + return + if cursor.is_ancestor_of(node): + return + if not node is Node3D: + return + # Apply the position of the new node to the 3D Cursors position if the + # 3D cursor is available, the node is not the 3D cursor itself, the node + # is no descendant of the 3D Cursor and the node inherits [Node3D] + node.global_position = cursor.global_position + + +## Set the postion of the 3D Cursor to the origin (or [Vector3.ZERO]) +func _3d_cursor_to_origin() -> void: + if not _cursor_available(): + return + + _create_undo_redo_action( + cursor, + "global_position", + Vector3.ZERO, + "Move 3D Cursor to Origin", + ) + + +## Set the position of the 3D Cursor to the selected object and if multiple +## Nodes are selected to the average of the positions of all selected nodes +## that inherit [Node3D] +func _3d_cursor_to_selected_objects() -> void: + if not _cursor_available(): + return + + # Get the selection and through this the selected nodes as an Array of Nodes + var selection: EditorSelection = EditorInterface.get_selection() + var selected_nodes: Array[Node] = selection.get_selected_nodes() + + if selected_nodes.is_empty(): + return + if selected_nodes.size() == 1 and not selected_nodes.front() is Node3D: + return + + # If only one Node is selected and it inherits Node3D set the position + # of the 3D Cursor to its position + if selected_nodes.size() == 1: + _create_undo_redo_action( + cursor, + "global_position", + selected_nodes.front().global_position, + "Move 3D Cursor to selected Object", + ) + return + + # Introduce a count variable to keep track of the amount of valid positions + # to calculate the average position later + var count = 0 + var position_sum: Vector3 = Vector3.ZERO + + for node in selected_nodes: + if not (node is Node3D or node is Cursor3D): + continue + + # If the node is a valid object increment count and add the position + # to position_sum + count += 1 + position_sum += node.global_position + + if count == 0: + return + + # Calculate the average position for multiple selected Nodes and set + # the 3D Cursor to this position + var average_position = position_sum / count + _create_undo_redo_action( + cursor, + "global_position", + average_position, + "Move 3D Cursor to selected Objects", + ) + cursor.global_position = average_position + + +## Set the position of the selected object that inherits [Node3D] +## to the position of the 3D Cursor. If multiple nodes are selected the first +## valid node (i.e. a node that inherits [Node3D]) will be moved to +## position of the 3D Cursor. This funcitonality is disabled if the cursor +## is not set or hidden in the scene. +func _selected_object_to_3d_cursor() -> void: + if not _cursor_available(): + return + + # Get the selection and through this the selected nodes as an Array of Nodes + var selection: EditorSelection = EditorInterface.get_selection() + var selected_nodes: Array[Node] = selection.get_selected_nodes() + + if selected_nodes.is_empty(): + return + if selected_nodes.size() == 1 and not selected_nodes.front() is Node3D: + return + selected_nodes = selected_nodes.filter(func(node): return node is Node3D and not node is Cursor3D) + if selected_nodes.is_empty(): + return + + _create_undo_redo_action( + selected_nodes.front(), + "global_position", + cursor.global_position, + "Move Object to 3D Cursor" + ) + + +## Disable the 3D Cursor to prevent the node placement at the position of +## the 3D Cursor. +func _toggle_3d_cursor() -> void: + if not _cursor_available(true): + return + + cursor.visible = not cursor.visible + _set_visibility_toggle_label() + + +## Sets the correct label on the toggle visibility button in the pie menu +func _set_visibility_toggle_label() -> void: + pie_menu.change_toggle_label(cursor.visible) + + +## Remove every 3D Cursor from the scene including the active one. +func _remove_3d_cursor_from_scene() -> void: + if cursor == null: + return + + # Remove the active 3D Cursor + cursor.queue_free() + cursor = null + + # Get the root nodes children to filter for old instances of [Cursor3D] + var root_children = edited_scene_root.get_children() + if root_children.any(func(node): return node is Cursor3D): + # Iterate over all old instances and free them + for old_cursor: Cursor3D in root_children.filter(func(node): return node is Cursor3D): + old_cursor.queue_free() + + +## Check whether the 3D Cursor is set up and ready for use. A hidden 3D Cursor +## should also disable its functionality. Therefore this function yields false +## if the cursor is hidden in the scene +func _cursor_available(ignore_hidden = false) -> bool: + # CAUTION: Do not mess with this statement! It can render your editor + # responseless. If it happens remove the plugin and restart the engine. + editor_viewport.set_input_as_handled() + if cursor == null: + return false + if not cursor.is_inside_tree(): + return false + if ignore_hidden and not cursor.is_visible_in_tree(): + return true + if not cursor.is_visible_in_tree(): + return false + return true + + +## This function uses raycasting to determine the position of the mouse click +## to set the position of the 3D Cursor. This means that it is necessary for +## the clicked on objects to have a collider the raycast can hit +func _get_selection() -> void: + # If the scene is switched stop + if edited_scene_root != null and edited_scene_root != EditorInterface.get_edited_scene_root() and cursor != null: + # Reset scene root, viewport and camera for new scene + edited_scene_root = null + editor_viewport = EditorInterface.get_editor_viewport_3d() + editor_camera = editor_viewport.get_camera_3d() + + # Clear the 3D Cursor on the old screen. + cursor.queue_free() + cursor = null + + if not cursor_set: + _recover_cursor() + + if temp_camera == null: + # Set up the temp_camera to resemble the one of the 3D Viewport + _create_temp_camera() + + # Get the transform of the camera from the 3D Viewport + var editor_camera_transform = _get_editor_camera_transform() + + # Position the temp_camera so that it is exactly where the 3D Viewport + # camera is located + temp_camera.global_transform = editor_camera_transform + + # if the editor_camera_transform is Transform3D.IDENTITY that means + # that for some reason the editor_camera is null. + if editor_camera_transform == Transform3D.IDENTITY: + return + + # Set up the raycast parameters + var ray_origin = temp_camera.project_ray_origin(mouse_position) + var ray_end = temp_camera.project_position(mouse_position, 1000) + var ray_length = 1000 + + if edited_scene_root == null: + edited_scene_root = EditorInterface.get_edited_scene_root() + + # The space state where the raycast should be performed in + var space_state = edited_scene_root.get_world_3d().direct_space_state + + # Perform a raycast with the parameters above and store the result + var result = space_state.intersect_ray(PhysicsRayQueryParameters3D.create(ray_origin, ray_end)) + + var just_created: bool = false + + # When the cursor is not yet created instantiate it, add it to the scene + # and position it at the collision detected by the raycast + if cursor == null: + cursor = cursor_scene.instantiate() + edited_scene_root.add_child(cursor) + cursor.owner = edited_scene_root + just_created = true + + # If the cursor is not in the node tree at this point it means that the + # user probably deleted it. Then add it again + if not cursor.is_inside_tree(): + edited_scene_root.add_child(cursor) + cursor.owner = edited_scene_root + just_created = true + + # No collision means do nothing + if result.is_empty(): + return + + if just_created: + # Position the 3D Cursor to the position of the collision + cursor.global_transform.origin = result.position + return + + _create_undo_redo_action( + cursor, + "global_position", + result.position, + "Set Position for 3D Cursor" + ) + + +## This function creates the temp_camera and sets it up so that it resembles +## the camera from 3D Tab itself +func _create_temp_camera() -> void: + temp_camera = Camera3D.new() + temp_camera.hide() + + # Add the temp_camera to the editor_viewport so that we can perform raycasts + # later on + editor_viewport.add_child(temp_camera) + + # These are the most important settings the temp_camera needs to copy + # from the editor_camera so that their image is congruent + temp_camera.fov = editor_camera.fov + temp_camera.near = editor_camera.near + temp_camera.far = editor_camera.far + + +## This function returns the transform of the camera from the 3D Editor itself +func _get_editor_camera_transform() -> Transform3D: + if editor_camera != null: + return editor_camera.get_camera_transform() + return Transform3D.IDENTITY + + +## This function recovers any 3D Cursor present in the scene if you reload +## the project +func _recover_cursor() -> void: + # This boolean ensures this function is run exactly once + cursor_set = true + # Gets the children of the active scenes root node + var root_children = EditorInterface.get_edited_scene_root().get_children() + # Checks whether there are any nodes of type [Cursor3D] in the list of + # children + if root_children.any(func(node): return node is Cursor3D): + # Get the first and probably only instance of [Cursor3D] and assign + # it to the cursor variable. Now the 3D Cursor is considered recovered + cursor = root_children.filter(func(node): return node is Cursor3D).front() + + +func _create_undo_redo_action(node: Node3D, property: String, value: Variant, action_name: String = "") -> void: + if node == null or property.is_empty() or value == null: + return + + if action_name.is_empty(): + action_name = "Set " + property + " for " + node.name + + undo_redo.create_action(action_name) + var old_value: Variant = node.get(property) + undo_redo.add_do_property(node, property, value) + undo_redo.add_undo_property(node, property, old_value) + undo_redo.commit_action() diff --git a/source/addons/godot_3d_cursor/plugin.gd.uid b/source/addons/godot_3d_cursor/plugin.gd.uid new file mode 100644 index 0000000..29d393f --- /dev/null +++ b/source/addons/godot_3d_cursor/plugin.gd.uid @@ -0,0 +1 @@ +uid://bjeulwhnkq10f diff --git a/source/addons/godot_3d_cursor/selection_indicator.png b/source/addons/godot_3d_cursor/selection_indicator.png new file mode 100644 index 0000000..342f69a Binary files /dev/null and b/source/addons/godot_3d_cursor/selection_indicator.png differ diff --git a/source/addons/godot_3d_cursor/selection_indicator.png.import b/source/addons/godot_3d_cursor/selection_indicator.png.import new file mode 100644 index 0000000..a6bff3f --- /dev/null +++ b/source/addons/godot_3d_cursor/selection_indicator.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://r1rpga7n1quy" +path="res://.godot/imported/selection_indicator.png-2775b0425d2acc692b5fb62566ad8521.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/godot_3d_cursor/selection_indicator.png" +dest_files=["res://.godot/imported/selection_indicator.png-2775b0425d2acc692b5fb62566ad8521.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/godot_3d_cursor/selection_ring.png b/source/addons/godot_3d_cursor/selection_ring.png new file mode 100644 index 0000000..7885c6d Binary files /dev/null and b/source/addons/godot_3d_cursor/selection_ring.png differ diff --git a/source/addons/godot_3d_cursor/selection_ring.png.import b/source/addons/godot_3d_cursor/selection_ring.png.import new file mode 100644 index 0000000..95bcb7f --- /dev/null +++ b/source/addons/godot_3d_cursor/selection_ring.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dd7qbrp08e8qe" +path="res://.godot/imported/selection_ring.png-dfbd0ecaedb53139c0c4b6c57699fb8f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/godot_3d_cursor/selection_ring.png" +dest_files=["res://.godot/imported/selection_ring.png-dfbd0ecaedb53139c0c4b6c57699fb8f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/panku_console/modules/engine_tools/module.gd b/source/addons/panku_console/modules/engine_tools/module.gd index ffd6fd8..9005585 100644 --- a/source/addons/panku_console/modules/engine_tools/module.gd +++ b/source/addons/panku_console/modules/engine_tools/module.gd @@ -64,8 +64,8 @@ func toggle_3d_collision_shape_visibility() -> void: or node is GPUParticlesCollisionBox3D \ or node is GPUParticlesCollisionHeightField3D \ or node is GPUParticlesCollisionSDF3D \ - or node is GPUParticlesCollisionSphere3D \ - or node is CSGPrimitive3D: + or node is GPUParticlesCollisionSphere3D:# \ + #or node is CSGPrimitive3D: # remove and re-add the node to the tree to force a redraw # https://github.com/godotengine/godot/blob/26b1fd0d842fa3c2f090ead47e8ea7cd2d6515e1/scene/3d/collision_object_3d.cpp#L39 var parent: Node = node.get_parent() diff --git a/source/addons/proto_shape/README.md b/source/addons/proto_shape/README.md new file mode 100644 index 0000000..f914734 --- /dev/null +++ b/source/addons/proto_shape/README.md @@ -0,0 +1,27 @@ +# ProtoShape tooling + +## Shapes + +- [ProtoRamp](proto_ramp/README.md) + +## Gizmos + +The gizmo for `ProtoRamp` supports setting 3 properties: + +- Width +- Height +- Depth + +It utilizes `ProtoGizmoUtils` for advanced 3D math calculations and plane projections to get the desired handle drag offset and set the properties accordingly. + +### Undo/Redo support + +[ProtoRampGizmos](proto_ramp/proto_ramp_gizmos.gd) supports scene-wide undo/redo functionality. It uses the `EditorUndoRedoManager` to set up ramp properties, so the editor takes gizmo-based modifications into account! Editor now warns you to save on exit if you have unsaved changes made with the gizmos. + +### [ProtoGizmoWrapper](proto_gizmo_wrapper/README.md) + +`ProtoGizmoWrapper` is an advanced wrapper for creating gizmo functionality for custom 3D nodes. It exposes 2 signals to implement custom gizmos for your nodes to *redraw* and *update* the properties of your node. + +The gizmo is an `EditorNode3DGizmoPlugin` and is visible when the `ProtoRamp` node or a children of `ProtoGizmoWrapper` is selected. Selection hightlights the mesh with a transparent blue color and shows handles, which you can drag to adjust the shape. + +https://github.com/HLCaptain/proto-shape/assets/22623259/1db3f18d-4d90-400f-9d33-7b03d44f62c7 diff --git a/source/addons/proto_shape/icon/proto-gizmo-handler.png b/source/addons/proto_shape/icon/proto-gizmo-handler.png new file mode 100644 index 0000000..2038c3a Binary files /dev/null and b/source/addons/proto_shape/icon/proto-gizmo-handler.png differ diff --git a/source/addons/proto_shape/icon/proto-gizmo-handler.png.import b/source/addons/proto_shape/icon/proto-gizmo-handler.png.import new file mode 100644 index 0000000..8a83ae8 --- /dev/null +++ b/source/addons/proto_shape/icon/proto-gizmo-handler.png.import @@ -0,0 +1,41 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bkrb0hpbnnlve" +path.s3tc="res://.godot/imported/proto-gizmo-handler.png-4cc70ab2e8b20344f35efde1c0e3d8c1.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/proto_shape/icon/proto-gizmo-handler.png" +dest_files=["res://.godot/imported/proto-gizmo-handler.png-4cc70ab2e8b20344f35efde1c0e3d8c1.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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=0 diff --git a/source/addons/proto_shape/icon/proto-gizmo-handler.svg b/source/addons/proto_shape/icon/proto-gizmo-handler.svg new file mode 100644 index 0000000..fafd20b --- /dev/null +++ b/source/addons/proto_shape/icon/proto-gizmo-handler.svg @@ -0,0 +1,51 @@ + + + + + + + diff --git a/source/addons/proto_shape/icon/proto-gizmo-handler.svg.import b/source/addons/proto_shape/icon/proto-gizmo-handler.svg.import new file mode 100644 index 0000000..f99f7d7 --- /dev/null +++ b/source/addons/proto_shape/icon/proto-gizmo-handler.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dpo2412p634n8" +path="res://.godot/imported/proto-gizmo-handler.svg-450f3c4251bc782c6a7cb75ed4928a42.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/proto_shape/icon/proto-gizmo-handler.svg" +dest_files=["res://.godot/imported/proto-gizmo-handler.svg-450f3c4251bc782c6a7cb75ed4928a42.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon-mini.png b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon-mini.png new file mode 100644 index 0000000..9cd6869 Binary files /dev/null and b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon-mini.png differ diff --git a/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon-mini.png.import b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon-mini.png.import new file mode 100644 index 0000000..66f18ba --- /dev/null +++ b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon-mini.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://5cxcoew7dwry" +path="res://.godot/imported/proto-gizmo-wrapper-icon-mini.png-1a69b62ae400df904dfb4e09a5f63122.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/proto_shape/icon/proto-gizmo-wrapper-icon-mini.png" +dest_files=["res://.godot/imported/proto-gizmo-wrapper-icon-mini.png-1a69b62ae400df904dfb4e09a5f63122.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.png b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.png new file mode 100644 index 0000000..fd31c4c Binary files /dev/null and b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.png differ diff --git a/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.png.import b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.png.import new file mode 100644 index 0000000..38a2842 --- /dev/null +++ b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://yd88id1ixiwv" +path="res://.godot/imported/proto-gizmo-wrapper-icon.png-74f8ebc9cdc56ecf6bd0c731cb0bf5b3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/proto_shape/icon/proto-gizmo-wrapper-icon.png" +dest_files=["res://.godot/imported/proto-gizmo-wrapper-icon.png-74f8ebc9cdc56ecf6bd0c731cb0bf5b3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.svg b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.svg new file mode 100644 index 0000000..48dce8f --- /dev/null +++ b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + diff --git a/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.svg.import b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.svg.import new file mode 100644 index 0000000..0ec397c --- /dev/null +++ b/source/addons/proto_shape/icon/proto-gizmo-wrapper-icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bhgl0xyyytune" +path="res://.godot/imported/proto-gizmo-wrapper-icon.svg-3927e78352256edd1286e1ee01c9bee9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/proto_shape/icon/proto-gizmo-wrapper-icon.svg" +dest_files=["res://.godot/imported/proto-gizmo-wrapper-icon.svg-3927e78352256edd1286e1ee01c9bee9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/proto_shape/icon/proto-ramp-icon.png b/source/addons/proto_shape/icon/proto-ramp-icon.png new file mode 100644 index 0000000..cd3c23e Binary files /dev/null and b/source/addons/proto_shape/icon/proto-ramp-icon.png differ diff --git a/source/addons/proto_shape/icon/proto-ramp-icon.png.import b/source/addons/proto_shape/icon/proto-ramp-icon.png.import new file mode 100644 index 0000000..19ef643 --- /dev/null +++ b/source/addons/proto_shape/icon/proto-ramp-icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cltx6vocg7rfm" +path="res://.godot/imported/proto-ramp-icon.png-3135a56706d7082ddb1524561da4bad1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/proto_shape/icon/proto-ramp-icon.png" +dest_files=["res://.godot/imported/proto-ramp-icon.png-3135a56706d7082ddb1524561da4bad1.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/proto_shape/icon/proto-ramp.psd b/source/addons/proto_shape/icon/proto-ramp.psd new file mode 100644 index 0000000..f33db2a Binary files /dev/null and b/source/addons/proto_shape/icon/proto-ramp.psd differ diff --git a/source/addons/proto_shape/icon/proto-shape-icon.png b/source/addons/proto_shape/icon/proto-shape-icon.png new file mode 100644 index 0000000..2f69a34 Binary files /dev/null and b/source/addons/proto_shape/icon/proto-shape-icon.png differ diff --git a/source/addons/proto_shape/icon/proto-shape-icon.png.import b/source/addons/proto_shape/icon/proto-shape-icon.png.import new file mode 100644 index 0000000..36248f9 --- /dev/null +++ b/source/addons/proto_shape/icon/proto-shape-icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bhqug4bqtc5ay" +path="res://.godot/imported/proto-shape-icon.png-9ff7624efc04c6128c436057018bb46d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/proto_shape/icon/proto-shape-icon.png" +dest_files=["res://.godot/imported/proto-shape-icon.png-9ff7624efc04c6128c436057018bb46d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/proto_shape/icon/proto-shape-icon.svg b/source/addons/proto_shape/icon/proto-shape-icon.svg new file mode 100644 index 0000000..22d90f0 --- /dev/null +++ b/source/addons/proto_shape/icon/proto-shape-icon.svg @@ -0,0 +1,6143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/addons/proto_shape/icon/proto-shape-icon.svg.import b/source/addons/proto_shape/icon/proto-shape-icon.svg.import new file mode 100644 index 0000000..3435efa --- /dev/null +++ b/source/addons/proto_shape/icon/proto-shape-icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvundrxfgnyhn" +path="res://.godot/imported/proto-shape-icon.svg-8b4b44dede01bdec8a626e48489697a9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/proto_shape/icon/proto-shape-icon.svg" +dest_files=["res://.godot/imported/proto-shape-icon.svg-8b4b44dede01bdec8a626e48489697a9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/proto_shape/plugin.cfg b/source/addons/proto_shape/plugin.cfg new file mode 100644 index 0000000..cd5f9a7 --- /dev/null +++ b/source/addons/proto_shape/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="ProtoShape" +description="ProtoShape is a Godot plugin that adds a library of dynamic shapes like ramps and stairs to quickly block out your maps and iterate on your ideas. Create your custom dynamic shapes more easily with the included Gizmo system." +author="Illyan" +version="1.1.5" +script="proto_shape.gd" diff --git a/source/addons/proto_shape/proto_gizmo/README.md b/source/addons/proto_shape/proto_gizmo/README.md new file mode 100644 index 0000000..392262e --- /dev/null +++ b/source/addons/proto_shape/proto_gizmo/README.md @@ -0,0 +1,39 @@ +# ProtoGizmo + +ProtoGizmo is an `EditorNode3DGizmoPlugin` that provides a base for creating custom gizmos in the Godot editor. It is used to create custom gizmos for the `ProtoShape` addon. With the use of `ProtoGizmoWrapper`, you can create custom gizmos for your 3D nodes. + +## Default materials + +- `proto_handler` - Same as internal "handlers" material for gizmo handles, but blue instead of redish color. +- `selected` - Material for selected nodes (bluish transparent color). +- `main` - Base redish color material for general or debuging use. (Used for drawing camera projected debug planes). + +## ProtoGizmoUtils + +ProtoGizmoUtils are advanced 3D math utilities used for calculating handle offsets and projecting planes for gizmos based on the camera position and screen coordinates. The projected plane, the user can drag the handles on can be drawn via `ProtoGizmoUtils::debug_draw_handle_grid` on gizmo *redraw*. + +### Calculate handle offset in 3D space + +`ProtoGizmoUtils::get_handle_offset` calculates the offset of the dragged handle in the 3D space on a camera projected plane. + +Properties: + +- `camera: Camera3D` - Camera used for calculating the plane the `screen_pos` is projected on. +- `screen_pos: Vector2` - Screen position of the mouse cursor. +- `local_gizmo_position: Vector3` - Gizmo position in the node's local space. +- `local_offset_axis: Vector3` - Axis the handle can be dragged on in node's local space. Used for calculating the plane the `screen_pos` is projected on. +- `node: Node3D` - Node the gizmo is attached to. Used to get global transform and position. + +Returns: `Vector3` - Offset of the dragged handle in the 3D space on a camera projected plane in global space. + +Unfortunately, to get the proper offsets, projections, offsets and drawing of the gizmos, some transitions must be made between the local and global space to get the result. The `camera.position` is in global space, so the `local_gizmo_position` and `local_offset_axis` must be transformed to global space to get the proper offset from projecting `screen_pos` onto a global space plane. + +#### Get camera oriented plane + +The global plane is created by using 3 points: + +- `global_gizmo_position` - The gizmo position in global space, transformed from `local_gizmo_position` with the Node3D's `global_position` and `global_transform.basis`. +- `global_offset_axis` - The axis the handle can be dragged on in global space, transformed from `local_offset_axis` with the Node3D's `global_transform.basis`. +- A point on the line defined by `camera.position` closest to another line defined by `global_gizmo_position` and `global_offset_axis`. The line with point `camera.position` is perpendicular to the other line and its axis is the plane's normal vector, so the plane is always oriented to the camera. The calculation is found in `ProtoGizmoUtils::get_camera_oriented_plane`. + +The plane can be visualized by drawing a grid with `ProtoGizmoUtils::debug_draw_handle_grid`. \ No newline at end of file diff --git a/source/addons/proto_shape/proto_gizmo/proto_gizmo.gd b/source/addons/proto_shape/proto_gizmo/proto_gizmo.gd new file mode 100644 index 0000000..9664841 --- /dev/null +++ b/source/addons/proto_shape/proto_gizmo/proto_gizmo.gd @@ -0,0 +1,81 @@ +extends EditorNode3DGizmoPlugin + +const ProtoGizmoWrapper = preload("res://addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd") +const ProtoRamp = preload("res://addons/proto_shape/proto_ramp/proto_ramp.gd") + +# Must be initialized externally by ProtoShape plugin +var undo_redo: EditorUndoRedoManager + +signal snapping_changed(snapping: bool) +signal fine_snapping_changed(fine_snapping: bool) + +# Must be initialized externally by ProtoShape plugin +var _snapping: bool = false +var snapping: bool: set = set_snapping, get = get_snapping +var _fine_snapping: bool = false +var fine_snapping: bool: set = set_fine_snapping, get = get_fine_snapping + +func set_snapping(snapping: bool) -> void: + _snapping = snapping + snapping_changed.emit(snapping) + +func set_fine_snapping(fine_snapping: bool) -> void: + _fine_snapping = fine_snapping + fine_snapping_changed.emit(fine_snapping) + +func get_snapping() -> bool: + return _snapping + +func get_fine_snapping() -> bool: + return _fine_snapping + +func _init() -> void: + create_material("main", Color(1, 0.3725, 0.3725, 0.5)) + create_material("selected", Color(0, 0, 1, 0.1)) + create_handle_material("proto_handler", false, load("res://addons/proto_shape/icon/proto-gizmo-handler.png")) + +func _has_gizmo(node: Node3D) -> bool: + if node.get_parent() is ProtoGizmoWrapper or node is ProtoRamp: + return true + else: + return false + +func _get_gizmo_name() -> String: + return "ProtoGizmo" + +func _redraw(gizmo: EditorNode3DGizmo) -> void: + var node := gizmo.get_node_3d() + if node is ProtoRamp: + node.gizmos.redraw_gizmos(gizmo, self) + return + if node.get_parent() is ProtoGizmoWrapper: + node.get_parent().redraw_gizmos_for_child(gizmo, self) + return + +func _set_handle( + gizmo: EditorNode3DGizmo, + handle_id: int, + secondary: bool, + camera: Camera3D, + screen_pos: Vector2) -> void: + var node := gizmo.get_node_3d() + if node is ProtoRamp: + node.gizmos.set_handle(gizmo, self, handle_id, secondary, camera, screen_pos) + return + if node.get_parent() is ProtoGizmoWrapper: + node.get_parent().set_handle_for_child(gizmo, self, handle_id, secondary, camera, screen_pos) + return + +func _commit_handle( + gizmo: EditorNode3DGizmo, + handle_id: int, + secondary: bool, + restore: Variant, + cancel: bool) -> void: + var node := gizmo.get_node_3d() + if node is ProtoRamp: + node.gizmos.commit_handle(gizmo, handle_id, secondary, restore, cancel) + return + if node.get_parent() is ProtoGizmoWrapper: + node.get_parent().commit_handle_for_child(gizmo, handle_id, secondary, restore, cancel) + return diff --git a/source/addons/proto_shape/proto_gizmo/proto_gizmo.gd.uid b/source/addons/proto_shape/proto_gizmo/proto_gizmo.gd.uid new file mode 100644 index 0000000..91c47e8 --- /dev/null +++ b/source/addons/proto_shape/proto_gizmo/proto_gizmo.gd.uid @@ -0,0 +1 @@ +uid://dld6sttc0b5f3 diff --git a/source/addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd b/source/addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd new file mode 100644 index 0000000..354c6b7 --- /dev/null +++ b/source/addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd @@ -0,0 +1,155 @@ +## Calculates plane based on the gizmo's position facing the camera +## Returns offset based on the intersection of the ray from the camera to the cursor hitting the plane +func get_handle_offset( + camera: Camera3D, + screen_pos: Vector2, + local_gizmo_position: Vector3, + local_offset_axis: Vector3, + node: Node3D) -> Vector3: + + var transform := node.global_transform + var position: Vector3 = node.global_position + var quat: Quaternion = transform.basis.get_rotation_quaternion() + var quat_axis: Vector3 = quat.get_axis() if quat.get_axis().is_normalized() else Vector3.UP + var quat_angle: float = quat.get_angle() + var scale: Vector3 = transform.basis.get_scale() + var global_gizmo_position: Vector3 = local_gizmo_position.rotated(quat_axis, quat_angle) * scale + position + var global_offset_axis: Vector3 = local_offset_axis.rotated(quat_axis, quat_angle) + var global_plane: Plane = get_camera_oriented_plane(camera.position, global_gizmo_position, global_offset_axis) + var local_offset: Vector3 = (global_plane.intersects_ray(camera.position, camera.project_position(screen_pos, 1.0) - camera.position) - position).rotated(quat_axis, -quat_angle) / scale + return local_offset + +func get_handle_offset_by_plane( + camera: Camera3D, + screen_pos: Vector2, + local_gizmo_position: Vector3, + plane_normal: Vector3, + node: Node3D) -> Vector3: + + var transform := node.global_transform + var position: Vector3 = node.global_position + var quat: Quaternion = transform.basis.get_rotation_quaternion() + var quat_axis: Vector3 = quat.get_axis() if quat.get_axis().is_normalized() else Vector3.UP + var quat_angle: float = quat.get_angle() + var scale: Vector3 = transform.basis.get_scale() + var global_gizmo_position: Vector3 = local_gizmo_position.rotated(quat_axis, quat_angle) * scale + position + var global_plane_normal: Vector3 = plane_normal.rotated(quat_axis, quat_angle) + var global_plane: Plane = Plane(global_plane_normal, global_gizmo_position) + var local_offset: Vector3 = (global_plane.intersects_ray(camera.position, camera.project_position(screen_pos, 1.0) - camera.position) - position).rotated(quat_axis, -quat_angle) / scale + return local_offset + +# Adds debug lines for the plane the gizmo can move on +# Should only be called on gizmo redraw +func debug_draw_handle_grid( + camera_position: Vector3, + screen_pos: Vector2, + local_gizmo_position: Vector3, + local_offset_axis: Vector3, + node: Node3D, + gizmo: EditorNode3DGizmo, + plugin: EditorNode3DGizmoPlugin, + grid_size: float = 1.0) -> void: + + var transform := node.global_transform + var position: Vector3 = node.global_position + var quat: Quaternion = transform.basis.get_rotation_quaternion() + var quat_axis: Vector3 = quat.get_axis() if quat.get_axis().is_normalized() else Vector3.UP + var quat_angle: float = quat.get_angle() + var scale: Vector3 = transform.basis.get_scale() + var local_camera_position: Vector3 = (camera_position - position).rotated(quat_axis, -quat_angle) / scale + var local_plane: Plane = get_camera_oriented_plane(local_camera_position, local_gizmo_position, local_offset_axis) + + debug_draw_grid_on_plane(local_gizmo_position, local_offset_axis, gizmo, plugin, local_plane, grid_size) + +func debug_draw_handle_grid_on_plane( + local_gizmo_position: Vector3, + local_offset_axis: Vector3, + plane_normal: Vector3, + node: Node3D, + gizmo: EditorNode3DGizmo, + plugin: EditorNode3DGizmoPlugin, + grid_size: float = 1.0) -> void: + + var transform := node.global_transform + var position: Vector3 = node.global_position + var quat: Quaternion = transform.basis.get_rotation_quaternion() + var quat_axis: Vector3 = quat.get_axis() if quat.get_axis().is_normalized() else Vector3.UP + var quat_angle: float = quat.get_angle() + var scale: Vector3 = transform.basis.get_scale() + var local_plane: Plane = Plane(plane_normal, local_gizmo_position) + + debug_draw_grid_on_plane(local_gizmo_position, local_offset_axis, gizmo, plugin, local_plane, grid_size) + +func debug_draw_grid_on_plane( + local_gizmo_position: Vector3, + local_offset_axis: Vector3, + gizmo: EditorNode3DGizmo, + plugin: EditorNode3DGizmoPlugin, + local_plane: Plane, + grid_size: float = 1.0 + ) -> void: + # Add debug lines + var plane_lines = PackedVector3Array() + # Push back gizmo positions like a grid on the plane + var lines_on_grid: int = 11 # 11 lines in horizontal and vertical axis + # var gradient_granularity: int = 10 # 10 sub-lines with varying opacity with each line + for i in range(lines_on_grid): + var horizontal_distance: float = (i - lines_on_grid / 2) * grid_size / lines_on_grid + var horizontal_axis: Vector3 = local_gizmo_position + local_offset_axis.normalized() * horizontal_distance + for j in range(lines_on_grid): + var vertical_distance: float = (j - lines_on_grid / 2) * grid_size / lines_on_grid + var vertical_axis: Vector3 = local_plane.normal.cross(local_offset_axis) * vertical_distance + plane_lines.push_back(horizontal_axis + vertical_axis - local_plane.normal * 0.2 * grid_size / lines_on_grid) + plane_lines.push_back(horizontal_axis + vertical_axis + local_plane.normal * 0.2 * grid_size / lines_on_grid) + plane_lines.push_back(horizontal_axis + local_offset_axis.normalized() * 0.25 * grid_size / lines_on_grid + vertical_axis) + plane_lines.push_back(horizontal_axis - local_offset_axis.normalized() * 0.25 * grid_size / lines_on_grid + vertical_axis) + plane_lines.push_back(horizontal_axis + vertical_axis + local_plane.normal.cross(local_offset_axis) * 0.25 * grid_size / lines_on_grid) + plane_lines.push_back(horizontal_axis + vertical_axis - local_plane.normal.cross(local_offset_axis) * 0.25 * grid_size / lines_on_grid) + # TODO: set the opacity of the lines based on the distance from the center + + gizmo.add_lines(plane_lines, plugin.get_material("main", gizmo)) + +## Gets the plane along [param gizmo_position] going through [param gizmo_axis] and facing towards the [param camera_position]. +## Consider [param camera_position], [param gizmo_position] and [param gizmo_axis] in the same space. +func get_camera_oriented_plane( + camera_position: Vector3, + gizmo_position: Vector3, + gizmo_axis: Vector3) -> Plane: + # camera: Camera to orient the plane to + # gizmo_position: gizmo's current position in the world + # gizmo_axis: axis the gizmo is moving along + + var closest_point_to_camera: Vector3 = get_closest_point_on_line(gizmo_position, gizmo_axis, camera_position) + var closest_point_to_camera_difference: Vector3 = closest_point_to_camera - camera_position + var parallel_to_gizmo_dir: Vector3 = closest_point_to_camera - gizmo_position + var perpendicular_to_gizmo_dir: Vector3 = parallel_to_gizmo_dir.cross(closest_point_to_camera_difference).normalized() + + # Transform 3 points to global space + var x: Vector3 = gizmo_position + var y: Vector3 = gizmo_position + gizmo_axis + var z: Vector3 = gizmo_position + perpendicular_to_gizmo_dir + var plane := Plane(x, y, z) + + return plane + +## [param point_in_line] is a point on the line +## [param line_dir] is the direction of the line +## [param point] is the point to find the closest point on the line to +func get_closest_point_on_line( + point_on_line: Vector3, + line_dir: Vector3, + point: Vector3) -> Vector3: + var A := point_on_line + var B := point_on_line + line_dir # This can be any other point in the direction of the line + var AP := point - A + var AB := B - A + + var t := AP.dot(AB) / AB.dot(AB) + var closest_point := A + t * AB + + return closest_point + +func snap_to_grid( + value: float, + grid_unit: float) -> float: + return round(value / grid_unit) * grid_unit diff --git a/source/addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd.uid b/source/addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd.uid new file mode 100644 index 0000000..7458df6 --- /dev/null +++ b/source/addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd.uid @@ -0,0 +1 @@ +uid://cs58a4cejxs6p diff --git a/source/addons/proto_shape/proto_gizmo_wrapper/README.md b/source/addons/proto_shape/proto_gizmo_wrapper/README.md new file mode 100644 index 0000000..1a637c2 --- /dev/null +++ b/source/addons/proto_shape/proto_gizmo_wrapper/README.md @@ -0,0 +1,179 @@ +# ProtoGizmoWrapper + +ProtoGizmoWrapper is a wrapper to create 3D gizmos for custom nodes in Godot. With the use of [ProtoGizmoUtils](../proto_gizmo/README.md#protogizmoutils) and only 2 method implementations, you can implement custom gizmos for your nodes. + + + +## Usage + +### Create ProtoGizmoWrapper + +When adding a new child node, search for `ProtoGizmoWrapper` and add it to the scene. + +### Make your nodes compatible with gizmos + +ProtoGizmoWrapper exposes 2 essential methods as signals to implement gizmo functionality. + +To make your nodes respond to gizmo related changes, you need to subscribe to these signals. + +The signals have `EditorNode3DGizmo` and `EditorNode3DGizmoPlugin` typed arguments, which are only available in the editor and not in packaged games. To avoid packaging issues, `gizmo` and `plugin` arguments are dynamically typed. + +So basically the signals are: + +```gdscript +# This +signal redraw_gizmos_for_child_signal(gizmo, plugin) + +# Instead of this +signal redraw_gizmos_for_child_signal(gizmo: EditorNode3DGizmo, plugin: EditorNode3DGizmoPlugin) +``` + +Just like in [ProtoRampGizmos](../proto_ramp/README.md#protorampgizmos), you can connect methods with static typing to these signals to avoid dynamic typing and runtime errors. + +***To see a fully working example, check out [ProtoRampGizmos](../proto_ramp/proto_ramp_gizmos.gd) source code.*** + +#### Redraw + +This signal is emitted when a wrapper's child node's gizmos need to be redrawn. + +When a `ProtoGizmoWrapper` has multiple child nodes (each subscribed to this signal), children should check if the affected node is itself with `gizmo.get_node_3d() == self`. Else, the children not affected are also redrawn, resulting in multiple (unnecessary) gizmos being drawn. + +Propagating `EditorNode3DGizmoPlugin::_redraw`. + +```gdscript +signal redraw_gizmos_for_child_signal(gizmo: EditorNode3DGizmo, plugin: EditorNode3DGizmoPlugin) +``` + +Each child is responsible to initialize their handles (generate UID for each handle, to use them later). In case of [ProtoRamp](../proto_ramp/README.md), the handles are initialized this way: + +```gdscript +# For initializing gizmo handles +var width_gizmo_id: int +var depth_gizmo_id: int +var height_gizmo_id: int + +# Optional variables for drawing debug grid for camera projection (assigned in `set_handle` function) +var screen_pos: Vector2 +var local_gizmo_position: Vector3 +var local_offset_axis: Vector3 +var camera_position: Vector3 + +func redraw_gizmos(gizmo: EditorNode3DGizmo, plugin: EditorNode3DGizmoPlugin) -> void: + + # Check if this is the affected node + if gizmo.get_node_3d() != self: + return + + # Initializing gizmo handles + if width_gizmo_id == 0 or depth_gizmo_id == 0 or height_gizmo_id == 0: + width_gizmo_id = randi_range(0, 1_000_000) + depth_gizmo_id = randi_range(0, 1_000_000) + height_gizmo_id = randi_range(0, 1_000_000) + + # Clearing previous drawn gizmos + gizmo.clear() + + var handles = PackedVector3Array() + # ... calculate and add gizmo handle positions to the array + gizmo.add_handles(handles, plugin.get_material("proto_handler", gizmo), [depth_gizmo_id, width_gizmo_id, height_gizmo_id]) + + # Add selection with mouse click on screen by adding the node's mesh to the gizmo + if get_meshes().size() > 1: + gizmo.add_collision_triangles(get_meshes()[1].generate_triangle_mesh()) + gizmo.add_mesh(get_meshes()[1], plugin.get_material("selected", gizmo)) + + # Drawing debug grid for camera projection (optional) + # Adding debug lines for gizmo if we have cursor screen position set + if screen_pos: + var grid_size_modifier = 1.0 + gizmo_utils.debug_draw_handle_grid(camera_position, screen_pos, local_gizmo_position, local_offset_axis, self, gizmo, plugin, grid_size_modifier) +``` + +#### Set handle + +This signal is emitted when a handle is dragged by the user. + +With identifying the affected handle, being drawn by `handle_id` on the affected node `gizmo.get_node_3d()`, the node can update its properties accordingly with the use of [ProtoGizmoUtils](../proto_gizmo/README.md#protogizmoutils). + +Propagating `EditorNode3DGizmoPlugin::_set_handle`. + +```gdscript +signal set_handle_for_child_signal(gizmo: EditorNode3DGizmo, plugin: EditorNode3DGizmoPlugin, handle_id: int, secondary: bool, camera: Camera3D, screen_pos: Vector2) +``` + +```gdscript +func set_handle( + gizmo: EditorNode3DGizmo, + plugin: EditorNode3DGizmoPlugin, + handle_id: int, + secondary: bool, + camera: Camera3D, + screen_pos: Vector2) -> void: + + # Check if this is the affected node + if gizmo.get_node_3d() != self: + return + + # Assign debug parameters used for drawing camera projected debug planes (optional) + self.screen_pos = screen_pos + self.local_gizmo_position = child.global_transform.origin + self.camera_position = camera.position + + # Match the handle_id to update the appropriate property + match handle_id: + depth_gizmo_id: + # Use ProtoGizmoUtils to update depth + + # `local_offset_axis` is used for debugging reasons, so it is not a local variable defined with `var` + # `local_offset_axis` is used to define the axis the handle can be dragged on. + local_offset_axis = Vector3(1, 0, 0) + # Also used for debugging + local_gizmo_position = ... set gizmo position based on node properties + + # `gizmo_utils` is a reference to the `ProtoGizmoUtils` instance used for handle offset calculations + # `handle_offset` is the offset of the dragged handle in the 3D space on a camera projected plane + var handle_offset = gizmo_utils.get_handle_offset(camera, screen_pos, local_gizmo_position, local_offset_axis, self) + + # Update custom node properties based on offset in the proper axis + _set_depth_handle(handle_offset.z) + width_gizmo_id: + # ... update width + height_gizmo_id: + # ... update height + + # Redraw gizmos after updating the properties + update_gizmos() +``` + +#### Initializing your nodes + +To use gizmos and `ProtoGizmoUtils` for getting handle offsets, you need to initialize an instance of it in your node, for example: + +```gdscript +# Import ProtoGizmoWrapper and ProtoGizmoUtils +const ProtoGizmoWrapper = preload("res://addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd") +const ProtoGizmoUtils = preload("res://addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd") +var gizmo_utils := ProtoGizmoUtils.new() + +func _enter_tree() -> void: + if get_parent() is ProtoGizmoWrapper: + # Connect to ProtoGizmoWrapper signals + var parent: ProtoGizmoWrapper = get_parent() + parent.redraw_gizmos_for_child_signal.connect(redraw_gizmos) + parent.set_handle_for_child_signal.connect(set_handle) + +func _exit_tree() -> void: + if get_parent() is ProtoGizmoWrapper: + # Disconnect from ProtoGizmoWrapper signals + var parent: ProtoGizmoWrapper = get_parent() + parent.redraw_gizmos_for_child_signal.disconnect(redraw_gizmos) + parent.set_handle_for_child_signal.disconnect(set_handle) +``` + +### Setup node hierarchy + +Add your custom nodes under the `ProtoGizmoWrapper` node, for the `ProtoGizmo` *EditorNode3DGizmoPlugin* to pick it up as a node with gizmos enabled. + +When your node connects to the `ProtoGizmoWrapper` signals, it will start responding to gizmo related changes, called by `ProtoGizmo`. + +If you implemented your functions correctly, you should see the gizmos for your 3D nodes and be able to drag them around. \ No newline at end of file diff --git a/source/addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd b/source/addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd new file mode 100644 index 0000000..f3a7398 --- /dev/null +++ b/source/addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd @@ -0,0 +1,29 @@ +@tool +extends Node + +## Editor tool plugins removed to avoid game packaging errors. +## [gizmo] is [EditorNode3DGizmo]. +## [plugin] is [EditorNode3DGizmoPlugin]. +signal redraw_gizmos_for_child_signal(gizmo, plugin) + +## Editor tool plugins removed to avoid game packaging errors. +## [gizmo] is [EditorNode3DGizmo]. +## [plugin] is [EditorNode3DGizmoPlugin]. +signal set_handle_for_child_signal(gizmo, plugin, handle_id: int, secondary: bool, camera: Camera3D, screen_pos: Vector2) + +## Editor tool plugins removed to avoid game packaging errors. +## [gizmo] is [EditorNode3DGizmo]. +## [plugin] is [EditorNode3DGizmoPlugin]. +signal commit_handle(gizmo, handle_id: int, secondary: bool, restore: Variant, cancel: bool) + +func redraw_gizmos_for_child(gizmo, plugin) -> void: + redraw_gizmos_for_child_signal.emit(gizmo, plugin) + +func set_handle_for_child( + gizmo, + plugin, + handle_id: int, + secondary: bool, + camera: Camera3D, + screen_pos: Vector2) -> void: + set_handle_for_child_signal.emit(gizmo, handle_id, secondary, camera, screen_pos, plugin) diff --git a/source/addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd.uid b/source/addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd.uid new file mode 100644 index 0000000..26ecd48 --- /dev/null +++ b/source/addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd.uid @@ -0,0 +1 @@ +uid://wr0twoe15d87 diff --git a/source/addons/proto_shape/proto_ramp/README.md b/source/addons/proto_shape/proto_ramp/README.md new file mode 100644 index 0000000..4c5b778 --- /dev/null +++ b/source/addons/proto_shape/proto_ramp/README.md @@ -0,0 +1,63 @@ +# ProtoRamp + +ProtoRamp is a dynamic ramp/staircase shape based on Godot's Constructive Solid Geometry (CSG). It is designed to be used for prototyping levels and game mechanics. + + + + +## Usage + +### Create a ProtoRamp + +When adding a new child node, search for `ProtoRamp` and add it to the scene. + +https://github.com/HLCaptain/proto-shape/assets/22623259/bccfb0e7-6799-4a94-82c4-84e5aa9d9563 + +Since `1.1.4`, ProtoRamp node is now independent from `CSGShape3D` base class for correct shape generation. + +### Use Gizmos + +ProtoRamp supports custom gizmos to adjust the shape. + +https://github.com/HLCaptain/proto-shape/assets/22623259/1db3f18d-4d90-400f-9d33-7b03d44f62c7 + +#### Undo/Redo + +You can also undo/redo changes made with the gizmos. + +### Enable collisions and bake Navigation Meshes + +ProtoRamp supports navigation mesh generation. It also features a toggle to enable collisions (aqua blue if enabled). + +![Navigation mesh on ProtoRamp](navigation_mesh_proto_ramp.png) + +#### ProtoRampGizmos + +Gizmo functionality is delegated to [ProtoRampGizmos](proto_ramp_gizmos.gd). It is a helper class that provides gizmo functionality for the `ProtoRamp` node, which only gets instantiated in the editor. This way, the packaged game will not rely on any editor-plugin specific code. + +`Engine.is_editor_hint()` is used in `ProtoRamp` itself to check if the game is running in the editor. If it is, the `ProtoRampGizmos` class is instantiated and added as a child node to the `ProtoRamp` node. + +### Adjust parameters + +Modify height, width, depth, anchor position and more! + +https://github.com/HLCaptain/proto-shape/assets/22623259/cee061ee-5c15-4e56-9c48-6eedb77409db + +### Grid snapping + +Grid snapping is supported for `ProtoRamp` since `1.1.3`! Holding down Ctrl enables regular snapping by 1.0 units (ramp size, not node scale unit), while holding down Ctrl + Shift enables fine snapping by 0.1 units. + +### Change step counts in multiple ways + +There are two ways of changing the step count: + +- Fitting into `Staircase` dimensions: current width-height-depth of the staircase is respected, step dimensions will adjust. +- Using `Step` dimensions as a base: step dimensions are constant, staircase will shrink or grow to fit the steps. + +Fitting in more steps: + +https://github.com/HLCaptain/proto-shape/assets/22623259/f14dd269-fa6b-4ee7-a195-23d64c7cb15a + +You can also use both! + +https://github.com/HLCaptain/proto-shape/assets/22623259/1fdcc87e-3231-4c03-bc8f-ab0252557574 diff --git a/source/addons/proto_shape/proto_ramp/navigation_mesh_proto_ramp.png b/source/addons/proto_shape/proto_ramp/navigation_mesh_proto_ramp.png new file mode 100644 index 0000000..1d28223 Binary files /dev/null and b/source/addons/proto_shape/proto_ramp/navigation_mesh_proto_ramp.png differ diff --git a/source/addons/proto_shape/proto_ramp/navigation_mesh_proto_ramp.png.import b/source/addons/proto_shape/proto_ramp/navigation_mesh_proto_ramp.png.import new file mode 100644 index 0000000..155779c --- /dev/null +++ b/source/addons/proto_shape/proto_ramp/navigation_mesh_proto_ramp.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cfvso6a0xumye" +path="res://.godot/imported/navigation_mesh_proto_ramp.png-57c1d30243288c0f7a27d13080182d6c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/proto_shape/proto_ramp/navigation_mesh_proto_ramp.png" +dest_files=["res://.godot/imported/navigation_mesh_proto_ramp.png-57c1d30243288c0f7a27d13080182d6c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/source/addons/proto_shape/proto_ramp/proto_ramp.gd b/source/addons/proto_shape/proto_ramp/proto_ramp.gd new file mode 100644 index 0000000..987dd2b --- /dev/null +++ b/source/addons/proto_shape/proto_ramp/proto_ramp.gd @@ -0,0 +1,527 @@ +@tool +extends Node3D +## Dynamic ramp/staircase shape +## +## This node can generate ramps and staircases with a variety of parameters. + +## Called when the anchor is changed. Used by the `proto_gizmo.dg` script to update gizmo handler positions. +signal anchor_changed + +## Called when the width is changed. +signal width_changed + +## Called when the height is changed. +signal height_changed + +## Called when the depth is changed. +signal depth_changed + +## Called when step count is changed. +signal step_count_changed + +## Called when type is changed. +signal type_changed + +## Called when fill is changed. +signal fill_changed + +## Calculation mode for the staircase. +enum Calculation { + STAIRCASE_DIMENSIONS, ## Width, depth and height are the same size as the whole staircase. + STEP_DIMENSIONS, ## Width, depth and height are the same size as a single step. +} + +## Anchor point for the ramp. Used to position the ramp in the world. +enum Anchor { + BOTTOM_CENTER, ## Default anchor point. The anchor is at the bottom of the ramp. The ramp is positioned in the middle. + BOTTOM_LEFT, ## The anchor is at the bottom left of the ramp. The ramp is shifted right. + BOTTOM_RIGHT, ## The anchor is at the bottom right of the ramp. The ramp is shifted left. + TOP_CENTER, ## The anchor is at the top of the ramp. The ramp is positioned in the middle. + TOP_LEFT, ## The anchor is at the top left of the ramp. The ramp is shifted right. + TOP_RIGHT, ## The anchor is at the top right of the ramp. The ramp is shifted left. + BASE_CENTER, ## The anchor is at the base of the ramp. The ramp is positioned in the middle. + BASE_LEFT, ## The anchor is at the base left of the ramp. The ramp is shifted right. + BASE_RIGHT, ## The anchor is at the base right of the ramp. The ramp is shifted left. +} + +# TODO: RAMP VS STAIRCASE WRONG WIDTH +## Act as a ramp without stairs or a staircase with stairs. +enum Type { + RAMP, ## Simple CSGPolygon3D shape. + STAIRCASE, ## Staircase with stairs combined of CSGBox3D shapes. +} + +## Used to avoid setting properties traditionally on initialization to avoid bugs. +var is_entered_tree := false + +## Storing CSG shapes for easy access without interfering with children. +var shape_polygon: CSGPolygon3D = null + +## Used to avoid z-fighting and incorrect snapping between steps. +var epsilon: float = 0.0001 + +## Default public values +const _default_calculation := Calculation.STAIRCASE_DIMENSIONS +const _default_steps: int = 8 +const _default_width: float = 1.0 +const _default_height: float = 1.0 +const _default_depth: float = 1.0 +const _default_fill: float = 1.0 +const _default_type := Type.RAMP +const _default_anchor := Anchor.BOTTOM_CENTER +const _default_anchor_fixed := true +const _default_collisions_enabled := true + +## Default private values +var _calculation := _default_calculation +var _steps := _default_steps +var _width := _default_width +var _height := _default_height +var _depth := _default_depth +var _fill := _default_fill +var _type := _default_type +var _anchor := _default_anchor +var _anchor_fixed := _default_anchor_fixed +var _collisions_enabled := _default_collisions_enabled + +@export_category("Proto Ramp") +## Calculation method of width, depth and height. +var calculation: Calculation: set = set_calculation, get = get_calculation + +## Number of steps in the staircase. +var steps: int: set = set_steps, get = get_steps + +## Width of the ramp/staircase. +var width: float: set = set_width, get = get_width + +## Height of the ramp/staircase. +var height: float: set = set_height, get = get_height + +## Depth of the ramp/staircase. +var depth: float: set = set_depth, get = get_depth + +## Percentage of non-empty space under the ramp/staircase. +var fill: float: set = set_fill, get = get_fill + +## Act as a ramp or staircase with steps. +var type: Type: set = set_type, get = get_type + +## Anchor point of the ramp/staircase. +var anchor: Anchor: set = set_anchor, get = get_anchor + +## Collisions enabled for the ramp/staircase. +var collisions_enabled: bool: set = set_collisions_enabled, get = get_collisions_enabled + +## If true, the anchor point will not move in global space changed when the anchor is changed. +## Instead, the ramp/staircase will move in local space. +var anchor_fixed: bool: set = set_anchor_fixed, get = get_anchor_fixed + +var material: Variant: set = set_material, get = get_material + +func _get_property_list() -> Array[Dictionary]: + var list: Array[Dictionary] = [ + {"name": "type", "type": TYPE_INT, "hint": PROPERTY_HINT_ENUM, "hint_string": "Ramp,Staircase"}, + {"name": "collisions_enabled", "type": TYPE_BOOL}, + {"name": "width", "type": TYPE_FLOAT, "hint": PROPERTY_HINT_RANGE, "hint_string": "0.001,100,0.01,or_greater"}, + {"name": "height", "type": TYPE_FLOAT, "hint": PROPERTY_HINT_RANGE, "hint_string": "0.001,100,0.01,or_greater"}, + {"name": "depth", "type": TYPE_FLOAT, "hint": PROPERTY_HINT_RANGE, "hint_string": "0.001,100,0.01,or_greater"}, + {"name": "anchor", "type": TYPE_INT, "hint": PROPERTY_HINT_ENUM, "hint_string": "Bottom Center,Bottom Left,Bottom Right,Top Center,Top Left,Top Right,Base Center,Base Left,Base Right"}, + {"name": "anchor_fixed", "type": TYPE_BOOL}, + {"name": "fill", "type": TYPE_FLOAT, "hint": PROPERTY_HINT_RANGE, "hint_string": "0.000,1.000,0.001"}, + {"name": "material","class_name": &"BaseMaterial3D,ShaderMaterial", "type": 24, "hint": 17, "hint_string": "BaseMaterial3D,ShaderMaterial", "usage": 6 } + ] + + # Staircase exclusive properties + if type == Type.STAIRCASE: + list += [ + {"name": "calculation", "type": TYPE_INT, "hint": PROPERTY_HINT_ENUM, "hint_string": "Staircase Dimensions,Step Dimensions"}, + {"name": "steps", "type": TYPE_INT, "hint": PROPERTY_HINT_RANGE, "hint_string": "1,100,1,or_greater"}, + ] + + return list + +func _set(property: StringName, value: Variant) -> bool: + match property: + "type": + set_type(value) + return true + "calculation": + set_calculation(value) + return true + "steps": + set_steps(value) + return true + "width": + set_width(value) + return true + "height": + set_height(value) + return true + "depth": + set_depth(value) + return true + "fill": + set_fill(value) + return true + "anchor": + set_anchor(value) + return true + "anchor_fixed": + set_anchor_fixed(value) + return true + "material": + set_material(value) + return true + "collisions_enabled": + set_collisions_enabled(value) + return true + + return false + +func _property_can_revert(property: StringName) -> bool: + if property in ["type", "calculation", "steps", "width", "height", "depth", "fill", "anchor", "anchor_fixed", "material", "collisions_enabled"]: + return true + return false + +func _property_get_revert(property: StringName) -> Variant: + match property: + "type": + return _default_type + "calculation": + return _default_calculation + "steps": + return _default_steps + "width": + return _default_width + "height": + return _default_height + "depth": + return _default_depth + "fill": + return _default_fill + "anchor": + return _default_anchor + "anchor_fixed": + return _default_anchor_fixed + "material": + return null + "collisions_enabled": + return _default_collisions_enabled + return null + +func get_type() -> Type: + return _type + +func get_calculation() -> Calculation: + return _calculation + +func get_width() -> float: + return _width + +func get_height() -> float: + return _height + +func get_depth() -> float: + return _depth + +func get_fill() -> float: + return _fill + +func get_steps() -> int: + return _steps + +func get_anchor() -> Anchor: + return _anchor + +func get_collisions_enabled() -> bool: + return _collisions_enabled + +func get_anchor_fixed() -> bool: + return _anchor_fixed + +func get_material() -> Variant: + return material + +## Get the step depth of the staircase. +func get_true_step_depth() -> float: + if type == Type.RAMP or calculation == Calculation.STEP_DIMENSIONS: + return depth + else: + return depth / steps + +## Get the whole depth of the ramp/staircase. +func get_true_depth() -> float: + if type == Type.STAIRCASE: + return get_true_step_depth() * steps + else: + return get_true_step_depth() + +## Get the step height of the staircase. +func get_true_step_height() -> float: + if type == Type.RAMP or calculation == Calculation.STEP_DIMENSIONS: + return height + else: + return height / steps + +## Get the whole height of the ramp/staircase. +func get_true_height() -> float: + if type == Type.STAIRCASE: + return get_true_step_height() * steps + else: + return get_true_step_height() + +## Get the anchor offset for a specific anchor according to the dimensions of the ramp/staircase. +func get_anchor_offset(anchor: Anchor) -> Vector3: + var offset := Vector3() + var depth: float = get_true_depth() + var height: float = get_true_height() + match anchor: + Anchor.BOTTOM_CENTER: + offset = Vector3(0, 0, 0) + Anchor.BOTTOM_LEFT: + offset = Vector3(-width / 2.0, 0, 0) + Anchor.BOTTOM_RIGHT: + offset = Vector3(width / 2.0, 0, 0) + Anchor.TOP_CENTER: + offset = Vector3(0, -height, -depth) + Anchor.TOP_LEFT: + offset = Vector3(-width / 2.0, -height, -depth) + Anchor.TOP_RIGHT: + offset = Vector3(width / 2.0, -height, -depth) + Anchor.BASE_CENTER: + offset = Vector3(0, 0, -depth) + Anchor.BASE_LEFT: + offset = Vector3(-width / 2.0, 0, -depth) + Anchor.BASE_RIGHT: + offset = Vector3(width / 2.0, 0, -depth) + return offset + +func set_type(value: Type) -> void: + _type = value + notify_property_list_changed() + if is_entered_tree: + # Staircase: dimensions are reset from forced STAIRCASE_DIMENSIONS calculation + # Ramp: dimensions are forced to STAIRCASE_DIMENSIONS calculation + if calculation == Calculation.STEP_DIMENSIONS: + match type: + Type.STAIRCASE: + _height /= steps + _depth = (_depth + epsilon) / steps + Type.RAMP: + _height *= steps + _depth = (_depth + epsilon) * steps + refresh_shape() + type_changed.emit() + update_gizmos() + +## Sets the calculation method and recalculates the dimensions of the ramp/staircase. +func set_calculation(value: Calculation) -> void: + _calculation = value + # Calculate current step or staircase dimensions + # Only affecting dimensions when in STAIRCASE mode + if is_entered_tree: + match calculation: + Calculation.STAIRCASE_DIMENSIONS: + if type == Type.STAIRCASE: + _height *= steps + _depth = (_depth + epsilon) * steps + Calculation.STEP_DIMENSIONS: + if type == Type.STAIRCASE: + _height /= steps + _depth = (_depth + epsilon) / steps + +func set_width(value: float) -> void: + _width = value + refresh_shape() + width_changed.emit() + update_gizmos() + +func set_height(value: float) -> void: + _height = value + refresh_shape() + height_changed.emit() + update_gizmos() + +func set_depth(value: float) -> void: + _depth = value + refresh_shape() + depth_changed.emit() + update_gizmos() + +func set_fill(value: float) -> void: + _fill = max(0.0, min(1.0, value)) + refresh_shape() + fill_changed.emit() + update_gizmos() + +## Translates the ramp/staircase to a new anchor point in local space. +## Then recalculates the stairs/ramp with the new offset. +func set_anchor(value: Anchor) -> void: + # Transform node to new anchor + translate_anchor(anchor, value) + _anchor = value + refresh_shape() + anchor_changed.emit() + update_gizmos() + +func set_collisions_enabled(value: bool) -> void: + _collisions_enabled = value + notify_property_list_changed() + refresh_shape() + +## Translates the ramp/staircase to a new anchor point in local space if anchor is not fixed. +func translate_anchor(from_anchor: Anchor, to_anchor: Anchor) -> void: + if not anchor_fixed: + translate_object_local(get_anchor_offset(from_anchor) - get_anchor_offset(to_anchor)) + +func set_anchor_fixed(value: bool) -> void: + _anchor_fixed = value + +func set_material(value: Variant) -> void: + material = value + refresh_shape() + +func set_steps(value: int) -> void: + _steps = value + refresh_shape() + step_count_changed.emit() + update_gizmos() + +## Deletes all children and generates new steps/ramp. +func refresh_shape() -> void: + var offset := get_anchor_offset(anchor) + var polygon_offset := Vector3(offset.z, offset.y, -offset.x) + Vector3(0, 0, width / 2.0) + + # Resetting anchor offset to 0,0,0 to avoid problems during step calculations + translate_anchor(anchor, Anchor.BOTTOM_CENTER) + + if shape_polygon != null: + remove_child(shape_polygon) + shape_polygon.queue_free() + + shape_polygon = CSGPolygon3D.new() + shape_polygon.use_collision = false + + match type: + Type.STAIRCASE: + shape_polygon.polygon = create_staircase_array() + Type.RAMP: + shape_polygon.polygon = create_ramp_array() + + shape_polygon.rotate(Vector3.UP, -PI / 2.0) + shape_polygon.translate(polygon_offset) + shape_polygon.depth = width + + shape_polygon.use_collision = collisions_enabled + if collisions_enabled and material == null: + var shape_material := StandardMaterial3D.new() + shape_material.albedo_color = Color.AQUA + shape_polygon.material = shape_material + else: + shape_polygon.material = material + + add_child(shape_polygon) + + # Restore anchor offset + translate_anchor(Anchor.BOTTOM_CENTER, anchor) + +## Adds a new ramp based on current dimensions (without any anchor offset). +func create_ramp_array() -> PackedVector2Array: + # Create a single CSGPolygon3D + var array := PackedVector2Array() + if fill == 1: + array.append(Vector2(0, 0)) + array.append(Vector2(get_true_depth(), 0)) + array.append(Vector2(get_true_depth(), get_true_height())) + + if fill == 0: + array.append(Vector2(0, 0)) + array.append(Vector2(get_true_depth() * 0.001, 0)) + array.append(Vector2(get_true_depth(), get_true_height() * 0.999)) + array.append(Vector2(get_true_depth(), get_true_height())) + + if fill < 1.0 and fill > 0.0: + array.append(Vector2(0, 0)) + array.append(Vector2(get_true_depth() * fill, 0)) + array.append(Vector2(get_true_depth(), get_true_height() * (1 - fill))) + array.append(Vector2(get_true_depth(), get_true_height())) + + return array + +func create_staircase_array() -> PackedVector2Array: + # Create a staircase with CSGBox3Ds + var array := PackedVector2Array() + + if fill == 1: + # Base: + # 4 + # | + # | 1 + # | | + # 3--------2 + array.append(Vector2(0, get_true_step_height())) # 1 + array.append(Vector2(0, 0)) # 2 + array.append(Vector2(get_true_depth(), 0)) # 3 + array.append(Vector2(get_true_depth(), get_true_height())) # 4 + + if fill == 0: + # Base: + # 4 + # \ + # \ 1 + # \ | + # 2 + array.append(Vector2(0, get_true_step_height())) # 1 + array.append(Vector2(0, 0)) # 2 + # No #3 present + array.append(Vector2(get_true_depth(), get_true_height())) # 4 + + if fill < 1.0 and fill > 0.0: + # Base: + # 4 + # | + # 3b 1 + # \ | + # 3a---2 + array.append(Vector2(0, get_true_step_height())) # 1 + array.append(Vector2(0, 0)) # 2 + array.append(Vector2(get_true_depth() * fill, 0)) # 3a + array.append(Vector2(get_true_depth(), get_true_height() * (1 - fill))) # 3b + array.append(Vector2(get_true_depth(), get_true_height())) # 4 + + # Steps: + # 4---5 + # | | + # | 6---7 + # | | + # | 8---1 + # | | + # 3-----------2 + for i in range(steps - 1): + array.append(Vector2(get_true_depth() - get_true_step_depth() * (i + 1), get_true_height() - get_true_step_height() * i)) + array.append(Vector2(get_true_depth() - get_true_step_depth() * (i + 1), get_true_height() - get_true_step_height() * (i + 1))) + + return array + +## Using dynamic type for gizmos to avoid packaging errors. +## See proto_ramp_gizmos.gd for more information. +var gizmos = null + +func _enter_tree() -> void: + # is_entered_tree is used to avoid setting properties traditionally on initialization + refresh_shape() + if material: + set_material(material) + if Engine.is_editor_hint(): + var ProtoRampGizmos = load("res://addons/proto_shape/proto_ramp/proto_ramp_gizmos.gd") + gizmos = ProtoRampGizmos.new() + gizmos.attach_ramp(self) + + is_entered_tree = true + +func _exit_tree() -> void: + # Remove all children + remove_child(shape_polygon) + shape_polygon.queue_free() + if Engine.is_editor_hint(): + gizmos.remove_ramp() diff --git a/source/addons/proto_shape/proto_ramp/proto_ramp.gd.uid b/source/addons/proto_shape/proto_ramp/proto_ramp.gd.uid new file mode 100644 index 0000000..75879da --- /dev/null +++ b/source/addons/proto_shape/proto_ramp/proto_ramp.gd.uid @@ -0,0 +1 @@ +uid://briioihkxilxm diff --git a/source/addons/proto_shape/proto_ramp/proto_ramp_gizmos.gd b/source/addons/proto_shape/proto_ramp/proto_ramp_gizmos.gd new file mode 100644 index 0000000..ac180d4 --- /dev/null +++ b/source/addons/proto_shape/proto_ramp/proto_ramp_gizmos.gd @@ -0,0 +1,417 @@ +# Implementing Gizmo +const ProtoGizmoWrapper = preload("res://addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd") +const ProtoGizmoUtils = preload("res://addons/proto_shape/proto_gizmo/proto_gizmo_utils.gd") +const ProtoRamp = preload("res://addons/proto_shape/proto_ramp/proto_ramp.gd") +const ProtoGizmoPlugin = preload("res://addons/proto_shape/proto_gizmo/proto_gizmo.gd") +var width_gizmo_id: int +var depth_gizmo_id: int +var height_gizmo_id: int +var fill_gizmo_id1: int +var fill_gizmo_id2: int +var gizmo_utils := ProtoGizmoUtils.new() +var ramp: ProtoRamp = null + +var undo_redo: EditorUndoRedoManager +var is_editing := false + +func attach_ramp(node: ProtoRamp) -> void: + ramp = node + if ramp.get_parent() is ProtoGizmoWrapper: + var parent: ProtoGizmoWrapper = ramp.get_parent() + parent.redraw_gizmos_for_child_signal.connect(redraw_gizmos) + parent.set_handle_for_child_signal.connect(set_handle) + parent.commit_handle.connect(commit_handle) + +func remove_ramp() -> void: + if ramp.get_parent() is ProtoGizmoWrapper: + var parent: ProtoGizmoWrapper = ramp.get_parent() + parent.redraw_gizmos_for_child_signal.disconnect(redraw_gizmos) + parent.set_handle_for_child_signal.disconnect(set_handle) + parent.commit_handle.disconnect(commit_handle) + # Disconnecting any leftover connections + if plugin != null: + if current_fine_snap_callable != null: + if plugin.fine_snapping_changed.is_connected(current_fine_snap_callable): + plugin.fine_snapping_changed.disconnect(current_fine_snap_callable) + if current_snap_callable != null: + if plugin.snapping_changed.is_connected(current_snap_callable): + plugin.snapping_changed.disconnect(current_snap_callable) + plugin = null + ramp = null + +# Snapping to grid +var snapping_enabled: bool = false +var snap_unit: float = 1.0 +var fine_snapping_enabled: bool = false +var fine_snap_unit: float = 0.1 + +# Captured arguments required for signal connections +# ramp: ProtoRamp - on closing a scene, the ramp is freed, so we need to check for null +# Unfortunately, disconnecting does not work (bug?), the lambda is still called afterwards +# plugin: ProtoGizmoPlugin - captured plugin argument will not be null (even after setting the field to null in remove_ramp) +func node_snap_listener(ramp: ProtoRamp, plugin: ProtoGizmoPlugin) -> Callable: + return func (enabled: bool) -> void: + snapping_enabled = enabled + if ramp != null: + ramp.update_gizmos() + else: + plugin.snapping_changed.disconnect(current_snap_callable) + +func node_fine_snap_listener(ramp: ProtoRamp, plugin: ProtoGizmoPlugin) -> Callable: + return func (enabled: bool) -> void: + fine_snapping_enabled = enabled + if ramp != null: + ramp.update_gizmos() + else: + plugin.fine_snapping_changed.disconnect(current_fine_snap_callable) + +var current_snap_callable: Callable + +var current_fine_snap_callable: Callable + +var plugin: ProtoGizmoPlugin + +func init_gizmo(plugin: ProtoGizmoPlugin) -> void: + # Generate a "random" id for each gizmo + width_gizmo_id = Time.get_ticks_usec() + depth_gizmo_id = width_gizmo_id + 1 + height_gizmo_id = width_gizmo_id + 2 + fill_gizmo_id1 = width_gizmo_id + 3 + fill_gizmo_id2 = width_gizmo_id + 4 + undo_redo = plugin.undo_redo + plugin = plugin + current_snap_callable = node_snap_listener(ramp, plugin) + current_fine_snap_callable = node_fine_snap_listener(ramp, plugin) + plugin.fine_snapping_changed.connect(current_fine_snap_callable) + plugin.snapping_changed.connect(current_snap_callable) + snapping_enabled = plugin.snapping + fine_snapping_enabled = plugin.fine_snapping + +# Debug purposes +var screen_pos: Vector2 +var debug_gizmo_handler_id: int +var camera_position: Vector3 + +## As gizmos can only be used in the Editor, we can cast the [gizmo] to [EditorNode3DGizmo] and [plugin] to [EditorNode3DGizmoPlugin]. +func redraw_gizmos(gizmo: EditorNode3DGizmo, plugin: ProtoGizmoPlugin) -> void: + if gizmo.get_node_3d() != ramp: + return + + if width_gizmo_id == 0 or depth_gizmo_id == 0 or height_gizmo_id == 0 or fill_gizmo_id1 == 0 or fill_gizmo_id2 == 0: + init_gizmo(plugin) + + gizmo.clear() + var true_depth: float = ramp.get_true_depth() + var true_height: float = ramp.get_true_height() + var anchor_offset: Vector3 = ramp.get_anchor_offset(ramp.anchor) + var fill: float = ramp.get_fill() + var depth_gizmo_position := Vector3(0, true_height / 2, true_depth) + anchor_offset + var width_gizmo_position := Vector3(ramp.width / 2, true_height / 2, true_depth / 2) + anchor_offset + var height_gizmo_position := Vector3(0, true_height, true_depth / 2) + anchor_offset + + # Calculate perpendicular line points for hypotenuse and the offset + var fill_gizmo_hypotenuse_projection := _get_fill_max_offset() * (1 - fill) + var fill_gizmo_position1 := Vector3(-ramp.width / 2, fill_gizmo_hypotenuse_projection.y, true_depth - fill_gizmo_hypotenuse_projection.z) + anchor_offset + var fill_gizmo_position2 := Vector3(ramp.width / 2, fill_gizmo_hypotenuse_projection.y, true_depth - fill_gizmo_hypotenuse_projection.z) + anchor_offset + + # When on the left, width gizmo is on the right + # When in the back (top, base), depth gizmo is on the front + # When on the top, height gizmo is on the bottom + # Don't offset fill gizmo positions + match ramp.anchor: + ProtoRamp.Anchor.BOTTOM_LEFT: + width_gizmo_position.x = -ramp.width + ProtoRamp.Anchor.TOP_LEFT: + width_gizmo_position.x = -ramp.width + depth_gizmo_position.z = -true_depth + height_gizmo_position.y = -true_height + ProtoRamp.Anchor.BASE_LEFT: + width_gizmo_position.x = -ramp.width + depth_gizmo_position.z = -true_depth + ProtoRamp.Anchor.BASE_CENTER: + depth_gizmo_position.z = -true_depth + ProtoRamp.Anchor.BASE_RIGHT: + depth_gizmo_position.z = -true_depth + ProtoRamp.Anchor.TOP_RIGHT: + depth_gizmo_position.z = -true_depth + height_gizmo_position.y = -true_height + ProtoRamp.Anchor.TOP_CENTER: + depth_gizmo_position.z = -true_depth + height_gizmo_position.y = -true_height + + var handles = PackedVector3Array() + handles.push_back(depth_gizmo_position) + handles.push_back(width_gizmo_position) + handles.push_back(height_gizmo_position) + handles.push_back(fill_gizmo_position1) + handles.push_back(fill_gizmo_position2) + + gizmo.add_handles(handles, plugin.get_material("proto_handler", gizmo), [depth_gizmo_id, width_gizmo_id, height_gizmo_id, fill_gizmo_id1, fill_gizmo_id2]) + + if ramp.shape_polygon != null and ramp.shape_polygon.get_meshes().size() > 1: + var offset := ramp.get_anchor_offset(ramp.anchor) + var polygon_offset := offset - Vector3(ramp.width / 2, 0, 0) + var mesh: Mesh = ramp.shape_polygon.get_meshes()[1] + var mdt := MeshDataTool.new() + mdt.create_from_surface(mesh, 0) + for i in range(mdt.get_vertex_count()): + var vertex := mdt.get_vertex(i) + vertex = vertex.rotated(Vector3.UP, -PI / 2.0) + vertex += polygon_offset + mdt.set_vertex(i, vertex) + + var newMesh: Mesh = ArrayMesh.new() + newMesh.clear_surfaces() + mdt.commit_to_surface(newMesh) + mdt.clear() + gizmo.add_collision_triangles(newMesh.generate_triangle_mesh()) + gizmo.add_mesh(newMesh.create_outline(0.001), plugin.get_material("selected", gizmo)) + + # Adding debug lines for gizmo if we have cursor screen position set + if screen_pos: + var grid_size_modifier = 1.0 + # Grid size is always the max of the two other dimensions + match debug_gizmo_handler_id: + depth_gizmo_id: + # Setting depth + grid_size_modifier = max(ramp.get_true_height(), ramp.get_width()) + var local_offset_axis = Vector3(0, 0, 1) + gizmo_utils.debug_draw_handle_grid(camera_position, screen_pos, depth_gizmo_position, local_offset_axis, ramp, gizmo, plugin, grid_size_modifier) + width_gizmo_id: + # Setting width + grid_size_modifier = max(ramp.get_true_height(), ramp.get_true_depth()) + var local_offset_axis = Vector3(1, 0, 0) + gizmo_utils.debug_draw_handle_grid(camera_position, screen_pos, width_gizmo_position, local_offset_axis, ramp, gizmo, plugin, grid_size_modifier) + height_gizmo_id: + # Setting height + grid_size_modifier = max(ramp.get_width(), ramp.get_true_depth()) + var local_offset_axis = Vector3(0, 1, 0) + gizmo_utils.debug_draw_handle_grid(camera_position, screen_pos, height_gizmo_position, local_offset_axis, ramp, gizmo, plugin, grid_size_modifier) + fill_gizmo_id1: + # Setting fill 1 + grid_size_modifier = max(ramp.get_true_height(), ramp.get_true_depth()) + var local_plane_normal = Vector3(1, 0, 0) + var local_offset_axis = _get_fill_max_offset().normalized() + local_offset_axis.z = -local_offset_axis.z + gizmo_utils.debug_draw_handle_grid_on_plane(fill_gizmo_position1, local_offset_axis, local_plane_normal, ramp, gizmo, plugin, grid_size_modifier) + fill_gizmo_id2: + # Setting fill 2 + grid_size_modifier = max(ramp.get_true_height(), ramp.get_true_depth()) + var local_plane_normal = Vector3(1, 0, 0) + var local_offset_axis = _get_fill_max_offset().normalized() + local_offset_axis.z = -local_offset_axis.z + gizmo_utils.debug_draw_handle_grid_on_plane(fill_gizmo_position2, local_offset_axis, local_plane_normal, ramp, gizmo, plugin, grid_size_modifier) + +var start_offset := 0.0 +var end_offset := 0.0 + +func set_handle( + gizmo: EditorNode3DGizmo, + plugin: ProtoGizmoPlugin, + handle_id: int, + secondary: bool, + camera: Camera3D, + screen_pos: Vector2) -> void: + # Set debug parameters for redraw + var child := gizmo.get_node_3d() + if child != ramp: + return + + self.screen_pos = screen_pos + self.camera_position = camera.position + + match handle_id: + depth_gizmo_id: + end_offset = _get_depth_handle_offset(camera, screen_pos) + if snapping_enabled and not fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit) + elif fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit) + ramp.depth = _get_ramp_depth(end_offset) + width_gizmo_id: + end_offset = _get_width_handle_offset(camera, screen_pos) + if snapping_enabled and not fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit) + elif fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit) + ramp.width = _get_ramp_width(end_offset) + height_gizmo_id: + end_offset = _get_height_handle_offset(camera, screen_pos) + if snapping_enabled and not fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit) + elif fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit) + ramp.height = _get_ramp_height(end_offset) + fill_gizmo_id1: + end_offset = _get_fill_handle_offset(camera, screen_pos, Vector3(-ramp.width / 2, 0, 0)) + if snapping_enabled and not fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit) + elif fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit) + ramp.fill = end_offset + fill_gizmo_id2: + end_offset = _get_fill_handle_offset(camera, screen_pos, Vector3(ramp.width / 2, 0, 0)) + if snapping_enabled and not fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, snap_unit) + elif fine_snapping_enabled: + end_offset = gizmo_utils.snap_to_grid(end_offset, fine_snap_unit) + ramp.fill = end_offset + + if !is_editing: + match handle_id: + depth_gizmo_id: + debug_gizmo_handler_id = depth_gizmo_id + start_offset = _get_depth_handle_offset(camera, screen_pos) + width_gizmo_id: + debug_gizmo_handler_id = width_gizmo_id + start_offset = _get_width_handle_offset(camera, screen_pos) + height_gizmo_id: + debug_gizmo_handler_id = height_gizmo_id + start_offset = _get_height_handle_offset(camera, screen_pos) + fill_gizmo_id1: + debug_gizmo_handler_id = fill_gizmo_id1 + start_offset = _get_fill_handle_offset(camera, screen_pos, Vector3(-ramp.width / 2, 0, 0)) + fill_gizmo_id2: + debug_gizmo_handler_id = fill_gizmo_id2 + start_offset = _get_fill_handle_offset(camera, screen_pos, Vector3(ramp.width / 2, 0, 0)) + is_editing = true + + ramp.update_gizmos() + +func _get_depth_handle_offset( + camera: Camera3D, + screen_pos: Vector2) -> float: + var local_offset_axis = Vector3(0, 0, 1) + var gizmo_position = Vector3(0, ramp.get_true_height() / 2, ramp.get_true_depth()) + ramp.get_anchor_offset(ramp.anchor) + var handle_offset = gizmo_utils.get_handle_offset(camera, screen_pos, gizmo_position, local_offset_axis, ramp) + return handle_offset.z + +func _get_width_handle_offset( + camera: Camera3D, + screen_pos: Vector2) -> float: + var local_offset_axis = Vector3(1, 0, 0) + var gizmo_position = Vector3(ramp.width / 2, ramp.get_true_height() / 2, ramp.get_true_depth() / 2) + ramp.get_anchor_offset(ramp.anchor) + var handle_offset = gizmo_utils.get_handle_offset(camera, screen_pos, gizmo_position, local_offset_axis, ramp) + return handle_offset.x + +func _get_height_handle_offset( + camera: Camera3D, + screen_pos: Vector2) -> float: + var local_offset_axis = Vector3(0, 1, 0) + var gizmo_position = Vector3(0, ramp.get_true_height(), ramp.get_true_depth() / 2) + ramp.get_anchor_offset(ramp.anchor) + var handle_offset = gizmo_utils.get_handle_offset(camera, screen_pos, gizmo_position, local_offset_axis, ramp) + return handle_offset.y + +func _get_fill_handle_offset( + camera: Camera3D, + screen_pos: Vector2, + gizmo_position_offset: Vector3) -> float: + var fill_gizmo_axis := _get_fill_max_offset() + fill_gizmo_axis.z = ramp.get_true_depth() - fill_gizmo_axis.z + var fill_gizmo_offset := fill_gizmo_axis * (1 - ramp.fill) + var gizmo_position := Vector3(0, fill_gizmo_offset.y, fill_gizmo_offset.z) + ramp.get_anchor_offset(ramp.anchor) + gizmo_position_offset + var local_plane_normal := Vector3(1, 0, 0) + var handle_offset = gizmo_utils.get_handle_offset_by_plane(camera, screen_pos, gizmo_position, local_plane_normal, ramp) + var gizmo_base_position := Vector3(0, 0, ramp.get_true_depth()) + handle_offset -= gizmo_base_position + var gizmo_max_position := fill_gizmo_axis - gizmo_base_position + gizmo_max_position.x = 0 + handle_offset -= ramp.get_anchor_offset(ramp.anchor) + handle_offset.x = 0 + if (handle_offset.dot(gizmo_max_position) < 0): + return 1 + return min(1.0, max(0.0, 1 - handle_offset.project(gizmo_max_position).length() / gizmo_max_position.length())) + +func _get_ramp_width(offset: float) -> float: + # If anchor is on the left, offset is negative + # If anchor is not centered, offset is divided by 2 + match ramp.anchor: + ProtoRamp.Anchor.BOTTOM_LEFT: + offset = -offset / 2 + ProtoRamp.Anchor.TOP_LEFT: + offset = -offset / 2 + ProtoRamp.Anchor.BASE_LEFT: + offset = -offset / 2 + ProtoRamp.Anchor.BOTTOM_RIGHT: + offset = offset / 2 + ProtoRamp.Anchor.TOP_RIGHT: + offset = offset / 2 + ProtoRamp.Anchor.BASE_RIGHT: + offset = offset / 2 + return offset * 2 + +func _get_ramp_depth(offset: float) -> float: + if ramp.calculation == ProtoRamp.Calculation.STEP_DIMENSIONS and ramp.type == ProtoRamp.Type.STAIRCASE: + offset = offset / ramp.steps + # If anchor is on the back, offset is negative + match ramp.anchor: + ProtoRamp.Anchor.BASE_CENTER: + offset = -offset + ProtoRamp.Anchor.BASE_RIGHT: + offset = -offset + ProtoRamp.Anchor.BASE_LEFT: + offset = -offset + ProtoRamp.Anchor.TOP_CENTER: + offset = -offset + ProtoRamp.Anchor.TOP_RIGHT: + offset = -offset + ProtoRamp.Anchor.TOP_LEFT: + offset = -offset + return offset + +func _get_ramp_height(offset: float) -> float: + # If anchor is TOP, offset is negative + if ramp.calculation == ProtoRamp.Calculation.STEP_DIMENSIONS and ramp.type == ProtoRamp.Type.STAIRCASE: + offset = offset / ramp.steps + match ramp.anchor: + ProtoRamp.Anchor.TOP_LEFT: + offset = -offset + ProtoRamp.Anchor.TOP_CENTER: + offset = -offset + ProtoRamp.Anchor.TOP_RIGHT: + offset = -offset + return offset + +func _get_fill_max_offset() -> Vector3: + var A := Vector2(0, ramp.get_true_height()) + var B := Vector2(ramp.get_true_depth(), 0) + var fill_gizmo_base_position := Vector2(0, 0) + var dir := (B - A).normalized() + var t := (fill_gizmo_base_position - A).dot(dir) + var fill_gizmo_hypotenuse_projection := A + dir * t + var projection_vector := fill_gizmo_hypotenuse_projection - fill_gizmo_base_position + return Vector3(0, projection_vector.y, projection_vector.x) + +func commit_handle( + gizmo: EditorNode3DGizmo, + handle_id: int, + secondary: bool, + restore: Variant, + cancel: bool) -> void: + if gizmo.get_node_3d() != ramp: + return + + match handle_id: + depth_gizmo_id: + restore = _get_ramp_depth(start_offset) + undo_redo.create_action("Edit ramp depth", 0, ramp, true) + undo_redo.add_do_property(ramp, "depth", _get_ramp_depth(end_offset)) + undo_redo.add_undo_property(ramp, "depth", restore) + width_gizmo_id: + restore = _get_ramp_width(start_offset) + undo_redo.create_action("Edit ramp width", 0, ramp, true) + undo_redo.add_do_property(ramp, "width", _get_ramp_width(end_offset)) + undo_redo.add_undo_property(ramp, "width", restore) + height_gizmo_id: + restore = _get_ramp_height(start_offset) + undo_redo.create_action("Edit ramp height", 0, ramp, true) + undo_redo.add_do_property(ramp, "height", _get_ramp_height(end_offset)) + undo_redo.add_undo_property(ramp, "height", restore) + fill_gizmo_id1, fill_gizmo_id2: + restore = start_offset + undo_redo.create_action("Edit ramp fill", 0, ramp, true) + undo_redo.add_do_property(ramp, "fill", end_offset) + undo_redo.add_undo_property(ramp, "fill", restore) + + undo_redo.commit_action() + is_editing = false diff --git a/source/addons/proto_shape/proto_ramp/proto_ramp_gizmos.gd.uid b/source/addons/proto_shape/proto_ramp/proto_ramp_gizmos.gd.uid new file mode 100644 index 0000000..f9e33c5 --- /dev/null +++ b/source/addons/proto_shape/proto_ramp/proto_ramp_gizmos.gd.uid @@ -0,0 +1 @@ +uid://chy0qm1esgcji diff --git a/source/addons/proto_shape/proto_shape.gd b/source/addons/proto_shape/proto_shape.gd new file mode 100644 index 0000000..f88829e --- /dev/null +++ b/source/addons/proto_shape/proto_shape.gd @@ -0,0 +1,52 @@ +@tool +extends EditorPlugin + +const ProtoGizmo = preload("res://addons/proto_shape/proto_gizmo/proto_gizmo.gd") + +var gizmo_plugin = ProtoGizmo.new() +var undo_redo: EditorUndoRedoManager + +func _enter_tree(): + undo_redo = get_undo_redo() + gizmo_plugin.undo_redo = undo_redo + add_custom_type("ProtoRamp", "Node3D", preload("res://addons/proto_shape/proto_ramp/proto_ramp.gd"), preload("res://addons/proto_shape/icon/proto-ramp-icon.png")) + add_custom_type("ProtoGizmoWrapper", "Node", preload("res://addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd"), preload("res://addons/proto_shape/icon/proto-gizmo-wrapper-icon.png")) + add_node_3d_gizmo_plugin(gizmo_plugin) + + var snap_to_grid_action = InputEventKey.new() + snap_to_grid_action.keycode = KEY_CTRL + var fine_snap_to_grid_action = InputEventKey.new() + fine_snap_to_grid_action.keycode = KEY_SHIFT + InputMap.add_action("snap_to_grid") + InputMap.add_action("fine_snap_to_grid") + InputMap.action_add_event("snap_to_grid", snap_to_grid_action) + InputMap.action_add_event("fine_snap_to_grid", fine_snap_to_grid_action) + +func _exit_tree(): + remove_custom_type("ProtoRamp") + remove_custom_type("ProtoGizmoWrapper") + remove_node_3d_gizmo_plugin(gizmo_plugin) + InputMap.erase_action("snap_to_grid") + InputMap.erase_action("fine_snap_to_grid") + +func _shortcut_input(event: InputEvent) -> void: + if event.is_action_pressed("snap_to_grid"): + if event is InputEventKey: + if event.keycode == KEY_CTRL: + gizmo_plugin.snapping = true + if event.shift_pressed: + gizmo_plugin.fine_snapping = true + else: + gizmo_plugin.fine_snapping = false + if event.is_action_pressed("fine_snap_to_grid"): + if event is InputEventKey: + if event.keycode == KEY_SHIFT and event.ctrl_pressed: + gizmo_plugin.fine_snapping = true + else: + gizmo_plugin.snapping = false + gizmo_plugin.fine_snapping = false + if event.is_action_released("snap_to_grid"): + gizmo_plugin.snapping = false + gizmo_plugin.fine_snapping = false + if event.is_action_released("fine_snap_to_grid"): + gizmo_plugin.fine_snapping = false diff --git a/source/addons/proto_shape/proto_shape.gd.uid b/source/addons/proto_shape/proto_shape.gd.uid new file mode 100644 index 0000000..aabc0c4 --- /dev/null +++ b/source/addons/proto_shape/proto_shape.gd.uid @@ -0,0 +1 @@ +uid://bw3q4nuh1nyfu diff --git a/source/addons/proto_shape/scenes/Camera3D.gd b/source/addons/proto_shape/scenes/Camera3D.gd new file mode 100644 index 0000000..944b18f --- /dev/null +++ b/source/addons/proto_shape/scenes/Camera3D.gd @@ -0,0 +1,7 @@ +extends Camera3D + +# Camera rotation parameters +@export var node_to_look_at: Node3D + +func _process(delta): + look_at(node_to_look_at.position) diff --git a/source/addons/proto_shape/scenes/Camera3D.gd.uid b/source/addons/proto_shape/scenes/Camera3D.gd.uid new file mode 100644 index 0000000..69298a9 --- /dev/null +++ b/source/addons/proto_shape/scenes/Camera3D.gd.uid @@ -0,0 +1 @@ +uid://7a8og04b8bqs diff --git a/source/addons/proto_shape/scenes/CharacterBody3D.gd b/source/addons/proto_shape/scenes/CharacterBody3D.gd new file mode 100644 index 0000000..3faa66b --- /dev/null +++ b/source/addons/proto_shape/scenes/CharacterBody3D.gd @@ -0,0 +1,54 @@ +extends CharacterBody3D + + +const SPEED = 5.0 +const JUMP_VELOCITY = 4.5 + +# Camera rotation parameters +var sensitivity: float = 0.2 +var min_pitch: float = -60 +var max_pitch: float = 60 + +# Get the gravity from the project settings to be synced with RigidBody nodes. +var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") + +var mouse_sens = 0.3 +var camera_anglev=0 + +func _physics_process(delta): + # Add the gravity. + if not is_on_floor(): + velocity.y -= gravity * delta + + # Handle Jump. + if Input.is_action_just_pressed("ui_accept") and is_on_floor(): + velocity.y = JUMP_VELOCITY + + # Get the input direction and handle the movement/deceleration. + # As good practice, you should replace UI actions with custom gameplay actions. + var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() + if direction: + velocity.x = direction.x * SPEED + velocity.z = direction.z * SPEED + else: + velocity.x = move_toward(velocity.x, 0, SPEED) + velocity.z = move_toward(velocity.z, 0, SPEED) + + move_and_slide() + +func _ready(): + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + +func _exit_tree(): + Input.set_mouse_mode(Input.MOUSE_MODE_CONFINED) + +func _input(event): + if event is InputEventMouseMotion: + handle_camera_rotation(event.relative) + +func handle_camera_rotation(mouse_delta: Vector2): + # Rotate the camera + rotate_y(deg_to_rad(-mouse_delta.x * sensitivity)) + var new_pitch = rotation_degrees.x - mouse_delta.y * sensitivity + #rotation_degrees.x = clamp(new_pitch, min_pitch, max_pitch) diff --git a/source/addons/proto_shape/scenes/CharacterBody3D.gd.uid b/source/addons/proto_shape/scenes/CharacterBody3D.gd.uid new file mode 100644 index 0000000..93382b9 --- /dev/null +++ b/source/addons/proto_shape/scenes/CharacterBody3D.gd.uid @@ -0,0 +1 @@ +uid://dbi1dsujlxbfx diff --git a/source/addons/proto_shape/scenes/default_env.tres b/source/addons/proto_shape/scenes/default_env.tres new file mode 100644 index 0000000..44a56d9 --- /dev/null +++ b/source/addons/proto_shape/scenes/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=3 uid="uid://bvrv7sopv5vy5"] + +[sub_resource type="Sky" id="1"] + +[resource] +background_mode = 2 +sky = SubResource("1") diff --git a/source/addons/proto_shape/scenes/proto_ramp_logo.tscn b/source/addons/proto_shape/scenes/proto_ramp_logo.tscn new file mode 100644 index 0000000..3d60cbc --- /dev/null +++ b/source/addons/proto_shape/scenes/proto_ramp_logo.tscn @@ -0,0 +1,65 @@ +[gd_scene load_steps=7 format=3 uid="uid://drys0h14kvoud"] + +[ext_resource type="Script" uid="uid://briioihkxilxm" path="res://addons/proto_shape/proto_ramp/proto_ramp.gd" id="2_i41uw"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_bk5re"] +sun_angle_max = 60.0 + +[sub_resource type="Sky" id="Sky_figaw"] +sky_material = SubResource("ProceduralSkyMaterial_bk5re") + +[sub_resource type="Environment" id="Environment_sxnsr"] +background_mode = 2 +sky = SubResource("Sky_figaw") +ambient_light_source = 3 +ambient_light_color = Color(1, 1, 1, 1) +reflected_light_source = 2 + +[sub_resource type="CameraAttributesPhysical" id="CameraAttributesPhysical_miufq"] + +[sub_resource type="SphereShape3D" id="SphereShape3D_8fq5c"] +radius = 0.493361 + +[node name="Linear Stairs" type="Node3D"] + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_sxnsr") +camera_attributes = SubResource("CameraAttributesPhysical_miufq") + +[node name="Camera3D" type="Camera3D" parent="WorldEnvironment"] +transform = Transform3D(-0.707107, -0.40558, 0.579228, 0, 0.819152, 0.573576, -0.707107, 0.40558, -0.579228, 4, 4, -4) +projection = 1 +fov = 81.5625 +size = 2.0 + +[node name="CSGBox3D" type="CSGBox3D" parent="WorldEnvironment"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 17.6707, 0.972, 3.9) +size = Vector3(10.952, 5.55505, 10.5952) + +[node name="ProtoRamp2" type="CSGCombiner3D" parent="WorldEnvironment"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0) +use_collision = true +script = ExtResource("2_i41uw") +type = 1 +width = 1.001 +height = 1.001 +depth = 1.001 +anchor = 1 +anchor_fixed = false +material = null +calculation = 0 +steps = 8 +fill = true + +[node name="CSGPolygon3D" type="CSGPolygon3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.29666, -0.811041, -4.80418) +polygon = PackedVector2Array(2.51521, -1.5345, -1.03, 0.92, -0.6, -1.08) +depth = 1.48 + +[node name="RigidBody3D" type="RigidBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.499645, 1.87208, -0.158321) + +[node name="CSGSphere3D" type="CSGSphere3D" parent="RigidBody3D"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="RigidBody3D"] +shape = SubResource("SphereShape3D_8fq5c") diff --git a/source/addons/proto_shape/scenes/prototype_scene.tscn b/source/addons/proto_shape/scenes/prototype_scene.tscn new file mode 100644 index 0000000..09de26e --- /dev/null +++ b/source/addons/proto_shape/scenes/prototype_scene.tscn @@ -0,0 +1,123 @@ +[gd_scene load_steps=11 format=3 uid="uid://kh6kodj0l0ld"] + +[ext_resource type="Script" uid="uid://dbi1dsujlxbfx" path="res://addons/proto_shape/scenes/CharacterBody3D.gd" id="1_4rry8"] +[ext_resource type="Script" uid="uid://7a8og04b8bqs" path="res://addons/proto_shape/scenes/Camera3D.gd" id="2_5cyg4"] +[ext_resource type="Script" uid="uid://wr0twoe15d87" path="res://addons/proto_shape/proto_gizmo_wrapper/proto_gizmo_wrapper.gd" id="4_3jo5l"] +[ext_resource type="Script" uid="uid://briioihkxilxm" path="res://addons/proto_shape/proto_ramp/proto_ramp.gd" id="4_durks"] + +[sub_resource type="NavigationMesh" id="NavigationMesh_s4hfa"] + +[sub_resource type="PhysicalSkyMaterial" id="PhysicalSkyMaterial_mt60a"] +turbidity = 0.74 +sun_disk_scale = 7.67 +energy_multiplier = 3.37 + +[sub_resource type="Sky" id="Sky_a8iul"] +sky_material = SubResource("PhysicalSkyMaterial_mt60a") + +[sub_resource type="Environment" id="Environment_050vn"] +background_mode = 2 +background_energy_multiplier = 1.14 +sky = SubResource("Sky_a8iul") +ambient_light_source = 3 +reflected_light_source = 2 +ssao_enabled = true +ssil_enabled = true +sdfgi_enabled = true +sdfgi_use_occlusion = true + +[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_6hoe0"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_ou067"] +radius = 0.25 +height = 1.0 + +[node name="prototype_scene" type="Node3D"] + +[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.371784, 0) +navigation_mesh = SubResource("NavigationMesh_s4hfa") + +[node name="CSGBox3D3" type="CSGBox3D" parent="NavigationRegion3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5, 1, -2) +use_collision = true +size = Vector3(1, 3, 20) + +[node name="CSGBox3D4" type="CSGBox3D" parent="NavigationRegion3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 1, -7) +size = Vector3(1, 3, 10) + +[node name="CSGBox3D2" type="CSGBox3D" parent="NavigationRegion3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -7) +use_collision = true +size = Vector3(10, 1, 10) + +[node name="CSGBox3D5" type="CSGBox3D" parent="NavigationRegion3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 3) +use_collision = true +size = Vector3(10, 1, 10) + +[node name="CSGBox3D6" type="CSGBox3D" parent="NavigationRegion3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0.5, 6) +use_collision = true +size = Vector3(6, 2, 4) + +[node name="CSGBox3D" type="CSGBox3D" parent="NavigationRegion3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0, -1) +use_collision = true +size = Vector3(2, 1, 2) + +[node name="ProtoRamp" type="Node3D" parent="NavigationRegion3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, -0.5, 4) +script = ExtResource("4_durks") +type = 0 +collisions_enabled = true +width = 3.0 +height = 2.0 +depth = 3.0 +anchor = 7 +anchor_fixed = true +fill = 0.1 +material = null + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_050vn") +camera_attributes = SubResource("CameraAttributesPractical_6hoe0") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="WorldEnvironment"] +transform = Transform3D(0.123586, 0.941458, -0.31366, -0.696625, 0.307415, 0.648236, 0.706711, 0.138391, 0.693836, 2.88203, 1.98864, 9.43085) +shadow_enabled = true + +[node name="CharacterBody3D" type="CharacterBody3D" parent="WorldEnvironment"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.5, 0.5, 4) +script = ExtResource("1_4rry8") + +[node name="Camera3D" type="Camera3D" parent="WorldEnvironment/CharacterBody3D" node_paths=PackedStringArray("node_to_look_at")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 2) +script = ExtResource("2_5cyg4") +node_to_look_at = NodePath("..") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldEnvironment/CharacterBody3D"] +shape = SubResource("CapsuleShape3D_ou067") + +[node name="CSGCombiner3D" type="CSGCombiner3D" parent="WorldEnvironment/CharacterBody3D"] + +[node name="CSGCylinder3D" type="CSGCylinder3D" parent="WorldEnvironment/CharacterBody3D/CSGCombiner3D"] +radius = 0.25 +height = 0.5 +sides = 64 + +[node name="CSGSphere3D2" type="CSGSphere3D" parent="WorldEnvironment/CharacterBody3D/CSGCombiner3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0) +radius = 0.25 +radial_segments = 64 +rings = 32 + +[node name="CSGSphere3D" type="CSGSphere3D" parent="WorldEnvironment/CharacterBody3D/CSGCombiner3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.25, 0) +radius = 0.25 +radial_segments = 64 +rings = 32 + +[node name="ProtoGizmoWrapper" type="Node" parent="."] +script = ExtResource("4_3jo5l") diff --git a/source/project.godot b/source/project.godot index 20dd17e..cc6d037 100644 --- a/source/project.godot +++ b/source/project.godot @@ -40,7 +40,7 @@ settings/3d/volumetric_defaults/thickness=0.0 [editor_plugins] -enabled=PackedStringArray("res://addons/bbcode_edit.editor/plugin.cfg", "res://addons/boxconstructor/plugin.cfg", "res://addons/controller_icons/plugin.cfg", "res://addons/logger/plugin.cfg", "res://addons/panku_console/plugin.cfg", "res://addons/run-configs/plugin.cfg", "res://addons/version_display/plugin.cfg") +enabled=PackedStringArray("res://addons/Todo_Manager/plugin.cfg", "res://addons/bbcode_edit.editor/plugin.cfg", "res://addons/boxconstructor/plugin.cfg", "res://addons/controller_icons/plugin.cfg", "res://addons/godot_3d_cursor/plugin.cfg", "res://addons/logger/plugin.cfg", "res://addons/panku_console/plugin.cfg", "res://addons/proto_shape/plugin.cfg", "res://addons/run-configs/plugin.cfg", "res://addons/version_display/plugin.cfg") [file_customization]