├── Documentation └── Image │ ├── catlogo.jpg │ ├── catlogo.jpg.import │ ├── scene_builder_collection_names_resource.png │ ├── scene_builder_collection_names_resource.png.import │ ├── scene_builder_commands.png │ ├── scene_builder_commands.png.import │ ├── scene_builder_create_items.png │ ├── scene_builder_create_items.png.import │ ├── scene_builder_demo.png │ ├── scene_builder_demo.png.import │ ├── scene_builder_dock.png │ ├── scene_builder_dock.png.import │ ├── scene_builder_file_system.png │ ├── scene_builder_file_system.png.import │ ├── scene_builder_icon.jpg │ ├── scene_builder_icon.jpg.import │ ├── scene_builder_item.png │ ├── scene_builder_item.png.import │ ├── scene_builder_shortcuts.png │ ├── scene_builder_shortcuts.png.import │ ├── tutorial_link.png │ └── tutorial_link.png.import ├── LICENSE ├── README.md └── addons └── scene_builder ├── Commands ├── alphabetize_nodes.gd ├── alphabetize_nodes.gd.uid ├── change_places.gd ├── change_places.gd.uid ├── create_audio_stream_player_3d.gd ├── create_audio_stream_player_3d.gd.uid ├── create_scene_builder_items.gd ├── create_scene_builder_items.gd.uid ├── create_standard_material_3d.gd ├── create_standard_material_3d.gd.uid ├── fix_negative_scaling.gd ├── fix_negative_scaling.gd.uid ├── instantiate_in_a_row.gd ├── instantiate_in_a_row.gd.uid ├── push_parent_offset_to_child.gd ├── push_parent_offset_to_child.gd.uid ├── push_to_grid.gd ├── push_to_grid.gd.uid ├── repair_standard_material_3d.gd ├── repair_standard_material_3d.gd.uid ├── reset_node_name.gd ├── reset_node_name.gd.uid ├── reset_transform.gd ├── reset_transform.gd.uid ├── select_children.gd ├── select_children.gd.uid ├── select_parents.gd ├── select_parents.gd.uid ├── set_visibility.gd ├── set_visibility.gd.uid ├── swap_nodes.gd ├── swap_nodes.gd.uid ├── temporary_debug.gd └── temporary_debug.gd.uid ├── editor_utilities.gd.uid ├── icon_studio.tscn ├── icon_tmp.png ├── icon_tmp.png.import ├── plugin.cfg ├── scene_builder_collections.gd ├── scene_builder_collections.gd.uid ├── scene_builder_commands.gd ├── scene_builder_commands.gd.uid ├── scene_builder_config.gd ├── scene_builder_config.gd.uid ├── scene_builder_config.tres ├── scene_builder_create_items.tscn ├── scene_builder_dock.gd ├── scene_builder_dock.gd.uid ├── scene_builder_dock.tscn ├── scene_builder_item.gd ├── scene_builder_item.gd.uid ├── scene_builder_node_path_selector.gd ├── scene_builder_node_path_selector.gd.uid ├── scene_builder_plugin.gd ├── scene_builder_plugin.gd.uid ├── scene_builder_toolbox.gd └── scene_builder_toolbox.gd.uid /Documentation/Image/catlogo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/catlogo.jpg -------------------------------------------------------------------------------- /Documentation/Image/catlogo.jpg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bt40g16s17xeu" 6 | path.s3tc="res://.godot/imported/catlogo.jpg-cc0798d0398283b8aeeabcede89cd65b.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/catlogo.jpg" 15 | dest_files=["res://.godot/imported/catlogo.jpg-cc0798d0398283b8aeeabcede89cd65b.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_collection_names_resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_collection_names_resource.png -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_collection_names_resource.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c4stoqilaxqoh" 6 | path.s3tc="res://.godot/imported/scene_builder_collection_names_resource.png-25e543a098bc925dbd96342c26297fcb.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_collection_names_resource.png" 15 | dest_files=["res://.godot/imported/scene_builder_collection_names_resource.png-25e543a098bc925dbd96342c26297fcb.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_commands.png -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_commands.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cia4io1p576ss" 6 | path.s3tc="res://.godot/imported/scene_builder_commands.png-22b36cf3ca8abee0eb895227cc42cdd8.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_commands.png" 15 | dest_files=["res://.godot/imported/scene_builder_commands.png-22b36cf3ca8abee0eb895227cc42cdd8.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_create_items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_create_items.png -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_create_items.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://gdruqbveb26p" 6 | path.s3tc="res://.godot/imported/scene_builder_create_items.png-087e2fad86f6df64e297dce475681c30.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_create_items.png" 15 | dest_files=["res://.godot/imported/scene_builder_create_items.png-087e2fad86f6df64e297dce475681c30.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_demo.png -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_demo.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://8yobe13siepb" 6 | path.s3tc="res://.godot/imported/scene_builder_demo.png-6101d137f9ea4c35f0b51e2a7ced71f0.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_demo.png" 15 | dest_files=["res://.godot/imported/scene_builder_demo.png-6101d137f9ea4c35f0b51e2a7ced71f0.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_dock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_dock.png -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_dock.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bneiwx0xnpgwk" 6 | path.s3tc="res://.godot/imported/scene_builder_dock.png-537151289c0b725db98687db0c01102a.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_dock.png" 15 | dest_files=["res://.godot/imported/scene_builder_dock.png-537151289c0b725db98687db0c01102a.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_file_system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_file_system.png -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_file_system.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ccxlw7bq34awu" 6 | path.s3tc="res://.godot/imported/scene_builder_file_system.png-418db33ffee17ed2ec2121d8c0421b15.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_file_system.png" 15 | dest_files=["res://.godot/imported/scene_builder_file_system.png-418db33ffee17ed2ec2121d8c0421b15.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_icon.jpg -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_icon.jpg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b2hw34ubcm68y" 6 | path.s3tc="res://.godot/imported/scene_builder_icon.jpg-2aebaba69d523bf9130d02b1280007eb.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_icon.jpg" 15 | dest_files=["res://.godot/imported/scene_builder_icon.jpg-2aebaba69d523bf9130d02b1280007eb.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_item.png -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_item.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://nuyvrkn0ow5a" 6 | path.s3tc="res://.godot/imported/scene_builder_item.png-7ceffbcabb4068d7bcb9142a42c12f3e.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_item.png" 15 | dest_files=["res://.godot/imported/scene_builder_item.png-7ceffbcabb4068d7bcb9142a42c12f3e.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/scene_builder_shortcuts.png -------------------------------------------------------------------------------- /Documentation/Image/scene_builder_shortcuts.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://psl5fnipces7" 6 | path.s3tc="res://.godot/imported/scene_builder_shortcuts.png-0f77875159cc711468afaead44c3d2cc.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/scene_builder_shortcuts.png" 15 | dest_files=["res://.godot/imported/scene_builder_shortcuts.png-0f77875159cc711468afaead44c3d2cc.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /Documentation/Image/tutorial_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/Documentation/Image/tutorial_link.png -------------------------------------------------------------------------------- /Documentation/Image/tutorial_link.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bqdo215u7oo8j" 6 | path.s3tc="res://.godot/imported/tutorial_link.png-b3537b4351ee570ddf47dc5331aaa74d.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/scene_builder/Documentation/Image/tutorial_link.png" 15 | dest_files=["res://.godot/imported/tutorial_link.png-b3537b4351ee570ddf47dc5331aaa74d.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=false 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Paul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](./Documentation/Image/catlogo.jpg) 2 | 3 | # Scene Build & Productivity Commands for Godot 4.4+ 4 | 5 | Scene Builder is a 3D level design tool and asset browser for [Godot 4.4+](https://godotengine.org/), together with a set of common productivity commands. 6 | 7 | The latest version can be [downloaded here](https://github.com/sci-comp/scene-builder/archive/refs/heads/main.zip) on GitHub, but you may also find release versions on [Itch.io](https://sci-comp.itch.io/scene-builder) or in the [Godot Asset Library](https://godotengine.org/asset-library/asset/2881). 8 | 9 | > [!IMPORTANT] 10 | Since I use this tool for my own games, any form of collaboration, bug reports, or new feature suggestions are greatly appreciated. Please join us in Discord, or make a post in Github's Issue's section if you would like to help out. 11 | 12 | [![Patreon](https://img.shields.io/badge/Patreon-Support-%23FA8072?style=for-the-badge)](https://www.patreon.com/veryseriouscatrelatedgamedevelopment) [![Discord](https://img.shields.io/discord/827217464478924810?label=discord&logo=discord&logoColor=%90EE90&style=for-the-badge)](https://discord.gg/jnN5T2KXx5) [![Patreon](https://img.shields.io/badge/Steam-Wishlist-%23000000?style=for-the-badge&logo=steam)](https://store.steampowered.com/app/1710260/Catsploration/) 13 | 14 | ![scene_builder_demo](./Documentation/Image/scene_builder_demo.png) 15 | 16 | ## Documentation 17 | 18 | - [Quick Start Video Guide](#quick-start-video-guide) 19 | - [Scope](#scope) 20 | - [Compatibility](#compatibility) 21 | - [Multi-Mesh Instances](#multi-mesh-instances) 22 | - [Grid Snapping](#grid-snapping) 23 | - [Installation](#installation) 24 | - [Shortcuts](#shortcuts) 25 | - [The Alt Key](#the-alt-key) 26 | - [The Scene Builder Dock](#the-scene-builder-dock) 27 | - [Setting Up The Dock](#setting-up-the-dock) 28 | - [Scene Builder Items](#scene-builder-items) 29 | - [Updating the Scene Builder Dock](#updating-the-scene-builder-dock) 30 | - [Find World3D](#find-world3d) 31 | - [Reload All Items](#reload-all-items) 32 | - [Placement Mode](#placement-mode) 33 | - [Use Surface Normal](#use-surface-normal) 34 | - [Rotation Mode](#rotation-mode) 35 | - [Scale Mode](#scale-mode) 36 | - [Contributors](#contributors) 37 | - [License](#license) 38 | 39 | ## Quick Start Video Guide 40 | 41 | [![Quick Start](./Documentation/Image/tutorial_link.png)](https://youtu.be/PEcoOdYNfc4) 42 | 43 | ### Scope 44 | 45 | Scene builder is made to serve as a scene browser and scene placer for scenes of type Node3D. Scene builder also aims to offer a handful of productivity commands. Scenes may only be placed on collision shapes. 46 | 47 | #### Compatibility 48 | 49 | Scene builder should work with any type of collider, such those generated by [CSG Tools](https://docs.godotengine.org/en/3.5/tutorials/3d/csg_tools.html) or [Terrain3D](https://github.com/TokisanGames/Terrain3D)-- just make sure that the colliders have been generated during editor time. 50 | 51 | #### Multi-Mesh Instances 52 | 53 | Scattering large numbers of objects, often with help from multi-mesh instances, is currently out of scope. Please see one of the specialized tools below, 54 | 55 | - [Terrain3D](https://github.com/TokisanGames/Terrain3D) has its own built-in pipelines for vegetation and instanced objects, and a new particle grass feature as well. 56 | - [Scatter](https://github.com/HungryProton/scatter) is a powerful tool designed to randomly fill areas with props or other scenes. 57 | - [Simple Grass Textured](https://github.com/IcterusGames/SimpleGrassTextured) is the perfect tool for painting 2d textures over a collision area. 58 | - [Spatial Gardener](https://github.com/dreadpon/godot_spatial_gardener) is an efficient tool that uses an octree to scatter plants or props over arbitrary (possibly large) surfaces. 59 | 60 | #### Grid Snapping 61 | 62 | Grid snapping is currently out of scope due to the existence of [GridMap](https://docs.godotengine.org/en/stable/tutorials/3d/using_gridmaps.html). GridMap is a fantastic tool built directly into Godot. 63 | 64 | (Edit: actually, this might be worth considering?) 65 | 66 | --- 67 | 68 | ## Installation 69 | 70 | In addition to being available in the AssetLib, Scene builder may be installed by simply cloning the entire repo into, 71 | 72 | `res://addons/scene_builder/` 73 | 74 | which means that a recursive directory pattern will exist, 75 | 76 | `res://addons/scene_builder/addons/scene_builder/` 77 | 78 | > [!NOTE] 79 | Implementation details: Scene builder is logically divided into two main parts: scene builder commands and the scene builder dock. The script `scene_builder_commands.gd` adds commands to the Godot toolbar (Project > Tools) and listens for keyboard shortcuts. Each command's implementation is contained within the command's respective GDScript file, located in `addons/scene_builder/Commands/`. In the other hand, we have `scene_builder_dock.gd`, which handles logic for the interactable scene builder dock. 80 | 81 | --- 82 | 83 | ## Shortcuts 84 | 85 | With an item selected in the dock, 86 | 87 | - Enter x rotation mode: 1 88 | - Enter y rotation mode: 2 89 | - Enter z rotation mode: 3 90 | - Enter x offset mode: Shift + 1 91 | - Enter y offset mode: Shift + 2 92 | - Enter z offset mode: Shift + 3 93 | - Enter scale mode: 4 94 | - Reset orientation: 5 95 | - Select previous/next items by pressing: Shift + Left/Right Arrow 96 | - Select previous/next category by pressing: Alt + Left/Right Arrow 97 | - Exit placement mode: Escape 98 | 99 | ### The Alt Key 100 | 101 | SceneBuilder commands all require the Alt key pressed for activation. Although Godot generally doesn't make use of the Alt key, it does conflict in a few cases. To keep things simple, I simply move Godot's shortcut somewhere else. Shortcuts are still in progress-- new commands are still being added. 102 | 103 | To update default shortcuts, edit the resource `scene_builder_config.tres`, then reload the project. 104 | 105 | ![scene_builder_shortcuts](./Documentation/Image/scene_builder_shortcuts.png) 106 | 107 | > [!CAUTION] 108 | Some default shortcuts for scene builder will likely conflict with Godot's default shortcuts. Scene builder's shortcuts may be updated by changing the values in the resource file: `addons/scene_builder/scene_builder_config.tres`, then reloading the project. Godot's shortcuts can be updated in the toolbar, `Editor > Editor Settings... > Shortcuts`. 109 | 110 | We can also access commands from the menu, as is shown here, 111 | 112 | ![scene_builder_commands](./Documentation/Image/scene_builder_commands.png) 113 | 114 | ## The Scene Builder Dock 115 | 116 | ![scene_builder_dock](./Documentation/Image/scene_builder_dock.png) 117 | 118 | ### Setting Up The Dock 119 | 120 | By default, our data directory is located here: `res://Data/SceneBuilderCollections/`. You may change this in the configuration file. 121 | 122 | 1. Enter your desired collection names into the CollectionNames resource. Empty folders with matching names will be created, if they do not already exist. 123 | 124 | ![scene_builder_collection_names_resource](./Documentation/Image/scene_builder_collection_names_resource.png) 125 | 126 | The dock is populated from SceneBuilderItem resources located in these folders. 127 | 128 | ![scene_builder_file_system](./Documentation/Image/scene_builder_file_system.png) 129 | 130 | If you have a folder in `res://Data/SceneBuilderCollections/` that is not listed in the CollectionNames resource, then it will simply be ignored. Conversely, if a collection name is written in CollectionNames, then a harmless error will occur if a matching folder is not found. 131 | 132 | The scene builder dock only provides space for 18 collections, however, you can make additional folders. We can update which 18 collections are currently in use by swapping out names in the CollectionNames resource, then hitting the "Reload all items" button on the scene builder dock. 133 | 134 | We can enable the plugin now to preview collection names in the dock. Since our collection folders are empty, you will see a harmless error: `Directory exists, but contains no items: Furniture`. 135 | 136 | ### Scene Builder Items 137 | 138 | ![scene_builder_item](./Documentation/Image/scene_builder_item.png) 139 | 140 | To create an item resource, 141 | 142 | 1. Select one or more paths in FileSystem that contain an imported scene with a root node that derives from type Node3D. 143 | 2. Run the "Create scene builder items" command by going to Project > Tools > Scene Builder > Create scene builder items, or by pressing the keyboard shortcut `Alt + /` 144 | 3. Fill out the fields in the popup window, then hit okay. 145 | 4. After pressing okay, the `icon_studio` scene will be opened. Please close the scene when it's done without saving changes to icon_studio.tscn. However, if you would like to make changes to icon_studio.tscn, then that's a great way to customize your icons. 146 | 5. In the scene builder dock, click the button "Reload all items" 147 | 148 | ![scene_builder_create_items](./Documentation/Image/scene_builder_create_items.png) 149 | 150 | ### Update the Scene Builder Dock 151 | 152 | #### Find World3D 153 | 154 | The scene builder dock needs to know which scene it should be placing items into. Since this is typically done automatically, this button is usually not needed. 155 | 156 | #### Reload All Items 157 | 158 | Whenever we make changes to SceneBuilderItem resources or collection names, we must then press the "Reload all items" button on the scene builder dock. 159 | 160 | ### Placement Mode 161 | 162 | When an icon is highlighted green in the scene builder dock, then placement mode has been enabled. 163 | 164 | To exit placement mode, we may click the highlighted icon or press the escape key. 165 | 166 | When placement mode is active, an item preview is created in the current edited scene. The preview node will have a parent node named "SceneBuilderTemp." We may safely delete this node when we are done, while the item preview node will be automatically clear when exiting placement mode. 167 | 168 | #### Use Surface Normal 169 | 170 | Instantiated items will align their Y axis with the specified orientation when the checkbox "Surface normal" is toggled on. 171 | 172 | Due to gimbal lock, you may notice that the preview item will often acquire orientations with undesired offsets. Press `5` to reset the item preview's orientation. 173 | 174 | #### Rotation Mode 175 | 176 | Press `1`, `2`, or, `3` to enter rotation mode, where these digits represents the x, y, and z-axis respectively. When rotation mode is enabled, the corresponding digit will be highlighted in the bottom right of the scene builder dock. 177 | 178 | Rotation is applied through mouse movement (proportional to the mouse motion's greatest relative value in the x or y axis) 179 | 180 | Rotation will be applied in local or world space according to the "Use Local Space" button in Godot's 3D toolbar. 181 | 182 | To exit rotation mode: left click to apply the rotation or right-click to cancel and restore the original rotation. 183 | 184 | #### Scale Mode 185 | 186 | Press `4` to enter scale mode. 187 | 188 | Scale mode works similarly to rotation mode. 189 | 190 | To exit scale mode: left click to apply or right-click to cancel and restore the original values. 191 | 192 | ## Contributors 193 | 194 | Scene Builder is made by Paul Hill, with help from contributions, who can be found [here](https://github.com/sci-comp/scene-builder/graphs/contributors). 195 | 196 | ## License 197 | 198 | Licensed under the MIT license, see `LICENSE` for more information. 199 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/alphabetize_nodes.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Alphabetize children of selected nodes. 4 | ## * Nodes will be sorted together, at the top of the heirarchy 5 | ## * Node3Ds will be sorted together, below any Nodes 6 | 7 | func execute(): 8 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 9 | 10 | var selection: EditorSelection = EditorInterface.get_selection() 11 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 12 | 13 | if selected_nodes.is_empty(): 14 | return 15 | 16 | undo_redo.create_action("Alphabetical Sort Children") 17 | 18 | for parent_node in selected_nodes: 19 | 20 | var children = parent_node.get_children() 21 | 22 | var names_to_nodes = {} 23 | var names_to_node3ds = {} 24 | var nodes_to_sort = {} 25 | 26 | for child in children: 27 | nodes_to_sort[child.name] = child 28 | if (child is Node3D): 29 | names_to_node3ds[child.name] = child 30 | else: 31 | names_to_nodes[child.name] = child 32 | 33 | # Sort 34 | var sorted_node_names = names_to_nodes.keys() 35 | var sorted_node3d_names = names_to_node3ds.keys() 36 | 37 | sorted_node_names.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0) 38 | sorted_node3d_names.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0) 39 | 40 | var sorted_names = sorted_node_names + sorted_node3d_names 41 | 42 | # Add methods 43 | for i in range(len(sorted_names)): 44 | var _name = sorted_names[i] 45 | var child_node = nodes_to_sort[_name] 46 | undo_redo.add_do_method(parent_node, "move_child", child_node, i) 47 | undo_redo.add_undo_method(parent_node, "move_child", child_node, parent_node.get_children().find(child_node)) 48 | 49 | undo_redo.commit_action() 50 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/alphabetize_nodes.gd.uid: -------------------------------------------------------------------------------- 1 | uid://vh5l6b0dt38g 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/change_places.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## If exactly two Node3Ds are selected in Scene, then swap their positions 4 | ## and rotations. 5 | 6 | func execute(): 7 | var selection: EditorSelection = EditorInterface.get_selection() 8 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 9 | 10 | if selected_nodes.size() != 2: 11 | print("[Change Places] Exactly two nodes must be selected.") 12 | return 13 | 14 | var node_a = selected_nodes[0] 15 | var node_b = selected_nodes[1] 16 | 17 | if not node_a is Node3D or not node_b is Node3D: 18 | return 19 | 20 | # Check for parent-child relationship 21 | if node_a.get_parent() == node_b or node_b.get_parent() == node_a: 22 | print("[Change Places] The selected nodes cannot have a parent-child relationship.") 23 | return 24 | 25 | # Set-up undo_redo 26 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 27 | undo_redo.create_action("Swap Positions and Rotations") 28 | 29 | var transform_a = node_a.global_transform 30 | var transform_b = node_b.global_transform 31 | 32 | undo_redo.add_do_method(node_a, "set_global_transform", transform_b) 33 | undo_redo.add_do_method(node_b, "set_global_transform", transform_a) 34 | undo_redo.add_undo_method(node_a, "set_global_transform", transform_a) 35 | undo_redo.add_undo_method(node_b, "set_global_transform", transform_b) 36 | 37 | undo_redo.commit_action() 38 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/change_places.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cvitqwq46nx80 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/create_audio_stream_player_3d.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func execute(): 5 | var editor_selection : EditorSelection = EditorInterface.get_selection() 6 | var selected_nodes : Array = editor_selection.get_selected_nodes() 7 | var selected_paths : PackedStringArray = EditorInterface.get_selected_paths() 8 | 9 | var current_scene : Node = EditorInterface.get_edited_scene_root() 10 | if current_scene == null or selected_paths.is_empty(): 11 | print("Something is null or empty, returning early") 12 | return 13 | 14 | if selected_nodes.size() == 1: 15 | var node = selected_nodes[0] 16 | 17 | if selected_paths.size() > 0: 18 | print("Creating " + str(selected_paths.size()) + " audio sources for: " + node.name) 19 | 20 | for path in selected_paths: 21 | if path.ends_with(".wav"): 22 | var audio_player: AudioStreamPlayer3D = AudioStreamPlayer3D.new() 23 | 24 | audio_player.name = path.get_file().get_basename() 25 | audio_player.stream = load(path) 26 | 27 | node.add_child(audio_player) 28 | audio_player.owner = current_scene 29 | 30 | print("Assigned: " + path.get_file()) 31 | 32 | else: 33 | print("Selected path is not a wav file: " + str(path)) 34 | else: 35 | print("Select a sound group in the scene.") 36 | else: 37 | print("Only one node in the scene should be selected.") 38 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/create_audio_stream_player_3d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bvqcj5qvumxwp 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/create_scene_builder_items.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Creates Scene Builder items from selection on Editor. 4 | ## A Popup is created in which user can change creation settings. 5 | 6 | var path_root = "res://Data/SceneBuilderCollections/" 7 | 8 | var editor: EditorInterface 9 | var popup_instance: PopupPanel 10 | 11 | # Nodes 12 | var create_items: VBoxContainer 13 | var collection_line_edit: LineEdit 14 | var randomize_vertical_offset_checkbox: CheckButton 15 | var randomize_rotation_checkbox: CheckButton 16 | var randomize_scale_checkbox: CheckButton 17 | var vertical_offset_spin_box_min: SpinBox 18 | var vertical_offset_spin_box_max: SpinBox 19 | var rotx_slider: HSlider 20 | var roty_slider: HSlider 21 | var rotz_slider: HSlider 22 | var scale_spin_box_min: SpinBox 23 | var scale_spin_box_max: SpinBox 24 | var ok_button: Button 25 | 26 | var max_diameter: float 27 | var icon_studio: SubViewport 28 | 29 | signal done 30 | 31 | func execute(root_dir: String): 32 | if !root_dir.is_empty(): 33 | path_root = root_dir 34 | 35 | print("[Create Scene Builder Items] Requesting user input...") 36 | 37 | editor = get_editor_interface() 38 | 39 | popup_instance = PopupPanel.new() 40 | add_child(popup_instance) 41 | popup_instance.popup_centered(Vector2(500, 300)) 42 | 43 | var create_items_scene_path = SceneBuilderToolbox.find_resource_with_dynamic_path("scene_builder_create_items.tscn") 44 | if create_items_scene_path == "": 45 | printerr("[Create Scene Builder Items] Could not find scene_builder_create_items.tscn") 46 | return 47 | 48 | var create_items_scene := load(create_items_scene_path) 49 | create_items = create_items_scene.instantiate() 50 | popup_instance.add_child(create_items) 51 | 52 | collection_line_edit = create_items.get_node("Collection/LineEdit") 53 | randomize_vertical_offset_checkbox = create_items.get_node("Boolean/VerticalOffset") 54 | randomize_rotation_checkbox = create_items.get_node("Boolean/Rotation") 55 | randomize_scale_checkbox = create_items.get_node("Boolean/Scale") 56 | vertical_offset_spin_box_min = create_items.get_node("VerticalOffset/min") 57 | vertical_offset_spin_box_max = create_items.get_node("VerticalOffset/max") 58 | rotx_slider = create_items.get_node("Rotation/x") 59 | roty_slider = create_items.get_node("Rotation/y") 60 | rotz_slider = create_items.get_node("Rotation/z") 61 | scale_spin_box_min = create_items.get_node("Scale/min") 62 | scale_spin_box_max = create_items.get_node("Scale/max") 63 | ok_button = create_items.get_node("Okay") 64 | 65 | ok_button.pressed.connect(_on_ok_pressed) 66 | 67 | func _on_ok_pressed(): 68 | print("[Create Scene Builder Items] On okay pressed") 69 | 70 | var path_to_icon_studio : String = SceneBuilderToolbox.find_resource_with_dynamic_path("icon_studio.tscn") 71 | 72 | if path_to_icon_studio == "": 73 | printerr("[Create Scene Builder Items] Path to icon studio not found") 74 | return 75 | 76 | EditorInterface.open_scene_from_path(path_to_icon_studio) 77 | 78 | icon_studio = EditorInterface.get_edited_scene_root() as SubViewport 79 | if icon_studio == null: 80 | print("[Create Scene Builder Items] Failed to load icon studio") 81 | return 82 | 83 | var selected_paths = EditorInterface.get_selected_paths() 84 | print("[Create Scene Builder Items] Selected paths: " + str(selected_paths.size())) 85 | 86 | for path in selected_paths: 87 | await _create_resource(path) 88 | 89 | popup_instance.queue_free() 90 | done.emit() 91 | 92 | func _create_resource(path: String): 93 | var scene_builder_item_path: String 94 | var scene_builder_item_path1: String = "res://addons/SceneBuilder/scene_builder_item.gd" 95 | var scene_builder_item_path2: String = "res://addons/SceneBuilder/addons/SceneBuilder/scene_builder_item.gd" 96 | 97 | if FileAccess.file_exists(scene_builder_item_path1): 98 | scene_builder_item_path = scene_builder_item_path1 99 | elif FileAccess.file_exists(scene_builder_item_path2): 100 | scene_builder_item_path = scene_builder_item_path2 101 | else: 102 | print("[Create Scene Builder Items] Path to scene builder item not found") 103 | return 104 | 105 | var resource: SceneBuilderItem = load(scene_builder_item_path).new() 106 | 107 | if ResourceLoader.exists(path): 108 | var packed_scene: PackedScene = load(path) 109 | 110 | if packed_scene == null: 111 | return 112 | 113 | # Populate resource 114 | var uid = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(path)) 115 | resource.uid = uid 116 | resource.item_name = path.get_file().get_basename() 117 | if collection_line_edit.text.is_empty(): 118 | print("[Create Scene Builder Items] Collection name was not given, using: Unnamed") 119 | resource.collection_name = "Unnamed" 120 | else: 121 | resource.collection_name = collection_line_edit.text 122 | resource.use_random_vertical_offset = randomize_vertical_offset_checkbox.button_pressed 123 | resource.use_random_rotation = randomize_rotation_checkbox.button_pressed 124 | resource.use_random_scale = randomize_scale_checkbox.button_pressed 125 | resource.random_offset_y_min = vertical_offset_spin_box_min.value 126 | resource.random_offset_y_max = vertical_offset_spin_box_max.value 127 | resource.random_rot_x = rotx_slider.value 128 | resource.random_rot_y = roty_slider.value 129 | resource.random_rot_z = rotz_slider.value 130 | resource.random_scale_min = scale_spin_box_min.value 131 | resource.random_scale_min = scale_spin_box_min.value 132 | 133 | # Create directories 134 | var path_to_collection_folder = path_root + resource.collection_name 135 | _create_directory_if_not_exists(path_to_collection_folder) 136 | 137 | #region Create icon 138 | 139 | # Add packed_scene to studio scene 140 | var subject: Node3D = packed_scene.instantiate() 141 | icon_studio.add_child(subject) 142 | subject.owner = icon_studio 143 | 144 | var camera_root: Node3D = icon_studio.get_node("CameraRoot") as Node3D 145 | var studio_camera: Camera3D = icon_studio.get_node("CameraRoot/Pitch/Camera3D") as Camera3D 146 | 147 | # Center item in portrait frame 148 | # Defaulting to 5 node child layers to get AABB 149 | # Possible improvement : add parameter to UI 150 | var node_depth = 5 151 | var aabb = await _get_merged_aabb(subject, node_depth) 152 | print("[Create Scene Builder Items] Subject AABB: ", aabb) 153 | var center = aabb.get_center() 154 | print("[Create Scene Builder Items] Subject center: ", center) 155 | camera_root.position = center 156 | # Using 120% of longest axis for cases where the subject gets too close to camera 157 | studio_camera.position = Vector3(0, 0, aabb.get_longest_axis_size() * 1.2) 158 | 159 | await get_tree().process_frame 160 | await get_tree().process_frame 161 | 162 | var viewport_tex: Texture = icon_studio.get_texture() 163 | var img: Image = viewport_tex.get_image() 164 | var tex: Texture = ImageTexture.create_from_image(img) 165 | 166 | resource.texture = tex 167 | 168 | await get_tree().process_frame 169 | subject.queue_free() 170 | 171 | #endregion 172 | 173 | var save_path: String = path_root + resource.collection_name + "/%s.tres" % resource.item_name 174 | ResourceSaver.save(resource, save_path) 175 | 176 | print("[Create Scene Builder Items] Resource saved: " + save_path) 177 | 178 | func _create_directory_if_not_exists(path_to_directory: String) -> void: 179 | var dir = DirAccess.open(path_to_directory) 180 | if not dir: 181 | print("[Create Scene Builder Items] Creating directory: " + path_to_directory) 182 | DirAccess.make_dir_recursive_absolute(path_to_directory) 183 | 184 | # Returns the merged AABB of node and it's children up to node_depth layers 185 | func _get_merged_aabb(node: Node, node_depth: int) -> AABB: 186 | var aabb := AABB() 187 | 188 | if node is GeometryInstance3D: 189 | if node is CSGShape3D: 190 | await _wait_for_csg_update() 191 | aabb = node.global_transform * node.get_aabb() 192 | 193 | if node_depth > 0: 194 | for child in node.get_children(): 195 | var child_aabb = await _get_merged_aabb(child, node_depth - 1) 196 | aabb = aabb.merge(child_aabb) 197 | 198 | return aabb 199 | 200 | # CSGShape3D does not update its AABB immediately 201 | # This behavior is intentional and documented here: 202 | # https://github.com/godotengine/godot/issues/38273 203 | func _wait_for_csg_update(): 204 | await get_tree().process_frame 205 | await get_tree().process_frame 206 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/create_scene_builder_items.gd.uid: -------------------------------------------------------------------------------- 1 | uid://hto73c88m3gm 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/create_standard_material_3d.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | var material_prefix : String = "MI_" 5 | var texture_prefix : String = "T_" 6 | var nmap_suffix : String = "_n" 7 | var emap_suffix : String = "_e" 8 | 9 | var popup_instance : PopupPanel 10 | var vbox : VBoxContainer 11 | var lbl_title : Label 12 | var lbl_transparency : Label 13 | var lbl_vertex_color : Label 14 | var lbl_back_lighting : Label 15 | var lbl_shadows : Label 16 | var lbl_billboard : Label 17 | var lbl_use_particle_settings : Label 18 | 19 | var checkbox_set_alpha_transparency : CheckBox 20 | var checkbox_use_vertex_color_as_albedo : CheckBox 21 | var checkbox_vertex_color_is_srgb : CheckBox 22 | var checkbox_enable_back_lighting : CheckBox 23 | var checkbox_set_backlight_to_white : CheckBox 24 | var checkbox_disable_receive_shadows : CheckBox 25 | var checkbox_set_mode_to_particle_billboard : CheckBox 26 | var checkbox_keep_scale_with_billboards : CheckBox 27 | var checkbox_use_particle_settings : CheckBox 28 | 29 | var ok_button : Button 30 | 31 | signal done 32 | 33 | var toolbox = SceneBuilderToolbox.new() 34 | 35 | func execute(): 36 | 37 | # Popup 38 | popup_instance = PopupPanel.new() 39 | add_child(popup_instance) 40 | popup_instance.popup_centered(Vector2(300, 200)) 41 | # VBox 42 | vbox = VBoxContainer.new() 43 | popup_instance.add_child(vbox) 44 | 45 | # -- Title ----------------------------------------------------------------- 46 | 47 | lbl_title = Label.new() 48 | lbl_title.text = "Create a BaseMaterial3D for the current selection in FileSystem" 49 | lbl_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER 50 | vbox.add_child(lbl_title) 51 | 52 | # -- Transparency ---------------------------------------------------------- 53 | 54 | lbl_transparency = Label.new() 55 | lbl_transparency.text = "Transparency" 56 | vbox.add_child(lbl_transparency) 57 | 58 | # Set alpha transparency 59 | checkbox_set_alpha_transparency = CheckBox.new() 60 | checkbox_set_alpha_transparency.text = "Use alpha transparency" 61 | vbox.add_child(checkbox_set_alpha_transparency) 62 | 63 | # -- Vertex Color ---------------------------------------------------------- 64 | 65 | lbl_vertex_color = Label.new() 66 | lbl_vertex_color.text = "Vertex Color" 67 | vbox.add_child(lbl_vertex_color) 68 | 69 | # Use vertex color as albedo 70 | checkbox_use_vertex_color_as_albedo = CheckBox.new() 71 | checkbox_use_vertex_color_as_albedo.text = "Use as albedo" 72 | vbox.add_child(checkbox_use_vertex_color_as_albedo) 73 | 74 | # Vertex color is sRGB 75 | checkbox_vertex_color_is_srgb = CheckBox.new() 76 | checkbox_vertex_color_is_srgb.text = "Is sRGB" 77 | vbox.add_child(checkbox_vertex_color_is_srgb) 78 | 79 | # -- Back Lighting --------------------------------------------------------- 80 | 81 | lbl_back_lighting = Label.new() 82 | lbl_back_lighting.text = "Back Lighting" 83 | vbox.add_child(lbl_back_lighting) 84 | 85 | # Enable back lighting 86 | checkbox_enable_back_lighting = CheckBox.new() 87 | checkbox_enable_back_lighting.text = "Enabled" 88 | vbox.add_child(checkbox_enable_back_lighting) 89 | 90 | # Set backlight color to ffffff 91 | checkbox_set_backlight_to_white = CheckBox.new() 92 | checkbox_set_backlight_to_white.text = "Set backlight color to white" 93 | vbox.add_child(checkbox_set_backlight_to_white) 94 | 95 | # -- Shadows --------------------------------------------------------------- 96 | 97 | lbl_shadows = Label.new() 98 | lbl_shadows.text = "Shadows" 99 | vbox.add_child(lbl_shadows) 100 | 101 | # Disable receive shadows 102 | checkbox_disable_receive_shadows = CheckBox.new() 103 | checkbox_disable_receive_shadows.text = "Disable receive shadows" 104 | vbox.add_child(checkbox_disable_receive_shadows) 105 | 106 | # -- Billboard ------------------------------------------------------------- 107 | 108 | lbl_billboard = Label.new() 109 | lbl_billboard.text = "Billboard" 110 | vbox.add_child(lbl_billboard) 111 | 112 | # Set mode to particle billboard 113 | checkbox_set_mode_to_particle_billboard = CheckBox.new() 114 | checkbox_set_mode_to_particle_billboard.text = "Set mode to particle billboard" 115 | vbox.add_child(checkbox_set_mode_to_particle_billboard) 116 | 117 | # Keep scale with billboards 118 | checkbox_keep_scale_with_billboards = CheckBox.new() 119 | checkbox_keep_scale_with_billboards.text = "Keep scale with billboards" 120 | vbox.add_child(checkbox_keep_scale_with_billboards) 121 | 122 | # -- Set all true ---------------------------------------------------------- 123 | 124 | lbl_use_particle_settings = Label.new() 125 | lbl_use_particle_settings.text = "-- Use settings for particle materials --" 126 | vbox.add_child(lbl_use_particle_settings) 127 | 128 | checkbox_use_particle_settings = CheckBox.new() 129 | checkbox_use_particle_settings.text = "Use settings for particle materials" 130 | vbox.add_child(checkbox_use_particle_settings) 131 | 132 | # -- End CheckBox group -- 133 | 134 | # Ok button 135 | ok_button = Button.new() 136 | ok_button.text = "Ok" 137 | ok_button.pressed.connect(_on_ok_pressed) 138 | vbox.add_child(ok_button) 139 | 140 | func _on_ok_pressed(): 141 | var selected_paths = get_editor_interface().get_selected_paths() 142 | var texture_to_material = {} # Maps from albedo texture path to material instance 143 | 144 | # Generate materials for albedo textures 145 | for path in selected_paths: 146 | var file_name = path.get_file() 147 | var base_name = file_name.get_basename() 148 | 149 | if base_name == "" or path.ends_with(".import"): 150 | continue 151 | 152 | if base_name.begins_with(texture_prefix) and !base_name.ends_with(nmap_suffix) and !base_name.ends_with(emap_suffix): 153 | var new_mat_name = material_prefix + toolbox.replace_first(base_name, texture_prefix, "") 154 | var albedo_texture = load(path) 155 | var mat = StandardMaterial3D.new() 156 | mat.albedo_texture = albedo_texture 157 | 158 | texture_to_material[path] = mat 159 | 160 | # Attach normal maps to materials 161 | for path in texture_to_material.keys(): 162 | var file_name = path.get_file() 163 | var base_name = file_name.get_basename() 164 | var nmap_path = toolbox.replace_last(path, base_name, base_name + nmap_suffix) 165 | 166 | if nmap_path in selected_paths: 167 | var normal_texture = load(nmap_path) 168 | var mat : StandardMaterial3D = texture_to_material[path] 169 | mat.normal_enabled = true 170 | mat.normal_texture = normal_texture 171 | 172 | # Attach emissive maps to materials 173 | for path in texture_to_material.keys(): 174 | var file_name = path.get_file() 175 | var base_name = file_name.get_basename() 176 | var emap_path = toolbox.replace_last(path, base_name, base_name + emap_suffix) 177 | 178 | if emap_path in selected_paths: 179 | var emissive_texture = load(emap_path) 180 | var mat : StandardMaterial3D = texture_to_material[path] 181 | mat.emission_enabled = true 182 | mat.emission_texture = emissive_texture 183 | 184 | # Set other properties 185 | var use_particle_settings : bool = checkbox_use_particle_settings.is_pressed() 186 | 187 | for mat : StandardMaterial3D in texture_to_material.values(): 188 | 189 | # Transparency 190 | 191 | if checkbox_set_alpha_transparency.is_pressed() or use_particle_settings: 192 | mat.transparency = mat.TRANSPARENCY_ALPHA 193 | 194 | # Vertex Color 195 | 196 | if checkbox_use_vertex_color_as_albedo.is_pressed() or use_particle_settings: 197 | mat.vertex_color_use_as_albedo = true 198 | 199 | if checkbox_vertex_color_is_srgb.is_pressed() or use_particle_settings: 200 | mat.vertex_color_is_srgb = true 201 | 202 | # Back Lighting 203 | 204 | if checkbox_enable_back_lighting.is_pressed() or use_particle_settings: 205 | mat.backlight_enabled = true 206 | 207 | if checkbox_set_backlight_to_white.is_pressed() or use_particle_settings: 208 | mat.backlight = Color.WHITE 209 | 210 | # Shadows 211 | 212 | if checkbox_disable_receive_shadows.is_pressed() or use_particle_settings: 213 | mat.disable_receive_shadows = true 214 | 215 | # Billboard 216 | 217 | if checkbox_set_mode_to_particle_billboard.is_pressed() or use_particle_settings: 218 | mat.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES 219 | 220 | if checkbox_keep_scale_with_billboards.is_pressed() or use_particle_settings: 221 | mat.billboard_keep_scale = true 222 | 223 | # Save materials 224 | for path in texture_to_material.keys(): 225 | var dir = path.get_base_dir() 226 | var file_name = path.get_file() 227 | var base_name = file_name.get_basename() 228 | var new_mat_name = material_prefix + toolbox.replace_first(base_name, texture_prefix, "") 229 | var save_path = dir.path_join(new_mat_name + ".tres") 230 | 231 | ResourceSaver.save(texture_to_material[path], save_path) 232 | 233 | popup_instance.queue_free() 234 | emit_signal("done") 235 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/create_standard_material_3d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bdws4ej1x3hlu 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/fix_negative_scaling.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Takes the absolute value of transform scale values. 4 | 5 | func execute(): 6 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 7 | var selection: EditorSelection = EditorInterface.get_selection() 8 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 9 | 10 | if selected_nodes.is_empty(): 11 | return 12 | 13 | undo_redo.create_action("Fix negative scaling") 14 | 15 | for selected: Node3D in selected_nodes: 16 | 17 | var new_scale: Vector3 = selected.scale 18 | if selected.scale.x < 0 || selected.scale.y < 0 || selected.scale.z < 0: 19 | new_scale = abs(selected.scale) 20 | print("[Fix Negative Scaling] Negative scale found for: ", selected.name) 21 | 22 | undo_redo.add_do_method(selected, "set_scale", new_scale) 23 | undo_redo.add_undo_method(selected, "set_scale", selected.scale) 24 | 25 | undo_redo.commit_action() 26 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/fix_negative_scaling.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dpets2h63xugp 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/instantiate_in_a_row.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Used to layout new assets in a row, a simple but often helpful task. 4 | 5 | func execute(_spacing: int): 6 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 7 | 8 | var current_scene: Node = EditorInterface.get_edited_scene_root() 9 | var selected_paths: PackedStringArray = EditorInterface.get_selected_paths() 10 | 11 | if current_scene == null or selected_paths.is_empty(): 12 | return 13 | 14 | undo_redo.create_action("Instantiate Scenes") 15 | 16 | var instantiated_nodes = [] 17 | var x_offset = 0 18 | 19 | for path in selected_paths: 20 | if ResourceLoader.exists(path) and load(path) is PackedScene: 21 | var scene = load(path) as PackedScene 22 | var instance = scene.instantiate() 23 | 24 | var suffix = 1 25 | var base_name = instance.name 26 | 27 | if base_name.ends_with("-n" + str(suffix)): 28 | var parts = base_name.split("-n") 29 | if parts.size() > 1: 30 | base_name = parts[0] 31 | else: 32 | print("Unexpected format.") 33 | else: 34 | instance.name += "-n1" 35 | 36 | while current_scene.has_node(NodePath(instance.name)): 37 | suffix += 1 38 | instance.name = str(base_name) + "-n" + str(suffix) 39 | 40 | undo_redo.add_do_method(current_scene, "add_child", instance) 41 | undo_redo.add_do_method(instance, "set_owner", current_scene) 42 | undo_redo.add_do_method(instance, "set_global_position", Vector3(x_offset, 0, 0)) 43 | undo_redo.add_undo_method(current_scene, "remove_child", instance) 44 | 45 | 46 | 47 | instantiated_nodes.append(instance) 48 | x_offset += _spacing 49 | 50 | print("Instantiated: " + instance.name) 51 | 52 | undo_redo.commit_action() 53 | 54 | var selection = EditorInterface.get_selection() 55 | selection.clear() 56 | 57 | # Select newly instantiated nodes 58 | for node in instantiated_nodes: 59 | selection.add_node(node) 60 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/instantiate_in_a_row.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dn5a37leofnps 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/push_parent_offset_to_child.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Set selected node positions to 0,0,0 while preserving children's world positions. 4 | 5 | func execute(): 6 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 7 | undo_redo.create_action("Push parent offset to child") 8 | 9 | var selection: EditorSelection = EditorInterface.get_selection() 10 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 11 | 12 | for selected in selected_nodes: 13 | if selected is Node3D and selected.get_child_count() > 0: 14 | var parent: Node3D = selected 15 | 16 | var child_actions = [] 17 | for _child in parent.get_children(): 18 | if _child is Node3D: 19 | var child: Node3D = _child; 20 | var original_position = child.global_position 21 | child_actions.append([child, original_position]) 22 | 23 | undo_redo.add_do_method(parent, "set_global_position", Vector3.ZERO) 24 | undo_redo.add_undo_method(parent, "set_global_position", parent.global_position) 25 | 26 | for action_data in child_actions: 27 | var child = action_data[0] 28 | var original_position = action_data[1] 29 | undo_redo.add_do_method(child, "set_global_position", original_position) 30 | undo_redo.add_undo_method(child, "set_global_position", original_position) 31 | 32 | undo_redo.commit_action() 33 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/push_parent_offset_to_child.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bh4jo76q5wxtk 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/push_to_grid.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Round the position of selected nodes to the nearest whole grid value. 4 | 5 | func execute(): 6 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 7 | 8 | var selection: EditorSelection = EditorInterface.get_selection() 9 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 10 | 11 | if selected_nodes.is_empty(): 12 | return 13 | 14 | undo_redo.create_action("Snap to Grid") 15 | 16 | var grid_size: float = 0.25 # Todo: this should be adjustable by the user 17 | 18 | for selected: Node3D in selected_nodes: 19 | var old_pos = selected.position 20 | var new_pos = Vector3( 21 | round(selected.position.x / grid_size) * grid_size, 22 | round(selected.position.y / grid_size) * grid_size, 23 | round(selected.position.z / grid_size) * grid_size 24 | ) 25 | 26 | undo_redo.add_do_property(selected, "position", new_pos) 27 | undo_redo.add_undo_property(selected, "position", old_pos) 28 | 29 | undo_redo.commit_action() 30 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/push_to_grid.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cy2rjf3wr7hcu 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/repair_standard_material_3d.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | var texture_prefix : String = "T_" 5 | var nmap_suffix : String = "_n" 6 | var emap_suffix : String = "_e" 7 | 8 | func execute(): 9 | print("testing... this has not been reviewed or tested yet. Not yet functional") 10 | 11 | func assign_textures_to_materials(): 12 | var selected_paths = get_editor_interface().get_selected_paths() 13 | var assigned_count = 0 14 | 15 | for path in selected_paths: 16 | if path.ends_with(".tres") or path.ends_with(".material"): 17 | var material = load(path) as StandardMaterial3D 18 | if material: 19 | var file_name = path.get_file() 20 | var material_name = file_name.get_basename() 21 | var object_name = material_name.replace("MI_", "") 22 | 23 | assigned_count += find_and_assign_textures(material, object_name, path) 24 | 25 | if assigned_count > 0: 26 | print("Successfully assigned " + str(assigned_count) + " textures to materials") 27 | else: 28 | print("No textures assigned. Make sure to select StandardMaterial3D resources.") 29 | 30 | func find_and_assign_textures(material: StandardMaterial3D, object_name: String, material_path: String) -> int: 31 | var assigned_count = 0 32 | var base_dir = material_path.get_base_dir() 33 | 34 | var texture_paths = find_matching_textures(object_name) 35 | 36 | var albedo_texture_path = texture_paths["albedo"] 37 | var normal_texture_path = texture_paths["normal"] 38 | var emission_texture_path = texture_paths["emission"] 39 | 40 | if albedo_texture_path: 41 | var albedo_texture = load(albedo_texture_path) 42 | material.albedo_texture = albedo_texture 43 | assigned_count += 1 44 | 45 | if normal_texture_path: 46 | var normal_texture = load(normal_texture_path) 47 | material.normal_enabled = true 48 | material.normal_texture = normal_texture 49 | assigned_count += 1 50 | 51 | if emission_texture_path: 52 | var emission_texture = load(emission_texture_path) 53 | material.emission_enabled = true 54 | material.emission_texture = emission_texture 55 | assigned_count += 1 56 | 57 | if assigned_count > 0: 58 | ResourceSaver.save(material, material_path) 59 | 60 | return assigned_count 61 | 62 | func find_matching_textures(object_name: String) -> Dictionary: 63 | var result = { 64 | "albedo": "", 65 | "normal": "", 66 | "emission": "" 67 | } 68 | 69 | var all_files = get_all_filesystem_paths() 70 | 71 | var albedo_pattern = texture_prefix + object_name 72 | var normal_pattern = texture_prefix + object_name + nmap_suffix 73 | var emission_pattern = texture_prefix + object_name + emap_suffix 74 | 75 | for file_path in all_files: 76 | var file_name = file_path.get_file().get_basename() 77 | 78 | if file_name == albedo_pattern and (file_path.ends_with(".png") or file_path.ends_with(".jpg")): 79 | result["albedo"] = file_path 80 | elif file_name == normal_pattern and (file_path.ends_with(".png") or file_path.ends_with(".jpg")): 81 | result["normal"] = file_path 82 | elif file_name == emission_pattern and (file_path.ends_with(".png") or file_path.ends_with(".jpg")): 83 | result["emission"] = file_path 84 | 85 | return result 86 | 87 | func get_all_filesystem_paths() -> Array: 88 | var file_system = get_editor_interface().get_resource_filesystem() 89 | var paths = [] 90 | 91 | var root_path = "res://" 92 | _scan_filesystem_recursive(file_system, root_path, paths) 93 | 94 | return paths 95 | 96 | func _scan_filesystem_recursive(file_system, current_path: String, results: Array): 97 | var dir = DirAccess.open(current_path) 98 | if dir: 99 | dir.list_dir_begin() 100 | var file_name = dir.get_next() 101 | 102 | while file_name != "": 103 | var full_path = current_path.path_join(file_name) 104 | 105 | if dir.current_is_dir(): 106 | _scan_filesystem_recursive(file_system, full_path, results) 107 | else: 108 | if file_name.ends_with(".png") or file_name.ends_with(".jpg") or file_name.ends_with(".jpeg"): 109 | results.append(full_path) 110 | 111 | file_name = dir.get_next() 112 | 113 | dir.list_dir_end() 114 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/repair_standard_material_3d.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cpgnthxyu1g5f 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/reset_node_name.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## If selected nodes have a scene file path, then rename them to their name in 4 | ## FileSystem. A suffix is applied for duplicates: -n1, -n2, and so on. 5 | 6 | func execute(): 7 | var toolbox: SceneBuilderToolbox = SceneBuilderToolbox.new() 8 | 9 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 10 | var current_scene: Node = EditorInterface.get_edited_scene_root() 11 | var selection: EditorSelection = EditorInterface.get_selection() 12 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 13 | 14 | if selected_nodes.is_empty(): 15 | return 16 | 17 | undo_redo.create_action("Reset node name") 18 | 19 | var all_names = toolbox.get_all_node_names(current_scene) 20 | 21 | for node in selected_nodes: 22 | 23 | if node.scene_file_path: 24 | var path_name = node.scene_file_path.get_file().get_basename() 25 | var new_name = toolbox.increment_name_until_unique(path_name, all_names) 26 | undo_redo.add_do_method(node, "set_name", new_name) 27 | undo_redo.add_undo_method(node, "set_name", node.name) 28 | else: 29 | print("[Reset Transform] Passing over: " + node.name) 30 | 31 | undo_redo.commit_action() 32 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/reset_node_name.gd.uid: -------------------------------------------------------------------------------- 1 | uid://byv4mqhsmsf0 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/reset_transform.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Sets selected nodes' position, rotation, and scale to initial values. 4 | 5 | func execute(): 6 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 7 | var selection: EditorSelection = EditorInterface.get_selection() 8 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 9 | 10 | if selected_nodes.is_empty(): 11 | return 12 | 13 | undo_redo.create_action("Reset transform") 14 | 15 | for selected: Node3D in selected_nodes: 16 | 17 | undo_redo.add_do_method(selected, "set_position", Vector3(0, 0, 0)) 18 | undo_redo.add_undo_method(selected, "set_position", selected.position) 19 | 20 | undo_redo.add_do_method(selected, "set_rotation_degrees", Vector3(0, 0, 0)) 21 | undo_redo.add_undo_method(selected, "set_rotation_degrees", selected.rotation_degrees) 22 | 23 | undo_redo.add_do_method(selected, "set_scale", Vector3(1, 1, 1)) 24 | undo_redo.add_undo_method(selected, "set_scale", selected.scale) 25 | 26 | undo_redo.commit_action() 27 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/reset_transform.gd.uid: -------------------------------------------------------------------------------- 1 | uid://emjhcgt1l0rd 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/select_children.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Used to quickly navigate the scene tree. 4 | 5 | func execute(): 6 | var selection : EditorSelection = EditorInterface.get_selection() 7 | var selected_nodes : Array[Node] = selection.get_selected_nodes() 8 | 9 | if selected_nodes.is_empty(): 10 | print("[Select Children] Selection is empty") 11 | return 12 | 13 | selection.clear() 14 | 15 | for node in selected_nodes: 16 | for child in node.get_children(): 17 | selection.add_node(child) 18 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/select_children.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d33lvrxcl4f4y 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/select_parents.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Used to quickly navigate the scene tree. 4 | 5 | func execute(): 6 | var selection: EditorSelection = EditorInterface.get_selection() 7 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 8 | 9 | if selected_nodes.is_empty(): 10 | print("[Select Parents] Selection is empty") 11 | return 12 | 13 | selection.clear() 14 | 15 | for node in selected_nodes: 16 | var parent = node.get_parent() 17 | 18 | if parent: 19 | selection.add_node(parent) 20 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/select_parents.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cixpx0175eb0o 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/set_visibility.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Sets visibility for all selected nodes based on the first selected node. 4 | ## If the first node is visible, all nodes will be made invisible, and vice versa. 5 | 6 | func execute(): 7 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 8 | var selection: EditorSelection = EditorInterface.get_selection() 9 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 10 | 11 | if selected_nodes.is_empty(): 12 | return 13 | 14 | # Determine target visibility based on first node 15 | var first_visible = false 16 | if "visible" in selected_nodes[0]: 17 | first_visible = selected_nodes[0].visible 18 | 19 | # Target visibility is opposite of first node's visibility 20 | var target_visible = !first_visible 21 | 22 | undo_redo.create_action("Set Visibility") 23 | 24 | for node in selected_nodes: 25 | if "visible" in node: 26 | undo_redo.add_do_property(node, "visible", target_visible) 27 | undo_redo.add_undo_property(node, "visible", node.visible) 28 | 29 | undo_redo.commit_action() 30 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/set_visibility.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b3jnxp0c15qcd 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/swap_nodes.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | ## Replaces each selected node in the Scene with an instance of the selected 4 | ## PackedScene from the FileSystem. 5 | ## 6 | ## The instantiated PackedScene inherits transform information from the node it 7 | ## replaces. Exactly one PackedScene should be selected in the FileSystem and 8 | ## at least one node should be selected in the Scene. 9 | ## 10 | ## * Undo/redo is currently not supported for this command. 11 | ## 12 | ## * Assumes that the selected PackedScene path, and selected nodes all 13 | ## have a Node3D as their root, skips selected nodes otherwise. 14 | 15 | var utilities = SceneBuilderToolbox.new() 16 | 17 | func execute(): 18 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 19 | var current_scene: Node = EditorInterface.get_edited_scene_root() 20 | var selection: EditorSelection = EditorInterface.get_selection() 21 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 22 | var selected_paths: PackedStringArray = EditorInterface.get_selected_paths() 23 | 24 | # Verify that only one FileSystem path is selected 25 | if selected_paths.size() != 1: 26 | print("[Swap Nodes] Please select exactly one PackedScene in the FileSystem.") 27 | return 28 | 29 | # Verify that selected Filesystem item is a PackedScene 30 | var selected_path = selected_paths[0] 31 | var resource = load(selected_path) 32 | if not resource or not resource is PackedScene: 33 | print("[Swap Nodes] The selected path is not a PackedScene.") 34 | return 35 | 36 | # Verify selected nodes 37 | if selected_nodes.is_empty(): 38 | print("[Swap Nodes] Select at least one node in the Scene.") 39 | return 40 | 41 | undo_redo.create_action("Swap selected nodes with a single selected node in FileSystem") 42 | 43 | for node in selected_nodes: 44 | var instance: Node3D = resource.instantiate() 45 | instance.transform = node.transform 46 | 47 | var parent = node.get_parent() 48 | if parent and instance and node: 49 | undo_redo.add_do_method(parent, "add_child", instance) 50 | undo_redo.add_do_method(instance, "set_owner", current_scene) 51 | undo_redo.add_do_method(instance, "set_name", utilities.get_unique_name(instance.name, parent)) 52 | undo_redo.add_do_method(node, "queue_free") 53 | 54 | undo_redo.add_undo_method(instance, "queue_free") 55 | undo_redo.add_undo_method(self, "print_message", "Nodes cleared from memory, undo unavailable") 56 | 57 | print("[Swap Nodes] Node has been swapped: " + node.name) 58 | else: 59 | printerr("[Swap Nodes] parent not found for node: " + node.name) 60 | 61 | undo_redo.commit_action() 62 | 63 | func print_message(message: String): 64 | print(message) 65 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/swap_nodes.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ca6yv1pfjka3o 2 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/temporary_debug.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func execute(): 5 | var toolbox: SceneBuilderToolbox = SceneBuilderToolbox.new() 6 | 7 | var current_scene: Node = EditorInterface.get_edited_scene_root() 8 | var selection: EditorSelection = EditorInterface.get_selection() 9 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 10 | 11 | print("[Temporary Debug] Empty") 12 | -------------------------------------------------------------------------------- /addons/scene_builder/Commands/temporary_debug.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cpjg5r7k3vxnw 2 | -------------------------------------------------------------------------------- /addons/scene_builder/editor_utilities.gd.uid: -------------------------------------------------------------------------------- 1 | uid://f11bs3426r4s 2 | -------------------------------------------------------------------------------- /addons/scene_builder/icon_studio.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://gea6yaqoucvf"] 2 | 3 | [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_5nnfa"] 4 | sky_top_color = Color(0.211765, 0.239216, 0.290196, 1) 5 | sky_horizon_color = Color(0.211765, 0.239216, 0.290196, 1) 6 | ground_bottom_color = Color(0.211765, 0.239216, 0.290196, 1) 7 | ground_horizon_color = Color(0.211765, 0.239216, 0.290196, 1) 8 | 9 | [sub_resource type="Sky" id="Sky_27nt7"] 10 | sky_material = SubResource("ProceduralSkyMaterial_5nnfa") 11 | 12 | [sub_resource type="Environment" id="Environment_tc7kr"] 13 | background_mode = 2 14 | sky = SubResource("Sky_27nt7") 15 | ambient_light_source = 2 16 | ambient_light_color = Color(0.752941, 0.752941, 0.752941, 1) 17 | reflected_light_source = 2 18 | tonemap_mode = 2 19 | 20 | [node name="IconStudio" type="SubViewport"] 21 | size = Vector2i(80, 80) 22 | render_target_update_mode = 4 23 | 24 | [node name="WorldEnvironment" type="WorldEnvironment" parent="."] 25 | environment = SubResource("Environment_tc7kr") 26 | 27 | [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] 28 | transform = Transform3D(0.866024, 0.433016, -0.250001, 1.47562e-08, 0.499998, 0.866026, 0.500003, -0.749999, 0.43301, 0, 0, 0) 29 | shadow_enabled = true 30 | 31 | [node name="CameraRoot" type="Node3D" parent="."] 32 | transform = Transform3D(0.866025, 0, -0.5, 0, 1, 0, 0.5, 0, 0.866025, 0, 0, 0) 33 | 34 | [node name="Pitch" type="Node3D" parent="CameraRoot"] 35 | transform = Transform3D(1, 0, 0, 0, 0.965926, 0.258819, 0, -0.258819, 0.965926, 0, 0.5, 1) 36 | 37 | [node name="Camera3D" type="Camera3D" parent="CameraRoot/Pitch"] 38 | transform = Transform3D(1, -2.23517e-08, 2.98023e-08, 0, 1, 4.47035e-08, 0, -2.38419e-07, 1, 0, 0, 10.6067) 39 | fov = 60.0 40 | -------------------------------------------------------------------------------- /addons/scene_builder/icon_tmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sci-comp/SceneBuilder/a78f3550ecdce75df566e2532de43dff76c7cd02/addons/scene_builder/icon_tmp.png -------------------------------------------------------------------------------- /addons/scene_builder/icon_tmp.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b1tpwsohf1w35" 6 | path="res://.godot/imported/icon_tmp.png-68b7006acbd0e9eead2f33d699ceaf26.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/scene_builder/addons/scene_builder/icon_tmp.png" 14 | dest_files=["res://.godot/imported/icon_tmp.png-68b7006acbd0e9eead2f33d699ceaf26.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/scene_builder/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Scene Builder" 4 | description="General purpose level design tool for Godot" 5 | author="Paul" 6 | version="0.0" 7 | script="scene_builder_plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_collections.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Resource 3 | class_name CollectionNames 4 | 5 | @export_category("Row #1") 6 | @export var name_01: String = "" 7 | @export var name_02: String = "" 8 | @export var name_03: String = "" 9 | @export var name_04: String = "" 10 | @export var name_05: String = "" 11 | @export var name_06: String = "" 12 | @export var font_color_01: Color = Color.WHITE 13 | @export var font_color_02: Color = Color.WHITE 14 | @export var font_color_03: Color = Color.WHITE 15 | @export var font_color_04: Color = Color.WHITE 16 | @export var font_color_05: Color = Color.WHITE 17 | @export var font_color_06: Color = Color.WHITE 18 | 19 | @export_category("Row #2") 20 | @export var name_07: String = "" 21 | @export var name_08: String = "" 22 | @export var name_09: String = "" 23 | @export var name_10: String = "" 24 | @export var name_11: String = "" 25 | @export var name_12: String = "" 26 | @export var font_color_07: Color = Color.WHITE 27 | @export var font_color_08: Color = Color.WHITE 28 | @export var font_color_09: Color = Color.WHITE 29 | @export var font_color_10: Color = Color.WHITE 30 | @export var font_color_11: Color = Color.WHITE 31 | @export var font_color_12: Color = Color.WHITE 32 | 33 | @export_category("Row #3") 34 | @export var name_13: String = "" 35 | @export var name_14: String = "" 36 | @export var name_15: String = "" 37 | @export var name_16: String = "" 38 | @export var name_17: String = "" 39 | @export var name_18: String = "" 40 | @export var font_color_13: Color = Color.WHITE 41 | @export var font_color_14: Color = Color.WHITE 42 | @export var font_color_15: Color = Color.WHITE 43 | @export var font_color_16: Color = Color.WHITE 44 | @export var font_color_17: Color = Color.WHITE 45 | @export var font_color_18: Color = Color.WHITE 46 | 47 | @export_category("Row #4") 48 | @export var name_19: String = "" 49 | @export var name_20: String = "" 50 | @export var name_21: String = "" 51 | @export var name_22: String = "" 52 | @export var name_23: String = "" 53 | @export var name_24: String = "" 54 | @export var font_color_19: Color = Color.WHITE 55 | @export var font_color_20: Color = Color.WHITE 56 | @export var font_color_21: Color = Color.WHITE 57 | @export var font_color_22: Color = Color.WHITE 58 | @export var font_color_23: Color = Color.WHITE 59 | @export var font_color_24: Color = Color.WHITE 60 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_collections.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bbqiktl78acmh 2 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_commands.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | class_name SceneBuilderCommands 4 | 5 | var submenu_scene: PopupMenu 6 | var reusable_instance 7 | var config: SceneBuilderConfig 8 | 9 | enum SceneCommands 10 | { 11 | alphabetize_nodes = 1, 12 | change_places = 8, 13 | create_audio_stream_player_3d = 9, 14 | create_scene_builder_items = 10, 15 | create_standard_material_3d = 12, 16 | fix_negative_scaling = 20, 17 | instantiate_in_a_row_1 = 32, 18 | instantiate_in_a_row_2 = 33, 19 | instantiate_in_a_row_3 = 34, 20 | push_to_grid = 45, 21 | push_parent_offset_to_child = 46, 22 | reset_node_name = 50, 23 | reset_transform = 61, 24 | select_children = 70, 25 | select_parents = 71, 26 | set_visibility = 75, 27 | swap_nodes = 80 28 | } 29 | 30 | func _unhandled_input(event: InputEvent): 31 | if event is InputEventKey: 32 | if event.is_pressed() and !event.is_echo(): 33 | 34 | if event.alt_pressed: 35 | match event.keycode: 36 | config.alphabetize_nodes: 37 | alphabetize_nodes() 38 | config.change_places: 39 | change_places() 40 | config.create_audio_stream_player_3d: 41 | create_audio_stream_player_3d() 42 | config.create_scene_builder_items: 43 | create_scene_builder_items() 44 | config.create_standard_material_3d: 45 | create_standard_material_3d() 46 | config.instantiate_in_a_row_1: 47 | instantiate_in_a_row(1) 48 | config.instantiate_in_a_row_2: 49 | instantiate_in_a_row(2) 50 | config.instantiate_in_a_row_5: 51 | instantiate_in_a_row(5) 52 | config.push_to_grid: 53 | push_to_grid() 54 | config.push_parent_offset_to_child: 55 | push_parent_offset_to_child() 56 | config.reset_node_name: 57 | reset_node_name() 58 | config.reset_transform: 59 | reset_transform() 60 | config.swap_nodes: 61 | swap_nodes() 62 | config.set_visibility: 63 | set_visibility() 64 | config.temporary_debug: 65 | temporary_debug() 66 | 67 | elif event.ctrl_pressed: 68 | if event.keycode == KEY_RIGHT: 69 | select_children() 70 | elif event.keycode == KEY_LEFT: 71 | select_parents() 72 | 73 | func _enter_tree(): 74 | submenu_scene = PopupMenu.new() 75 | submenu_scene.connect("id_pressed", Callable(self, "_on_scene_submenu_item_selected")) 76 | add_tool_submenu_item("Scene Builder", submenu_scene) 77 | submenu_scene.add_item("Alphabetize nodes", SceneCommands.alphabetize_nodes) 78 | submenu_scene.add_item("Change places", SceneCommands.change_places) 79 | submenu_scene.add_item("Create audio stream player 3d", SceneCommands.create_audio_stream_player_3d) 80 | submenu_scene.add_item("Create scene builder items", SceneCommands.create_scene_builder_items) 81 | submenu_scene.add_item("Create StandardMaterial3D", SceneCommands.create_standard_material_3d) 82 | submenu_scene.add_item("Fix negative scaling", SceneCommands.fix_negative_scaling) 83 | submenu_scene.add_item("Instantiate selected paths in a row (1m)", SceneCommands.instantiate_in_a_row_1) 84 | submenu_scene.add_item("Instantiate selected paths in a row (5m)", SceneCommands.instantiate_in_a_row_2) 85 | submenu_scene.add_item("Instantiate selected paths in a row (10m)", SceneCommands.instantiate_in_a_row_3) 86 | submenu_scene.add_item("Push to grid", SceneCommands.push_to_grid) 87 | submenu_scene.add_item("Push parent offset to child", SceneCommands.push_parent_offset_to_child) 88 | submenu_scene.add_item("Reset node names", SceneCommands.reset_node_name) 89 | submenu_scene.add_item("Reset transform", SceneCommands.reset_transform) 90 | submenu_scene.add_item("Select children", SceneCommands.select_children) 91 | submenu_scene.add_item("Select parents", SceneCommands.select_parents) 92 | submenu_scene.add_item("Swap nodes", SceneCommands.swap_nodes) 93 | 94 | func _exit_tree(): 95 | remove_tool_menu_item("Scene Builder") 96 | 97 | func _on_scene_submenu_item_selected(id: int): 98 | match id: 99 | SceneCommands.alphabetize_nodes: 100 | alphabetize_nodes() 101 | SceneCommands.change_places: 102 | change_places() 103 | SceneCommands.create_audio_stream_player_3d: 104 | create_audio_stream_player_3d() 105 | SceneCommands.create_scene_builder_items: 106 | create_scene_builder_items() 107 | SceneCommands.create_standard_material_3d: 108 | create_standard_material_3d() 109 | SceneCommands.fix_negative_scaling: 110 | fix_negative_scaling() 111 | SceneCommands.instantiate_in_a_row_1: 112 | instantiate_in_a_row(1) 113 | SceneCommands.instantiate_in_a_row_2: 114 | instantiate_in_a_row(5) 115 | SceneCommands.instantiate_in_a_row_3: 116 | instantiate_in_a_row(10) 117 | SceneCommands.push_to_grid: 118 | push_to_grid() 119 | SceneCommands.push_parent_offset_to_child: 120 | push_parent_offset_to_child() 121 | SceneCommands.reset_node_name: 122 | reset_node_name() 123 | SceneCommands.reset_transform: 124 | reset_transform() 125 | SceneCommands.select_children: 126 | select_children() 127 | SceneCommands.select_parents: 128 | select_parents() 129 | SceneCommands.set_visibility: 130 | set_visibility() 131 | SceneCommands.swap_nodes: 132 | swap_nodes() 133 | 134 | func alphabetize_nodes(): 135 | var _instance = preload("./Commands/alphabetize_nodes.gd").new() 136 | _instance.execute() 137 | 138 | func change_places(): 139 | var _instance = preload("./Commands/change_places.gd").new() 140 | _instance.execute() 141 | 142 | func create_scene_builder_items(): 143 | var reusable_instance = preload("./Commands/create_scene_builder_items.gd").new() 144 | add_child(reusable_instance) 145 | reusable_instance.done.connect(_on_reusable_instance_done) 146 | reusable_instance.execute(config.root_dir) 147 | 148 | func create_standard_material_3d(): 149 | reusable_instance = preload("./Commands/create_standard_material_3d.gd").new() 150 | add_child(reusable_instance) 151 | reusable_instance.done.connect(_on_reusable_instance_done) 152 | reusable_instance.execute() 153 | 154 | func create_audio_stream_player_3d(): 155 | var _instance = preload("./Commands/create_audio_stream_player_3d.gd").new() 156 | _instance.execute() 157 | 158 | func fix_negative_scaling(): 159 | var _instance = preload("./Commands/fix_negative_scaling.gd").new() 160 | _instance.execute() 161 | 162 | func instantiate_in_a_row(_space): 163 | var _instance = preload("./Commands/instantiate_in_a_row.gd").new() 164 | _instance.execute(_space) 165 | 166 | func push_to_grid(): 167 | var _instance = preload("./Commands/push_to_grid.gd").new() 168 | _instance.execute() 169 | 170 | func push_parent_offset_to_child(): 171 | var _instance = preload("./Commands/push_parent_offset_to_child.gd").new() 172 | _instance.execute() 173 | 174 | func reset_node_name(): 175 | var _instance = preload("./Commands/reset_node_name.gd").new() 176 | _instance.execute() 177 | 178 | func reset_transform(): 179 | var _instance = preload("./Commands/reset_transform.gd").new() 180 | _instance.execute() 181 | 182 | func select_children(): 183 | var _instance = preload("./Commands/select_children.gd").new() 184 | _instance.execute() 185 | 186 | func select_parents(): 187 | var _instance = preload("./Commands/select_parents.gd").new() 188 | _instance.execute() 189 | 190 | func set_visibility(): 191 | var _instance = preload("./Commands/set_visibility.gd").new() 192 | _instance.execute() 193 | 194 | func swap_nodes(): 195 | var _instance = preload("./Commands/swap_nodes.gd").new() 196 | _instance.execute() 197 | 198 | func temporary_debug(): 199 | var _instance = preload("./Commands/temporary_debug.gd").new() 200 | _instance.execute() 201 | 202 | func update_config(new_config) -> void: 203 | config = new_config 204 | 205 | # ------------------------------------------------------------------------------ 206 | 207 | func _on_reusable_instance_done(): 208 | if reusable_instance != null: 209 | print("Freeing reusable instance") 210 | reusable_instance.queue_free() 211 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_commands.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cc4t1u1k8rjs5 2 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_config.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | class_name SceneBuilderConfig 3 | 4 | @export var root_dir: String = "res://Data/scene_builder/" 5 | 6 | @export_group("For use with the Alt modifier") 7 | 8 | # Instantiate 9 | @export var instantiate_in_a_row_1: Key = KEY_L 10 | @export var instantiate_in_a_row_2: Key = KEY_SEMICOLON 11 | @export var instantiate_in_a_row_5: Key = KEY_APOSTROPHE 12 | 13 | # Name 14 | @export var alphabetize_nodes: Key = KEY_0 15 | @export var reset_node_name: Key = KEY_N 16 | 17 | # Node 18 | @export var change_places: Key = KEY_C 19 | @export var find_mismatched_types: Key = KEY_9 20 | @export var swap_nodes: Key = KEY_S 21 | @export var set_visibility: Key = KEY_H 22 | 23 | # Transform 24 | @export var reset_transform: Key = KEY_T 25 | @export var reset_transform_rotation: Key = KEY_R 26 | @export var push_to_grid: Key = KEY_P 27 | @export var push_parent_offset_to_child: Key = KEY_O 28 | 29 | # Resources 30 | @export var create_audio_stream_player_3d: Key = KEY_COMMA 31 | @export var create_scene_builder_items: Key = KEY_SLASH 32 | @export var create_standard_material_3d: Key = KEY_M 33 | 34 | # Debug 35 | @export var temporary_debug: Key = KEY_PERIOD 36 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_config.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dww7642buy6vx 2 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_config.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="SceneBuilderConfig" load_steps=2 format=3 uid="uid://d4jr8ot7ggqbf"] 2 | 3 | [ext_resource type="Script" uid="uid://dww7642buy6vx" path="res://addons/scene_builder/addons/scene_builder/scene_builder_config.gd" id="1_64inv"] 4 | 5 | [resource] 6 | script = ExtResource("1_64inv") 7 | root_dir = "res://Data/SceneBuilder/" 8 | instantiate_in_a_row_1 = 76 9 | instantiate_in_a_row_2 = 59 10 | instantiate_in_a_row_5 = 39 11 | alphabetize_nodes = 48 12 | reset_node_name = 78 13 | change_places = 67 14 | find_mismatched_types = 57 15 | swap_nodes = 83 16 | set_visibility = 72 17 | reset_transform = 84 18 | reset_transform_rotation = 89 19 | push_to_grid = 80 20 | push_parent_offset_to_child = 79 21 | create_audio_stream_player_3d = 44 22 | create_scene_builder_items = 47 23 | create_standard_material_3d = 77 24 | temporary_debug = 46 25 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_create_items.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=3 uid="uid://x8g21udghd81"] 2 | 3 | [node name="SceneBuilderCreateItems" type="VBoxContainer"] 4 | anchors_preset = 15 5 | anchor_right = 1.0 6 | anchor_bottom = 1.0 7 | offset_right = -768.0 8 | offset_bottom = -439.0 9 | grow_horizontal = 2 10 | grow_vertical = 2 11 | 12 | [node name="Collection" type="HBoxContainer" parent="."] 13 | layout_mode = 2 14 | 15 | [node name="Label" type="Label" parent="Collection"] 16 | layout_mode = 2 17 | text = "Collection:" 18 | 19 | [node name="LineEdit" type="LineEdit" parent="Collection"] 20 | layout_mode = 2 21 | size_flags_horizontal = 3 22 | text = "Unnamed" 23 | alignment = 1 24 | 25 | [node name="Boolean" type="HBoxContainer" parent="."] 26 | layout_mode = 2 27 | 28 | [node name="Label" type="Label" parent="Boolean"] 29 | layout_mode = 2 30 | size_flags_horizontal = 3 31 | text = "Randomize" 32 | 33 | [node name="Rotation" type="CheckButton" parent="Boolean"] 34 | layout_mode = 2 35 | size_flags_horizontal = 3 36 | text = "Rotation" 37 | 38 | [node name="Scale" type="CheckButton" parent="Boolean"] 39 | layout_mode = 2 40 | size_flags_horizontal = 3 41 | text = "Scale" 42 | 43 | [node name="VerticalOffset" type="CheckButton" parent="Boolean"] 44 | layout_mode = 2 45 | size_flags_horizontal = 3 46 | text = "Vertical Offset" 47 | 48 | [node name="Headers" type="HBoxContainer" parent="."] 49 | layout_mode = 2 50 | 51 | [node name="Label" type="Label" parent="Headers"] 52 | layout_mode = 2 53 | size_flags_horizontal = 3 54 | 55 | [node name="x" type="Label" parent="Headers"] 56 | layout_mode = 2 57 | size_flags_horizontal = 3 58 | text = "x" 59 | 60 | [node name="y" type="Label" parent="Headers"] 61 | layout_mode = 2 62 | size_flags_horizontal = 3 63 | text = "y" 64 | 65 | [node name="z" type="Label" parent="Headers"] 66 | layout_mode = 2 67 | size_flags_horizontal = 3 68 | text = "z" 69 | 70 | [node name="Rotation" type="HBoxContainer" parent="."] 71 | layout_mode = 2 72 | 73 | [node name="Label" type="Label" parent="Rotation"] 74 | layout_mode = 2 75 | size_flags_horizontal = 3 76 | text = "Rotation" 77 | 78 | [node name="x" type="HSlider" parent="Rotation"] 79 | layout_mode = 2 80 | size_flags_horizontal = 3 81 | max_value = 360.0 82 | 83 | [node name="y" type="HSlider" parent="Rotation"] 84 | layout_mode = 2 85 | size_flags_horizontal = 3 86 | max_value = 360.0 87 | 88 | [node name="z" type="HSlider" parent="Rotation"] 89 | layout_mode = 2 90 | size_flags_horizontal = 3 91 | max_value = 360.0 92 | 93 | [node name="Scale" type="HBoxContainer" parent="."] 94 | layout_mode = 2 95 | 96 | [node name="lbl_Scale" type="Label" parent="Scale"] 97 | layout_mode = 2 98 | size_flags_horizontal = 0 99 | text = "Scale" 100 | 101 | [node name="min" type="SpinBox" parent="Scale"] 102 | layout_mode = 2 103 | size_flags_horizontal = 6 104 | min_value = 0.01 105 | step = 0.01 106 | value = 0.9 107 | custom_arrow_step = 0.02 108 | 109 | [node name="lbl_to" type="Label" parent="Scale"] 110 | layout_mode = 2 111 | size_flags_horizontal = 4 112 | text = "to" 113 | 114 | [node name="max" type="SpinBox" parent="Scale"] 115 | layout_mode = 2 116 | size_flags_horizontal = 6 117 | min_value = 0.01 118 | step = 0.01 119 | value = 1.1 120 | custom_arrow_step = 0.02 121 | 122 | [node name="VerticalOffset" type="HBoxContainer" parent="."] 123 | layout_mode = 2 124 | 125 | [node name="lbl_offset" type="Label" parent="VerticalOffset"] 126 | layout_mode = 2 127 | size_flags_horizontal = 0 128 | text = "Vertical offset" 129 | 130 | [node name="min" type="SpinBox" parent="VerticalOffset"] 131 | layout_mode = 2 132 | size_flags_horizontal = 6 133 | step = 0.001 134 | value = 0.02 135 | custom_arrow_step = 0.001 136 | 137 | [node name="lbl_to" type="Label" parent="VerticalOffset"] 138 | layout_mode = 2 139 | size_flags_horizontal = 4 140 | text = "to" 141 | 142 | [node name="max" type="SpinBox" parent="VerticalOffset"] 143 | layout_mode = 2 144 | size_flags_horizontal = 6 145 | min_value = 0.001 146 | step = 0.001 147 | value = 0.05 148 | custom_arrow_step = 0.001 149 | 150 | [node name="Okay" type="Button" parent="."] 151 | layout_mode = 2 152 | size_flags_horizontal = 4 153 | size_flags_vertical = 4 154 | text = "Create scene builder items with icons" 155 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_dock.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | class_name SceneBuilderDock 4 | 5 | @onready var config: SceneBuilderConfig = SceneBuilderConfig.new() 6 | 7 | # Paths 8 | var data_dir: String = "" 9 | var path_to_collection_names: String 10 | 11 | # Constants 12 | var num_collections: int = 24 13 | 14 | var rng: RandomNumberGenerator = RandomNumberGenerator.new() 15 | var toolbox: SceneBuilderToolbox = SceneBuilderToolbox.new() 16 | var undo_redo: EditorUndoRedoManager = get_undo_redo() 17 | 18 | # Godot controls 19 | var base_control: Control 20 | var btn_use_local_space: Button 21 | 22 | # SceneBuilderDock controls 23 | var scene_builder_dock: VBoxContainer 24 | var tab_container: TabContainer 25 | var btns_collection_tabs: Array = [] # set in _enter_tree() 26 | # Options 27 | var btn_use_surface_normal: CheckButton 28 | var btn_surface_normal_x: CheckBox 29 | var btn_surface_normal_y: CheckBox 30 | var btn_surface_normal_z: CheckBox 31 | var btn_parent_node_selector: Button 32 | var btn_group_surface_orientation: ButtonGroup 33 | var btn_find_world_3d: Button 34 | var btn_reload_all_items: Button 35 | # Path3D 36 | var spinbox_separation_distance: SpinBox 37 | var spinbox_jitter_x: SpinBox 38 | var spinbox_jitter_y: SpinBox 39 | var spinbox_jitter_z: SpinBox 40 | var spinbox_y_offset: SpinBox 41 | var btn_place_fence: Button 42 | 43 | # Indicators 44 | var lbl_indicator_x: Label 45 | var lbl_indicator_y: Label 46 | var lbl_indicator_z: Label 47 | var lbl_indicator_scale: Label 48 | 49 | # Updated with update_world_3d() 50 | var editor: EditorInterface 51 | var physics_space: PhysicsDirectSpaceState3D 52 | var world3d: World3D 53 | var viewport: Viewport 54 | var camera: Camera3D 55 | var scene_root: Node3D 56 | 57 | # Updated when reloading all collections 58 | var collection_names: Array[String] = [] 59 | var items_by_collection: Dictionary = {} # A dictionary of dictionaries 60 | var ordered_keys_by_collection: Dictionary = {} 61 | var item_highlighters_by_collection: Dictionary = {} 62 | # Also updated on tab button click 63 | var selected_collection: Dictionary = {} 64 | var selected_collection_name: String = "" 65 | var selected_collection_index: int = 0 66 | # Also updated on item click 67 | var selected_item: SceneBuilderItem = null 68 | var selected_item_name: String = "" 69 | var preview_instance: Node3D = null 70 | var preview_instance_rid_array: Array[RID] = [] 71 | var selected_parent_node: Node3D = null 72 | 73 | enum TransformMode { 74 | NONE, 75 | POSITION_X, 76 | POSITION_Y, 77 | POSITION_Z, 78 | ROTATION_X, 79 | ROTATION_Y, 80 | ROTATION_Z, 81 | SCALE 82 | } 83 | 84 | var placement_mode_enabled: bool = false 85 | var current_transform_mode: TransformMode = TransformMode.NONE 86 | 87 | func is_transform_mode_enabled() -> bool: 88 | return current_transform_mode != TransformMode.NONE 89 | 90 | # Preview item 91 | var pos_offset_x: float = 0 92 | var pos_offset_y: float = 0 93 | var pos_offset_z: float = 0 94 | var original_preview_position: Vector3 = Vector3.ZERO 95 | var original_preview_basis: Basis = Basis.IDENTITY 96 | var original_mouse_position: Vector2 = Vector2.ONE 97 | var random_offset_y: float = 0 98 | var original_preview_scale: Vector3 = Vector3.ONE 99 | var scene_builder_temp: Node # Used as a parent to the preview item 100 | 101 | # ---- Notifications ----------------------------------------------------------- 102 | 103 | func _enter_tree() -> void: 104 | path_to_collection_names = config.root_dir + "collection_names.tres" 105 | 106 | editor = get_editor_interface() 107 | base_control = EditorInterface.get_base_control() 108 | 109 | # Found using: https://github.com/Zylann/godot_editor_debugger_plugin 110 | var _panel : Panel = get_editor_interface().get_base_control() 111 | var _vboxcontainer15 : VBoxContainer = _panel.get_child(0) 112 | var _vboxcontainer26 : VBoxContainer = _vboxcontainer15.get_child(1).get_child(1).get_child(1).get_child(0) 113 | var _main_screen : VBoxContainer = _vboxcontainer26.get_child(0).get_child(0).get_child(0).get_child(1).get_child(0) 114 | var _hboxcontainer11486 : HBoxContainer = _main_screen.get_child(1).get_child(0).get_child(0).get_child(0) 115 | 116 | btn_use_local_space = _hboxcontainer11486.get_child(13) 117 | if !btn_use_local_space: 118 | printerr("[SceneBuilderDock] Unable to find use local space button") 119 | 120 | #region Initialize controls for the SceneBuilderDock 121 | 122 | var path : String = SceneBuilderToolbox.find_resource_with_dynamic_path("scene_builder_dock.tscn") 123 | if path == "": 124 | printerr("[SceneBuilderDock] scene_builder_dock.tscn was not found") 125 | return 126 | 127 | scene_builder_dock = load(path).instantiate() 128 | 129 | add_control_to_dock(EditorPlugin.DOCK_SLOT_RIGHT_UL, scene_builder_dock) 130 | 131 | # Collection tabs 132 | tab_container = scene_builder_dock.get_node("Collection/TabContainer") 133 | for i: int in range(num_collections): 134 | var tab_button: Button = scene_builder_dock.get_node("Collection/Panel/Grid/%s" % str(i + 1)) 135 | tab_button.pressed.connect(select_collection.bind(i)) 136 | btns_collection_tabs.append(tab_button) 137 | 138 | # Options tab 139 | btn_use_surface_normal = scene_builder_dock.get_node("%UseSurfaceNormal") 140 | btn_surface_normal_x = scene_builder_dock.get_node("%Orientation/ButtonGroup/X") 141 | btn_surface_normal_y = scene_builder_dock.get_node("%Orientation/ButtonGroup/Y") 142 | btn_surface_normal_z = scene_builder_dock.get_node("%Orientation/ButtonGroup/Z") 143 | 144 | btn_parent_node_selector = scene_builder_dock.get_node("%ParentNodeSelector") 145 | var script_path = SceneBuilderToolbox.find_resource_with_dynamic_path("scene_builder_node_path_selector.gd") 146 | if script_path != "": 147 | btn_parent_node_selector.set_script(load(script_path)) 148 | btn_parent_node_selector.path_selected.connect(set_parent_node) 149 | else: 150 | printerr("[SceneBuilderDock] Failed to find scene_builder_node_path_selector.gd") 151 | 152 | # 153 | btn_group_surface_orientation = ButtonGroup.new() 154 | btn_surface_normal_x.button_group = btn_group_surface_orientation 155 | btn_surface_normal_y.button_group = btn_group_surface_orientation 156 | btn_surface_normal_z.button_group = btn_group_surface_orientation 157 | # 158 | btn_find_world_3d = scene_builder_dock.get_node("Settings/Tab/Options/Bottom/FindWorld3D") 159 | btn_reload_all_items = scene_builder_dock.get_node("Settings/Tab/Options/Bottom/ReloadAllItems") 160 | btn_find_world_3d.pressed.connect(update_world_3d) 161 | btn_reload_all_items.pressed.connect(reload_all_items) 162 | 163 | # Path3D tab 164 | spinbox_separation_distance = scene_builder_dock.get_node("Settings/Tab/Path3D/Separation/SpinBox") 165 | spinbox_jitter_x = scene_builder_dock.get_node("Settings/Tab/Path3D/Jitter/X") 166 | spinbox_jitter_y = scene_builder_dock.get_node("Settings/Tab/Path3D/Jitter/Y") 167 | spinbox_jitter_z = scene_builder_dock.get_node("Settings/Tab/Path3D/Jitter/Z") 168 | spinbox_y_offset = scene_builder_dock.get_node("Settings/Tab/Path3D/YOffset/Value") 169 | btn_place_fence = scene_builder_dock.get_node("Settings/Tab/Path3D/PlaceFence") 170 | btn_place_fence.pressed.connect(place_fence) 171 | 172 | # Indicators 173 | lbl_indicator_x = scene_builder_dock.get_node("Settings/Indicators/1") 174 | lbl_indicator_y = scene_builder_dock.get_node("Settings/Indicators/2") 175 | lbl_indicator_z = scene_builder_dock.get_node("Settings/Indicators/3") 176 | lbl_indicator_scale = scene_builder_dock.get_node("Settings/Indicators/4") 177 | 178 | #endregion 179 | 180 | # 181 | reload_all_items() 182 | update_world_3d() 183 | 184 | func _exit_tree() -> void: 185 | remove_control_from_docks(scene_builder_dock) 186 | scene_builder_dock.queue_free() 187 | 188 | func _process(_delta: float) -> void: 189 | # Update preview item position 190 | if placement_mode_enabled: 191 | 192 | if not scene_root or not scene_root is Node3D or not scene_root.is_inside_tree(): 193 | print("[SceneBuilderDock] Scene root invalid, ending placement mode") 194 | end_placement_mode() 195 | return 196 | 197 | if !is_transform_mode_enabled(): 198 | if preview_instance: 199 | populate_preview_instance_rid_array(preview_instance) 200 | var result = perform_raycast_with_exclusion(preview_instance_rid_array) 201 | if result and result.collider: 202 | var _preview_item = scene_root.get_node_or_null("SceneBuilderTemp") 203 | if _preview_item and _preview_item.get_child_count() > 0: 204 | var _instance: Node3D = _preview_item.get_child(0) 205 | 206 | var new_position: Vector3 = result.position 207 | 208 | new_position += Vector3(pos_offset_x, pos_offset_y, pos_offset_z) 209 | 210 | # This offset prevents z-fighting when placing overlapping quads 211 | if selected_item.use_random_vertical_offset: 212 | new_position.y += random_offset_y 213 | 214 | _instance.global_transform.origin = new_position 215 | if btn_use_surface_normal.button_pressed: 216 | _instance.basis = align_up(_instance.global_transform.basis, result.normal) 217 | var quaternion = Quaternion(_instance.basis.orthonormalized()) 218 | if btn_surface_normal_x.button_pressed: 219 | quaternion = quaternion * Quaternion(Vector3(1, 0, 0), deg_to_rad(90)) 220 | elif btn_surface_normal_z.button_pressed: 221 | quaternion = quaternion * Quaternion(Vector3(0, 0, 1), deg_to_rad(90)) 222 | 223 | func forward_3d_gui_input(_camera: Camera3D, event: InputEvent) -> AfterGUIInput: 224 | if event is InputEventMouseMotion: 225 | if placement_mode_enabled: 226 | var relative_motion: float 227 | if abs(event.relative.x) > abs(event.relative.y): 228 | relative_motion = event.relative.x 229 | else: 230 | relative_motion = -event.relative.y 231 | relative_motion *= 0.01 # Sensitivity factor 232 | 233 | match current_transform_mode: 234 | TransformMode.POSITION_X: 235 | pos_offset_x += relative_motion 236 | preview_instance.position.x = original_preview_position.x + pos_offset_x 237 | TransformMode.POSITION_Y: 238 | pos_offset_y += relative_motion 239 | preview_instance.position.y = original_preview_position.y + pos_offset_y 240 | TransformMode.POSITION_Z: 241 | pos_offset_z += relative_motion 242 | preview_instance.position.z = original_preview_position.z + pos_offset_z 243 | TransformMode.ROTATION_X: 244 | if btn_use_local_space.button_pressed: 245 | preview_instance.rotate_object_local(Vector3(1, 0, 0), relative_motion) 246 | else: 247 | preview_instance.rotate_x(relative_motion) 248 | TransformMode.ROTATION_Y: 249 | if btn_use_local_space.button_pressed: 250 | preview_instance.rotate_object_local(Vector3(0, 1, 0), relative_motion) 251 | else: 252 | preview_instance.rotate_y(relative_motion) 253 | TransformMode.ROTATION_Z: 254 | if btn_use_local_space.button_pressed: 255 | preview_instance.rotate_object_local(Vector3(0, 0, 1), relative_motion) 256 | else: 257 | preview_instance.rotate_z(relative_motion) 258 | TransformMode.SCALE: 259 | var new_scale: Vector3 = preview_instance.scale * (1 + relative_motion) 260 | if is_zero_approx(new_scale.x) or is_zero_approx(new_scale.y) or is_zero_approx(new_scale.z): 261 | new_scale = original_preview_scale 262 | preview_instance.scale = new_scale 263 | 264 | if event is InputEventMouseButton: 265 | if event.is_pressed() and !event.is_echo(): 266 | 267 | if placement_mode_enabled: 268 | var mouse_pos = viewport.get_mouse_position() 269 | if mouse_pos.x >= 0 and mouse_pos.y >= 0: 270 | if mouse_pos.x <= viewport.size.x and mouse_pos.y <= viewport.size.y: 271 | 272 | if event.button_index == MOUSE_BUTTON_LEFT: 273 | 274 | if is_transform_mode_enabled(): 275 | # Confirm changes 276 | original_preview_basis = preview_instance.basis 277 | original_preview_scale = preview_instance.scale 278 | end_transform_mode() 279 | viewport.warp_mouse(original_mouse_position) 280 | else: 281 | instantiate_selected_item_at_position() 282 | 283 | return EditorPlugin.AFTER_GUI_INPUT_STOP 284 | 285 | elif event.button_index == MOUSE_BUTTON_RIGHT: 286 | 287 | if is_transform_mode_enabled(): 288 | # Revert preview transformations 289 | print("[SceneBuilderDock] warping to: ", original_mouse_position) 290 | preview_instance.basis = original_preview_basis 291 | preview_instance.scale = original_preview_scale 292 | end_transform_mode() 293 | viewport.warp_mouse(original_mouse_position) 294 | 295 | return EditorPlugin.AFTER_GUI_INPUT_STOP 296 | 297 | elif event is InputEventKey: 298 | if event.is_pressed() and !event.is_echo(): 299 | 300 | if !event.alt_pressed and !event.ctrl_pressed: 301 | 302 | if event.shift_pressed: 303 | 304 | if event.keycode == KEY_1: 305 | 306 | if is_transform_mode_enabled(): 307 | if current_transform_mode == TransformMode.POSITION_X: 308 | end_transform_mode() 309 | else: 310 | end_transform_mode() 311 | start_transform_mode(TransformMode.POSITION_X) 312 | else: 313 | start_transform_mode(TransformMode.POSITION_X) 314 | 315 | elif event.keycode == KEY_2: 316 | if is_transform_mode_enabled(): 317 | if current_transform_mode == TransformMode.POSITION_Y: 318 | end_transform_mode() 319 | else: 320 | end_transform_mode() 321 | start_transform_mode(TransformMode.POSITION_Y) 322 | else: 323 | start_transform_mode(TransformMode.POSITION_Y) 324 | 325 | elif event.keycode == KEY_3: 326 | if is_transform_mode_enabled(): 327 | if current_transform_mode == TransformMode.POSITION_Z: 328 | end_transform_mode() 329 | else: 330 | end_transform_mode() 331 | start_transform_mode(TransformMode.POSITION_Z) 332 | else: 333 | start_transform_mode(TransformMode.POSITION_Z) 334 | 335 | else: 336 | 337 | if event.keycode == KEY_1: 338 | 339 | if is_transform_mode_enabled(): 340 | if current_transform_mode == TransformMode.ROTATION_X: 341 | end_transform_mode() 342 | else: 343 | end_transform_mode() 344 | start_transform_mode(TransformMode.ROTATION_X) 345 | else: 346 | start_transform_mode(TransformMode.ROTATION_X) 347 | 348 | elif event.keycode == KEY_2: 349 | if is_transform_mode_enabled(): 350 | if current_transform_mode == TransformMode.ROTATION_Y: 351 | end_transform_mode() 352 | else: 353 | end_transform_mode() 354 | start_transform_mode(TransformMode.ROTATION_Y) 355 | else: 356 | start_transform_mode(TransformMode.ROTATION_Y) 357 | 358 | elif event.keycode == KEY_3: 359 | if is_transform_mode_enabled(): 360 | if current_transform_mode == TransformMode.ROTATION_Z: 361 | end_transform_mode() 362 | else: 363 | end_transform_mode() 364 | start_transform_mode(TransformMode.ROTATION_Z) 365 | else: 366 | start_transform_mode(TransformMode.ROTATION_Z) 367 | 368 | elif event.keycode == KEY_4: 369 | if is_transform_mode_enabled(): 370 | if current_transform_mode == TransformMode.SCALE: 371 | end_transform_mode() 372 | else: 373 | end_transform_mode() 374 | start_transform_mode(TransformMode.SCALE) 375 | else: 376 | start_transform_mode(TransformMode.SCALE) 377 | 378 | elif event.keycode == KEY_5: 379 | if is_transform_mode_enabled(): 380 | end_transform_mode() 381 | reroll_preview_instance_transform() 382 | 383 | if event.keycode == KEY_ESCAPE: 384 | end_placement_mode() 385 | 386 | if placement_mode_enabled: 387 | if event.shift_pressed: 388 | if event.keycode == KEY_LEFT: 389 | select_previous_item() 390 | elif event.keycode == KEY_RIGHT: 391 | select_next_item() 392 | 393 | if event.alt_pressed: 394 | if event.keycode == KEY_LEFT: 395 | select_previous_collection() 396 | elif event.keycode == KEY_RIGHT: 397 | select_next_collection() 398 | 399 | return EditorPlugin.AFTER_GUI_INPUT_PASS 400 | 401 | # ---- Buttons ----------------------------------------------------------------- 402 | 403 | func is_collection_populated(tab_index: int) -> bool: 404 | var _collection_name: String = collection_names[tab_index] 405 | if _collection_name == "" or _collection_name == " ": 406 | return false 407 | else: 408 | if _collection_name in items_by_collection: 409 | var _items: Dictionary = items_by_collection[_collection_name] 410 | if _items.is_empty(): 411 | return false 412 | else: 413 | return true 414 | else: 415 | return false 416 | 417 | func select_collection(tab_index: int) -> void: 418 | if collection_names.size() == 0: 419 | print("[SceneBuilderDock] Unable to select collection, none exist") 420 | return 421 | 422 | end_placement_mode() 423 | 424 | for button: Button in btns_collection_tabs: 425 | button.button_pressed = false 426 | 427 | tab_container.current_tab = tab_index 428 | selected_collection_name = collection_names[tab_index] 429 | selected_collection_index = tab_index 430 | 431 | if selected_collection_name == "" or selected_collection_name == " ": 432 | selected_collection = {} 433 | else: 434 | if selected_collection_name in items_by_collection: 435 | selected_collection = items_by_collection[selected_collection_name] 436 | else: 437 | selected_collection = {} 438 | printerr("Missing collection folder: " + selected_collection_name) 439 | 440 | func on_item_icon_clicked(_button_name: String) -> void: 441 | if !update_world_3d(): 442 | return 443 | 444 | if placement_mode_enabled: 445 | end_placement_mode() 446 | elif selected_item_name != _button_name: 447 | select_item(selected_collection_name, _button_name) 448 | 449 | func set_parent_node(node_path: NodePath) -> void: 450 | if not scene_root: 451 | return 452 | 453 | # If no path provided, set to scene root 454 | if node_path.is_empty() or str(node_path) == "": 455 | selected_parent_node = scene_root 456 | if scene_root: 457 | var node_name := scene_root.get_class().split(".")[-1] 458 | var node_icon := get_editor_interface().get_base_control().get_theme_icon(node_name, "EditorIcons") 459 | 460 | if node_icon == get_editor_interface().get_base_control().get_theme_icon("invalid icon", "EditorIcons"): 461 | node_icon = get_editor_interface().get_base_control().get_theme_icon("Node", "EditorIcons") 462 | 463 | btn_parent_node_selector.set_node_info(scene_root, node_icon) 464 | else: 465 | btn_parent_node_selector.set_node_info(null, null) 466 | return 467 | 468 | selected_parent_node = scene_root.get_node(node_path) 469 | if not selected_parent_node: 470 | # Fall back to scene root if path not found 471 | selected_parent_node = scene_root 472 | btn_parent_node_selector.set_node_info(scene_root, get_editor_interface().get_base_control().get_theme_icon("Node", "EditorIcons")) 473 | printerr("[SceneBuilderDock] ", node_path, " not found in scene, defaulting to scene root") 474 | return 475 | 476 | var node_name := selected_parent_node.get_class().split(".")[-1] 477 | var node_icon := get_editor_interface().get_base_control().get_theme_icon(node_name, "EditorIcons") 478 | 479 | # if there's an invalid icon, we use the default node icon 480 | if node_icon == get_editor_interface().get_base_control().get_theme_icon("invalid icon", "EditorIcons"): 481 | node_icon = get_editor_interface().get_base_control().get_theme_icon("Node", "EditorIcons") 482 | 483 | btn_parent_node_selector.set_node_info(selected_parent_node, node_icon) 484 | print("[SceneBuilderDock] Parent node set to ", selected_parent_node.name) 485 | 486 | func reload_all_items() -> void: 487 | print("[SceneBuilderDock] Freeing all texture buttons") 488 | for i in range(1, num_collections + 1): 489 | var grid_container: GridContainer = tab_container.get_node("%s/Grid" % i) 490 | for _node in grid_container.get_children(): 491 | _node.queue_free() 492 | 493 | refresh_collection_names() 494 | 495 | print("[SceneBuilderDock] Loading all items from all collections") 496 | var i = 0 497 | for collection_name in collection_names: 498 | i += 1 499 | 500 | if collection_name != "" and DirAccess.dir_exists_absolute(config.root_dir + "/%s" % collection_name): 501 | print("[SceneBuilderDock] Populating grid with icons") 502 | 503 | var grid_container: GridContainer = tab_container.get_node("%s/Grid" % i) 504 | 505 | load_items_from_collection_folder_on_disk(collection_name) 506 | 507 | item_highlighters_by_collection[collection_name] = {} 508 | 509 | for key: String in ordered_keys_by_collection[collection_name]: 510 | var item: SceneBuilderItem = items_by_collection[collection_name][key] 511 | var texture_button: TextureButton = TextureButton.new() 512 | texture_button.toggle_mode = true 513 | texture_button.texture_normal = item.texture 514 | texture_button.tooltip_text = item.item_name 515 | texture_button.ignore_texture_size = true 516 | texture_button.stretch_mode = TextureButton.STRETCH_SCALE 517 | texture_button.custom_minimum_size = Vector2(80, 80) 518 | texture_button.pressed.connect(on_item_icon_clicked.bind(item.item_name)) 519 | grid_container.add_child(texture_button) 520 | 521 | var nine_patch: NinePatchRect = NinePatchRect.new() 522 | nine_patch.texture = CanvasTexture.new() 523 | nine_patch.draw_center = false 524 | nine_patch.set_anchors_preset(Control.PRESET_FULL_RECT) 525 | nine_patch.patch_margin_left = 4 526 | nine_patch.patch_margin_top = 4 527 | nine_patch.patch_margin_right = 4 528 | nine_patch.patch_margin_bottom = 4 529 | nine_patch.self_modulate = Color("000000") # black # 6a9d2e green 530 | item_highlighters_by_collection[collection_name][key] = nine_patch 531 | texture_button.add_child(nine_patch) 532 | 533 | select_collection(0) 534 | 535 | # Info blurb 536 | var _num_populated_collections: int = 0 537 | for colletion_name in collection_names: 538 | if colletion_name != "": 539 | _num_populated_collections += 1 540 | var _total_items: int = 0 541 | for key: String in items_by_collection.keys(): 542 | _total_items += items_by_collection[key].size() 543 | 544 | print("[SceneBuilderDock] Ready with %s collections and %s items" % [str(_num_populated_collections), str(_total_items)]) 545 | 546 | func update_world_3d() -> bool: 547 | var new_scene_root = EditorInterface.get_edited_scene_root() 548 | if new_scene_root != null and new_scene_root is Node3D: 549 | if scene_root == new_scene_root: 550 | return true 551 | end_placement_mode() 552 | scene_root = new_scene_root 553 | viewport = EditorInterface.get_editor_viewport_3d() 554 | world3d = viewport.find_world_3d() 555 | physics_space = world3d.direct_space_state 556 | camera = viewport.get_camera_3d() 557 | set_parent_node(NodePath()) 558 | return true 559 | else: 560 | print("[SceneBuilderDock] Failed to update world 3d") 561 | end_placement_mode() 562 | scene_root = null 563 | viewport = null 564 | world3d = null 565 | physics_space = null 566 | camera = null 567 | set_parent_node(NodePath()) 568 | return false 569 | 570 | # ---- Helpers ----------------------------------------------------------------- 571 | 572 | func align_up(node_basis, normal) -> Basis: 573 | var result: Basis = Basis() 574 | var scale: Vector3 = node_basis.get_scale() 575 | var orientation: String = btn_group_surface_orientation.get_pressed_button().name 576 | 577 | var arbitrary_vector: Vector3 = Vector3(1, 0, 0) if abs(normal.dot(Vector3(1, 0, 0))) < 0.999 else Vector3(0, 1, 0) 578 | var cross1: Vector3 579 | var cross2: Vector3 580 | 581 | match orientation: 582 | "X": 583 | cross1 = normal.cross(node_basis.y).normalized() 584 | if cross1.length_squared() < 0.001: 585 | cross1 = normal.cross(arbitrary_vector).normalized() 586 | cross2 = cross1.cross(normal).normalized() 587 | result = Basis(normal, cross2, cross1) 588 | "Y": 589 | cross1 = normal.cross(node_basis.z).normalized() 590 | if cross1.length_squared() < 0.001: 591 | cross1 = normal.cross(arbitrary_vector).normalized() 592 | cross2 = cross1.cross(normal).normalized() 593 | result = Basis(cross1, normal, cross2) 594 | "Z": 595 | arbitrary_vector = Vector3(0, 0, 1) if abs(normal.dot(Vector3(0, 0, -1))) < 0.99 else Vector3(-1, 0, 0) 596 | cross1 = normal.cross(node_basis.x).normalized() 597 | if cross1.length_squared() < 0.001: 598 | cross1 = normal.cross(arbitrary_vector).normalized() 599 | cross2 = cross1.cross(normal).normalized() 600 | result = Basis(cross2, cross1, normal) 601 | 602 | result = result.orthonormalized() 603 | result.x *= scale.x 604 | result.y *= scale.y 605 | result.z *= scale.z 606 | 607 | return result 608 | 609 | func clear_preview_instance() -> void: 610 | preview_instance = null 611 | preview_instance_rid_array = [] 612 | 613 | if scene_root != null: 614 | scene_builder_temp = scene_root.get_node_or_null("SceneBuilderTemp") 615 | if scene_builder_temp: 616 | for child in scene_builder_temp.get_children(): 617 | child.queue_free() 618 | 619 | func create_preview_instance() -> void: 620 | if scene_root == null: 621 | printerr("[SceneBuilderDock] scene_root is null inside create_preview_item_instance") 622 | return 623 | 624 | clear_preview_instance() 625 | 626 | scene_builder_temp = scene_root.get_node_or_null("SceneBuilderTemp") 627 | if not scene_builder_temp: 628 | scene_builder_temp = Node.new() 629 | scene_builder_temp.name = "SceneBuilderTemp" 630 | scene_root.add_child(scene_builder_temp) 631 | scene_builder_temp.owner = scene_root 632 | 633 | preview_instance = get_instance_from_path(selected_item.uid) 634 | scene_builder_temp.add_child(preview_instance) 635 | preview_instance.owner = scene_root 636 | 637 | reroll_preview_instance_transform() 638 | 639 | # Instantiating a node automatically selects it, which is annoying. 640 | # Let's re-select scene_root instead, 641 | EditorInterface.get_selection().clear() 642 | EditorInterface.get_selection().add_node(scene_root) 643 | 644 | func end_placement_mode() -> void: 645 | clear_preview_instance() 646 | end_transform_mode() 647 | 648 | placement_mode_enabled = false 649 | 650 | if selected_item_name != "": 651 | if item_highlighters_by_collection.has(selected_collection_name): 652 | var item_highlighters: Dictionary = item_highlighters_by_collection[selected_collection_name] 653 | if item_highlighters.has(selected_item_name): 654 | var selected_nine_path: NinePatchRect = item_highlighters[selected_item_name] 655 | if selected_nine_path: 656 | selected_nine_path.self_modulate = Color.BLACK 657 | else: 658 | print("[SceneBuilderDock] NinePatchRect is null for selected_item_name: ", selected_item_name) 659 | else: 660 | print("[SceneBuilderDock] Key missing from highlighter collection, key: ", selected_item_name, ", from collection: ", selected_collection_name) 661 | else: 662 | print("[SceneBuilderDock] Highlighter collection missing for collection: ", selected_collection_name) 663 | 664 | selected_item = null 665 | selected_item_name = "" 666 | 667 | func end_transform_mode() -> void: 668 | current_transform_mode = TransformMode.NONE 669 | reset_indicators() 670 | 671 | func load_items_from_collection_folder_on_disk(_collection_name: String): 672 | print("[SceneBuilderDock] Collecting items from collection folder") 673 | 674 | var items = {} 675 | var ordered_item_keys = [] 676 | 677 | var dir = DirAccess.open(config.root_dir + _collection_name) 678 | if dir: 679 | dir.list_dir_begin() 680 | var item_filename = dir.get_next() 681 | while item_filename != "": 682 | var item_path = config.root_dir + _collection_name + "/" + item_filename 683 | var resource = ResourceLoader.load(item_path, "Resource", 0) 684 | if resource and resource is SceneBuilderItem: 685 | var scene_builder_item: SceneBuilderItem = resource 686 | 687 | print("[SceneBuilderDock] Loaded item: ", item_filename) 688 | 689 | items[scene_builder_item.item_name] = scene_builder_item 690 | ordered_item_keys.append(scene_builder_item.item_name) 691 | else: 692 | print("[SceneBuilderDock] The resource is not a SceneBuilderItem or failed to load, item_path: ", item_path) 693 | 694 | item_filename = dir.get_next() 695 | 696 | dir.list_dir_end() 697 | 698 | items_by_collection[_collection_name] = items 699 | ordered_keys_by_collection[_collection_name] = ordered_item_keys 700 | 701 | func get_all_node_names(_node) -> Array[String]: 702 | var _all_node_names = [] 703 | for _child in _node.get_children(): 704 | _all_node_names.append(_child.name) 705 | if _child.get_child_count() > 0: 706 | var _result = get_all_node_names(_child) 707 | for _item in _result: 708 | _all_node_names.append(_item) 709 | return _all_node_names 710 | 711 | func instantiate_selected_item_at_position() -> void: 712 | if preview_instance == null or selected_item == null: 713 | printerr("[SceneBuilderDock] Preview instance or selected item is null") 714 | return 715 | 716 | populate_preview_instance_rid_array(preview_instance) 717 | var result = perform_raycast_with_exclusion(preview_instance_rid_array) 718 | 719 | if result and result.collider: 720 | var instance = get_instance_from_path(selected_item.uid) 721 | if selected_parent_node: 722 | selected_parent_node.add_child(instance) 723 | else: 724 | scene_root.add_child(instance) 725 | instance.owner = scene_root 726 | initialize_node_name(instance, selected_item.item_name) 727 | 728 | var new_position: Vector3 = result.position 729 | if selected_item.use_random_vertical_offset: 730 | new_position.y += random_offset_y 731 | 732 | instance.global_transform.origin = new_position 733 | instance.position += Vector3(pos_offset_x, pos_offset_y, pos_offset_z) 734 | print("[SceneBuilderDock] pos_offset_y: ", pos_offset_y) 735 | instance.basis = preview_instance.basis 736 | 737 | undo_redo.create_action("Instantiate selected item") 738 | undo_redo.add_undo_method(scene_root, "remove_child", instance) 739 | undo_redo.add_do_reference(instance) 740 | undo_redo.commit_action() 741 | 742 | else: 743 | print("[SceneBuilderDock] Raycast missed, items must be instantiated on a StaticBody with a CollisionShape") 744 | 745 | func initialize_node_name(node: Node3D, new_name: String) -> void: 746 | var all_names = toolbox.get_all_node_names(scene_root) 747 | node.name = toolbox.increment_name_until_unique(new_name, all_names) 748 | 749 | func perform_raycast_with_exclusion(exclude_rids: Array = []) -> Dictionary: 750 | if viewport == null: 751 | update_world_3d() 752 | if viewport == null: 753 | print("[SceneBuilderDock] The editor's root scene must be of type Node3D, deselecting item") 754 | end_placement_mode() 755 | return {} 756 | 757 | var mouse_pos = viewport.get_mouse_position() 758 | var origin = camera.project_ray_origin(mouse_pos) 759 | var end = origin + camera.project_ray_normal(mouse_pos) * 1000 760 | var query = PhysicsRayQueryParameters3D.new() 761 | query.from = origin 762 | query.to = end 763 | query.exclude = exclude_rids 764 | return physics_space.intersect_ray(query) 765 | 766 | ## This function prevents us from trying to raycast against our preview item. 767 | func populate_preview_instance_rid_array(instance: Node) -> void: 768 | if instance is PhysicsBody3D: 769 | preview_instance_rid_array.append(instance.get_rid()) 770 | 771 | for child in instance.get_children(): 772 | populate_preview_instance_rid_array(child) 773 | 774 | func refresh_collection_names() -> void: 775 | print("[SceneBuilderDock] Refreshing collection names") 776 | 777 | if !DirAccess.dir_exists_absolute(config.root_dir): 778 | DirAccess.make_dir_recursive_absolute(config.root_dir) 779 | print("[SceneBuilderDock] Creating a new data folder: ", config.root_dir) 780 | 781 | if !ResourceLoader.exists(path_to_collection_names): 782 | var _collection_names: CollectionNames = CollectionNames.new() 783 | print("[SceneBuilderDock] path_to_collection_names: ", path_to_collection_names) 784 | var save_result = ResourceSaver.save(_collection_names, path_to_collection_names) 785 | print("[SceneBuilderDock] A CollectionNames resource has been created at location: ", path_to_collection_names) 786 | 787 | if save_result != OK: 788 | printerr("[SceneBuilderDock] We were unable to create a CollectionNames resource at location: ", path_to_collection_names) 789 | collection_names = Array() 790 | collection_names.resize(24) 791 | collection_names.fill("") 792 | 793 | return 794 | 795 | var _names: CollectionNames = load(path_to_collection_names) 796 | if _names != null: 797 | var new_collection_names: Array[String] = [] 798 | var new_collection_font_colors: Array[Color] = [] 799 | 800 | # Row 1 801 | new_collection_names.append(_names.name_01) 802 | new_collection_names.append(_names.name_02) 803 | new_collection_names.append(_names.name_03) 804 | new_collection_names.append(_names.name_04) 805 | new_collection_names.append(_names.name_05) 806 | new_collection_names.append(_names.name_06) 807 | new_collection_font_colors.append(_names.font_color_01) 808 | new_collection_font_colors.append(_names.font_color_02) 809 | new_collection_font_colors.append(_names.font_color_03) 810 | new_collection_font_colors.append(_names.font_color_04) 811 | new_collection_font_colors.append(_names.font_color_05) 812 | new_collection_font_colors.append(_names.font_color_06) 813 | 814 | # Row 2 815 | new_collection_names.append(_names.name_07) 816 | new_collection_names.append(_names.name_08) 817 | new_collection_names.append(_names.name_09) 818 | new_collection_names.append(_names.name_10) 819 | new_collection_names.append(_names.name_11) 820 | new_collection_names.append(_names.name_12) 821 | new_collection_font_colors.append(_names.font_color_07) 822 | new_collection_font_colors.append(_names.font_color_08) 823 | new_collection_font_colors.append(_names.font_color_09) 824 | new_collection_font_colors.append(_names.font_color_10) 825 | new_collection_font_colors.append(_names.font_color_11) 826 | new_collection_font_colors.append(_names.font_color_12) 827 | 828 | # Row 3 829 | new_collection_names.append(_names.name_13) 830 | new_collection_names.append(_names.name_14) 831 | new_collection_names.append(_names.name_15) 832 | new_collection_names.append(_names.name_16) 833 | new_collection_names.append(_names.name_17) 834 | new_collection_names.append(_names.name_18) 835 | new_collection_font_colors.append(_names.font_color_13) 836 | new_collection_font_colors.append(_names.font_color_14) 837 | new_collection_font_colors.append(_names.font_color_15) 838 | new_collection_font_colors.append(_names.font_color_16) 839 | new_collection_font_colors.append(_names.font_color_17) 840 | new_collection_font_colors.append(_names.font_color_18) 841 | 842 | # Row 4 843 | new_collection_names.append(_names.name_19) 844 | new_collection_names.append(_names.name_20) 845 | new_collection_names.append(_names.name_21) 846 | new_collection_names.append(_names.name_22) 847 | new_collection_names.append(_names.name_23) 848 | new_collection_names.append(_names.name_24) 849 | new_collection_font_colors.append(_names.font_color_19) 850 | new_collection_font_colors.append(_names.font_color_20) 851 | new_collection_font_colors.append(_names.font_color_21) 852 | new_collection_font_colors.append(_names.font_color_22) 853 | new_collection_font_colors.append(_names.font_color_23) 854 | new_collection_font_colors.append(_names.font_color_24) 855 | 856 | # Validate 857 | for _name in new_collection_names: 858 | if _name != "": 859 | var dir = DirAccess.open(config.root_dir + _name) 860 | if dir: 861 | dir.list_dir_begin() 862 | var item = dir.get_next() 863 | if item != "": 864 | print("[SceneBuilderDock] Collection directory is present and contains items: " + _name) 865 | else: 866 | printerr("[SceneBuilderDock] Directory exists, but contains no items: " + _name) 867 | dir.list_dir_end() 868 | else: 869 | printerr("[SceneBuilderDock] Collection directory does not exist: " + _name) 870 | collection_names = new_collection_names 871 | 872 | for i in range(num_collections): 873 | var collection_name = collection_names[i] 874 | if collection_name == "": 875 | collection_name = " " 876 | btns_collection_tabs[i].text = collection_name 877 | btns_collection_tabs[i].add_theme_color_override("font_color", new_collection_font_colors[i]) 878 | 879 | else: 880 | printerr("[SceneBuilderDock] An unknown file exists at location %s. A resource of type CollectionNames should exist here.".format(path_to_collection_names)) 881 | collection_names = Array() 882 | collection_names.resize(24) 883 | collection_names.fill("") 884 | 885 | #endregion 886 | 887 | # ---- Shortcut ---------------------------------------------------------------- 888 | 889 | func place_fence(): 890 | var selection: EditorSelection = EditorInterface.get_selection() 891 | var selected_nodes: Array[Node] = selection.get_selected_nodes() 892 | 893 | if scene_root == null: 894 | print("[SceneBuilderDock] Scene root is null") 895 | return 896 | 897 | if selected_nodes.size() != 1: 898 | printerr("[SceneBuilderDock] Exactly one node sould be selected in the scene") 899 | return 900 | 901 | if not selected_nodes[0] is Path3D: 902 | printerr("[SceneBuilderDock] The selected node should be of type Node3D") 903 | return 904 | 905 | undo_redo.create_action("Make a fence") 906 | 907 | var path: Path3D = selected_nodes[0] 908 | 909 | var fence_piece_names: Array = ordered_keys_by_collection[selected_collection_name] 910 | var path_length: float = path.curve.get_baked_length() 911 | 912 | for distance in range(0, path_length, spinbox_separation_distance.value): 913 | 914 | var transform: Transform3D = path.curve.sample_baked_with_rotation(distance) 915 | var position: Vector3 = transform.origin 916 | var basis: Basis = transform.basis.rotated(Vector3(0, 1, 0), deg_to_rad(spinbox_y_offset.value)) 917 | 918 | var chosen_piece_name: String = fence_piece_names[randi() % fence_piece_names.size()] 919 | var chosen_piece = items_by_collection[selected_collection_name][chosen_piece_name] 920 | var instance = get_instance_from_path(chosen_piece.scene_path) 921 | 922 | undo_redo.add_do_method(scene_root, "add_child", instance) 923 | undo_redo.add_do_method(instance, "set_owner", scene_root) 924 | undo_redo.add_do_method(instance, "set_global_transform", Transform3D( 925 | basis.rotated(Vector3(1, 0, 0), randf() * deg_to_rad(spinbox_jitter_x.value)) 926 | .rotated(Vector3(0, 1, 0), randf() * deg_to_rad(spinbox_jitter_y.value)) 927 | .rotated(Vector3(0, 0, 1), randf() * deg_to_rad(spinbox_jitter_z.value)), 928 | position 929 | )) 930 | 931 | undo_redo.add_undo_method(scene_root, "remove_child", instance) 932 | 933 | print("[SceneBuilderDock] Commiting action") 934 | undo_redo.commit_action() 935 | 936 | func reroll_preview_instance_transform() -> void: 937 | if preview_instance == null: 938 | printerr("[SceneBuilderDock] preview_instance is null inside reroll_preview_instance_transform()") 939 | return 940 | 941 | random_offset_y = rng.randf_range(selected_item.random_offset_y_min, selected_item.random_offset_y_max) 942 | 943 | if selected_item.use_random_scale: 944 | var random_scale: float = rng.randf_range(selected_item.random_scale_min, selected_item.random_scale_max) 945 | original_preview_scale = Vector3(random_scale, random_scale, random_scale) 946 | else: 947 | original_preview_scale = Vector3(1, 1, 1) 948 | 949 | preview_instance.scale = original_preview_scale 950 | 951 | if selected_item.use_random_rotation: 952 | var x_rot: float = rng.randf_range(0, selected_item.random_rot_x) 953 | var y_rot: float = rng.randf_range(0, selected_item.random_rot_y) 954 | var z_rot: float = rng.randf_range(0, selected_item.random_rot_z) 955 | preview_instance.rotation = Vector3(deg_to_rad(x_rot), deg_to_rad(y_rot), deg_to_rad(z_rot)) 956 | original_preview_basis = preview_instance.basis 957 | else: 958 | preview_instance.rotation = Vector3(0, 0, 0) 959 | original_preview_basis = preview_instance.basis 960 | 961 | original_preview_basis = preview_instance.basis 962 | 963 | pos_offset_x = 0 964 | pos_offset_y = 0 965 | pos_offset_z = 0 966 | 967 | func select_item(collection_name: String, item_name: String) -> void: 968 | end_placement_mode() 969 | var nine_path: NinePatchRect = item_highlighters_by_collection[collection_name][item_name] 970 | nine_path.self_modulate = Color.GREEN 971 | selected_item_name = item_name 972 | selected_item = selected_collection[selected_item_name] 973 | placement_mode_enabled = true; 974 | create_preview_instance() 975 | 976 | func select_first_item() -> void: 977 | if (!ordered_keys_by_collection.has(selected_collection_name)): 978 | printerr("[SceneBuilderDock] Trying to select the first item, but the selected collection name does not exist: ", selected_collection_name) 979 | return 980 | var keys: Array = ordered_keys_by_collection[selected_collection_name] 981 | if keys.is_empty(): 982 | printerr("[SceneBuilderDock] Trying to select the first item, but there are no items to select in this collection: ", selected_collection_name) 983 | return 984 | var _first_item: String = ordered_keys_by_collection[selected_collection_name][0] 985 | print(_first_item) 986 | select_item(selected_collection_name, _first_item) 987 | 988 | func select_next_collection() -> void: 989 | end_placement_mode() 990 | for idx in range(selected_collection_index + 1, selected_collection_index + num_collections + 1): 991 | var next_idx = idx % num_collections 992 | if is_collection_populated(next_idx): 993 | select_collection(next_idx) 994 | select_first_item() 995 | break 996 | else: 997 | print("[SceneBuilderDock] Collection is not populated: ", collection_names[next_idx]) 998 | 999 | func select_next_item() -> void: 1000 | var ordered_keys: Array = ordered_keys_by_collection[selected_collection_name] 1001 | var idx = ordered_keys.find(selected_item_name) 1002 | if idx >= 0: 1003 | var next_idx = (idx + 1) % ordered_keys.size() 1004 | var next_name = ordered_keys[next_idx] 1005 | select_item(selected_collection_name, next_name) 1006 | else: 1007 | printerr("[SceneBuilderDock] Next item not found? Current index: ", idx) 1008 | 1009 | func select_previous_item() -> void: 1010 | var ordered_keys: Array = ordered_keys_by_collection[selected_collection_name] 1011 | var idx = ordered_keys.find(selected_item_name) 1012 | if idx >= 0: 1013 | select_item(selected_collection_name, ordered_keys[(idx - 1) % ordered_keys.size()]) 1014 | else: 1015 | printerr("[SceneBuilderDock] Previous item not found") 1016 | 1017 | func select_previous_collection() -> void: 1018 | end_placement_mode() 1019 | for idx in range(selected_collection_index - 1, selected_collection_index - num_collections - 1, -1): 1020 | var prev_idx = (idx + num_collections) % num_collections 1021 | if is_collection_populated(prev_idx): 1022 | select_collection(prev_idx) 1023 | select_first_item() 1024 | break 1025 | else: 1026 | print("[SceneBuilderDock] Collection is not populated: ", collection_names[prev_idx]) 1027 | 1028 | func start_transform_mode(mode: TransformMode) -> void: 1029 | original_mouse_position = viewport.get_mouse_position() 1030 | current_transform_mode = mode 1031 | 1032 | match mode: 1033 | TransformMode.POSITION_X, TransformMode.POSITION_Y, TransformMode.POSITION_Z: 1034 | original_preview_position = preview_instance.position 1035 | TransformMode.ROTATION_X, TransformMode.ROTATION_Y, TransformMode.ROTATION_Z: 1036 | original_preview_basis = preview_instance.basis 1037 | TransformMode.SCALE: 1038 | original_preview_scale = preview_instance.scale 1039 | 1040 | reset_indicators() 1041 | match mode: 1042 | TransformMode.POSITION_X, TransformMode.ROTATION_X: 1043 | lbl_indicator_x.self_modulate = Color.GREEN 1044 | TransformMode.POSITION_Y, TransformMode.ROTATION_Y: 1045 | lbl_indicator_y.self_modulate = Color.GREEN 1046 | TransformMode.POSITION_Z, TransformMode.ROTATION_Z: 1047 | lbl_indicator_z.self_modulate = Color.GREEN 1048 | TransformMode.SCALE: 1049 | lbl_indicator_scale.self_modulate = Color.GREEN 1050 | 1051 | func get_icon(collection_name: String, item_name: String) -> Texture: 1052 | var icon_path: String = "res://Data/SceneBuilderCollections/%s/Thumbnail/%s.png" % [collection_name, item_name] 1053 | var tex: Texture = load(icon_path) as Texture 1054 | if tex == null: 1055 | printerr("[SceneBuilderDock] Icon not found: ", icon_path) 1056 | return null 1057 | return tex 1058 | 1059 | func get_instance_from_path(_uid: String) -> Node3D: 1060 | var uid: int = ResourceUID.text_to_id(_uid) 1061 | 1062 | var path: String = "" 1063 | if ResourceUID.has_id(uid): 1064 | path = ResourceUID.get_id_path(uid) 1065 | else: 1066 | printerr("[SceneBuilderDock] Does not have uid: ", ResourceUID.id_to_text(uid)) 1067 | return 1068 | 1069 | if ResourceLoader.exists(path): 1070 | var loaded = load(path) 1071 | if loaded is PackedScene: 1072 | var instance = loaded.instantiate() 1073 | if instance is Node3D: 1074 | return instance 1075 | else: 1076 | printerr("[SceneBuilderDock] The instantiated scene's root is not a Node3D: ", loaded.name) 1077 | else: 1078 | printerr("[SceneBuilderDock] Loaded resource is not a PackedScene: ", path) 1079 | else: 1080 | printerr("[SceneBuilderDock] Path does not exist: ", path) 1081 | return null 1082 | 1083 | # -- 1084 | 1085 | func update_config(_config: SceneBuilderConfig) -> void: 1086 | config = _config 1087 | 1088 | 1089 | func reset_indicators() -> void: 1090 | lbl_indicator_x.self_modulate = Color.WHITE 1091 | lbl_indicator_y.self_modulate = Color.WHITE 1092 | lbl_indicator_z.self_modulate = Color.WHITE 1093 | lbl_indicator_scale.self_modulate = Color.WHITE 1094 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_dock.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ctcx2pqfgnaxx 2 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_dock.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://18pjyynbqqci"] 2 | 3 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rn2d1"] 4 | bg_color = Color(0, 0, 0, 1) 5 | border_width_left = 8 6 | border_width_top = 8 7 | border_width_right = 8 8 | border_width_bottom = 8 9 | border_color = Color(0.211765, 0.239216, 0.290196, 1) 10 | border_blend = true 11 | corner_radius_top_left = 8 12 | corner_radius_top_right = 8 13 | corner_radius_bottom_right = 8 14 | corner_radius_bottom_left = 8 15 | 16 | [node name="Scene Builder" type="VBoxContainer"] 17 | custom_minimum_size = Vector2(352, 0) 18 | anchors_preset = 15 19 | anchor_right = 1.0 20 | anchor_bottom = 1.0 21 | offset_right = -800.0 22 | grow_horizontal = 2 23 | grow_vertical = 2 24 | 25 | [node name="Collection" type="VBoxContainer" parent="."] 26 | layout_mode = 2 27 | size_flags_vertical = 3 28 | 29 | [node name="Panel" type="PanelContainer" parent="Collection"] 30 | layout_mode = 2 31 | theme_override_styles/panel = SubResource("StyleBoxFlat_rn2d1") 32 | 33 | [node name="Grid" type="GridContainer" parent="Collection/Panel"] 34 | layout_mode = 2 35 | columns = 6 36 | 37 | [node name="1" type="Button" parent="Collection/Panel/Grid"] 38 | layout_mode = 2 39 | size_flags_horizontal = 3 40 | toggle_mode = true 41 | button_pressed = true 42 | text = "1" 43 | text_overrun_behavior = 1 44 | clip_text = true 45 | 46 | [node name="2" type="Button" parent="Collection/Panel/Grid"] 47 | layout_mode = 2 48 | size_flags_horizontal = 3 49 | toggle_mode = true 50 | text = "2" 51 | text_overrun_behavior = 1 52 | clip_text = true 53 | 54 | [node name="3" type="Button" parent="Collection/Panel/Grid"] 55 | layout_mode = 2 56 | size_flags_horizontal = 3 57 | toggle_mode = true 58 | text = "3" 59 | text_overrun_behavior = 1 60 | clip_text = true 61 | 62 | [node name="4" type="Button" parent="Collection/Panel/Grid"] 63 | layout_mode = 2 64 | size_flags_horizontal = 3 65 | toggle_mode = true 66 | text = "4" 67 | text_overrun_behavior = 1 68 | clip_text = true 69 | 70 | [node name="5" type="Button" parent="Collection/Panel/Grid"] 71 | layout_mode = 2 72 | size_flags_horizontal = 3 73 | toggle_mode = true 74 | text = "5" 75 | text_overrun_behavior = 1 76 | clip_text = true 77 | 78 | [node name="6" type="Button" parent="Collection/Panel/Grid"] 79 | layout_mode = 2 80 | size_flags_horizontal = 3 81 | toggle_mode = true 82 | text = "6" 83 | text_overrun_behavior = 1 84 | clip_text = true 85 | 86 | [node name="7" type="Button" parent="Collection/Panel/Grid"] 87 | layout_mode = 2 88 | size_flags_horizontal = 3 89 | toggle_mode = true 90 | text = "7" 91 | text_overrun_behavior = 1 92 | clip_text = true 93 | 94 | [node name="8" type="Button" parent="Collection/Panel/Grid"] 95 | layout_mode = 2 96 | size_flags_horizontal = 3 97 | toggle_mode = true 98 | text = "8" 99 | text_overrun_behavior = 1 100 | clip_text = true 101 | 102 | [node name="9" type="Button" parent="Collection/Panel/Grid"] 103 | layout_mode = 2 104 | size_flags_horizontal = 3 105 | toggle_mode = true 106 | text = "9" 107 | text_overrun_behavior = 1 108 | clip_text = true 109 | 110 | [node name="10" type="Button" parent="Collection/Panel/Grid"] 111 | layout_mode = 2 112 | size_flags_horizontal = 3 113 | toggle_mode = true 114 | text = "10" 115 | text_overrun_behavior = 1 116 | clip_text = true 117 | 118 | [node name="11" type="Button" parent="Collection/Panel/Grid"] 119 | layout_mode = 2 120 | size_flags_horizontal = 3 121 | toggle_mode = true 122 | text = "11" 123 | text_overrun_behavior = 1 124 | clip_text = true 125 | 126 | [node name="12" type="Button" parent="Collection/Panel/Grid"] 127 | layout_mode = 2 128 | size_flags_horizontal = 3 129 | toggle_mode = true 130 | text = "12" 131 | text_overrun_behavior = 1 132 | clip_text = true 133 | 134 | [node name="13" type="Button" parent="Collection/Panel/Grid"] 135 | layout_mode = 2 136 | size_flags_horizontal = 3 137 | toggle_mode = true 138 | text = "13" 139 | text_overrun_behavior = 1 140 | clip_text = true 141 | 142 | [node name="14" type="Button" parent="Collection/Panel/Grid"] 143 | layout_mode = 2 144 | size_flags_horizontal = 3 145 | toggle_mode = true 146 | text = "14" 147 | text_overrun_behavior = 1 148 | clip_text = true 149 | 150 | [node name="15" type="Button" parent="Collection/Panel/Grid"] 151 | layout_mode = 2 152 | size_flags_horizontal = 3 153 | toggle_mode = true 154 | text = "15" 155 | text_overrun_behavior = 1 156 | clip_text = true 157 | 158 | [node name="16" type="Button" parent="Collection/Panel/Grid"] 159 | layout_mode = 2 160 | size_flags_horizontal = 3 161 | toggle_mode = true 162 | text = "16" 163 | text_overrun_behavior = 1 164 | clip_text = true 165 | 166 | [node name="17" type="Button" parent="Collection/Panel/Grid"] 167 | layout_mode = 2 168 | size_flags_horizontal = 3 169 | toggle_mode = true 170 | text = "17" 171 | text_overrun_behavior = 1 172 | clip_text = true 173 | 174 | [node name="18" type="Button" parent="Collection/Panel/Grid"] 175 | layout_mode = 2 176 | size_flags_horizontal = 3 177 | toggle_mode = true 178 | text = "18" 179 | text_overrun_behavior = 1 180 | clip_text = true 181 | 182 | [node name="19" type="Button" parent="Collection/Panel/Grid"] 183 | layout_mode = 2 184 | size_flags_horizontal = 3 185 | toggle_mode = true 186 | text = "19" 187 | text_overrun_behavior = 1 188 | clip_text = true 189 | 190 | [node name="20" type="Button" parent="Collection/Panel/Grid"] 191 | layout_mode = 2 192 | size_flags_horizontal = 3 193 | toggle_mode = true 194 | text = "20" 195 | text_overrun_behavior = 1 196 | clip_text = true 197 | 198 | [node name="21" type="Button" parent="Collection/Panel/Grid"] 199 | layout_mode = 2 200 | size_flags_horizontal = 3 201 | toggle_mode = true 202 | text = "21" 203 | text_overrun_behavior = 1 204 | clip_text = true 205 | 206 | [node name="22" type="Button" parent="Collection/Panel/Grid"] 207 | layout_mode = 2 208 | size_flags_horizontal = 3 209 | toggle_mode = true 210 | text = "22" 211 | text_overrun_behavior = 1 212 | clip_text = true 213 | 214 | [node name="23" type="Button" parent="Collection/Panel/Grid"] 215 | layout_mode = 2 216 | size_flags_horizontal = 3 217 | toggle_mode = true 218 | text = "23" 219 | text_overrun_behavior = 1 220 | clip_text = true 221 | 222 | [node name="24" type="Button" parent="Collection/Panel/Grid"] 223 | layout_mode = 2 224 | size_flags_horizontal = 3 225 | toggle_mode = true 226 | text = "24" 227 | text_overrun_behavior = 1 228 | clip_text = true 229 | 230 | [node name="HSeparator3" type="HSeparator" parent="Collection"] 231 | layout_mode = 2 232 | 233 | [node name="TabContainer" type="TabContainer" parent="Collection"] 234 | layout_mode = 2 235 | size_flags_vertical = 3 236 | current_tab = 0 237 | tabs_visible = false 238 | 239 | [node name="1" type="ScrollContainer" parent="Collection/TabContainer"] 240 | layout_mode = 2 241 | size_flags_vertical = 3 242 | horizontal_scroll_mode = 0 243 | metadata/_tab_index = 0 244 | 245 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/1"] 246 | layout_mode = 2 247 | size_flags_horizontal = 3 248 | columns = 4 249 | 250 | [node name="2" type="ScrollContainer" parent="Collection/TabContainer"] 251 | visible = false 252 | layout_mode = 2 253 | size_flags_vertical = 3 254 | horizontal_scroll_mode = 0 255 | metadata/_tab_index = 1 256 | 257 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/2"] 258 | layout_mode = 2 259 | size_flags_horizontal = 3 260 | columns = 4 261 | 262 | [node name="3" type="ScrollContainer" parent="Collection/TabContainer"] 263 | visible = false 264 | layout_mode = 2 265 | size_flags_vertical = 3 266 | horizontal_scroll_mode = 0 267 | metadata/_tab_index = 2 268 | 269 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/3"] 270 | custom_minimum_size = Vector2(32, 0) 271 | layout_mode = 2 272 | size_flags_horizontal = 3 273 | columns = 4 274 | 275 | [node name="4" type="ScrollContainer" parent="Collection/TabContainer"] 276 | visible = false 277 | layout_mode = 2 278 | size_flags_vertical = 3 279 | horizontal_scroll_mode = 0 280 | metadata/_tab_index = 3 281 | 282 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/4"] 283 | custom_minimum_size = Vector2(32, 0) 284 | layout_mode = 2 285 | size_flags_horizontal = 3 286 | columns = 4 287 | 288 | [node name="5" type="ScrollContainer" parent="Collection/TabContainer"] 289 | visible = false 290 | layout_mode = 2 291 | size_flags_vertical = 3 292 | horizontal_scroll_mode = 0 293 | metadata/_tab_index = 4 294 | 295 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/5"] 296 | custom_minimum_size = Vector2(32, 0) 297 | layout_mode = 2 298 | size_flags_horizontal = 3 299 | columns = 4 300 | 301 | [node name="6" type="ScrollContainer" parent="Collection/TabContainer"] 302 | visible = false 303 | layout_mode = 2 304 | size_flags_vertical = 3 305 | horizontal_scroll_mode = 0 306 | metadata/_tab_index = 5 307 | 308 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/6"] 309 | custom_minimum_size = Vector2(32, 0) 310 | layout_mode = 2 311 | size_flags_horizontal = 3 312 | columns = 4 313 | 314 | [node name="7" type="ScrollContainer" parent="Collection/TabContainer"] 315 | visible = false 316 | layout_mode = 2 317 | size_flags_vertical = 3 318 | horizontal_scroll_mode = 0 319 | metadata/_tab_index = 6 320 | 321 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/7"] 322 | custom_minimum_size = Vector2(32, 0) 323 | layout_mode = 2 324 | size_flags_horizontal = 3 325 | columns = 4 326 | 327 | [node name="8" type="ScrollContainer" parent="Collection/TabContainer"] 328 | visible = false 329 | layout_mode = 2 330 | size_flags_vertical = 3 331 | horizontal_scroll_mode = 0 332 | metadata/_tab_index = 7 333 | 334 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/8"] 335 | custom_minimum_size = Vector2(32, 0) 336 | layout_mode = 2 337 | size_flags_horizontal = 3 338 | columns = 4 339 | 340 | [node name="9" type="ScrollContainer" parent="Collection/TabContainer"] 341 | visible = false 342 | layout_mode = 2 343 | size_flags_vertical = 3 344 | horizontal_scroll_mode = 0 345 | metadata/_tab_index = 8 346 | 347 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/9"] 348 | custom_minimum_size = Vector2(32, 0) 349 | layout_mode = 2 350 | size_flags_horizontal = 3 351 | columns = 4 352 | 353 | [node name="10" type="ScrollContainer" parent="Collection/TabContainer"] 354 | visible = false 355 | layout_mode = 2 356 | size_flags_vertical = 3 357 | horizontal_scroll_mode = 0 358 | metadata/_tab_index = 9 359 | 360 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/10"] 361 | custom_minimum_size = Vector2(32, 0) 362 | layout_mode = 2 363 | size_flags_horizontal = 3 364 | columns = 4 365 | 366 | [node name="11" type="ScrollContainer" parent="Collection/TabContainer"] 367 | visible = false 368 | layout_mode = 2 369 | size_flags_vertical = 3 370 | horizontal_scroll_mode = 0 371 | metadata/_tab_index = 10 372 | 373 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/11"] 374 | custom_minimum_size = Vector2(32, 0) 375 | layout_mode = 2 376 | size_flags_horizontal = 3 377 | columns = 4 378 | 379 | [node name="12" type="ScrollContainer" parent="Collection/TabContainer"] 380 | visible = false 381 | layout_mode = 2 382 | size_flags_vertical = 3 383 | horizontal_scroll_mode = 0 384 | metadata/_tab_index = 11 385 | 386 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/12"] 387 | custom_minimum_size = Vector2(32, 0) 388 | layout_mode = 2 389 | size_flags_horizontal = 3 390 | columns = 4 391 | 392 | [node name="13" type="ScrollContainer" parent="Collection/TabContainer"] 393 | visible = false 394 | layout_mode = 2 395 | size_flags_vertical = 3 396 | horizontal_scroll_mode = 0 397 | metadata/_tab_index = 12 398 | 399 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/13"] 400 | custom_minimum_size = Vector2(32, 0) 401 | layout_mode = 2 402 | size_flags_horizontal = 3 403 | columns = 4 404 | 405 | [node name="14" type="ScrollContainer" parent="Collection/TabContainer"] 406 | visible = false 407 | layout_mode = 2 408 | size_flags_vertical = 3 409 | horizontal_scroll_mode = 0 410 | metadata/_tab_index = 13 411 | 412 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/14"] 413 | custom_minimum_size = Vector2(32, 0) 414 | layout_mode = 2 415 | size_flags_horizontal = 3 416 | columns = 4 417 | 418 | [node name="15" type="ScrollContainer" parent="Collection/TabContainer"] 419 | visible = false 420 | layout_mode = 2 421 | size_flags_vertical = 3 422 | horizontal_scroll_mode = 0 423 | metadata/_tab_index = 14 424 | 425 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/15"] 426 | custom_minimum_size = Vector2(32, 0) 427 | layout_mode = 2 428 | size_flags_horizontal = 3 429 | columns = 4 430 | 431 | [node name="16" type="ScrollContainer" parent="Collection/TabContainer"] 432 | visible = false 433 | layout_mode = 2 434 | size_flags_vertical = 3 435 | horizontal_scroll_mode = 0 436 | metadata/_tab_index = 15 437 | 438 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/16"] 439 | custom_minimum_size = Vector2(32, 0) 440 | layout_mode = 2 441 | size_flags_horizontal = 3 442 | columns = 4 443 | 444 | [node name="17" type="ScrollContainer" parent="Collection/TabContainer"] 445 | visible = false 446 | layout_mode = 2 447 | size_flags_vertical = 3 448 | horizontal_scroll_mode = 0 449 | metadata/_tab_index = 16 450 | 451 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/17"] 452 | custom_minimum_size = Vector2(32, 0) 453 | layout_mode = 2 454 | size_flags_horizontal = 3 455 | columns = 4 456 | 457 | [node name="18" type="ScrollContainer" parent="Collection/TabContainer"] 458 | visible = false 459 | layout_mode = 2 460 | size_flags_vertical = 3 461 | horizontal_scroll_mode = 0 462 | metadata/_tab_index = 17 463 | 464 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/18"] 465 | custom_minimum_size = Vector2(32, 0) 466 | layout_mode = 2 467 | size_flags_horizontal = 3 468 | columns = 4 469 | 470 | [node name="19" type="ScrollContainer" parent="Collection/TabContainer"] 471 | visible = false 472 | layout_mode = 2 473 | size_flags_vertical = 3 474 | horizontal_scroll_mode = 0 475 | metadata/_tab_index = 18 476 | 477 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/19"] 478 | custom_minimum_size = Vector2(32, 0) 479 | layout_mode = 2 480 | size_flags_horizontal = 3 481 | columns = 4 482 | 483 | [node name="20" type="ScrollContainer" parent="Collection/TabContainer"] 484 | visible = false 485 | layout_mode = 2 486 | size_flags_vertical = 3 487 | horizontal_scroll_mode = 0 488 | metadata/_tab_index = 19 489 | 490 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/20"] 491 | custom_minimum_size = Vector2(32, 0) 492 | layout_mode = 2 493 | size_flags_horizontal = 3 494 | columns = 4 495 | 496 | [node name="21" type="ScrollContainer" parent="Collection/TabContainer"] 497 | visible = false 498 | layout_mode = 2 499 | size_flags_vertical = 3 500 | horizontal_scroll_mode = 0 501 | metadata/_tab_index = 20 502 | 503 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/21"] 504 | custom_minimum_size = Vector2(32, 0) 505 | layout_mode = 2 506 | size_flags_horizontal = 3 507 | columns = 4 508 | 509 | [node name="22" type="ScrollContainer" parent="Collection/TabContainer"] 510 | visible = false 511 | layout_mode = 2 512 | size_flags_vertical = 3 513 | horizontal_scroll_mode = 0 514 | metadata/_tab_index = 21 515 | 516 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/22"] 517 | custom_minimum_size = Vector2(32, 0) 518 | layout_mode = 2 519 | size_flags_horizontal = 3 520 | columns = 4 521 | 522 | [node name="23" type="ScrollContainer" parent="Collection/TabContainer"] 523 | visible = false 524 | layout_mode = 2 525 | size_flags_vertical = 3 526 | horizontal_scroll_mode = 0 527 | metadata/_tab_index = 22 528 | 529 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/23"] 530 | custom_minimum_size = Vector2(32, 0) 531 | layout_mode = 2 532 | size_flags_horizontal = 3 533 | columns = 4 534 | 535 | [node name="24" type="ScrollContainer" parent="Collection/TabContainer"] 536 | visible = false 537 | layout_mode = 2 538 | size_flags_vertical = 3 539 | horizontal_scroll_mode = 0 540 | metadata/_tab_index = 23 541 | 542 | [node name="Grid" type="GridContainer" parent="Collection/TabContainer/24"] 543 | custom_minimum_size = Vector2(32, 0) 544 | layout_mode = 2 545 | size_flags_horizontal = 3 546 | columns = 4 547 | 548 | [node name="Settings" type="HBoxContainer" parent="."] 549 | layout_mode = 2 550 | 551 | [node name="Tab" type="TabContainer" parent="Settings"] 552 | custom_minimum_size = Vector2(320, 0) 553 | layout_mode = 2 554 | current_tab = 0 555 | 556 | [node name="Options" type="VBoxContainer" parent="Settings/Tab"] 557 | layout_mode = 2 558 | metadata/_tab_index = 0 559 | 560 | [node name="FirstRow" type="HBoxContainer" parent="Settings/Tab/Options"] 561 | layout_mode = 2 562 | 563 | [node name="Left" type="VBoxContainer" parent="Settings/Tab/Options/FirstRow"] 564 | layout_mode = 2 565 | 566 | [node name="UseSurfaceNormal" type="CheckButton" parent="Settings/Tab/Options/FirstRow/Left"] 567 | unique_name_in_owner = true 568 | layout_mode = 2 569 | size_flags_horizontal = 3 570 | text = "Surface normal" 571 | 572 | [node name="Orientation" type="VBoxContainer" parent="Settings/Tab/Options/FirstRow"] 573 | unique_name_in_owner = true 574 | layout_mode = 2 575 | size_flags_horizontal = 3 576 | 577 | [node name="Label" type="Label" parent="Settings/Tab/Options/FirstRow/Orientation"] 578 | layout_mode = 2 579 | size_flags_horizontal = 4 580 | text = "Orientation" 581 | vertical_alignment = 1 582 | 583 | [node name="ButtonGroup" type="HBoxContainer" parent="Settings/Tab/Options/FirstRow/Orientation"] 584 | layout_mode = 2 585 | 586 | [node name="X" type="CheckBox" parent="Settings/Tab/Options/FirstRow/Orientation/ButtonGroup"] 587 | layout_mode = 2 588 | size_flags_horizontal = 6 589 | text = "X" 590 | 591 | [node name="Y" type="CheckBox" parent="Settings/Tab/Options/FirstRow/Orientation/ButtonGroup"] 592 | layout_mode = 2 593 | size_flags_horizontal = 6 594 | button_pressed = true 595 | text = "Y" 596 | 597 | [node name="Z" type="CheckBox" parent="Settings/Tab/Options/FirstRow/Orientation/ButtonGroup"] 598 | layout_mode = 2 599 | size_flags_horizontal = 6 600 | text = "-Z" 601 | 602 | [node name="ParentNode" type="HBoxContainer" parent="Settings/Tab/Options"] 603 | layout_mode = 2 604 | 605 | [node name="EmptySpace" type="Control" parent="Settings/Tab/Options/ParentNode"] 606 | custom_minimum_size = Vector2(2, 0) 607 | layout_mode = 2 608 | 609 | [node name="Label" type="Label" parent="Settings/Tab/Options/ParentNode"] 610 | layout_mode = 2 611 | text = "Parent node" 612 | 613 | [node name="ParentNodeSelector" type="Button" parent="Settings/Tab/Options/ParentNode"] 614 | unique_name_in_owner = true 615 | custom_minimum_size = Vector2(100, 0) 616 | layout_mode = 2 617 | text = "(root)" 618 | text_overrun_behavior = 3 619 | 620 | [node name="HSeparator" type="HSeparator" parent="Settings/Tab/Options"] 621 | layout_mode = 2 622 | 623 | [node name="EmptySpace" type="Label" parent="Settings/Tab/Options"] 624 | visible = false 625 | layout_mode = 2 626 | size_flags_vertical = 3 627 | 628 | [node name="Bottom" type="HBoxContainer" parent="Settings/Tab/Options"] 629 | layout_mode = 2 630 | 631 | [node name="FindWorld3D" type="Button" parent="Settings/Tab/Options/Bottom"] 632 | layout_mode = 2 633 | size_flags_horizontal = 0 634 | size_flags_vertical = 2 635 | text = "Find world 3d" 636 | 637 | [node name="EmptySpace2" type="Label" parent="Settings/Tab/Options/Bottom"] 638 | layout_mode = 2 639 | size_flags_horizontal = 3 640 | 641 | [node name="ReloadAllItems" type="Button" parent="Settings/Tab/Options/Bottom"] 642 | layout_mode = 2 643 | size_flags_horizontal = 8 644 | text = "Reload all items" 645 | 646 | [node name="Path3D" type="VBoxContainer" parent="Settings/Tab"] 647 | visible = false 648 | layout_mode = 2 649 | metadata/_tab_index = 1 650 | 651 | [node name="Separation" type="HBoxContainer" parent="Settings/Tab/Path3D"] 652 | layout_mode = 2 653 | size_flags_horizontal = 5 654 | 655 | [node name="Label" type="Label" parent="Settings/Tab/Path3D/Separation"] 656 | layout_mode = 2 657 | text = "Separation Distance" 658 | 659 | [node name="SpinBox" type="SpinBox" parent="Settings/Tab/Path3D/Separation"] 660 | layout_mode = 2 661 | min_value = 0.25 662 | step = 0.25 663 | value = 1.0 664 | suffix = "m" 665 | 666 | [node name="Jitter" type="HBoxContainer" parent="Settings/Tab/Path3D"] 667 | layout_mode = 2 668 | size_flags_horizontal = 4 669 | 670 | [node name="Label" type="Label" parent="Settings/Tab/Path3D/Jitter"] 671 | layout_mode = 2 672 | text = "Jitter" 673 | 674 | [node name="X" type="SpinBox" parent="Settings/Tab/Path3D/Jitter"] 675 | layout_mode = 2 676 | max_value = 360.0 677 | suffix = "deg" 678 | 679 | [node name="Y" type="SpinBox" parent="Settings/Tab/Path3D/Jitter"] 680 | layout_mode = 2 681 | max_value = 360.0 682 | suffix = "deg" 683 | 684 | [node name="Z" type="SpinBox" parent="Settings/Tab/Path3D/Jitter"] 685 | layout_mode = 2 686 | max_value = 360.0 687 | suffix = "deg" 688 | 689 | [node name="YOffset" type="HBoxContainer" parent="Settings/Tab/Path3D"] 690 | layout_mode = 2 691 | size_flags_horizontal = 5 692 | 693 | [node name="Label" type="Label" parent="Settings/Tab/Path3D/YOffset"] 694 | layout_mode = 2 695 | text = "Y-Offset" 696 | 697 | [node name="Value" type="SpinBox" parent="Settings/Tab/Path3D/YOffset"] 698 | layout_mode = 2 699 | max_value = 360.0 700 | suffix = "deg" 701 | 702 | [node name="PlaceFence" type="Button" parent="Settings/Tab/Path3D"] 703 | layout_mode = 2 704 | text = "Place Fence" 705 | 706 | [node name="VSeparator" type="VSeparator" parent="Settings"] 707 | layout_mode = 2 708 | 709 | [node name="Indicators" type="VBoxContainer" parent="Settings"] 710 | layout_mode = 2 711 | size_flags_horizontal = 8 712 | 713 | [node name="1" type="Label" parent="Settings/Indicators"] 714 | layout_mode = 2 715 | size_flags_vertical = 6 716 | theme_override_font_sizes/font_size = 28 717 | text = "1" 718 | horizontal_alignment = 1 719 | vertical_alignment = 1 720 | 721 | [node name="2" type="Label" parent="Settings/Indicators"] 722 | layout_mode = 2 723 | size_flags_vertical = 6 724 | theme_override_font_sizes/font_size = 28 725 | text = "2" 726 | horizontal_alignment = 1 727 | vertical_alignment = 1 728 | 729 | [node name="3" type="Label" parent="Settings/Indicators"] 730 | layout_mode = 2 731 | size_flags_vertical = 6 732 | theme_override_font_sizes/font_size = 28 733 | text = "3" 734 | horizontal_alignment = 1 735 | vertical_alignment = 1 736 | 737 | [node name="4" type="Label" parent="Settings/Indicators"] 738 | layout_mode = 2 739 | size_flags_vertical = 6 740 | theme_override_font_sizes/font_size = 28 741 | text = "4" 742 | horizontal_alignment = 1 743 | vertical_alignment = 1 744 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_item.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | class_name SceneBuilderItem 3 | 4 | @export var collection_name: String = "Temporary" 5 | @export var item_name: String = "TempItemName" 6 | #@export var scene_path : String = "" 7 | 8 | @export var texture: Texture 9 | @export var uid: String 10 | 11 | # Boolean 12 | @export var use_random_vertical_offset: bool = false 13 | @export var use_random_rotation: bool = false 14 | @export var use_random_scale: bool = false 15 | 16 | # Position 17 | @export var random_offset_y_min: float = 0 18 | @export var random_offset_y_max: float = 0 19 | 20 | # Rotation 21 | @export var random_rot_x: float = 0 22 | @export var random_rot_z: float = 0 23 | @export var random_rot_y: float = 0 24 | 25 | # Scale 26 | @export var random_scale_min: float = 0.9 27 | @export var random_scale_max: float = 1.1 28 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_item.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cslitbypqo8au 2 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_node_path_selector.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Button 3 | class_name ParentNodeSelector 4 | 5 | signal path_selected(path: NodePath) 6 | 7 | func _ready() -> void: 8 | pressed.connect(_on_pressed) 9 | 10 | func _on_pressed() -> void: 11 | # pressing the button clears the selection 12 | path_selected.emit("") 13 | 14 | func _can_drop_data(_at_position: Vector2, data: Variant) -> bool: 15 | # can only drop if the data is a dictionary with a single node selected 16 | return ( 17 | (typeof(data) == TYPE_DICTIONARY and data.get("type") == "nodes" and data.get("nodes")) 18 | and data.get("nodes").size() == 1 19 | ) 20 | 21 | func _drop_data(_position: Vector2, data: Variant) -> void: 22 | path_selected.emit(data.get("nodes")[0]) 23 | 24 | func set_node_info(node: Node3D, node_icon: Texture2D): 25 | # Set the button text to the node name 26 | if node: 27 | text = node.name 28 | tooltip_text = node.name 29 | icon = node_icon 30 | else: 31 | text = "(root)" 32 | tooltip_text = "" 33 | icon = null 34 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_node_path_selector.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ciuy3glceywqd 2 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const BASE_PATHS = [ 5 | "res://addons/scene_builder/", 6 | "res://addons/scene_builder/addons/scene_builder/" 7 | ] 8 | 9 | var scene_builder_dock : SceneBuilderDock 10 | var scene_builder_commands : SceneBuilderCommands 11 | var scene_builder_config : SceneBuilderConfig 12 | 13 | func _enter_tree() -> void: 14 | var base_path = _find_valid_base_path() 15 | if not base_path: 16 | printerr("SceneBuilder addon files not found in expected locations.") 17 | return 18 | 19 | if not _load_or_create_components(base_path): 20 | return 21 | 22 | scene_builder_dock.update_config(scene_builder_config) 23 | scene_builder_commands.update_config(scene_builder_config) 24 | 25 | add_child(scene_builder_commands) 26 | add_child(scene_builder_dock) 27 | 28 | func _find_valid_base_path() -> String: 29 | for path in BASE_PATHS: 30 | if ResourceLoader.exists(path + "scene_builder_dock.gd") and \ 31 | ResourceLoader.exists(path + "scene_builder_commands.gd") and \ 32 | ResourceLoader.exists(path + "scene_builder_config.gd"): 33 | return path 34 | return "" 35 | 36 | func _load_or_create_components(base_path: String) -> bool: 37 | scene_builder_dock = load(base_path + "scene_builder_dock.gd").new() 38 | scene_builder_commands = load(base_path + "scene_builder_commands.gd").new() 39 | 40 | var config_path = base_path + "scene_builder_config.tres" 41 | var config_script = load(base_path + "scene_builder_config.gd") 42 | 43 | if ResourceLoader.exists(config_path): 44 | print("[SceneBuilderPlugin] Configuration file found") 45 | scene_builder_config = load(config_path) 46 | else: 47 | scene_builder_config = config_script.new() 48 | var err = ResourceSaver.save(scene_builder_config, config_path) 49 | print("[SceneBuilderPlugin] Configuration file not found, a new one has been saved to: ", config_path) 50 | if err != OK: 51 | printerr("[SceneBuilderPlugin] Failed to save new config file") 52 | return false 53 | return true 54 | 55 | func _exit_tree(): 56 | if scene_builder_commands: 57 | scene_builder_commands.queue_free() 58 | if scene_builder_dock: 59 | scene_builder_dock.queue_free() 60 | 61 | func _handles(object): 62 | return object is Node3D 63 | 64 | func _forward_3d_gui_input(camera : Camera3D, event : InputEvent) -> AfterGUIInput: 65 | return scene_builder_dock.forward_3d_gui_input(camera, event) 66 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ce1tnlbkiup1n 2 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_toolbox.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name SceneBuilderToolbox 3 | 4 | static func replace_first(s: String, pattern: String, replacement: String) -> String: 5 | var index = s.find(pattern) 6 | if index == -1: 7 | return s 8 | return s.substr(0, index) + replacement + s.substr(index + pattern.length()) 9 | 10 | static func replace_last(s: String, pattern: String, replacement: String) -> String: 11 | var index = s.rfind(pattern) 12 | if index == -1: 13 | return s 14 | return s.substr(0, index) + replacement + s.substr(index + pattern.length()) 15 | 16 | static func get_unique_name(base_name: String, parent: Node) -> String: 17 | if !parent.has_node(base_name): 18 | return base_name 19 | 20 | var counter = 1 21 | var new_name = base_name 22 | 23 | # Strip existing numeric suffix if present 24 | var regex = RegEx.new() 25 | regex.compile("^(.*?)(\\d+)$") 26 | var result = regex.search(base_name) 27 | 28 | if result: 29 | new_name = result.get_string(1) 30 | counter = int(result.get_string(2)) + 1 31 | 32 | # Find first available name 33 | while parent.has_node(new_name + str(counter)): 34 | counter += 1 35 | 36 | return new_name + str(counter) 37 | 38 | static func find_resource_with_dynamic_path(file_name: String) -> String: 39 | # The recursive directory will exist when installing from a submodule 40 | 41 | var base_paths = [ 42 | "res://addons/scene_builder/", 43 | "res://addons/scene_builder/addons/scene_builder/" 44 | ] 45 | 46 | for path in base_paths: 47 | var full_path = path + file_name 48 | if ResourceLoader.exists(full_path): 49 | return full_path 50 | 51 | return "" 52 | 53 | static func get_all_node_names(_node): 54 | var _all_node_names = [] 55 | for _child in _node.get_children(): 56 | _all_node_names.append(_child.name) 57 | if _child.get_child_count() > 0: 58 | var _result = get_all_node_names(_child) 59 | for _item in _result: 60 | _all_node_names.append(_item) 61 | return _all_node_names 62 | 63 | static func increment_name_until_unique(new_name, all_names) -> String: 64 | if new_name in all_names: 65 | var backup_name = new_name 66 | var suffix_counter = 1 67 | var increment_until = true 68 | while (increment_until): 69 | var _backup_name = backup_name + "-n" + str(suffix_counter) 70 | if _backup_name in all_names: 71 | suffix_counter += 1 72 | else: 73 | increment_until = false 74 | backup_name = _backup_name 75 | if suffix_counter > 9000: 76 | print("suffix_counter is over 9000, error?") 77 | increment_until = false 78 | return backup_name 79 | else: 80 | return new_name 81 | -------------------------------------------------------------------------------- /addons/scene_builder/scene_builder_toolbox.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b86v5tk7lslf7 2 | --------------------------------------------------------------------------------