└── addons └── asset_placer ├── asset_placer.gd.uid ├── utils ├── async.gd.uid ├── system_icon.gd.uid ├── aabb_provider.gd.uid ├── toaster_compat.gd.uid ├── transformations.gd.uid ├── asset_context_provider.gd.uid ├── resource_id_compat.gd.uid ├── asset_placer_input_option.gd.uid ├── preview_material.tres ├── toaster_compat.gd ├── resource_id_compat.gd ├── async.gd ├── asset_context_provider.gd ├── aabb_provider.gd ├── transformations.gd ├── system_icon.gd └── asset_placer_input_option.gd ├── data ├── asset_folder.gd.uid ├── id_generator.gd.uid ├── synchronizer.gd.uid ├── asset_library.gd.uid ├── asset_resource.gd.uid ├── assets_collection.gd.uid ├── assets_repository.gd.uid ├── folder_repository.gd.uid ├── asset_placer_options.gd.uid ├── asset_placer_settings.gd.uid ├── asset_collection_repository.gd.uid ├── asset_library_data_source.gd.uid ├── asset_placer_settings_repository.gd.uid ├── migrations │ ├── collection_id_migration.gd.uid │ └── collection_id_migration.gd ├── asset_folder.gd ├── assets_collection.gd ├── id_generator.gd ├── asset_library.gd ├── asset_placer_options.gd ├── asset_resource.gd ├── asset_collection_repository.gd ├── folder_repository.gd ├── assets_repository.gd ├── asset_placer_settings.gd ├── asset_library_data_source.gd ├── synchronizer.gd └── asset_placer_settings_repository.gd ├── updater ├── update.gd.uid ├── version.gd.uid ├── plugin_config.gd.uid ├── plugin_updater.gd.uid ├── plugin_updater_http_client.gd.uid ├── plugin_config.gd ├── update.gd ├── plugin_updater_http_client.gd ├── version.gd └── plugin_updater.gd ├── asset_placer_plugin.gd.uid ├── asset_placer_presenter.gd.uid ├── ui ├── about │ ├── about_window.gd.uid │ ├── about_window.gd │ └── about_window.tscn ├── asset_library_panel.gd.uid ├── components │ ├── chip.gd.uid │ ├── asset_thumbnail.gd.uid │ ├── vector3_spinbox.gd.uid │ ├── asset_resource_preview.gd.uid │ ├── asset_thumbnail.gd │ ├── asset_resource_preview.gd │ ├── vector3_spinbox.gd │ ├── chip.gd │ └── asset_resource_preview.tscn ├── settings │ ├── settings_panel.gd.uid │ ├── settings_presenter.gd.uid │ ├── components │ │ ├── keybinding_button.gd.uid │ │ ├── keybinding_group.gd.uid │ │ ├── keybinding_option.gd.uid │ │ ├── keybinding_group.gd │ │ ├── keybinding_option.gd │ │ ├── keybinding_option.tscn │ │ └── keybinding_button.gd │ ├── settings_presenter.gd │ └── settings_panel.gd ├── asset_placer_dock_presenter.gd.uid ├── folders_window │ ├── folder_view.gd.uid │ ├── folders_window.gd.uid │ ├── folder_presenter.gd.uid │ ├── folder_view.gd │ ├── folder_presenter.gd │ ├── folders_window.gd │ ├── folders_window.tscn │ └── folder_view.tscn ├── plane_preview │ ├── plan_preview.gd.uid │ ├── plane_placer.gd.uid │ ├── plane_preview_material.tres │ ├── plane_placer.gd │ ├── plan_preview.tscn │ ├── plane_texture.svg.import │ ├── plan_preview.gd │ └── plane_texture.svg ├── viewport_overlay │ ├── viewport_overlay.gd.uid │ ├── viewport_overlay.gd │ └── viewport_overlay.tscn ├── collection_picker │ ├── collection_picker.gd.uid │ └── collection_picker.gd ├── asset_library_window │ ├── asset_library_presenter.gd.uid │ ├── asset_library_window.gd.uid │ ├── asset_library_presenter.gd │ ├── asset_library_window.gd │ └── asset_library_window.tscn ├── asset_placer_options │ ├── asset_placer_options.gd.uid │ └── asset_placer_options.gd ├── asset_collections_window │ ├── asset_collection_window.gd.uid │ ├── asset_collections_presenter.gd.uid │ ├── edit │ │ ├── collection_edit_popup.gd.uid │ │ ├── collection_edit_popup.gd │ │ └── collection_edit_popup.tscn │ ├── components │ │ ├── collection_list_item.gd.uid │ │ ├── collection_list_item.gd │ │ └── collection_list_item.tscn │ ├── asset_collections_presenter.gd │ ├── asset_collection_window.gd │ └── asset_collection_window.tscn ├── asset_placer_dock_presenter.gd ├── asset_library_panel.gd └── asset_library_panel.tscn ├── placement ├── placement_mode.gd.uid ├── plane_options.gd.uid ├── asset_placement_strategy.gd.uid ├── plane_placement_strategy.gd.uid ├── surface_asset_placement_strategy.gd.uid ├── terrain_3d_placement_strategy.gd.uid ├── plane_options.gd ├── placement_mode.gd ├── asset_placement_strategy.gd ├── plane_placement_strategy.gd ├── surface_asset_placement_strategy.gd └── terrain_3d_placement_strategy.gd ├── icon.png ├── plugin.cfg ├── icon.png.import ├── LICENSE ├── asset_placer_presenter.gd ├── asset_placer_plugin.gd └── asset_placer.gd /addons/asset_placer/asset_placer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b7hdywoby2pre 2 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/async.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c4mct5y23jlhr 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_folder.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cc5yxagekjpy 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/id_generator.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c042nkgq1a0ue 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/synchronizer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dip2sbl8o412g 2 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/update.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bgxngk7finm8s 2 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/version.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dxhudw7mfmhqe 2 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/system_icon.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dmicn3kmr620j 2 | -------------------------------------------------------------------------------- /addons/asset_placer/asset_placer_plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://1hm3jr7drtyh 2 | -------------------------------------------------------------------------------- /addons/asset_placer/asset_placer_presenter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://5isoapst4sjj 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_library.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c7nn0sp4y58cq 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_resource.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c1y1lmxbmk18r 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/assets_collection.gd.uid: -------------------------------------------------------------------------------- 1 | uid://jn7aknlss6x0 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/assets_repository.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c5ey2tar77uw3 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/folder_repository.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dtbkai78rxfgt 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/about/about_window.gd.uid: -------------------------------------------------------------------------------- 1 | uid://43kdnn7nr2ch 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_library_panel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bracm8jmjlw2v 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/chip.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dswo2ldepb0us 2 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/plugin_config.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ccf5a2pdshwwc 2 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/plugin_updater.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dorh27qq8bdf 2 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/aabb_provider.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bopfvngd8756r 2 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/toaster_compat.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b63hgo7tra4ar 2 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/transformations.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cffcthj8d56n5 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_placer_options.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cglqm7ywt76tc 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_placer_settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bpx5p855lxa7q 2 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/placement_mode.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c65k23ayt8wgs 2 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/plane_options.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bfetoygykr5qm 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/settings_panel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ckohy5300ciba 2 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/asset_context_provider.gd.uid: -------------------------------------------------------------------------------- 1 | uid://5erhvk41mxeu 2 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/resource_id_compat.gd.uid: -------------------------------------------------------------------------------- 1 | uid://do88ksfdjr8mw 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_collection_repository.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dtliiench7qmb 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_library_data_source.gd.uid: -------------------------------------------------------------------------------- 1 | uid://u4ft0rh3en1w 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_placer_dock_presenter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dqxbplfufgbgy 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/asset_thumbnail.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dephufwlu765k 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/vector3_spinbox.gd.uid: -------------------------------------------------------------------------------- 1 | uid://btqju8qjrlawq 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/folders_window/folder_view.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dclcoehldtdcd 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/folders_window/folders_window.gd.uid: -------------------------------------------------------------------------------- 1 | uid://tkdl83npcow1 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/plane_preview/plan_preview.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ctjnj8oc85upu 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/plane_preview/plane_placer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://gpft3nlbfmnr 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/settings_presenter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bg1fheolbylfe 2 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/asset_placer_input_option.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cdn6punfkgd23 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_placer_settings_repository.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cea60e0cxle66 2 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/asset_placement_strategy.gd.uid: -------------------------------------------------------------------------------- 1 | uid://7m4xm22tnecf 2 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/plane_placement_strategy.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cn5hl4fpsft3s 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/asset_resource_preview.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dacpe4563g5x2 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/folders_window/folder_presenter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bsh1hf517r7gj 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/viewport_overlay/viewport_overlay.gd.uid: -------------------------------------------------------------------------------- 1 | uid://e3l8cnd506a1 2 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/plugin_updater_http_client.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dn1t7tn6xabxe 2 | -------------------------------------------------------------------------------- /addons/asset_placer/data/migrations/collection_id_migration.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b2i2qid3awgry 2 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/surface_asset_placement_strategy.gd.uid: -------------------------------------------------------------------------------- 1 | uid://fbgdluelg8lw 2 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/terrain_3d_placement_strategy.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b5pbo86241x10 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/collection_picker/collection_picker.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dv5rhnuua40g4 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/components/keybinding_button.gd.uid: -------------------------------------------------------------------------------- 1 | uid://gh7lhadj2xqn 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/components/keybinding_group.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c4ms6t1dhy278 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/components/keybinding_option.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cuktm42swvhsn 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_library_window/asset_library_presenter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://brteca48suhk5 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_library_window/asset_library_window.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d3hm5bn8vmd2 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_placer_options/asset_placer_options.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b4q0diu63qlg8 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/asset_collection_window.gd.uid: -------------------------------------------------------------------------------- 1 | uid://1l3hqqsryn3j 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/asset_collections_presenter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://drknd61ng6c7x 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/edit/collection_edit_popup.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dwwbuico5765w 2 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/components/collection_list_item.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bf5fmwafbhhb0 2 | -------------------------------------------------------------------------------- /addons/asset_placer/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/godot-asset-placer/HEAD/addons/asset_placer/icon.png -------------------------------------------------------------------------------- /addons/asset_placer/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Godot Asset Placer" 4 | description="Addon to facilitate 3D asset placement and management" 5 | author="Roman Levinzon" 6 | version="1.2.4" 7 | script="asset_placer_plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/preview_material.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" format=3 uid="uid://diadbyea0m1p8"] 2 | 3 | [resource] 4 | transparency = 1 5 | cull_mode = 2 6 | albedo_color = Color(0.117647, 1, 1, 0.431373) 7 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/plane_options.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PlaneOptions 3 | 4 | var normal: Vector3 5 | var origin: Vector3 6 | 7 | 8 | func _init(normal: Vector3, origin: Vector3): 9 | self.normal = normal 10 | self.origin = origin 11 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_folder.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetFolder 3 | 4 | var path: String 5 | var include_subfolders: bool 6 | 7 | func _init(path: String, include_subfolders: bool = false): 8 | self.path = path 9 | self.include_subfolders = include_subfolders 10 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/plugin_config.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PluginConfiguration 3 | 4 | var version: Version 5 | 6 | func _init(file_path: String): 7 | var config = ConfigFile.new() 8 | config.load(file_path) 9 | self.version = Version.new(config.get_value("plugin", "version", "unknown")) 10 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_placer_dock_presenter.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacerDockPresenter 3 | 4 | enum Tab { 5 | Assets, Folders, Collections, About 6 | } 7 | 8 | 9 | signal show_tab(tab: Tab) 10 | 11 | func _init(): 12 | instance = self 13 | 14 | static var instance: AssetPlacerDockPresenter 15 | -------------------------------------------------------------------------------- /addons/asset_placer/data/assets_collection.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetCollection 3 | 4 | var name: String 5 | var backgroundColor: Color 6 | var id: int 7 | 8 | 9 | func _init(name: String, backgroundColor: Color, id: int): 10 | self.backgroundColor = backgroundColor 11 | self.name = name 12 | self.id = id 13 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/toaster_compat.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name EditorToasterCompat 3 | 4 | 5 | static func toast(message: String): 6 | if EditorInterface.has_method("get_editor_toaster"): 7 | var toaster = EditorInterface["get_editor_toaster"].call() 8 | toaster.push_toast(message, 0, "Asset Placer") 9 | else: 10 | push_warning(message) 11 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/update.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PluginUpdate 3 | 4 | var version: Version 5 | var change_log: String 6 | var download_url: String 7 | 8 | 9 | 10 | func _init(version: String, change_log: String, download_url: String): 11 | self.version = Version.new(version) 12 | self.change_log = change_log 13 | self.download_url = download_url 14 | -------------------------------------------------------------------------------- /addons/asset_placer/data/id_generator.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacerIdGenerator 3 | 4 | 5 | const KEY: String = "asset_placer/internal/last_int" 6 | 7 | func _init(): 8 | pass 9 | 10 | func next_int() -> int: 11 | var last_int = ProjectSettings.get_setting(KEY, -1) 12 | var new_int = last_int + 1 13 | ProjectSettings.set_setting(KEY, new_int) 14 | return new_int 15 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/resource_id_compat.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name ResourceIdCompat 3 | 4 | static func path_to_uid(path:String): 5 | if ResourceUID.has_method("path_to_uid"): 6 | return ResourceUID["path_to_uid"].call(path) 7 | if path.begins_with("uid://"): 8 | return path 9 | var uid = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(path)) 10 | if uid == "uid://": 11 | uid = path 12 | return uid 13 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/async.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacerAsync 3 | 4 | var _job_ids: Array[int] = [] 5 | 6 | static var instance: AssetPlacerAsync 7 | 8 | func _init(): 9 | instance = self 10 | 11 | 12 | func enqueue(callable: Callable): 13 | var id = WorkerThreadPool.add_task(callable, false, "Asset Placer Task") 14 | _job_ids.append(id) 15 | 16 | func await_completion(): 17 | for id in _job_ids: 18 | WorkerThreadPool.wait_for_task_completion(id) 19 | 20 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/plane_preview/plane_preview_material.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://c7je2oxif285m"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://dago1cpuysosa" path="res://addons/asset_placer/ui/plane_preview/plane_texture.svg" id="1_655gw"] 4 | 5 | [resource] 6 | transparency = 1 7 | cull_mode = 2 8 | shading_mode = 0 9 | albedo_color = Color(0, 1.598561, 1.598561, 0.62352943) 10 | albedo_texture = ExtResource("1_655gw") 11 | uv1_scale = Vector3(20, 20, 20) 12 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/placement_mode.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PlacementMode 3 | 4 | 5 | class SurfacePlacement extends PlacementMode: 6 | pass 7 | 8 | class PlanePlacement extends PlacementMode: 9 | var plane_options: PlaneOptions 10 | 11 | func _init(options: PlaneOptions = PlaneOptions.new(Vector3.UP, Vector3.ZERO)): 12 | self.plane_options = options 13 | 14 | class Terrain3DPlacement extends PlacementMode: 15 | var terrain3dNode: Node3D 16 | func _init(node: Node3D): 17 | self.terrain3dNode = node 18 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/asset_placement_strategy.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacementStrategy 3 | 4 | class CollisionHit: 5 | var position: Vector3 6 | var normal: Vector3 7 | 8 | func _init(position: Vector3, normal: Vector3): 9 | self.position = position 10 | self.normal = normal 11 | 12 | static func zero() -> CollisionHit: 13 | return CollisionHit.new(Vector3.ZERO, Vector3.UP) 14 | 15 | func get_placement_point(camera: Camera3D, mouse_position: Vector2) -> CollisionHit: 16 | return CollisionHit.zero() 17 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/asset_context_provider.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacerContextUtil 3 | 4 | 5 | 6 | static func select_context(): 7 | if !is_current_selection_node3d(): 8 | var selection = EditorInterface.get_edited_scene_root() 9 | EditorInterface.get_selection().add_node(selection) 10 | 11 | 12 | 13 | static func is_current_selection_node3d() -> bool: 14 | var selection = EditorInterface.get_selection() 15 | for node in selection.get_selected_nodes(): 16 | if node is Node3D: 17 | return true 18 | return false 19 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_library.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | class_name AssetLibrary 4 | 5 | var folders: Array[AssetFolder] = [] 6 | var items: Array[AssetResource] = [] 7 | var collections: Array[AssetCollection] = [] 8 | 9 | 10 | func _init(items: Array[AssetResource], folders: Array[AssetFolder], collections: Array[AssetCollection]): 11 | self.folders = folders 12 | self.items = items 13 | self.collections = collections 14 | 15 | 16 | 17 | func index_of_asset(asset: AssetResource): 18 | var idx:int = -1; for a in len(items): if items[a].id == asset.id: idx = a; break 19 | return idx 20 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_placer_options.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacerOptions 3 | 4 | var snapping_enabled: bool = false 5 | var snapping_grid_step: float = 1.0 6 | 7 | var use_asset_origin: bool = true 8 | var align_normals: bool = false 9 | var enable_random_placement = false 10 | 11 | var rotate_on_placement: bool = true 12 | var max_rotation: Vector3 = Vector3.UP * 180 13 | var min_rotation: Vector3 = Vector3.ZERO 14 | 15 | var scale_on_placement: bool = false 16 | var min_scale: Vector3 = Vector3.ONE 17 | var max_scale: Vector3 = Vector3.ONE * 2 18 | var uniform_scaling: bool = true 19 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/components/keybinding_group.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Container 3 | 4 | var _keybinding_buttons: Array[APInputOptionButton] 5 | 6 | func _ready(): 7 | _resolve_children(self) 8 | for button in _keybinding_buttons: 9 | button.pressed.connect(func(): 10 | get_viewport().set_input_as_handled() 11 | button.bind() 12 | cancel_others(button) 13 | ) 14 | 15 | 16 | func cancel_others(except: APInputOptionButton): 17 | for button in _keybinding_buttons: 18 | if button != except: 19 | button.cancel() 20 | 21 | 22 | func _resolve_children(parent: Control): 23 | for child in parent.get_children(): 24 | if child is APInputOptionButton: 25 | _keybinding_buttons.push_back(child) 26 | return 27 | elif child is Control: 28 | _resolve_children(child) 29 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/plane_preview/plane_placer.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PlanePlacer 3 | 4 | var presenter: AssetPlacerPresenter 5 | var _current_plane_options = PlaneOptions.new(Vector3.UP, Vector3.ZERO) 6 | var _new_plane_options: PlaneOptions = PlaneOptions.new(Vector3.UP, Vector3.ZERO); 7 | var _last_mouse_position: Vector2 8 | 9 | func _init(presenter: AssetPlacerPresenter, plane: Node3D): 10 | self.presenter = presenter 11 | presenter.placement_mode_changed.connect(func(mode: PlacementMode): 12 | if mode is PlacementMode.PlanePlacement: 13 | _new_plane_options = mode.plane_options 14 | ) 15 | 16 | func move_plane_up(value: float): 17 | _new_plane_options.origin += _new_plane_options.normal * value 18 | presenter.placement_mode = PlacementMode.PlanePlacement.new(_new_plane_options) 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/components/keybinding_option.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | 5 | signal keybind_changed(option: APInputOption) 6 | 7 | 8 | @onready var key_bind_button: Button = %KeyBindButton 9 | @onready var key_bind_label: Label = %KeyBindLabel 10 | @export var allow_mouse_buttons: bool: 11 | set(value): 12 | allow_mouse_buttons = value 13 | if key_bind_button: 14 | key_bind_button.allow_mouse_buttons = value 15 | 16 | @export var label: String = "Label": 17 | set(value): 18 | label = value 19 | if key_bind_label: 20 | key_bind_label.text = value 21 | 22 | 23 | 24 | func _ready(): 25 | key_bind_button.key_binding_changed.connect(keybind_changed.emit) 26 | key_bind_label.text = label 27 | key_bind_button.allow_mouse_buttons = allow_mouse_buttons 28 | 29 | func set_keybind(key: APInputOption): 30 | key_bind_button.set_keybind_no_signal(key) 31 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/plane_placement_strategy.gd: -------------------------------------------------------------------------------- 1 | extends AssetPlacementStrategy 2 | class_name PlanePlacementStrategy 3 | 4 | var plane_options: PlaneOptions 5 | 6 | 7 | func _init(plane_options: PlaneOptions): 8 | self.plane_options = plane_options 9 | 10 | 11 | func get_placement_point(camera: Camera3D, mouse_position: Vector2) -> AssetPlacementStrategy.CollisionHit: 12 | var ray_origin = camera.project_ray_origin(mouse_position) 13 | var ray_dir = camera.project_ray_normal(mouse_position) 14 | var plane = Plane(plane_options.normal, plane_options.origin) 15 | var intersection = plane.intersects_ray(ray_origin, ray_dir) 16 | if intersection: 17 | var final_normal = plane.normal 18 | if ray_dir.dot(plane.normal) > 0: 19 | final_normal = -plane.normal 20 | return AssetPlacementStrategy.CollisionHit.new(intersection, final_normal) 21 | else: 22 | return AssetPlacementStrategy.CollisionHit.zero() 23 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/folders_window/folder_view.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | class_name FolderView 4 | 5 | @onready var path_label: Label = %PathLabel 6 | @onready var subfolders_checkbox: CheckBox = %SubfoldersCheckbox 7 | @onready var delete_button: Button = %DeleteButton 8 | @onready var sync_button: Button = %SyncButton 9 | 10 | signal folder_delete_clicked 11 | signal folder_sync_clicked 12 | signal folder_include_subfloders_change(bool) 13 | 14 | func _ready(): 15 | delete_button.pressed.connect(func(): 16 | folder_delete_clicked.emit() 17 | ) 18 | 19 | sync_button.pressed.connect(func(): 20 | folder_sync_clicked.emit() 21 | ) 22 | 23 | subfolders_checkbox.toggled.connect(func(toggled): 24 | folder_include_subfloders_change.emit(toggled) 25 | ) 26 | 27 | func set_folder(folder: AssetFolder): 28 | path_label.text = folder.path 29 | subfolders_checkbox.button_pressed = folder.include_subfolders 30 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/aabb_provider.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AABBProvider 3 | 4 | static func provide_aabb(node: Node3D) -> AABB: 5 | return get_node_aabb(node, false) 6 | 7 | ## Return the [AABB] of the node. 8 | static func get_node_aabb(node : Node, exclude_top_level_transform: bool = true) -> AABB: 9 | var bounds : AABB = AABB() 10 | 11 | # Do not include children that is queued for deletion 12 | if node.is_queued_for_deletion(): 13 | return bounds 14 | 15 | # Get the aabb of the visual instance 16 | if node is VisualInstance3D: 17 | bounds = node.get_aabb(); 18 | 19 | # Recurse through all children 20 | for child in node.get_children(): 21 | var child_bounds : AABB = get_node_aabb(child, false) 22 | if bounds.size == Vector3.ZERO: 23 | bounds = child_bounds 24 | else: 25 | bounds = bounds.merge(child_bounds) 26 | 27 | if !exclude_top_level_transform: 28 | bounds = node.transform * bounds 29 | 30 | return bounds 31 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/plane_preview/plan_preview.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://btg3diy1if28d"] 2 | 3 | [ext_resource type="Script" uid="uid://ctjnj8oc85upu" path="res://addons/asset_placer/ui/plane_preview/plan_preview.gd" id="1_ldq3v"] 4 | [ext_resource type="Material" uid="uid://c7je2oxif285m" path="res://addons/asset_placer/ui/plane_preview/plane_preview_material.tres" id="2_ldq3v"] 5 | 6 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_aho0j"] 7 | 8 | [sub_resource type="PlaneMesh" id="PlaneMesh_h6n2v"] 9 | material = SubResource("StandardMaterial3D_aho0j") 10 | size = Vector2(200, 200) 11 | 12 | [node name="PlanPreview" type="Node3D"] 13 | visible = false 14 | script = ExtResource("1_ldq3v") 15 | 16 | [node name="MeshInstance3D" type="MeshInstance3D" parent="."] 17 | transform = Transform3D(0, 0, -1, 0, 1, 0, -1, 0, 0, 0, 2.4, 0) 18 | mesh = SubResource("PlaneMesh_h6n2v") 19 | surface_material_override/0 = ExtResource("2_ldq3v") 20 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_library_panel.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | class_name AssetLibraryPanel 4 | 5 | @onready var asset_library_window: AssetLibraryWindow = $Panel/TabContainer/Assets 6 | @onready var tab_container: TabContainer = $Panel/TabContainer 7 | func _ready(): 8 | PluginUpdater.instance.updater_update_available.connect(func(a): 9 | show_update_available(true) 10 | ) 11 | 12 | PluginUpdater.instance.update_ready.connect(func(a): 13 | show_update_available(true) 14 | ) 15 | 16 | PluginUpdater.instance.updater_up_to_date.connect(func(): 17 | show_update_available(false) 18 | ) 19 | AssetPlacerDockPresenter.instance.show_tab.connect(func(tab): 20 | tab_container.current_tab = tab 21 | ) 22 | 23 | 24 | func show_update_available(avaialbe: bool): 25 | var index = tab_container.get_tab_count() - 1 26 | if avaialbe: 27 | tab_container.set_tab_button_icon(index, EditorIconTexture2D.new("Warning")) 28 | else: 29 | tab_container.set_tab_button_icon(index, null) 30 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/components/keybinding_option.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bjgh4jo3pc65i"] 2 | 3 | [ext_resource type="Script" uid="uid://cuktm42swvhsn" path="res://addons/asset_placer/ui/settings/components/keybinding_option.gd" id="1_xnfus"] 4 | [ext_resource type="Script" uid="uid://gh7lhadj2xqn" path="res://addons/asset_placer/ui/settings/components/keybinding_button.gd" id="2_nqwub"] 5 | 6 | [node name="KeybindingOption" type="HBoxContainer"] 7 | anchors_preset = 10 8 | anchor_right = 1.0 9 | offset_bottom = 23.0 10 | grow_horizontal = 2 11 | size_flags_horizontal = 3 12 | script = ExtResource("1_xnfus") 13 | label = "Laasbelasdas " 14 | 15 | [node name="KeyBindLabel" type="Label" parent="."] 16 | unique_name_in_owner = true 17 | layout_mode = 2 18 | size_flags_horizontal = 3 19 | text = "Laasbelasdas " 20 | 21 | [node name="KeyBindButton" type="Button" parent="."] 22 | unique_name_in_owner = true 23 | layout_mode = 2 24 | size_flags_horizontal = 3 25 | text = "None" 26 | script = ExtResource("2_nqwub") 27 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/asset_thumbnail.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends TextureRect 3 | class_name AssetThumbnail 4 | 5 | var resource: AssetResource 6 | var previewer: EditorResourcePreview 7 | var last_time_modified = 0 8 | 9 | 10 | func set_resource(resource: AssetResource): 11 | self.resource = resource 12 | preview_resource(resource) 13 | 14 | func preview_resource(resource: AssetResource): 15 | if Engine.is_editor_hint() and resource and resource.scene: 16 | last_time_modified = FileAccess.get_modified_time(resource.scene.resource_path) 17 | previewer = EditorInterface.get_resource_previewer() 18 | previewer.queue_edited_resource_preview(resource.scene, self, "_on_preview_generated", null) 19 | 20 | func _process(_delta): 21 | if Engine.is_editor_hint() and resource and resource.scene: 22 | var new_time_modified = FileAccess.get_modified_time(resource.scene.resource_path) 23 | if new_time_modified != last_time_modified: 24 | preview_resource(resource) 25 | 26 | func _on_preview_generated(path: String, texture: Texture2D, thumbnail, data): 27 | self.texture = texture 28 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/surface_asset_placement_strategy.gd: -------------------------------------------------------------------------------- 1 | extends AssetPlacementStrategy 2 | class_name SurfaceAssetPlacementStrategy 3 | 4 | var exclude_rids = [] 5 | 6 | func _init(exclude_rids): 7 | self.exclude_rids = exclude_rids 8 | 9 | func get_placement_point(camera: Camera3D, mouse_position: Vector2) -> CollisionHit: 10 | var ray_origin = camera.project_ray_origin(mouse_position) 11 | var ray_dir = camera.project_ray_normal(mouse_position) 12 | var space_state = camera.get_world_3d().direct_space_state 13 | 14 | var params = PhysicsRayQueryParameters3D.new() 15 | params.from = ray_origin 16 | params.exclude = exclude_rids 17 | params.to = ray_origin + ray_dir * 1000 18 | var result = space_state.intersect_ray(params) 19 | if not result.has('position') or not result.has('normal'): 20 | AssetPlacerPresenter._instance.show_error.emit("No Surface to Collide With") 21 | return AssetPlacementStrategy.CollisionHit.zero() 22 | 23 | var position: Vector3 = result.position 24 | var normal: Vector3 = result.normal 25 | return AssetPlacementStrategy.CollisionHit.new(position, normal) 26 | -------------------------------------------------------------------------------- /addons/asset_placer/placement/terrain_3d_placement_strategy.gd: -------------------------------------------------------------------------------- 1 | extends AssetPlacementStrategy 2 | class_name Terrain3DAssetPlacementStrategy 3 | 4 | var terrain_3d_node: Node3D 5 | 6 | func _init(node: Node3D): 7 | self.terrain_3d_node = node 8 | 9 | func get_placement_point( 10 | camera: Camera3D, 11 | mouse_position: Vector2 12 | ) -> CollisionHit: 13 | if terrain_3d_node.has_method("get_intersection"): 14 | var from := camera.project_ray_origin(mouse_position) 15 | var to := from + camera.project_ray_normal(mouse_position) * 1000 16 | var direction = (to - from).normalized() 17 | var hit_position: Vector3 = terrain_3d_node.get_intersection(from, direction, false) 18 | if hit_position != Vector3.INF: 19 | var data = terrain_3d_node.data 20 | var normal = data.get_normal(hit_position) 21 | return CollisionHit.new(hit_position, normal) 22 | else: 23 | var message := "No collision with Terrain3D" 24 | AssetPlacerPresenter._instance.show_error.emit(message) 25 | return CollisionHit.zero() 26 | else: 27 | push_error("Provided Node is Not Terrain3D") 28 | return CollisionHit.zero() 29 | -------------------------------------------------------------------------------- /addons/asset_placer/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://be61c5ff55gfo" 6 | path="res://.godot/imported/icon.png-7770fcbefdc198ae88466c5d3f27bcdd.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/asset_placer/icon.png" 14 | dest_files=["res://.godot/imported/icon.png-7770fcbefdc198ae88466c5d3f27bcdd.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/uastc_level=0 22 | compress/rdo_quality_loss=0.0 23 | compress/hdr_compression=1 24 | compress/normal_map=0 25 | compress/channel_pack=0 26 | mipmaps/generate=false 27 | mipmaps/limit=-1 28 | roughness/mode=0 29 | roughness/src_normal="" 30 | process/channel_remap/red=0 31 | process/channel_remap/green=1 32 | process/channel_remap/blue=2 33 | process/channel_remap/alpha=3 34 | process/fix_alpha_border=true 35 | process/premult_alpha=false 36 | process/normal_map_invert_y=false 37 | process/hdr_as_srgb=false 38 | process/hdr_clamp_exposure=false 39 | process/size_limit=0 40 | detect_3d/compress_to=1 41 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/asset_resource_preview.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Button 3 | class_name AssetResourcePreview 4 | 5 | signal left_clicked(asset: AssetResource) 6 | signal right_clicked(asset: AssetResource) 7 | 8 | @onready var label = %Label 9 | @onready var asset_thumbnail = %AssetThumbnail 10 | var resource: AssetResource 11 | var settings_repo = AssetPlacerSettingsRepository.instance 12 | var default_size: Vector2 13 | 14 | func _ready(): 15 | default_size = size 16 | mouse_filter = Control.MOUSE_FILTER_STOP 17 | toggled.connect(func(_a): left_clicked.emit(resource)) 18 | set_settings(settings_repo.get_settings()) 19 | settings_repo.settings_changed.connect(set_settings) 20 | 21 | func set_asset(asset: AssetResource): 22 | self.resource = asset 23 | label.text = asset.name 24 | asset_thumbnail.set_resource(asset) 25 | 26 | func set_settings(settings: AssetPlacerSettings): 27 | custom_minimum_size = default_size * settings.ui_scale 28 | 29 | func _gui_input(event): 30 | if event is InputEventMouseButton: 31 | if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: 32 | right_clicked.emit(resource) 33 | 34 | -------------------------------------------------------------------------------- /addons/asset_placer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Roman Levinzon 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 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_resource.gd: -------------------------------------------------------------------------------- 1 | 2 | extends RefCounted 3 | class_name AssetResource 4 | 5 | var name: String 6 | var id: String 7 | var tags: Array[int] 8 | var folder_path: String 9 | var _scene: Resource = null 10 | 11 | 12 | var shallow_collections: Array[AssetCollection]: 13 | get(): 14 | var shallow: Array[AssetCollection] = [] 15 | for id in tags: 16 | shallow.push_back(AssetCollection.new("name", Color.TRANSPARENT, id)) 17 | return shallow 18 | 19 | func _init(resId: String, name: String, tags: Array[int] = [], folder_path: String = ""): 20 | self.name = name 21 | self.id = resId 22 | self.tags = tags 23 | self.folder_path = folder_path 24 | 25 | 26 | var scene: Resource: 27 | get(): 28 | if _scene == null: 29 | _scene = load(id) 30 | return _scene 31 | else: 32 | return _scene 33 | 34 | 35 | 36 | func belongs_to_collection(collection : AssetCollection) -> bool: 37 | return tags.any(func(tag: int): return tag == collection.id) 38 | 39 | 40 | func belongs_to_some_collection(collections: Array[AssetCollection]) -> bool: 41 | return collections.any(func(collection: AssetCollection): 42 | return self.belongs_to_collection(collection) 43 | ) 44 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/components/collection_list_item.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | class_name AssetCollectionListItem 4 | 5 | @onready var delete_button: Button = %DeleteButton 6 | @onready var edit_button: Button = %EditButton 7 | @onready var texture_rect: TextureRect = %TextureRect 8 | @onready var name_label: Label = %NameLabel 9 | 10 | signal delete_collection_click 11 | signal edit_collection_click 12 | 13 | func _ready(): 14 | delete_button.pressed.connect(delete_collection_click.emit) 15 | edit_button.pressed.connect(edit_collection_click.emit) 16 | 17 | func set_collection(collection: AssetCollection): 18 | name_label.text = collection.name 19 | texture_rect.texture = make_circle_icon(32, collection.backgroundColor) 20 | 21 | 22 | 23 | func make_circle_icon(radius: int, color: Color) -> Texture2D: 24 | var size = radius * 2 25 | var img := Image.create(size, size, false, Image.FORMAT_RGBA8) 26 | img.fill(Color(0, 0, 0, 0)) # Transparent background 27 | 28 | for y in size: 29 | for x in size: 30 | var dist = Vector2(x, y).distance_to(Vector2(radius, radius)) 31 | if dist <= radius: 32 | img.set_pixel(x, y, color) 33 | 34 | img.generate_mipmaps() 35 | 36 | var tex := ImageTexture.create_from_image(img) 37 | return tex 38 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/plane_preview/plane_texture.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dago1cpuysosa" 6 | path.s3tc="res://.godot/imported/plane_texture.svg-32714c12aa275ec18acde20ae96a9021.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/asset_placer/ui/plane_preview/plane_texture.svg" 15 | dest_files=["res://.godot/imported/plane_texture.svg-32714c12aa275ec18acde20ae96a9021.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/uastc_level=0 23 | compress/rdo_quality_loss=0.0 24 | compress/hdr_compression=1 25 | compress/normal_map=0 26 | compress/channel_pack=0 27 | mipmaps/generate=true 28 | mipmaps/limit=-1 29 | roughness/mode=0 30 | roughness/src_normal="" 31 | process/channel_remap/red=0 32 | process/channel_remap/green=1 33 | process/channel_remap/blue=2 34 | process/channel_remap/alpha=3 35 | process/fix_alpha_border=true 36 | process/premult_alpha=false 37 | process/normal_map_invert_y=false 38 | process/hdr_as_srgb=false 39 | process/hdr_clamp_exposure=false 40 | process/size_limit=0 41 | detect_3d/compress_to=0 42 | svg/scale=1.0 43 | editor/scale_with_editor_scale=false 44 | editor/convert_colors_with_editor_theme=false 45 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/edit/collection_edit_popup.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Popup 3 | class_name CollectionEditPopupMenu 4 | 5 | signal on_save(collection: AssetCollection) 6 | var collection_to_edit: AssetCollection 7 | @onready var color_picker_button: ColorPickerButton = %ColorPickerButton 8 | @onready var save_button: Button = %SaveButton 9 | @onready var text_edit: TextEdit= %TextEdit 10 | 11 | func _ready(): 12 | if not collection_to_edit: 13 | return 14 | color_picker_button.color = collection_to_edit.backgroundColor 15 | text_edit.text = collection_to_edit.name 16 | text_edit.grab_focus() 17 | text_edit.text_changed.connect(func(): 18 | save_button.disabled = text_edit.text.is_empty() 19 | collection_to_edit.name = text_edit.text 20 | ) 21 | 22 | color_picker_button.color_changed.connect(func(color): 23 | collection_to_edit.backgroundColor = color 24 | ) 25 | 26 | save_button.pressed.connect(func(): 27 | on_save.emit(collection_to_edit) 28 | queue_free() 29 | ) 30 | 31 | 32 | static func show_popup(collection: AssetCollection, on_save: Callable): 33 | var dialog = load("res://addons/asset_placer/ui/asset_collections_window/edit/collection_edit_popup.tscn").instantiate() 34 | dialog.collection_to_edit = collection 35 | dialog.on_save.connect(on_save) 36 | EditorInterface.popup_dialog_centered(dialog) 37 | 38 | 39 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/folders_window/folder_presenter.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name FolderPresenter 3 | 4 | signal folders_loaded(folder: Array[AssetFolder]) 5 | 6 | var folder_repository: FolderRepository 7 | var asset_repository: AssetsRepository 8 | var sync: Synchronize 9 | 10 | func _init(): 11 | self.folder_repository = FolderRepository.instance 12 | self.asset_repository = AssetsRepository.instance 13 | self.sync = Synchronize.new(self.folder_repository, self.asset_repository) 14 | 15 | 16 | func _ready(): 17 | folders_loaded.emit(folder_repository.get_all()) 18 | 19 | folder_repository.folder_changed.connect(func(): 20 | var folders = folder_repository.get_all() 21 | folders_loaded.emit(folders) 22 | ) 23 | 24 | 25 | func delete_folder(folder: AssetFolder): 26 | folder_repository.delete(folder.path) 27 | for asset in asset_repository.get_all_assets(): 28 | if asset.folder_path == folder.path: 29 | asset_repository.delete(asset.id) 30 | 31 | func sync_folder(folder: AssetFolder): 32 | sync.sync_folder(folder) 33 | 34 | func include_subfolders(include: bool, folder: AssetFolder): 35 | folder.include_subfolders = include 36 | folder_repository.update(folder) 37 | 38 | func add_folder(folder: String): 39 | if folder.get_extension().is_empty(): 40 | folder_repository.add(folder) 41 | 42 | func add_folders(folders: PackedStringArray): 43 | for folder in folders: 44 | add_folder(folder) 45 | 46 | func is_file_supported(file: String) -> bool: 47 | return file.ends_with(".tscn") || file.ends_with(".glb") || file.ends_with(".fbx") 48 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/vector3_spinbox.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends HBoxContainer 3 | class_name SpinBoxVector3 4 | 5 | signal value_changed(vector: Vector3) 6 | 7 | @export var min: Vector3 8 | @export var max: Vector3 9 | @export var label: String 10 | 11 | @onready var spin_x := SpinBox.new() 12 | @onready var spin_y := SpinBox.new() 13 | @onready var spin_z := SpinBox.new() 14 | 15 | 16 | @export var uniform: bool = false 17 | @export var normalized: bool = false 18 | 19 | func _ready(): 20 | _add_spinbox("X", spin_x, min.x, max.y) 21 | _add_spinbox("Y", spin_y, min.y, max.y) 22 | _add_spinbox("Z", spin_z, min.z, max.z) 23 | 24 | spin_x.value_changed.connect(react_to_value_change) 25 | spin_z.value_changed.connect(react_to_value_change) 26 | spin_y.value_changed.connect(react_to_value_change) 27 | 28 | func set_value_no_signal(vector: Vector3): 29 | spin_x.set_value_no_signal(vector.x) 30 | spin_y.set_value_no_signal(vector.y) 31 | spin_z.set_value_no_signal(vector.z) 32 | 33 | 34 | func _add_spinbox(axis: String, spinbox: SpinBox, min: float, max: float) -> void: 35 | var label := Label.new() 36 | label.text = "%s:" % axis 37 | label.size = Vector2(10, 0) 38 | spinbox.prefix = "%s:" % axis 39 | spinbox.min_value = min 40 | spinbox.max_value = max 41 | spinbox.step = 0.1 42 | spinbox.editable = true 43 | add_child(spinbox) 44 | 45 | func react_to_value_change(v: float): 46 | if uniform: 47 | set_value_no_signal(Vector3(v, v,v)) 48 | 49 | if normalized: 50 | set_value_no_signal(get_vector().normalized()) 51 | 52 | value_changed.emit(get_vector()) 53 | 54 | 55 | func get_vector() -> Vector3: 56 | return Vector3(spin_x.value, spin_y.value, spin_z.value) 57 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/chip.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Button 3 | class_name Chip 4 | 5 | @export var backgroundColor: Color = Color(0, 0, 0, 0.5): set = set_background_color 6 | @export var corner_radius: int = 32: set = set_corner_radius 7 | @export var content_margin_vertical: int = 4: set = set_content_margin_v 8 | @export var content_margin_horizontal: int = 8: set = set_content_margin_h 9 | 10 | var asset_collection: AssetCollection 11 | 12 | func _ready(): 13 | add_theme_font_size_override("font_size", 22) 14 | add_theme_color_override("font_color", Color.BLACK) 15 | _update_stylebox() 16 | _update_minimum_size() 17 | 18 | func set_background_color(color: Color): 19 | backgroundColor = color 20 | _update_stylebox() 21 | 22 | func set_corner_radius(radius: int): 23 | corner_radius = radius 24 | _update_stylebox() 25 | 26 | func set_content_margin_v(margin): 27 | content_margin_vertical = margin 28 | _update_stylebox() 29 | _update_minimum_size() 30 | 31 | func set_content_margin_h(margin): 32 | content_margin_horizontal = margin 33 | _update_stylebox() 34 | _update_minimum_size() 35 | 36 | func _update_stylebox(): 37 | var stylebox = StyleBoxFlat.new() 38 | stylebox.bg_color = backgroundColor 39 | stylebox.set_corner_radius_all(corner_radius) 40 | stylebox.content_margin_left = content_margin_horizontal 41 | stylebox.content_margin_right = content_margin_horizontal 42 | stylebox.content_margin_bottom = content_margin_vertical 43 | stylebox.content_margin_top = content_margin_vertical 44 | add_theme_stylebox_override("normal", stylebox) 45 | 46 | func _update_minimum_size(): 47 | custom_minimum_size = Vector2(content_margin_horizontal * 2, content_margin_vertical * 2) + get_minimum_size() 48 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/asset_collections_presenter.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetCollectionsPresenter 3 | 4 | var _repository: AssetCollectionRepository 5 | var _assets_repository: AssetsRepository 6 | 7 | var _new_collection_name: String = "" 8 | var _new_collection_color: Color 9 | 10 | signal show_collections(items: Array[AssetCollection]) 11 | signal enable_create_button(enable: bool) 12 | signal show_empty_view 13 | signal clear_text_field() 14 | 15 | 16 | func _init(): 17 | self._repository = AssetCollectionRepository.instance 18 | self._repository.collections_changed.connect(_load_collections) 19 | self._assets_repository = AssetsRepository.instance 20 | 21 | func ready(): 22 | _load_collections() 23 | _update_state_new_collection_state() 24 | 25 | func set_color(color: Color): 26 | _new_collection_color = color 27 | _update_state_new_collection_state() 28 | 29 | func set_name(name: String): 30 | _new_collection_name = name 31 | _update_state_new_collection_state() 32 | 33 | func create_collection(): 34 | _repository.add_collection(_new_collection_name, _new_collection_color) 35 | clear_text_field.emit() 36 | enable_create_button.emit(false) 37 | 38 | 39 | func _update_state_new_collection_state(): 40 | var valid_name := !_new_collection_name.is_empty() 41 | var valid_color = _new_collection_color != null 42 | enable_create_button.emit(valid_color && valid_name) 43 | 44 | func _load_collections(): 45 | var collections := _repository.get_collections() 46 | if collections.size() == 0: 47 | show_empty_view.emit() 48 | else: 49 | show_collections.emit(collections) 50 | 51 | 52 | func delete_collection(collection: AssetCollection): 53 | _repository.delete_collection(collection.id) 54 | 55 | 56 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/plane_preview/plan_preview.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node3D 3 | 4 | @onready var mesh_instance: MeshInstance3D = $MeshInstance3D 5 | 6 | func _ready(): 7 | var presenter := AssetPlacerPresenter._instance 8 | var mode = AssetPlacerPresenter._instance.placement_mode 9 | var settings_repository := AssetPlacerSettingsRepository.instance 10 | _set_plane_material(settings_repository.get_settings()) 11 | settings_repository.settings_changed.connect(_set_plane_material) 12 | 13 | _react_placement_mode_change(mode) 14 | presenter.placement_mode_changed.connect(_react_placement_mode_change) 15 | presenter.asset_deselected.connect(func(): 16 | hide() 17 | ) 18 | 19 | presenter.asset_selected.connect(func(a): 20 | if presenter.placement_mode is PlacementMode.PlanePlacement: 21 | show() 22 | ) 23 | 24 | AssetPlacerPresenter 25 | 26 | 27 | func _set_plane_material(settings: AssetPlacerSettings): 28 | mesh_instance.set_surface_override_material(0, load(settings.plane_material_resource)) 29 | 30 | func _react_placement_mode_change(placement_mode: PlacementMode): 31 | if placement_mode is PlacementMode.PlanePlacement: 32 | show() 33 | _update_mes_per_plane_configuration(placement_mode.plane_options) 34 | else: 35 | hide() 36 | 37 | func _update_mes_per_plane_configuration(plane_options: PlaneOptions): 38 | var normal = plane_options.normal.normalized() 39 | var forward = normal.cross(Vector3.UP).normalized() if (abs(normal.dot(Vector3.UP)) < 0.99) else normal.cross(Vector3.FORWARD).normalized() 40 | var right = forward.cross(normal).normalized() 41 | var basis = Basis(right, normal, forward) 42 | mesh_instance.transform.basis = basis.orthonormalized() 43 | mesh_instance.global_transform = Transform3D(basis.orthonormalized(), plane_options.origin) 44 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_collection_repository.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetCollectionRepository 3 | 4 | var _data_source: AssetLibraryDataSource 5 | var _id_generator: AssetPlacerIdGenerator 6 | 7 | static var instance: AssetCollectionRepository 8 | 9 | signal collections_changed 10 | 11 | func _init(data_source: AssetLibraryDataSource): 12 | self._id_generator = AssetPlacerIdGenerator.new() 13 | self._data_source = data_source 14 | instance = self 15 | 16 | 17 | func get_collections() -> Array[AssetCollection]: 18 | return _data_source.get_library().collections 19 | 20 | 21 | func update_collection(collection: AssetCollection): 22 | var lib = _data_source.get_library() 23 | var collections = lib.collections 24 | for item in collections: 25 | if item.id == collection.id: 26 | item.name = collection.name 27 | item.backgroundColor = collection.backgroundColor 28 | break 29 | lib.collections = collections 30 | _data_source.save_libray(lib) 31 | collections_changed.emit() 32 | 33 | func add_collection(name: String, color: Color): 34 | var lib = _data_source.get_library() 35 | var collection = AssetCollection.new(name, color, _id_generator.next_int()) 36 | lib.collections.append(collection) 37 | _data_source.save_libray(lib) 38 | collections_changed.emit() 39 | 40 | func delete_collection(id: int): 41 | var lib = _data_source.get_library() 42 | var new_collections = lib.collections.filter(func(c): return c.id != id) 43 | lib.collections = new_collections 44 | var assets = lib.items 45 | 46 | for asset in assets: 47 | var updated_tags = asset.tags.filter(func(f): return f != id) 48 | if updated_tags != asset.tags: 49 | asset.tags = updated_tags 50 | 51 | lib.items = assets 52 | 53 | _data_source.save_libray(lib) 54 | collections_changed.emit() 55 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/folders_window/folders_window.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | class_name FoldersWindow 4 | 5 | @onready var v_box_container = %VBoxContainer 6 | @onready var presenter: FolderPresenter = FolderPresenter.new() 7 | @onready var add_folder_button: Button = %AddFolderButton 8 | 9 | @onready var folder_res = preload("res://addons/asset_placer/ui/folders_window/folder_view.tscn") 10 | 11 | 12 | func _ready(): 13 | presenter.folders_loaded.connect(show_folders) 14 | presenter._ready() 15 | 16 | add_folder_button.pressed.connect(func(): 17 | show_folder_dialog() 18 | ) 19 | 20 | func _can_drop_data(at_position, data): 21 | if data is Dictionary: 22 | var type = data["type"] 23 | var dirs = type == "files_and_dirs" 24 | return dirs and data.has("files") 25 | return false 26 | 27 | func _drop_data(at_position, data): 28 | var dirs: PackedStringArray = data["files"] 29 | presenter.add_folders(dirs) 30 | 31 | func show_folders(folders: Array[AssetFolder]): 32 | for child in v_box_container.get_children(): 33 | child.queue_free() 34 | 35 | for folder in folders: 36 | var instance: FolderView = folder_res.instantiate() 37 | v_box_container.add_child(instance) 38 | instance.set_folder(folder) 39 | instance.folder_delete_clicked.connect(func(): 40 | presenter.delete_folder(folder) 41 | ) 42 | instance.folder_include_subfloders_change.connect(func(include): 43 | presenter.include_subfolders(include, folder) 44 | ) 45 | instance.folder_sync_clicked.connect(func(): 46 | presenter.sync_folder(folder) 47 | ) 48 | 49 | 50 | func show_folder_dialog(): 51 | var folder_dialog = EditorFileDialog.new() 52 | folder_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_DIR 53 | folder_dialog.access = EditorFileDialog.ACCESS_RESOURCES 54 | folder_dialog.dir_selected.connect(presenter.add_folder) 55 | EditorInterface.popup_dialog_centered(folder_dialog) 56 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/transformations.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name AssetTransformations 3 | 4 | 5 | static func apply_transforms(node: Node3D, options: AssetPlacerOptions): 6 | node.global_transform = transform_rotation(node.global_transform, options) 7 | node.global_transform = transform_scale(node.global_transform, options) 8 | 9 | static func transform_rotation(transform: Transform3D, options: AssetPlacerOptions) -> Transform3D: 10 | 11 | if not options.rotate_on_placement: 12 | return transform 13 | 14 | 15 | var rx = randf_range(options.min_rotation.x, options.max_rotation.x) 16 | transform = transform.rotated(Vector3(1, 0, 0), deg_to_rad(rx)) 17 | var ry = randf_range(options.min_rotation.y, options.max_rotation.y) 18 | transform = transform.rotated(Vector3(0, 1, 0), deg_to_rad(ry)) 19 | var rz = randf_range(options.min_rotation.z, options.max_rotation.z) 20 | transform = transform.rotated(Vector3(0, 0, 1), deg_to_rad(rz)) 21 | return transform 22 | 23 | 24 | static func transform_scale(transform: Transform3D, options: AssetPlacerOptions) -> Transform3D: 25 | if not options.scale_on_placement: 26 | return transform 27 | 28 | if options.uniform_scaling: 29 | var scale = randf_range(options.min_scale.x, options.max_scale.x) 30 | var basiz = transform.basis.orthonormalized().scaled(Vector3(scale, scale, scale)) 31 | transform.basis = basiz 32 | return transform 33 | else: 34 | var scale_x = randf_range(options.min_scale.x, options.max_scale.x) 35 | var scale_y = randf_range(options.min_scale.y, options.max_scale.y) 36 | var scale_z = randf_range(options.min_scale.z, options.max_scale.z) 37 | var basiz = transform.basis.orthonormalized().scaled(Vector3(scale_x, scale_y, scale_z)) 38 | transform.basis = basiz 39 | return transform 40 | 41 | static func make_uniform(v: Vector3) -> Vector3: 42 | var avg = (v.x + v.y + v.z) / 3.0 43 | return Vector3(avg, avg, avg) 44 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/components/asset_resource_preview.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://bfnj2xve84rvc"] 2 | 3 | [ext_resource type="Script" uid="uid://dacpe4563g5x2" path="res://addons/asset_placer/ui/components/asset_resource_preview.gd" id="1_q3676"] 4 | [ext_resource type="Texture2D" uid="uid://be61c5ff55gfo" path="res://addons/asset_placer/icon.png" id="2_55ohh"] 5 | [ext_resource type="Script" uid="uid://dephufwlu765k" path="res://addons/asset_placer/ui/components/asset_thumbnail.gd" id="2_xdgah"] 6 | 7 | [sub_resource type="LabelSettings" id="LabelSettings_55ohh"] 8 | font_size = 14 9 | 10 | [node name="AssetResourcePreview" type="Button"] 11 | offset_right = 150.0 12 | offset_bottom = 194.33 13 | theme_type_variation = &"FlatButton" 14 | toggle_mode = true 15 | script = ExtResource("1_q3676") 16 | 17 | [node name="MarginContainer" type="MarginContainer" parent="."] 18 | layout_mode = 1 19 | anchors_preset = 15 20 | anchor_right = 1.0 21 | anchor_bottom = 1.0 22 | grow_horizontal = 2 23 | grow_vertical = 2 24 | theme_override_constants/margin_left = 12 25 | theme_override_constants/margin_top = 12 26 | theme_override_constants/margin_right = 12 27 | theme_override_constants/margin_bottom = 12 28 | 29 | [node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] 30 | layout_mode = 2 31 | 32 | [node name="AssetThumbnail" type="TextureRect" parent="MarginContainer/VBoxContainer"] 33 | unique_name_in_owner = true 34 | layout_mode = 2 35 | texture = ExtResource("2_55ohh") 36 | expand_mode = 4 37 | script = ExtResource("2_xdgah") 38 | metadata/_custom_type_script = "uid://dephufwlu765k" 39 | 40 | [node name="Label" type="Label" parent="MarginContainer/VBoxContainer"] 41 | unique_name_in_owner = true 42 | layout_mode = 2 43 | mouse_filter = 1 44 | text = "Asset" 45 | label_settings = SubResource("LabelSettings_55ohh") 46 | horizontal_alignment = 1 47 | autowrap_mode = 1 48 | justification_flags = 227 49 | -------------------------------------------------------------------------------- /addons/asset_placer/data/folder_repository.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name FolderRepository 3 | 4 | var data_source: AssetLibraryDataSource 5 | 6 | static var instance: FolderRepository 7 | 8 | 9 | signal folder_changed 10 | 11 | func _init(data_source: AssetLibraryDataSource): 12 | self.data_source = data_source 13 | instance = self 14 | 15 | func get_all() -> Array[AssetFolder]: 16 | return data_source.get_library().folders 17 | 18 | func find(path: String) -> AssetFolder: 19 | var folders = get_all() 20 | var folder:int = -1; for f in len(folders): if folders[f].path == path: folder = f; break 21 | return folders[folder] 22 | 23 | 24 | func update(folder: AssetFolder): 25 | var library = data_source.get_library() 26 | var folders = library.folders.duplicate() 27 | var to_update_index = folders.find_custom(func(f): return f.path == folder.path) 28 | if to_update_index != -1: 29 | folders[to_update_index] = folder 30 | library.folders = folders 31 | data_source.save_libray(library) 32 | 33 | func add(folder: String, incldude_subfolders: bool = true): 34 | var library := data_source.get_library() 35 | var duplicated_folders := library.folders.duplicate() 36 | 37 | if exists(folder): 38 | push_warning("Folder with this path already exists") 39 | return 40 | 41 | var _folder = AssetFolder.new(folder, incldude_subfolders) 42 | duplicated_folders.append(_folder) 43 | library.folders = duplicated_folders 44 | data_source.save_libray(library) 45 | folder_changed.emit() 46 | 47 | func exists(path: String) -> bool: 48 | for folder in get_all(): 49 | if folder.path == path: 50 | return true 51 | if folder.include_subfolders && path.contains(folder.path): 52 | return true 53 | 54 | return false 55 | func delete(folder: String): 56 | var library := data_source.get_library() 57 | library.folders = library.folders.filter(func(f): return f.path != folder) 58 | data_source.save_libray(library) 59 | folder_changed.emit() 60 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/plugin_updater_http_client.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PluginUpdaterHttpClient 3 | 4 | var _client = HTTPClient.new() 5 | 6 | signal client_response(body: PackedByteArray) 7 | 8 | 9 | func client_get(url: String): 10 | AssetPlacerAsync.instance.enqueue(func(): 11 | var response = _client_get(url) 12 | call_deferred("emit_signal", "client_response", response) 13 | ) 14 | 15 | 16 | 17 | func _client_get(url: String) -> PackedByteArray: 18 | var parts = url.split("/", false, 2) 19 | var host = parts[0] + "//" + parts[1] 20 | _client.connect_to_host(host) 21 | _do_connection_polling() 22 | _client.request(HTTPClient.METHOD_GET, "/" + parts[2], []) 23 | _do_response_polling() 24 | 25 | if _client.get_response_code() in [301, 302, 308]: 26 | var location = _client.get_response_headers_as_dictionary()["Location"] 27 | _client.close() 28 | await_disconection() 29 | return _client_get(location) 30 | 31 | var response = _do_response_body_polling() 32 | _client.close() 33 | return response 34 | 35 | func _do_response_body_polling() -> PackedByteArray: 36 | var response_body = PackedByteArray() 37 | while _client.get_status() == HTTPClient.STATUS_BODY: 38 | _client.poll() 39 | var chunk = _client.read_response_body_chunk() 40 | if chunk.size() != 0: 41 | response_body += chunk 42 | 43 | return response_body 44 | 45 | func _do_connection_polling(): 46 | while _is_connecting(): 47 | _client.poll() 48 | 49 | func _do_response_polling(): 50 | while _is_wating_for_response(): 51 | _client.poll() 52 | 53 | 54 | func _is_wating_for_response() -> bool: 55 | return _client.get_status() == HTTPClient.STATUS_REQUESTING 56 | 57 | 58 | func await_disconection(): 59 | while(_client.get_status() != HTTPClient.STATUS_DISCONNECTED): 60 | _client.poll() 61 | 62 | func _is_connecting() -> bool: 63 | var status = _client.get_status() 64 | return status == HTTPClient.STATUS_CONNECTING or status == HTTPClient.STATUS_RESOLVING 65 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/asset_collection_window.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | 5 | @onready var presenter := AssetCollectionsPresenter.new() 6 | @onready var name_text_field: LineEdit = %NameTextField 7 | @onready var color_picker_button: ColorPickerButton= %ColorPickerButton 8 | @onready var add_button: Button = %AddButton 9 | @onready var collections_container = %CollectionsContainer 10 | @onready var empty_view = %EmptyView 11 | 12 | 13 | var _collection_item_list_resource: PackedScene = preload("res://addons/asset_placer/ui/asset_collections_window/components/collection_list_item.tscn") 14 | 15 | func _ready(): 16 | presenter.enable_create_button.connect(func(enabled): 17 | add_button.disabled = !enabled 18 | ) 19 | presenter.set_color(color_picker_button.color) 20 | presenter.clear_text_field.connect(name_text_field.clear) 21 | presenter.show_collections.connect(show_collections) 22 | presenter.show_empty_view.connect(_show_empty_view) 23 | presenter.ready() 24 | 25 | add_button.pressed.connect(presenter.create_collection) 26 | name_text_field.text_changed.connect(presenter.set_name) 27 | color_picker_button.color_changed.connect(presenter.set_color) 28 | 29 | 30 | func _show_empty_view(): 31 | empty_view.show() 32 | collections_container.hide() 33 | 34 | func show_collections(items: Array[AssetCollection]): 35 | collections_container.show() 36 | empty_view.hide() 37 | for child in collections_container.get_children(): 38 | child.queue_free() 39 | 40 | for item in items: 41 | var list_item = _collection_item_list_resource.instantiate() as AssetCollectionListItem 42 | collections_container.add_child(list_item) 43 | list_item.delete_collection_click.connect(func(): 44 | presenter.delete_collection(item) 45 | ) 46 | list_item.edit_collection_click.connect(func(): 47 | CollectionEditPopupMenu.show_popup(item, func(collection): 48 | presenter._repository.update_collection(collection) 49 | ) 50 | ) 51 | list_item.set_collection(item) 52 | 53 | -------------------------------------------------------------------------------- /addons/asset_placer/data/assets_repository.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetsRepository 3 | 4 | var data_source: AssetLibraryDataSource 5 | 6 | static var instance: AssetsRepository 7 | 8 | signal assets_changed 9 | 10 | func _init(data_source: AssetLibraryDataSource): 11 | self.data_source = data_source 12 | instance = self 13 | 14 | func get_all_assets() -> Array[AssetResource]: 15 | return data_source.get_library().items 16 | 17 | func exists(assetId: String): 18 | return get_all_assets().any(func(item: AssetResource): 19 | return item.id == assetId 20 | ) 21 | 22 | func delete(assetId: String): 23 | var lib = data_source.get_library() 24 | var assets = lib.items.filter(func(a): return a.id != assetId) 25 | lib.items = assets 26 | data_source.save_libray(lib) 27 | call_deferred("emit_signal", "assets_changed") 28 | 29 | func find_by_uid(uid: String) -> AssetResource: 30 | for asset in get_all_assets(): 31 | if asset.id == uid: 32 | return asset 33 | return null 34 | 35 | func update(asset: AssetResource): 36 | var lib = data_source.get_library() 37 | var index = lib.index_of_asset(asset) 38 | if index != -1: 39 | lib.items[index] = asset 40 | data_source.save_libray(lib) 41 | call_deferred("emit_signal", "assets_changed") 42 | 43 | 44 | func add_asset(scene_path: String, tags: Array[int] = [], folder_path: String = "") -> bool: 45 | if not is_file_supported(scene_path.get_file()): 46 | return false 47 | 48 | var library = data_source.get_library() 49 | var id = ResourceIdCompat.path_to_uid(scene_path) 50 | if exists(id): 51 | return false 52 | var asset = AssetResource.new(id, scene_path.get_file(), tags, folder_path) 53 | var duplicated_items = library.items.duplicate() 54 | duplicated_items.append(asset) 55 | library.items = duplicated_items 56 | data_source.save_libray(library) 57 | call_deferred("emit_signal", "assets_changed") 58 | return true 59 | 60 | 61 | func is_file_supported(file: String) -> bool: 62 | var extension = file.get_extension() 63 | var supported_extensions = ["tscn", "glb", "fbx", "obj", "gltf"] 64 | return extension in supported_extensions 65 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/system_icon.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Texture2D 3 | class_name EditorIconTexture2D 4 | 5 | @export var icon_name: StringName = &"Node2D" 6 | 7 | var _resolved_icon: Texture2D = null; 8 | 9 | func _init(name: String = &"Node2D"): 10 | self.icon_name = name 11 | 12 | func _resolve(): 13 | if not Engine.is_editor_hint(): 14 | return # Don’t try resolving outside editor 15 | 16 | var theme := EditorInterface.get_editor_theme() 17 | if theme and icon_name != "": 18 | if theme.has_icon(icon_name, "EditorIcons"): 19 | _resolved_icon = theme.get_icon(icon_name, "EditorIcons") 20 | else: 21 | _resolved_icon = theme.get_icon(&"Node3D", "EditorIcons") 22 | 23 | # Called automatically by Control.draw() and other systems 24 | func _draw(to_canvas_item: RID, pos: Vector2, modulate: Color, transpose: bool) -> void: 25 | _resolve() 26 | if _resolved_icon: 27 | _resolved_icon.draw(to_canvas_item, pos, modulate, transpose) 28 | 29 | func _draw_rect(to_canvas_item: RID, rect: Rect2, tile: bool, modulate: Color, transpose: bool) -> void: 30 | _resolve() 31 | if _resolved_icon: 32 | _resolved_icon.draw_rect(to_canvas_item, rect, tile, modulate, transpose) 33 | 34 | func _draw_rect_region(to_canvas_item: RID, rect: Rect2, src_rect: Rect2, modulate: Color, transpose: bool, clip_uv: bool) -> void: 35 | _resolve() 36 | if _resolved_icon: 37 | _resolved_icon.draw_rect_region(to_canvas_item, rect, src_rect, modulate, transpose, clip_uv) 38 | 39 | func _get_width() -> int: 40 | _resolve() 41 | return _resolved_icon.get_width() if _resolved_icon else 1 42 | 43 | func _get_height() -> int: 44 | _resolve() 45 | return _resolved_icon.get_height() if _resolved_icon else 1 46 | 47 | func _has_alpha() -> bool: 48 | _resolve() 49 | return _resolved_icon.has_alpha() if _resolved_icon else true 50 | 51 | func _is_pixel_opaque(x: int, y: int) -> bool: 52 | _resolve() 53 | return _resolved_icon.is_pixel_opaque(x, y) if _resolved_icon else false 54 | 55 | func get_image() -> Image: 56 | _resolve() 57 | return _resolved_icon.get_image() if _resolved_icon else Image.new() 58 | 59 | func get_size() -> Vector2: 60 | return Vector2(_get_width(), _get_height()) 61 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/collection_picker/collection_picker.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends PopupMenu 3 | class_name CollectionPicker 4 | 5 | signal collection_selected(collection: AssetCollection, selected: bool) 6 | 7 | @onready var presenter := AssetCollectionsPresenter.new() 8 | 9 | var pre_selected: Array[AssetCollection] 10 | 11 | func _ready(): 12 | hide_on_checkable_item_selection = false 13 | presenter.show_collections.connect(show_collections) 14 | presenter.show_empty_view.connect(show_empty_view) 15 | presenter.ready() 16 | 17 | 18 | func show_empty_view(): 19 | add_item("No Collections added yet") 20 | index_pressed.connect(func(i): 21 | AssetPlacerDockPresenter.instance.show_tab.emit( 22 | AssetPlacerDockPresenter.Tab.Collections 23 | ) 24 | ) 25 | 26 | func show_collections(collections: Array[AssetCollection]): 27 | for i in collections.size(): 28 | var collection_id = collections[i].id 29 | var selected = pre_selected.any(func(c): return c.id == collection_id) 30 | add_check_item(collections[i].name) 31 | set_item_checked(i, selected) 32 | set_item_icon(i, make_circle_icon(16, collections[i].backgroundColor)) 33 | 34 | index_pressed.connect(func(index): 35 | toggle_item_checked(index) 36 | collection_selected.emit(collections[index], is_item_checked(index)) 37 | ) 38 | 39 | func make_circle_icon(radius: int, color: Color) -> Texture2D: 40 | var size = radius * 2 41 | var img := Image.create(size, size, false, Image.FORMAT_RGBA8) 42 | img.fill(Color(0, 0, 0, 0)) # Transparent background 43 | 44 | for y in size: 45 | for x in size: 46 | var dist = Vector2(x, y).distance_to(Vector2(radius, radius)) 47 | if dist <= radius: 48 | img.set_pixel(x, y, color) 49 | 50 | img.generate_mipmaps() 51 | 52 | var tex := ImageTexture.create_from_image(img) 53 | return tex 54 | 55 | static func show_in(context: Control, selected: Array[AssetCollection], on_select: Callable): 56 | var picker: CollectionPicker = CollectionPicker.new() 57 | picker.collection_selected.connect(on_select) 58 | picker.pre_selected = selected 59 | var size = picker.get_contents_minimum_size() 60 | var position = DisplayServer.mouse_get_position() 61 | EditorInterface.popup_dialog(picker, Rect2(position, size)) 62 | -------------------------------------------------------------------------------- /addons/asset_placer/data/migrations/collection_id_migration.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | var id_generator: AssetPlacerIdGenerator 4 | 5 | func _init(): 6 | id_generator = AssetPlacerIdGenerator.new() 7 | 8 | 9 | func run(): 10 | # Read the raw JSON data directly 11 | var file_path = "user://asset_library.json" 12 | var file = FileAccess.open(file_path, FileAccess.READ) 13 | if file == null || file.get_as_text().is_empty(): 14 | return 15 | 16 | var json_text = file.get_as_text() 17 | file.close() 18 | 19 | 20 | var data: Dictionary = JSON.parse_string(json_text) 21 | if data == null: 22 | push_error("Failed to parse JSON data") 23 | return 24 | 25 | if data.has("version") and data["version"] == 2: 26 | return 27 | 28 | # Create backup copy before migration 29 | var backup_path := "user://asset_library_backup_v1.json" 30 | var backup_file = FileAccess.open(backup_path, FileAccess.WRITE) 31 | if backup_file != null: 32 | backup_file.store_string(json_text) 33 | backup_file.close() 34 | 35 | # Create mapping from collection names to IDs 36 | var collection_name_to_id = {} 37 | 38 | # Process collections to add IDs 39 | var collections_dict = data["collections"] 40 | for collection_dict in collections_dict: 41 | var id := id_generator.next_int() 42 | collection_dict["id"] = id 43 | collection_name_to_id[collection_dict["name"]] = id 44 | 45 | # Process assets to convert collection names to IDs in tags 46 | var assets_dict = data["assets"] 47 | for asset_dict in assets_dict: 48 | if asset_dict.has("tags"): 49 | var updated_tags :Array[int] = [] 50 | for tag in asset_dict["tags"]: 51 | if collection_name_to_id.has(tag): 52 | updated_tags.append(collection_name_to_id[tag]) 53 | else: 54 | pass 55 | asset_dict["tags"] = updated_tags 56 | 57 | # Write the updated JSON back to file 58 | data["version"] = 2 59 | var updated_json = JSON.stringify(data) 60 | var write_file = FileAccess.open(file_path, FileAccess.WRITE) 61 | write_file.store_string(updated_json) 62 | write_file.close() 63 | 64 | print("Collection ID migration completed successfully!") 65 | print("Migrated %d collections and %d assets" % [collections_dict.size(), assets_dict.size()]) 66 | print("Collections now have IDs and are referenced by ID in asset tags") 67 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_library_panel.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://c47vtmub8cdjf"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://3qun24bndqll" path="res://addons/asset_placer/ui/asset_library_window/asset_library_window.tscn" id="1_535dl"] 4 | [ext_resource type="Script" uid="uid://bracm8jmjlw2v" path="res://addons/asset_placer/ui/asset_library_panel.gd" id="1_otjr6"] 5 | [ext_resource type="PackedScene" uid="uid://4jjhsl7nom4c" path="res://addons/asset_placer/ui/folders_window/folders_window.tscn" id="2_otjr6"] 6 | [ext_resource type="PackedScene" uid="uid://cjk0aw5iw4qb6" path="res://addons/asset_placer/ui/asset_collections_window/asset_collection_window.tscn" id="4_lanc6"] 7 | [ext_resource type="PackedScene" uid="uid://dynyrwa38r5ag" path="res://addons/asset_placer/ui/about/about_window.tscn" id="5_rgwf1"] 8 | [ext_resource type="PackedScene" uid="uid://ckokuu85fua3" path="res://addons/asset_placer/ui/settings/settings_panel.tscn" id="5_wbjry"] 9 | 10 | [node name="AssetLibraryPanel" type="Control"] 11 | custom_minimum_size = Vector2(0, 300) 12 | layout_mode = 3 13 | anchors_preset = 15 14 | anchor_right = 1.0 15 | anchor_bottom = 1.0 16 | grow_horizontal = 2 17 | grow_vertical = 2 18 | script = ExtResource("1_otjr6") 19 | 20 | [node name="Panel" type="Panel" parent="."] 21 | layout_mode = 1 22 | anchors_preset = 15 23 | anchor_right = 1.0 24 | anchor_bottom = 1.0 25 | grow_horizontal = 2 26 | grow_vertical = 2 27 | 28 | [node name="TabContainer" type="TabContainer" parent="Panel"] 29 | layout_mode = 1 30 | anchors_preset = 15 31 | anchor_right = 1.0 32 | anchor_bottom = 1.0 33 | grow_horizontal = 2 34 | grow_vertical = 2 35 | current_tab = 0 36 | 37 | [node name="Assets" parent="Panel/TabContainer" instance=ExtResource("1_535dl")] 38 | layout_mode = 2 39 | metadata/_tab_index = 0 40 | 41 | [node name="Folders" parent="Panel/TabContainer" instance=ExtResource("2_otjr6")] 42 | visible = false 43 | layout_mode = 2 44 | metadata/_tab_index = 1 45 | 46 | [node name="Collections" parent="Panel/TabContainer" instance=ExtResource("4_lanc6")] 47 | visible = false 48 | layout_mode = 2 49 | metadata/_tab_index = 2 50 | 51 | [node name="Settings" parent="Panel/TabContainer" instance=ExtResource("5_wbjry")] 52 | visible = false 53 | layout_mode = 2 54 | metadata/_tab_index = 3 55 | 56 | [node name="About" parent="Panel/TabContainer" instance=ExtResource("5_rgwf1")] 57 | visible = false 58 | layout_mode = 2 59 | metadata/_tab_index = 4 60 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_placer_settings.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacerSettings 3 | 4 | var transform_step: float 5 | var rotation_step: float 6 | var preview_material_resource: String 7 | var plane_material_resource: String 8 | var bindings: Dictionary 9 | var ui_scale: float 10 | var asset_library_path: String 11 | var update_channel: UpdateChannel 12 | 13 | var binding_positive_transform: APInputOption: 14 | get(): return bindings[Bindings.TransformPositive] 15 | 16 | var binding_negative_transform: APInputOption: 17 | get(): return bindings[Bindings.TransformNegative] 18 | 19 | 20 | 21 | enum Bindings { 22 | Rotate, 23 | Scale, 24 | Translate, 25 | GridSnapping, 26 | InPlaceTransform, 27 | TransformPositive, 28 | TransformNegative, 29 | ToggleAxisX, 30 | ToggleAxisZ, 31 | ToggleAxisY, 32 | TogglePlaneMode 33 | } 34 | 35 | enum UpdateChannel { 36 | Stable, 37 | Beta, 38 | Alpha 39 | } 40 | 41 | 42 | 43 | static func default() -> AssetPlacerSettings: 44 | var settings = AssetPlacerSettings.new() 45 | settings.bindings[Bindings.TransformNegative] = APInputOption.mouse_press(MouseButton.MOUSE_BUTTON_WHEEL_UP) 46 | settings.bindings[Bindings.TransformPositive] = APInputOption.mouse_press(MouseButton.MOUSE_BUTTON_WHEEL_DOWN) 47 | settings.bindings[Bindings.Rotate] = APInputOption.key_press(Key.KEY_E) 48 | settings.bindings[Bindings.Scale] = APInputOption.key_press(Key.KEY_R) 49 | settings.bindings[Bindings.Translate] = APInputOption.key_press(Key.KEY_W) 50 | settings.bindings[Bindings.GridSnapping] = APInputOption.key_press(Key.KEY_S) 51 | settings.bindings[Bindings.InPlaceTransform] = APInputOption.key_press(Key.KEY_E, KeyModifierMask.KEY_MASK_SHIFT) 52 | settings.bindings[Bindings.ToggleAxisX] = APInputOption.key_press(Key.KEY_X) 53 | settings.bindings[Bindings.ToggleAxisY] = APInputOption.key_press(Key.KEY_Y) 54 | settings.bindings[Bindings.ToggleAxisZ] = APInputOption.key_press(Key.KEY_Z) 55 | settings.bindings[Bindings.TogglePlaneMode] = APInputOption.key_press(Key.KEY_Q) 56 | settings.preview_material_resource = "res://addons/asset_placer/utils/preview_material.tres" 57 | settings.plane_material_resource = "res://addons/asset_placer/ui/plane_preview/plane_preview_material.tres" 58 | settings.transform_step = 0.1 59 | settings.rotation_step = 5 60 | settings.ui_scale = 1.0 61 | settings.asset_library_path = "user://asset_library.json" 62 | settings.update_channel = UpdateChannel.Stable 63 | return settings 64 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/components/collection_list_item.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://cs82bn2o7aref"] 2 | 3 | [ext_resource type="Script" uid="uid://dmicn3kmr620j" path="res://addons/asset_placer/utils/system_icon.gd" id="1_q0bvl"] 4 | [ext_resource type="Script" uid="uid://bf5fmwafbhhb0" path="res://addons/asset_placer/ui/asset_collections_window/components/collection_list_item.gd" id="1_xjg5v"] 5 | [ext_resource type="Texture2D" uid="uid://btqtv2stkxtdk" path="res://docs/logo.png" id="2_ligpx"] 6 | 7 | [sub_resource type="Texture2D" id="Texture2D_xjg5v"] 8 | resource_local_to_scene = false 9 | resource_name = "" 10 | script = ExtResource("1_q0bvl") 11 | icon_name = &"Edit" 12 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 13 | 14 | [sub_resource type="Texture2D" id="Texture2D_q0bvl"] 15 | resource_local_to_scene = false 16 | resource_name = "" 17 | script = ExtResource("1_q0bvl") 18 | icon_name = &"Remove" 19 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 20 | 21 | [node name="HBoxContainer" type="HBoxContainer"] 22 | anchors_preset = 10 23 | anchor_right = 1.0 24 | offset_bottom = 32.0 25 | grow_horizontal = 2 26 | script = ExtResource("1_xjg5v") 27 | 28 | [node name="MarginContainer" type="MarginContainer" parent="."] 29 | layout_mode = 2 30 | size_flags_horizontal = 3 31 | theme_override_constants/margin_left = 8 32 | theme_override_constants/margin_top = 8 33 | theme_override_constants/margin_right = 8 34 | theme_override_constants/margin_bottom = 8 35 | 36 | [node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"] 37 | layout_mode = 2 38 | theme_override_constants/separation = 8 39 | 40 | [node name="TextureRect" type="TextureRect" parent="MarginContainer/HBoxContainer"] 41 | unique_name_in_owner = true 42 | custom_minimum_size = Vector2(24, 24) 43 | layout_mode = 2 44 | texture = ExtResource("2_ligpx") 45 | expand_mode = 5 46 | stretch_mode = 5 47 | flip_h = true 48 | 49 | [node name="NameLabel" type="Label" parent="MarginContainer/HBoxContainer"] 50 | unique_name_in_owner = true 51 | layout_mode = 2 52 | size_flags_horizontal = 3 53 | text = "Label" 54 | 55 | [node name="EditButton" type="Button" parent="MarginContainer/HBoxContainer"] 56 | unique_name_in_owner = true 57 | layout_mode = 2 58 | text = "Edit" 59 | icon = SubResource("Texture2D_xjg5v") 60 | 61 | [node name="DeleteButton" type="Button" parent="MarginContainer/HBoxContainer"] 62 | unique_name_in_owner = true 63 | layout_mode = 2 64 | text = "Remove" 65 | icon = SubResource("Texture2D_q0bvl") 66 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/folders_window/folders_window.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://4jjhsl7nom4c"] 2 | 3 | [ext_resource type="Script" uid="uid://tkdl83npcow1" path="res://addons/asset_placer/ui/folders_window/folders_window.gd" id="1_umbsx"] 4 | [ext_resource type="Script" uid="uid://dmicn3kmr620j" path="res://addons/asset_placer/utils/system_icon.gd" id="2_1gh27"] 5 | 6 | [sub_resource type="Texture2D" id="Texture2D_caby4"] 7 | resource_local_to_scene = false 8 | resource_name = "" 9 | script = ExtResource("2_1gh27") 10 | icon_name = &"FolderCreate" 11 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 12 | 13 | [node name="FoldersWindow" type="Control"] 14 | layout_mode = 3 15 | anchors_preset = 15 16 | anchor_right = 1.0 17 | anchor_bottom = 1.0 18 | grow_horizontal = 2 19 | grow_vertical = 2 20 | mouse_filter = 1 21 | script = ExtResource("1_umbsx") 22 | 23 | [node name="Panel" type="Panel" parent="."] 24 | layout_mode = 1 25 | anchors_preset = 15 26 | anchor_right = 1.0 27 | anchor_bottom = 1.0 28 | grow_horizontal = 2 29 | grow_vertical = 2 30 | mouse_filter = 1 31 | 32 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel"] 33 | layout_mode = 1 34 | anchors_preset = 15 35 | anchor_right = 1.0 36 | anchor_bottom = 1.0 37 | grow_horizontal = 2 38 | grow_vertical = 2 39 | 40 | [node name="MarginContainer" type="MarginContainer" parent="Panel/VBoxContainer"] 41 | layout_mode = 2 42 | theme_override_constants/margin_left = 16 43 | theme_override_constants/margin_top = 16 44 | theme_override_constants/margin_right = 16 45 | theme_override_constants/margin_bottom = 16 46 | 47 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer/MarginContainer"] 48 | layout_mode = 2 49 | alignment = 2 50 | 51 | [node name="AddFolderButton" type="Button" parent="Panel/VBoxContainer/MarginContainer/HBoxContainer"] 52 | unique_name_in_owner = true 53 | layout_mode = 2 54 | tooltip_text = "FolderCreate" 55 | icon = SubResource("Texture2D_caby4") 56 | 57 | [node name="ScrollContainer" type="ScrollContainer" parent="Panel/VBoxContainer"] 58 | layout_mode = 2 59 | size_flags_vertical = 3 60 | 61 | [node name="MarginContainer" type="MarginContainer" parent="Panel/VBoxContainer/ScrollContainer"] 62 | layout_mode = 2 63 | size_flags_horizontal = 3 64 | size_flags_vertical = 3 65 | theme_override_constants/margin_left = 16 66 | theme_override_constants/margin_top = 16 67 | theme_override_constants/margin_right = 16 68 | theme_override_constants/margin_bottom = 16 69 | 70 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel/VBoxContainer/ScrollContainer/MarginContainer"] 71 | unique_name_in_owner = true 72 | layout_mode = 2 73 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/edit/collection_edit_popup.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://b532q2vsr1ryc"] 2 | 3 | [ext_resource type="Script" uid="uid://dmicn3kmr620j" path="res://addons/asset_placer/utils/system_icon.gd" id="1_20mcy"] 4 | [ext_resource type="Script" uid="uid://dwwbuico5765w" path="res://addons/asset_placer/ui/asset_collections_window/edit/collection_edit_popup.gd" id="1_jhaji"] 5 | 6 | [sub_resource type="Texture2D" id="Texture2D_jhaji"] 7 | resource_local_to_scene = false 8 | resource_name = "" 9 | script = ExtResource("1_20mcy") 10 | icon_name = &"Save" 11 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 12 | 13 | [node name="CollectionEditPopup" type="Popup"] 14 | oversampling_override = 1.0 15 | size = Vector2i(500, 300) 16 | visible = true 17 | script = ExtResource("1_jhaji") 18 | 19 | [node name="Panel" type="Panel" parent="."] 20 | anchors_preset = 15 21 | anchor_right = 1.0 22 | anchor_bottom = 1.0 23 | grow_horizontal = 2 24 | grow_vertical = 2 25 | 26 | [node name="MarginContainer" type="MarginContainer" parent="Panel"] 27 | layout_mode = 1 28 | anchors_preset = 15 29 | anchor_right = 1.0 30 | anchor_bottom = 1.0 31 | grow_horizontal = 2 32 | grow_vertical = 2 33 | theme_override_constants/margin_left = 12 34 | theme_override_constants/margin_top = 12 35 | theme_override_constants/margin_right = 12 36 | theme_override_constants/margin_bottom = 12 37 | 38 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"] 39 | layout_mode = 2 40 | theme_override_constants/separation = 12 41 | 42 | [node name="Label" type="Label" parent="Panel/MarginContainer/VBoxContainer"] 43 | layout_mode = 2 44 | text = "Edit Collection" 45 | 46 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"] 47 | layout_mode = 2 48 | size_flags_vertical = 3 49 | 50 | [node name="ColorPickerButton" type="ColorPickerButton" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer"] 51 | unique_name_in_owner = true 52 | custom_minimum_size = Vector2(50, 50) 53 | layout_mode = 2 54 | size_flags_horizontal = 0 55 | size_flags_vertical = 4 56 | 57 | [node name="TextEdit" type="TextEdit" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer"] 58 | unique_name_in_owner = true 59 | custom_minimum_size = Vector2(0, 48) 60 | layout_mode = 2 61 | size_flags_horizontal = 3 62 | size_flags_vertical = 4 63 | placeholder_text = "Type new Collectio Name 64 | " 65 | 66 | [node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"] 67 | layout_mode = 2 68 | 69 | [node name="SaveButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer2"] 70 | unique_name_in_owner = true 71 | layout_mode = 2 72 | size_flags_horizontal = 10 73 | text = "Save" 74 | icon = SubResource("Texture2D_jhaji") 75 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/settings_presenter.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name SettingsPresenter 3 | 4 | var _repository: AssetPlacerSettingsRepository 5 | 6 | signal show_settings(settings: AssetPlacerSettings) 7 | 8 | func _init(): 9 | _repository = AssetPlacerSettingsRepository.instance 10 | 11 | func ready(): 12 | show_settings.emit(_repository.get_settings()) 13 | _repository.settings_changed.connect(show_settings.emit) 14 | 15 | func reset_to_defaults(): 16 | var default = AssetPlacerSettings.default() 17 | # keep path 18 | default.asset_library_path = _repository.get_settings().asset_library_path 19 | _repository.set_settings(AssetPlacerSettings.default()) 20 | 21 | func set_default_transform_step(value: float): 22 | var settings = _repository.get_settings() 23 | settings.transform_step = value 24 | _repository.set_settings(settings) 25 | 26 | func set_ui_scale(value: float): 27 | var settings = _repository.get_settings() 28 | settings.ui_scale = value 29 | _repository.set_settings(settings) 30 | 31 | func set_rotation_step(value: float): 32 | var settings = _repository.get_settings() 33 | settings.rotation_step = value 34 | _repository.set_settings(settings) 35 | 36 | func set_update_channel(value: AssetPlacerSettings.UpdateChannel): 37 | var settings = _repository.get_settings() 38 | settings.update_channel = value 39 | _repository.set_settings(settings) 40 | 41 | func set_preview_material(material: String): 42 | if material.is_empty(): 43 | return 44 | var current = _repository.get_settings() 45 | current.preview_material_resource = material 46 | _repository.set_settings(current) 47 | 48 | func set_plane_material(material: String): 49 | if not material.is_empty(): 50 | var current = _repository.get_settings() 51 | current.plane_material_resource = material 52 | _repository.set_settings(current) 53 | 54 | 55 | func clear_preivew_material(): 56 | var current = _repository.get_settings() 57 | current.preview_material_resource = "" 58 | _repository.set_settings(current) 59 | 60 | func set_rotate_binding(key: APInputOption): 61 | var current = _repository.get_settings() 62 | current.binding_rotate = key 63 | _repository.set_settings(current) 64 | 65 | func set_grid_snap_binding(key: APInputOption): 66 | var current = _repository.get_settings() 67 | current.binding_grid_snap = key 68 | _repository.set_settings(current) 69 | 70 | func set_scale_binding(key: APInputOption): 71 | var current = _repository.get_settings() 72 | current.binding_scale = key 73 | _repository.set_settings(current) 74 | 75 | func set_binding_in_place_transform(key: APInputOption): 76 | var current = _repository.get_settings() 77 | current.binding_in_place_transform = key 78 | _repository.set_settings(current) 79 | 80 | func set_binding(key: AssetPlacerSettings.Bindings, input: APInputOption): 81 | var current_settings = _repository.get_settings() 82 | current_settings.bindings[key] = input 83 | _repository.set_settings(current_settings) 84 | 85 | func set_translate_binding(key: APInputOption): 86 | var current = _repository.get_settings() 87 | current.binding_translate = key 88 | _repository.set_settings(current) 89 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/folders_window/folder_view.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://d7ay5upbgnx0"] 2 | 3 | [ext_resource type="Script" uid="uid://dclcoehldtdcd" path="res://addons/asset_placer/ui/folders_window/folder_view.gd" id="1_q40po"] 4 | [ext_resource type="Script" uid="uid://dmicn3kmr620j" path="res://addons/asset_placer/utils/system_icon.gd" id="2_q40po"] 5 | 6 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q40po"] 7 | content_margin_left = 0.0 8 | content_margin_top = 0.0 9 | content_margin_right = 0.0 10 | content_margin_bottom = 0.0 11 | bg_color = Color(0.1, 0.1, 0.1, 0.6) 12 | corner_radius_top_left = 16 13 | corner_radius_top_right = 16 14 | corner_radius_bottom_right = 3 15 | corner_radius_bottom_left = 3 16 | corner_detail = 5 17 | 18 | [sub_resource type="Texture2D" id="Texture2D_q40po"] 19 | resource_local_to_scene = false 20 | resource_name = "" 21 | script = ExtResource("2_q40po") 22 | icon_name = &"Reload" 23 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 24 | 25 | [sub_resource type="Texture2D" id="Texture2D_ojff7"] 26 | resource_local_to_scene = false 27 | resource_name = "" 28 | script = ExtResource("2_q40po") 29 | icon_name = &"Remove" 30 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 31 | 32 | [node name="FolderView" type="Control"] 33 | custom_minimum_size = Vector2(0, 100) 34 | layout_mode = 3 35 | anchors_preset = 10 36 | anchor_right = 1.0 37 | grow_horizontal = 2 38 | size_flags_horizontal = 3 39 | script = ExtResource("1_q40po") 40 | 41 | [node name="Panel" type="Panel" parent="."] 42 | layout_mode = 1 43 | anchors_preset = 15 44 | anchor_right = 1.0 45 | anchor_bottom = 1.0 46 | grow_horizontal = 2 47 | grow_vertical = 2 48 | theme_override_styles/panel = SubResource("StyleBoxFlat_q40po") 49 | 50 | [node name="MarginContainer" type="MarginContainer" parent="Panel"] 51 | layout_mode = 1 52 | anchors_preset = -1 53 | anchor_top = 0.5 54 | anchor_right = 1.0 55 | anchor_bottom = 0.5 56 | offset_top = -18.0 57 | offset_bottom = 18.0 58 | grow_horizontal = 2 59 | grow_vertical = 2 60 | theme_override_constants/margin_left = 16 61 | theme_override_constants/margin_top = 16 62 | theme_override_constants/margin_right = 16 63 | theme_override_constants/margin_bottom = 16 64 | 65 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer"] 66 | layout_mode = 2 67 | theme_override_constants/separation = 32 68 | alignment = 2 69 | 70 | [node name="PathLabel" type="Label" parent="Panel/MarginContainer/HBoxContainer"] 71 | unique_name_in_owner = true 72 | layout_mode = 2 73 | size_flags_horizontal = 3 74 | text = "Folder path" 75 | 76 | [node name="SubfoldersCheckbox" type="CheckBox" parent="Panel/MarginContainer/HBoxContainer"] 77 | unique_name_in_owner = true 78 | layout_mode = 2 79 | text = "Include sub folders" 80 | 81 | [node name="SyncButton" type="Button" parent="Panel/MarginContainer/HBoxContainer"] 82 | unique_name_in_owner = true 83 | layout_mode = 2 84 | tooltip_text = "Reload" 85 | icon = SubResource("Texture2D_q40po") 86 | 87 | [node name="DeleteButton" type="Button" parent="Panel/MarginContainer/HBoxContainer"] 88 | unique_name_in_owner = true 89 | layout_mode = 2 90 | tooltip_text = "Remove" 91 | icon = SubResource("Texture2D_ojff7") 92 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_library_data_source.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetLibraryDataSource 3 | 4 | var _asset_lib_json: String 5 | 6 | func _init(asset_lib_path: String = "user://asset_library.json"): 7 | _asset_lib_json = asset_lib_path 8 | 9 | func get_library() -> AssetLibrary: 10 | var file = FileAccess.open(_asset_lib_json, FileAccess.READ) 11 | if file == null || file.get_as_text().is_empty(): 12 | return AssetLibrary.new([], [], []) 13 | else: 14 | var data = JSON.parse_string(file.get_as_text()) 15 | var folders_dicts: Array = data["folders"] 16 | var assets_dicts: Array = data["assets"] 17 | var collections_dict: Array = data["collections"] 18 | 19 | var folders: Array[AssetFolder] 20 | var assets: Array[AssetResource] 21 | var collections: Array[AssetCollection] 22 | 23 | for folder_dict in folders_dicts: 24 | var path = folder_dict["path"] 25 | var include_subfolders = folder_dict["include_subfolders"] 26 | var folder = AssetFolder.new(path, include_subfolders) 27 | folders.append(folder) 28 | 29 | for asset_dict in assets_dicts: 30 | var name = asset_dict["name"] 31 | var id = asset_dict["id"] 32 | var folder_path := "" 33 | if asset_dict.has("folder_path"): 34 | folder_path = asset_dict["folder_path"] 35 | var dict = asset_dict as Dictionary 36 | var tags: Array[int] = [] 37 | if dict.has("tags"): 38 | var raw_tags = dict["tags"] 39 | for tag in raw_tags: 40 | tags.append(int(tag)) 41 | var asset = AssetResource.new(id, name, tags, folder_path) 42 | assets.append(asset) 43 | 44 | for collection_dict in collections_dict: 45 | var name = collection_dict["name"] 46 | var color_string: String = collection_dict["color"] 47 | var color = Color.from_string(color_string, Color.AQUA) 48 | var id: int = collection_dict["id"] 49 | collections.append(AssetCollection.new(name, color, id)) 50 | 51 | file.close() 52 | return AssetLibrary.new(assets, folders, collections) 53 | 54 | 55 | func save_libray(library: AssetLibrary): 56 | if library: 57 | var assets_dict : Array[Dictionary] = [] 58 | var folders_dict: Array[Dictionary] = [] 59 | var collections_dict: Array[Dictionary] = [] 60 | 61 | for folder in library.folders: 62 | folders_dict.append({ 63 | "path": folder.path, 64 | "include_subfolders": folder.include_subfolders 65 | }) 66 | 67 | for asset in library.items: 68 | assets_dict.append({ 69 | "name": asset.name, 70 | "id": asset.id, 71 | "tags": asset.tags, 72 | "folder_path": asset.folder_path 73 | }) 74 | 75 | for collection in library.collections: 76 | collections_dict.append({ 77 | "name": collection.name, 78 | "color": collection.backgroundColor.to_html(), 79 | "id": collection.id 80 | }) 81 | 82 | var lib_dict = { 83 | "assets": assets_dict, 84 | "folders": folders_dict, 85 | "collections": collections_dict, 86 | "version": 2 87 | } 88 | 89 | var json = JSON.stringify(lib_dict) 90 | var file = FileAccess.open(_asset_lib_json, FileAccess.WRITE) 91 | file.store_string(json) 92 | file.close() 93 | 94 | else: 95 | push_error("AssetLibraryDataSource: Cannot save null library.") 96 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/version.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name Version 3 | 4 | var major: int 5 | var minor: int 6 | var patch: int 7 | var identifier: Identifier 8 | 9 | 10 | enum Track { 11 | Alpha = 0, 12 | Beta = 1, 13 | ReleaseCandidate = 2, 14 | } 15 | 16 | class Identifier: 17 | var track: Track 18 | var number: int 19 | 20 | func _to_string() -> String: 21 | var _track: String 22 | match track: 23 | Track.Alpha: _track = "alpha" 24 | Track.Beta: _track = "beta" 25 | Track.ReleaseCandidate: _track = "rc" 26 | return "%s%d" % [_track, number] 27 | 28 | func _init(version: String): 29 | var regex := RegEx.new() 30 | regex.compile(r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)))?(?:\+(?P[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$") 31 | 32 | var result := regex.search(version) 33 | if result: 34 | major = int(result.get_string("major")) 35 | minor = int(result.get_string("minor")) 36 | patch = int(result.get_string("patch")) 37 | var _identifier = result.get_string("prerelease") if result.get_string("prerelease") != "" else "" 38 | self.identifier = _parse_identifier(_identifier) 39 | 40 | 41 | func _to_string(): 42 | var main = "%s.%s.%s" % [major, minor, patch] 43 | return main if identifier == null else main + "-%s" % identifier._to_string() 44 | 45 | 46 | func changelog_version() -> String: 47 | var main = "%s%s%s" % [major, minor, patch] 48 | return main if identifier == null else main + "---%s" % identifier ._to_string() 49 | 50 | 51 | func compare_to(other: Version) -> int: 52 | # Compare MAJOR 53 | if major < other.major: 54 | return -1 55 | elif major > other.major: 56 | return 1 57 | 58 | # Compare MINOR 59 | if minor < other.minor: 60 | return -1 61 | elif minor > other.minor: 62 | return 1 63 | 64 | # Compare PATCH 65 | if patch < other.patch: 66 | return -1 67 | elif patch > other.patch: 68 | return 1 69 | 70 | # Compare prerelease identifiers 71 | return compare_prerelease(identifier, other.identifier) 72 | 73 | func compare_prerelease(id1: Identifier, id2: Identifier) -> int: 74 | 75 | if id1 == null: 76 | return 1 77 | if id2 == null: 78 | return -1 79 | 80 | if id1.track == id2.track: 81 | var diff = id1.number - id2.number; 82 | return clampi(diff, -1, 1) 83 | else: 84 | var track_diff = (id1.track - id2.track) 85 | return clamp(track_diff, -1, 1) 86 | 87 | static func _parse_identifier(text: String) -> Identifier: 88 | 89 | if text.is_empty(): return null 90 | 91 | # Match alpha04, beta03, rc12 (case-insensitive) 92 | var regex := RegEx.new() 93 | regex.compile(r"(?i)^(alpha|beta|rc)(\d+)$") 94 | 95 | var match := regex.search(text.strip_edges()) 96 | if not match: 97 | push_error("Invalid identifier format: %s" % text) 98 | return null 99 | 100 | var track_str := match.get_string(1).to_lower() 101 | var number_str := match.get_string(2) 102 | 103 | var track_map := { 104 | "alpha": Track.Alpha, 105 | "beta": Track.Beta, 106 | "rc": Track.ReleaseCandidate 107 | } 108 | 109 | var id := Identifier.new() 110 | id.track = track_map[track_str] 111 | id.number = int(number_str) 112 | return id 113 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/about/about_window.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | 5 | @onready var version_label: Label = %VersionLabel 6 | 7 | @onready var feature_request_button: Button = %FeatureRequestButton 8 | @onready var issue_button: Button = %IssueButton 9 | @onready var update_button: Button = %UpdateButton 10 | @onready var apply_update_button: Button = %ApplyUpdateAndRestartBtn 11 | @onready var update_version_label: Label = %UpdateVersionLabel 12 | @onready var changelog_link_button: LinkButton = %ChangelogLinkButton 13 | @onready var download_update_button: Button = %DownloadUpdateBtn 14 | @onready var progress_bar: ProgressBar = $UpdatePopup/MarginContainer/Vbox/ProgressBar 15 | @onready var update_popup: Popup = $UpdatePopup 16 | 17 | @onready var settings_repository := AssetPlacerSettingsRepository.instance 18 | 19 | const FEATURE_TEMPLATE = "https://github.com/levinzonr/godot-asset-placer/issues/new?template=feature_request.md&labels=enhancement&title=%5BFeature%5D%20" 20 | const ISSUE_TEMPLATE = "https://github.com/levinzonr/godot-asset-placer/issues/new?template=bug_report.md&labels=bug&title=%5BBUG%5D%20" 21 | 22 | 23 | var updater: PluginUpdater = PluginUpdater.instance 24 | 25 | func _ready(): 26 | var settings = settings_repository.get_settings() 27 | settings_repository.settings_changed.connect(func(s): 28 | updater.check_for_updates(s.update_channel) 29 | ) 30 | updater.show_update_loading.connect(_show_update_loading) 31 | updater.updater_update_available.connect(_show_update_available_for_download) 32 | updater.update_ready.connect(_show_update_ready_to_apply) 33 | updater.updater_up_to_date.connect(update_button.hide) 34 | updater.check_for_updates(settings.update_channel) 35 | update_button.pressed.connect(update_popup.popup) 36 | download_update_button.pressed.connect(updater.do_update) 37 | apply_update_button.pressed.connect(updater.apply_update) 38 | version_label.text = "Version %s" % get_plugin_version() 39 | issue_button.pressed.connect(func(): 40 | OS.shell_open(ISSUE_TEMPLATE) 41 | ) 42 | feature_request_button.pressed.connect(func(): 43 | OS.shell_open(FEATURE_TEMPLATE) 44 | ) 45 | 46 | func _show_update_available_for_download(update: PluginUpdate): 47 | update_button.text = "Version %s Availalbe" % update.version 48 | update_button.show() 49 | apply_update_button.hide() 50 | update_version_label.text = update.version._to_string() 51 | changelog_link_button.uri = "https://github.com/levinzonr/godot-asset-placer/releases/tag/" + update.version.to_string() 52 | 53 | 54 | func _show_update_loading(loading: bool): 55 | if loading: 56 | download_update_button.disabled = true 57 | progress_bar.show() 58 | else: 59 | download_update_button.disabled = false 60 | progress_bar.hide() 61 | 62 | func _show_update_ready_to_apply(update: PluginUpdate): 63 | update_button.text = "Version %s Availalbe" % update.version 64 | update_button.show() 65 | download_update_button.hide() 66 | apply_update_button.show() 67 | update_version_label.text = update.version._to_string() 68 | changelog_link_button.uri = "https://github.com/levinzonr/godot-asset-placer/blob/main/CHANGELOG.md#" + update.version.changelog_version() 69 | 70 | func get_plugin_version() -> String: 71 | return PluginConfiguration.new("res://addons/asset_placer/plugin.cfg").version.to_string() 72 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_collections_window/asset_collection_window.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://cjk0aw5iw4qb6"] 2 | 3 | [ext_resource type="Script" uid="uid://1l3hqqsryn3j" path="res://addons/asset_placer/ui/asset_collections_window/asset_collection_window.gd" id="1_g1b7h"] 4 | [ext_resource type="Script" uid="uid://dmicn3kmr620j" path="res://addons/asset_placer/utils/system_icon.gd" id="2_jdl0x"] 5 | 6 | [sub_resource type="Texture2D" id="Texture2D_x040h"] 7 | resource_local_to_scene = false 8 | resource_name = "" 9 | script = ExtResource("2_jdl0x") 10 | icon_name = &"Add" 11 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 12 | 13 | [node name="AssetCollectionWindow" type="Control"] 14 | layout_mode = 3 15 | anchors_preset = 15 16 | anchor_right = 1.0 17 | anchor_bottom = 1.0 18 | grow_horizontal = 2 19 | grow_vertical = 2 20 | script = ExtResource("1_g1b7h") 21 | 22 | [node name="Panel" type="Panel" parent="."] 23 | layout_mode = 1 24 | anchors_preset = 15 25 | anchor_right = 1.0 26 | anchor_bottom = 1.0 27 | grow_horizontal = 2 28 | grow_vertical = 2 29 | 30 | [node name="MarginContainer" type="MarginContainer" parent="Panel"] 31 | layout_mode = 1 32 | anchors_preset = 15 33 | anchor_right = 1.0 34 | anchor_bottom = 1.0 35 | grow_horizontal = 2 36 | grow_vertical = 2 37 | theme_override_constants/margin_left = 16 38 | theme_override_constants/margin_top = 16 39 | theme_override_constants/margin_right = 16 40 | theme_override_constants/margin_bottom = 16 41 | 42 | [node name="VBoxContainer2" type="VBoxContainer" parent="Panel/MarginContainer"] 43 | layout_mode = 2 44 | theme_override_constants/separation = 32 45 | 46 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer2"] 47 | layout_mode = 2 48 | theme_override_constants/separation = 16 49 | 50 | [node name="NameTextField" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer2/HBoxContainer"] 51 | unique_name_in_owner = true 52 | custom_minimum_size = Vector2(300, 0) 53 | layout_mode = 2 54 | placeholder_text = "New Collection Name" 55 | 56 | [node name="ColorPickerButton" type="ColorPickerButton" parent="Panel/MarginContainer/VBoxContainer2/HBoxContainer"] 57 | unique_name_in_owner = true 58 | layout_mode = 2 59 | text = "Color" 60 | color = Color(0.6156863, 0, 0, 1) 61 | edit_intensity = false 62 | 63 | [node name="AddButton" type="Button" parent="Panel/MarginContainer/VBoxContainer2/HBoxContainer"] 64 | unique_name_in_owner = true 65 | layout_mode = 2 66 | tooltip_text = "Godot" 67 | disabled = true 68 | text = "Add" 69 | icon = SubResource("Texture2D_x040h") 70 | icon_alignment = 2 71 | 72 | [node name="EmptyView" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer2"] 73 | unique_name_in_owner = true 74 | visible = false 75 | layout_mode = 2 76 | 77 | [node name="Label" type="Label" parent="Panel/MarginContainer/VBoxContainer2/EmptyView"] 78 | layout_mode = 2 79 | size_flags_vertical = 6 80 | text = "No collections yet" 81 | horizontal_alignment = 1 82 | 83 | [node name="ScrollContainer" type="ScrollContainer" parent="Panel/MarginContainer/VBoxContainer2"] 84 | layout_mode = 2 85 | size_flags_vertical = 3 86 | 87 | [node name="CollectionsContainer" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer2/ScrollContainer"] 88 | unique_name_in_owner = true 89 | layout_mode = 2 90 | size_flags_horizontal = 3 91 | size_flags_vertical = 3 92 | -------------------------------------------------------------------------------- /addons/asset_placer/data/synchronizer.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name Synchronize 3 | 4 | var folder_repository: FolderRepository 5 | var asset_repository: AssetsRepository 6 | 7 | static var instance: Synchronize 8 | 9 | signal sync_state_change(running: bool) 10 | signal sync_complete(added: int, removed: int, scanned: int) 11 | 12 | var _added = 0 13 | var _removed = 0 14 | var _scanned = 0 15 | 16 | var sync_running = false: 17 | set(value): 18 | sync_running = value 19 | call_deferred("emit_signal", "sync_state_change", value) 20 | 21 | func _init(folders_repository: FolderRepository, assets_repository: AssetsRepository): 22 | self.asset_repository = assets_repository 23 | self.folder_repository = folders_repository 24 | instance = self 25 | 26 | func sync_all(): 27 | 28 | if sync_running: 29 | push_error("Sync is already running") 30 | return 31 | 32 | AssetPlacerAsync.instance.enqueue(func(): 33 | sync_running = true 34 | _sync_all() 35 | _notify_scan_complete() 36 | sync_running = false 37 | ) 38 | 39 | func sync_folder(folder: AssetFolder): 40 | 41 | if sync_running: 42 | push_error("Sync is already running") 43 | return 44 | 45 | AssetPlacerAsync.instance.enqueue(func(): 46 | sync_running = true 47 | _sync_folder(folder) 48 | _notify_scan_complete() 49 | sync_running = false 50 | ) 51 | 52 | 53 | func _sync_folder(folder: AssetFolder): 54 | _clear_invalid_assets() 55 | _clear_unreachable_assets() 56 | add_assets_from_folder(folder.path, folder.include_subfolders) 57 | 58 | func _sync_all(): 59 | _clear_unreachable_assets() 60 | _clear_invalid_assets() 61 | for folder in folder_repository.get_all(): 62 | _sync_folder(folder) 63 | 64 | func add_assets_from_folder(folder_path: String, recursive: bool): 65 | var dir = DirAccess.open(folder_path) 66 | var tags : Array[int] = [] 67 | for file in dir.get_files(): 68 | _scanned += 1 69 | var path = folder_path + "/" + file 70 | if asset_repository.add_asset(path, tags, folder_path): 71 | _added += 1 72 | 73 | if recursive: 74 | for sub_dir in dir.get_directories(): 75 | var path: String = folder_path + "/" + sub_dir 76 | add_assets_from_folder(path, true) 77 | 78 | 79 | func _notify_scan_complete(): 80 | if _added != 0 || _removed != 0: 81 | call_deferred("emit_signal", "sync_complete", _added, _removed, _scanned) 82 | _clear_data() 83 | 84 | 85 | func _clear_unreachable_assets(): 86 | for asset in asset_repository.get_all_assets(): 87 | var path = asset.folder_path 88 | if not path.is_empty(): 89 | var folder = folder_repository.find(path) 90 | if folder == null: 91 | # remove asset if folder associated with that asset no longer exists 92 | asset_repository.delete(asset.id) 93 | elif not _is_asset_reachable_from_folder(asset, folder): 94 | asset_repository.delete(asset.id) 95 | 96 | func _is_asset_reachable_from_folder(asset: AssetResource, folder: AssetFolder) -> bool: 97 | var asset_folder_path := asset.folder_path 98 | var folder_path := folder.path 99 | if folder_path == asset_folder_path: 100 | return true 101 | 102 | if folder.include_subfolders and asset_folder_path.begins_with(folder_path + "/"): 103 | return true 104 | 105 | return false 106 | 107 | 108 | func _clear_invalid_assets(): 109 | for asset in asset_repository.get_all_assets(): 110 | if asset.scene == null: 111 | _removed += 1 112 | asset_repository.delete(asset.id) 113 | 114 | func _clear_data(): 115 | _removed = 0 116 | _added = 0 117 | _scanned = 0 118 | 119 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/viewport_overlay/viewport_overlay.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | @onready var rotate_check_button: CheckBox = %RotateCheckButton 4 | @onready var scale_check_button: CheckBox = %ScaleCheckButton 5 | @onready var translate_check_button: CheckBox = %TranslateCheckButton 6 | @onready var x_check_button: CheckButton = %XCheckButton 7 | @onready var z_check_button: CheckButton = %ZCheckButton 8 | @onready var y_check_button: CheckButton = %YCheckButton 9 | @onready var placement_mode_label: Label = %PlacementModeLabel 10 | @onready var error_label: Label = %ErrorLabel 11 | @onready var error_container: Container = %ErrorContainer 12 | @onready var error_timer: Timer = %ErrorTimer 13 | @onready var snapping_switch: CheckButton = %SnappingSwitch 14 | @onready var placement_shortcut_label: Label = %PlacementShortcutLabel 15 | 16 | var _error_position: Vector2 17 | var _error_hidden_position: Vector2 18 | @onready var _settings_repository := AssetPlacerSettingsRepository.instance 19 | 20 | func _ready(): 21 | hide() 22 | _error_position = error_container.position 23 | show_settings(_settings_repository.get_settings()) 24 | _settings_repository.settings_changed.connect(show_settings) 25 | var viewport_size = get_viewport_rect().size 26 | _error_hidden_position = Vector2(-viewport_size.x, _error_position.y) 27 | error_container.position = _error_hidden_position 28 | var presenter = AssetPlacerPresenter._instance 29 | presenter.transform_mode_changed.connect(set_mode) 30 | presenter.preview_transform_axis_changed.connect(set_axis) 31 | presenter.placer_active.connect(func(a): if a: show() else: hide()) 32 | presenter.placement_mode_changed.connect(set_placement_mode) 33 | presenter.options_changed.connect(show_options) 34 | presenter.ready() 35 | presenter.show_error.connect(show_error) 36 | error_timer.timeout.connect(hide_error) 37 | set_mode(presenter.transform_mode) 38 | set_axis(presenter.preview_transform_axis) 39 | 40 | 41 | func set_mode(mode: AssetPlacerPresenter.TransformMode): 42 | rotate_check_button.button_pressed = mode == AssetPlacerPresenter.TransformMode.Rotate 43 | scale_check_button.button_pressed = mode == AssetPlacerPresenter.TransformMode.Scale 44 | translate_check_button.button_pressed = mode == AssetPlacerPresenter.TransformMode.Move 45 | 46 | func set_placement_mode(mode: PlacementMode): 47 | if mode is PlacementMode.PlanePlacement: 48 | placement_mode_label.text = "Plane Placement" 49 | if mode is PlacementMode.SurfacePlacement: 50 | placement_mode_label.text = "Surface Placement" 51 | if mode is PlacementMode.Terrain3DPlacement: 52 | placement_mode_label.text = "Terrain3D Placement" 53 | 54 | 55 | func show_error(message: String): 56 | var tween = create_tween() 57 | tween.tween_property(error_container, "position", _error_position, 0.3) 58 | error_label.text = message 59 | error_timer.start() 60 | 61 | func hide_error(): 62 | var tween = create_tween() 63 | tween.tween_property(error_container, "position", _error_hidden_position, 0.3) 64 | 65 | func show_settings(settings: AssetPlacerSettings): 66 | rotate_check_button.text = "%s: To Rotate" % settings.bindings[AssetPlacerSettings.Bindings.Rotate].get_display_name() 67 | scale_check_button.text = "%s: To Scale" % settings.bindings[AssetPlacerSettings.Bindings.Scale].get_display_name() 68 | translate_check_button.text = "%s: To Translate" % settings.bindings[AssetPlacerSettings.Bindings.Translate].get_display_name() 69 | snapping_switch.text = "%s: Grid Snapping" % settings.bindings[AssetPlacerSettings.Bindings.GridSnapping].get_display_name() 70 | placement_shortcut_label.text = "(%s)" % settings.bindings[AssetPlacerSettings.Bindings.TogglePlaneMode].get_display_name() 71 | 72 | 73 | func show_options(options: AssetPlacerOptions): 74 | snapping_switch.button_pressed = options.snapping_enabled 75 | 76 | func set_axis(vector: Vector3): 77 | x_check_button.button_pressed = vector.x == 1 78 | y_check_button.button_pressed = vector.y == 1 79 | z_check_button.button_pressed = vector.z == 1 80 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/plane_preview/plane_texture.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/components/keybinding_button.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name APInputOptionButton 3 | extends Button 4 | 5 | signal key_binding_changed(option: APInputOption) 6 | signal key_binding_active 7 | 8 | @export var allow_modifiers: bool = true 9 | @export var allow_mouse_buttons: bool = false 10 | 11 | enum State { 12 | Bind, Idle 13 | } 14 | 15 | var _current_key: APInputOption = APInputOption.none() 16 | var _pending_key: APInputOption = null 17 | var _pending_modifier: KeyModifierMask = 0 18 | var _pressed: int = 0 19 | 20 | var _modifier_keys: Dictionary[Key, KeyModifierMask] = { 21 | Key.KEY_META: KeyModifierMask.KEY_MASK_META, 22 | Key.KEY_SHIFT: KeyModifierMask.KEY_MASK_SHIFT, 23 | Key.KEY_ALT: KeyModifierMask.KEY_MASK_ALT, 24 | Key.KEY_CTRL: KeyModifierMask.KEY_MASK_CTRL 25 | } 26 | 27 | var _state: = State.Idle: 28 | set(state): 29 | _state = state 30 | match _state: 31 | State.Idle: on_idle() 32 | State.Bind: on_bind() 33 | 34 | 35 | func on_idle(): 36 | text = _current_key.get_display_name() 37 | modulate = Color.WHITE 38 | 39 | if _current_key.equals(APInputOption.none()): 40 | modulate = Color.RED 41 | 42 | func on_bind(): 43 | text = _get_helper_text() 44 | modulate = Color(0.945, 1.0, 0.353, 1.0) 45 | key_binding_active.emit() 46 | 47 | func bind(): 48 | _state = State.Bind 49 | _pending_key = null 50 | _pending_modifier = 0 51 | _pressed = 0 52 | 53 | func cancel(): 54 | _state = State.Idle 55 | _pending_key = null 56 | _pending_modifier = 0 57 | _pressed = 0 58 | 59 | func _process(delta): 60 | match _state: 61 | State.Idle: 62 | pass 63 | State.Bind: 64 | text = _get_pending_text() 65 | if text.is_empty(): 66 | text = _get_helper_text() 67 | 68 | func set_keybind_no_signal(key: APInputOption) -> void: 69 | _current_key = key 70 | text = key.get_display_name() 71 | if _current_key.equals(APInputOption.none()): 72 | modulate = Color.RED 73 | else: 74 | modulate = Color.WHITE 75 | 76 | 77 | func _input(event: InputEvent): 78 | match _state: 79 | State.Bind: _process_bind_input(event) 80 | State.Idle: pass 81 | 82 | 83 | func _process_bind_input(event: InputEvent): 84 | 85 | if Input.is_key_pressed(Key.KEY_ESCAPE): 86 | cancel() 87 | return 88 | 89 | get_viewport().set_input_as_handled() 90 | if event is InputEventKey: 91 | if event.is_pressed(): 92 | _process_input_key_event_pressed(event) 93 | if event.is_released(): 94 | _process_input_key_event_released(event) 95 | if event is InputEventMouseButton: 96 | if event.is_pressed(): 97 | _pending_key = APInputOption.mouse_press(event.button_index) 98 | _pressed += 1 99 | else: 100 | _pressed -= 1 101 | if _pressed == 0: 102 | _stop_binding() 103 | 104 | 105 | 106 | func _process_input_key_event_pressed(event: InputEventKey): 107 | var key_code = event.keycode 108 | if key_code in _modifier_keys.keys() and allow_modifiers: 109 | _pending_modifier = _pending_modifier | _modifier_keys[key_code] 110 | else: 111 | _pending_key = APInputOption.key_press(key_code) 112 | 113 | _pressed += 1 114 | 115 | 116 | func _process_input_key_event_released(event: InputEventKey): 117 | _pressed -= 1 118 | if _pressed == 0: 119 | _stop_binding() 120 | 121 | 122 | func _stop_binding(): 123 | if allow_modifiers and _pending_modifier != Key.KEY_NONE: 124 | var final_keybind = _pending_key 125 | _pending_key.modifiers = _pending_modifier 126 | _current_key = final_keybind 127 | key_binding_changed.emit(_current_key) 128 | _state = State.Idle 129 | else: 130 | _current_key = _pending_key 131 | key_binding_changed.emit(_current_key) 132 | _state = State.Idle 133 | 134 | _pending_key = null 135 | _pending_modifier = 0 136 | 137 | 138 | func _get_helper_text() -> String: 139 | return "Press Any Key, ESC to Cancel" 140 | 141 | func _get_pending_text() -> String: 142 | var final_text := "" 143 | if _pending_key != null: 144 | final_text += _pending_key.get_display_name() 145 | 146 | if _pending_modifier != 0: 147 | final_text = OS.get_keycode_string(Key.KEY_NONE | _pending_modifier) + final_text 148 | 149 | return final_text 150 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_library_window/asset_library_presenter.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetLibraryPresenter 3 | 4 | var library: AssetLibrary 5 | var folder_repository: FolderRepository 6 | var assets_repository: AssetsRepository 7 | var synchronizer: Synchronize 8 | 9 | var _active_collections: Array[AssetCollection] = [] 10 | var _filtered_assets: Array[AssetResource] = [] 11 | var _current_assets: Array[AssetResource] 12 | var _current_query: String 13 | 14 | enum EmptyType { 15 | Search, Collection, All, None 16 | } 17 | 18 | signal assets_loaded(assets: Array[AssetResource]) 19 | signal asset_selection_change 20 | signal show_filter_info(size: int) 21 | signal show_sync_active(bool) 22 | signal show_empty_view(type: EmptyType) 23 | 24 | func _init(): 25 | self.folder_repository = FolderRepository.instance 26 | self.assets_repository = AssetsRepository.instance 27 | self.synchronizer = Synchronize.instance 28 | 29 | 30 | 31 | func on_ready(): 32 | _current_assets = assets_repository.get_all_assets() 33 | show_filter_info.emit(0) 34 | assets_repository.assets_changed.connect(_filter_by_collections_and_query) 35 | _filter_by_collections_and_query() 36 | synchronizer.sync_state_change.connect(func(v): 37 | show_sync_active.emit(v) 38 | ) 39 | 40 | func add_asset_folder(path: String): 41 | folder_repository.add(path) 42 | var dir_access = DirAccess.open(path) 43 | for file in dir_access.get_files(): 44 | add_asset(path + file, path) 45 | 46 | func on_query_change(query: String): 47 | self._current_query = query 48 | _filter_by_collections_and_query() 49 | 50 | 51 | func add_asset(path: String, folder_path: String): 52 | var tags: Array[int] = [] 53 | for collection in _active_collections: 54 | tags.push_back(collection.id) 55 | 56 | var id = ResourceIdCompat.path_to_uid(path) 57 | if !id: 58 | push_error("Error getting id from path %s" % path) 59 | return 60 | 61 | var existing = assets_repository.find_by_uid(id) 62 | if existing: 63 | var new_tags: Array[int] = [] 64 | for tag in tags: 65 | if tag not in existing.tags: 66 | new_tags.push_back(tag) 67 | 68 | existing.tags.append_array(new_tags) 69 | assets_repository.update(existing) 70 | else: 71 | assets_repository.add_asset(path, tags, folder_path) 72 | 73 | 74 | func delete_asset(asset: AssetResource): 75 | assets_repository.delete(asset.id) 76 | _filter_by_collections_and_query() 77 | 78 | func add_assets_or_folders(files: PackedStringArray): 79 | for file in files: 80 | if file.get_extension().is_empty(): 81 | add_asset_folder(file) 82 | else: 83 | add_asset(file, "") 84 | 85 | _filter_by_collections_and_query() 86 | 87 | func toggle_asset_collection(asset: AssetResource, collection: AssetCollection, add: bool): 88 | if add: 89 | asset.tags.append(collection.id) 90 | assets_repository.update(asset) 91 | else: 92 | asset.tags.erase(collection.id) 93 | assets_repository.update(asset) 94 | 95 | _filter_by_collections_and_query() 96 | 97 | func toggle_collection_filter(collection: AssetCollection, enabled: bool): 98 | if enabled: 99 | _active_collections.push_back(collection) 100 | else: 101 | _active_collections = _active_collections.filter(func(a): 102 | return a.id != collection.id 103 | ) 104 | show_filter_info.emit(_active_collections.size()) 105 | _filter_by_collections_and_query() 106 | 107 | 108 | 109 | func _filter_by_collections_and_query(): 110 | var all = assets_repository.get_all_assets() 111 | var filtered: Array[AssetResource] = [] 112 | 113 | for asset in all: 114 | var matches_query = asset.name.containsn(_current_query) || _current_query.is_empty() 115 | var belongs_to_collection = asset.belongs_to_some_collection(_active_collections) || _active_collections.is_empty() 116 | 117 | if matches_query and belongs_to_collection: 118 | filtered.push_back(asset) 119 | 120 | if filtered.is_empty(): 121 | if _active_collections.is_empty() && _current_query.is_empty(): 122 | show_empty_view.emit(EmptyType.All) 123 | elif not _active_collections.is_empty(): 124 | show_empty_view.emit(EmptyType.Collection) 125 | else: 126 | show_empty_view.emit(EmptyType.Search) 127 | else: 128 | assets_loaded.emit(filtered) 129 | show_empty_view.emit(EmptyType.None) 130 | 131 | _filtered_assets = filtered 132 | 133 | 134 | func sync(): 135 | synchronizer.sync_all() 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /addons/asset_placer/utils/asset_placer_input_option.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name APInputOption 3 | 4 | var modifiers: KeyModifierMask 5 | 6 | func is_pressed(event: InputEvent) -> bool: 7 | return false 8 | 9 | func get_display_name() -> String: 10 | return "" 11 | 12 | func serialize() -> String: 13 | return "" 14 | 15 | func equals(other: APInputOption) -> bool: 16 | return false 17 | 18 | class KeyPress extends APInputOption: 19 | var key: Key 20 | 21 | func _init(keys: Key, modifiers: KeyModifierMask = 0): 22 | self.key = keys 23 | self.modifiers = modifiers 24 | 25 | func get_display_name() -> String: 26 | if key == Key.KEY_NONE: 27 | return "Unassigned" 28 | return OS.get_keycode_string(key | modifiers) 29 | 30 | func is_pressed(event: InputEvent) -> bool: 31 | if event is InputEventKey and event.is_pressed(): 32 | var event_modifiers: KeyModifierMask = 0 33 | if Input.is_key_pressed(Key.KEY_SHIFT): 34 | event_modifiers |= KeyModifierMask.KEY_MASK_SHIFT 35 | if Input.is_key_pressed(Key.KEY_CTRL): 36 | event_modifiers |= KeyModifierMask.KEY_MASK_CTRL 37 | if Input.is_key_pressed(Key.KEY_ALT): 38 | event_modifiers |= KeyModifierMask.KEY_MASK_ALT 39 | if Input.is_key_pressed(Key.KEY_META): 40 | event_modifiers |= KeyModifierMask.KEY_MASK_META 41 | var with_mask = key | modifiers 42 | var event_combined = event.keycode | event_modifiers 43 | return event_combined == with_mask 44 | else: 45 | return false 46 | 47 | func serialize() -> String: 48 | return "key_%s_%s" % [str(key), str(modifiers)] 49 | 50 | func equals(other: APInputOption) -> bool: 51 | if not other is KeyPress: 52 | return false 53 | var other_key_press = other as KeyPress 54 | return key == other_key_press.key and modifiers == other_key_press.modifiers 55 | 56 | class MousePress extends APInputOption: 57 | var mouse_index: MouseButton 58 | 59 | func _init(mouse_index: MouseButton, modifiers: KeyModifierMask = 0): 60 | self.mouse_index = mouse_index 61 | self.modifiers = modifiers 62 | 63 | func serialize() -> String: 64 | return "mouse_%s_%s" % [str(mouse_index), str(modifiers)] 65 | 66 | func equals(other: APInputOption) -> bool: 67 | if not other is MousePress: 68 | return false 69 | var other_mouse_press = other as MousePress 70 | return mouse_index == other_mouse_press.mouse_index and modifiers == other_mouse_press.modifiers 71 | 72 | func is_pressed(event: InputEvent) -> bool: 73 | if event is InputEventMouseButton: 74 | var event_modifiers: KeyModifierMask = 0 75 | if Input.is_key_pressed(Key.KEY_SHIFT): 76 | event_modifiers |= KeyModifierMask.KEY_MASK_SHIFT 77 | if Input.is_key_pressed(Key.KEY_CTRL): 78 | event_modifiers |= KeyModifierMask.KEY_MASK_CTRL 79 | if Input.is_key_pressed(Key.KEY_ALT): 80 | event_modifiers |= KeyModifierMask.KEY_MASK_ALT 81 | if Input.is_key_pressed(Key.KEY_META): 82 | event_modifiers |= KeyModifierMask.KEY_MASK_META 83 | 84 | return event.button_index == mouse_index and event_modifiers == modifiers 85 | else: 86 | return false 87 | 88 | func get_display_name() -> String: 89 | var button_name: String 90 | match mouse_index: 91 | MouseButton.MOUSE_BUTTON_LEFT: 92 | button_name = "LMB" 93 | MouseButton.MOUSE_BUTTON_RIGHT: 94 | button_name = "RMB" 95 | MouseButton.MOUSE_BUTTON_MIDDLE: 96 | button_name = "MMB" 97 | MouseButton.MOUSE_BUTTON_XBUTTON1: 98 | button_name = "Extra 1" 99 | MouseButton.MOUSE_BUTTON_XBUTTON2: 100 | button_name = "Extra 2" 101 | MouseButton.MOUSE_BUTTON_WHEEL_DOWN: 102 | button_name = "Mouse Wheel Down" 103 | MouseButton.MOUSE_BUTTON_WHEEL_UP: 104 | button_name = "Mouse Wheel Up" 105 | _: 106 | button_name = "Not supported %s" % mouse_index 107 | 108 | if modifiers == 0: 109 | return button_name 110 | else: 111 | return OS.get_keycode_string(Key.KEY_NONE | modifiers) + button_name 112 | 113 | 114 | static func desirialize(raw: String) -> APInputOption: 115 | if raw.begins_with("mouse_"): 116 | var parts = raw.split("_") 117 | var mouse_index = parts[1].to_int() 118 | var modifiers = parts[2].to_int() if parts.size() > 2 else 0 119 | return mouse_press(mouse_index, modifiers) 120 | else: 121 | var parts = raw.split("_") 122 | var key = parts[1].to_int() 123 | var modifiers = parts[2].to_int() if parts.size() > 2 else 0 124 | return key_press(key, modifiers) 125 | 126 | static func none() -> APInputOption: 127 | return APInputOption.KeyPress.new(Key.KEY_NONE) 128 | 129 | 130 | static func key_press(key: Key, modifiers: KeyModifierMask = 0) -> APInputOption: 131 | return APInputOption.KeyPress.new(key, modifiers) 132 | 133 | static func mouse_press(mouse_button: MouseButton, modifiers: KeyModifierMask = 0) -> APInputOption: 134 | return APInputOption.MousePress.new(mouse_button, modifiers) 135 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_placer_options/asset_placer_options.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | var presenter: AssetPlacerPresenter 5 | @onready var grid_snapping_checkbox = %GridSnappingCheckbox 6 | @onready var grid_snap_value_spin_box: SpinBox = %GridSnapValueSpinBox 7 | @onready var min_rotation_selector: SpinBoxVector3 = %MinRotationSelector 8 | @onready var max_rotation_selector: SpinBoxVector3 = %MaxRotationSelector 9 | 10 | @onready var min_scale_selector: SpinBoxVector3 = %MinScaleSelector 11 | @onready var max_scale_selector: SpinBoxVector3 = %MaxScaleSelector 12 | @onready var uniform_scale_check_box: CheckBox = %UniformScaleCheckBox 13 | @onready var parent_button: Button = %ParentButton 14 | @onready var placement_mode_options_button: OptionButton = %PlacementModeOptionsButton 15 | @onready var plane_axis_spin_box: SpinBoxVector3 = %PlaneAxisSpinBox 16 | @onready var plane_origin_spin_box: SpinBoxVector3 = %PlaneOriginSpinBox 17 | @onready var plane_origin_container: Container = %PlaneOriginContainer 18 | @onready var plane_axis_container: Container = %PlaneAxisContainer 19 | @onready var random_rotation_check_box: CheckBox = %RandomRotationCheckBox 20 | @onready var random_scale_check_box: CheckBox = %RandomScaleCheckBox 21 | @onready var align_normals_checkbox: CheckBox = %AlignNormalsCheckbox 22 | @onready var use_assets_origin_checkbox: CheckBox = %UseAssetsOriginCheckbox 23 | @onready var random_asset_check_box = %RandomAssetCheckBox 24 | 25 | func _ready(): 26 | presenter = AssetPlacerPresenter._instance 27 | presenter.options_changed.connect(set_options) 28 | presenter.parent_changed.connect(show_parent) 29 | presenter.placement_mode_changed.connect(func(m): 30 | if m is PlacementMode.PlanePlacement: 31 | placement_mode_options_button.select(1) 32 | if m is PlacementMode.SurfacePlacement: 33 | placement_mode_options_button.select(0) 34 | if m is PlacementMode.Terrain3DPlacement: 35 | placement_mode_options_button.select(2) 36 | ) 37 | 38 | placement_mode_options_button.item_selected.connect(func(id): 39 | match id: 40 | 0: presenter.toggle_surface_placement() 41 | 1: presenter.toggle_plane_placement() 42 | 2: _show_terrain_3d_selector() 43 | ) 44 | 45 | grid_snapping_checkbox.toggled.connect(presenter.set_grid_snapping_enabled) 46 | grid_snap_value_spin_box.value_changed.connect(presenter.set_grid_snap_value) 47 | random_asset_check_box.toggled.connect(presenter.set_random_asset_enabled) 48 | max_rotation_selector.value_changed.connect(presenter.set_max_rotation) 49 | min_rotation_selector.value_changed.connect(presenter.set_min_rotation) 50 | min_scale_selector.value_changed.connect(presenter.set_min_scale) 51 | max_scale_selector.value_changed.connect(presenter.set_max_scale) 52 | uniform_scale_check_box.toggled.connect(presenter.set_unform_scaling) 53 | use_assets_origin_checkbox.toggled.connect(presenter.set_use_asset_origin) 54 | 55 | plane_axis_spin_box.value_changed.connect(func(normal: Vector3): 56 | var plane = PlaneOptions.new(normal, plane_origin_spin_box.get_vector()) 57 | presenter.placement_mode = PlacementMode.PlanePlacement.new(plane) 58 | ) 59 | 60 | plane_origin_spin_box.value_changed.connect(func(origin: Vector3): 61 | var plane = PlaneOptions.new(plane_axis_spin_box.get_vector(), origin) 62 | presenter.placement_mode = PlacementMode.PlanePlacement.new(plane) 63 | ) 64 | 65 | presenter.placement_mode_changed.connect(func(mode: PlacementMode): 66 | if mode is PlacementMode.PlanePlacement: 67 | plane_axis_container.show() 68 | plane_origin_container.show() 69 | plane_axis_spin_box.set_value_no_signal(mode.plane_options.normal) 70 | plane_origin_spin_box.set_value_no_signal(mode.plane_options.origin) 71 | else: 72 | plane_axis_container.hide() 73 | plane_origin_container.hide() 74 | ) 75 | 76 | parent_button.pressed.connect(func(): 77 | EditorInterface.popup_node_selector(presenter.select_parent, [&"Node3D"]) 78 | ) 79 | 80 | random_rotation_check_box.toggled.connect(presenter.set_random_rotation_enabled) 81 | random_scale_check_box.toggled.connect(presenter.set_random_scale_enabled) 82 | align_normals_checkbox.toggled.connect(presenter.set_align_normals) 83 | presenter.ready() 84 | 85 | func _show_terrain_3d_selector(): 86 | EditorInterface.popup_node_selector(presenter.toggle_terrain_3d_placement, [&"Terrain3D"]) 87 | 88 | func show_parent(parent: NodePath): 89 | if not parent.is_empty(): 90 | var scene = EditorInterface.get_edited_scene_root() 91 | var node = scene.get_node(parent) 92 | parent_button.text = node.name 93 | parent_button.icon = EditorIconTexture2D.new(node.get_class()) 94 | else: 95 | parent_button.text = "No parent selected" 96 | parent_button.icon = EditorIconTexture2D.new("NodeWarning") 97 | 98 | func set_options(options: AssetPlacerOptions): 99 | random_asset_check_box.set_pressed_no_signal(options.enable_random_placement) 100 | grid_snapping_checkbox.set_pressed_no_signal(options.snapping_enabled) 101 | grid_snap_value_spin_box.editable = options.snapping_enabled 102 | grid_snap_value_spin_box.set_value_no_signal(options.snapping_grid_step) 103 | max_rotation_selector.set_value_no_signal(options.max_rotation) 104 | min_rotation_selector.set_value_no_signal(options.min_rotation) 105 | min_scale_selector.set_value_no_signal(options.min_scale) 106 | max_scale_selector.set_value_no_signal(options.max_scale) 107 | min_scale_selector.uniform = options.uniform_scaling 108 | max_scale_selector.uniform = options.uniform_scaling 109 | uniform_scale_check_box.set_pressed_no_signal(options.uniform_scaling) 110 | random_rotation_check_box.set_pressed_no_signal(options.rotate_on_placement) 111 | random_scale_check_box.set_pressed_no_signal(options.scale_on_placement) 112 | align_normals_checkbox.set_pressed_no_signal(options.align_normals) 113 | use_assets_origin_checkbox.set_pressed_no_signal(options.use_asset_origin) 114 | -------------------------------------------------------------------------------- /addons/asset_placer/updater/plugin_updater.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name PluginUpdater 3 | 4 | 5 | static var instance: PluginUpdater 6 | 7 | 8 | signal updater_up_to_date 9 | signal updater_update_available(update: PluginUpdate) 10 | signal update_ready(update: PluginUpdate) 11 | signal show_update_loading(bool) 12 | 13 | var _local_plugin_path: String 14 | var _remote_plugin_path: String 15 | var _client: PluginUpdaterHttpClient 16 | var _latest_update: PluginUpdate 17 | 18 | const TMP_ZIP_TEMPLATE = "user://asset-placer-{version}.zip" 19 | 20 | func _init(local_config_path: String, remote_config_path: String): 21 | self._local_plugin_path = local_config_path 22 | self._remote_plugin_path = remote_config_path 23 | self._client = PluginUpdaterHttpClient.new() 24 | instance = self 25 | 26 | 27 | func check_for_updates(channel: AssetPlacerSettings.UpdateChannel = AssetPlacerSettings.UpdateChannel.Stable): 28 | _latest_update = await _get_latest_update(channel) 29 | if !_latest_update: 30 | return 31 | 32 | var current_version = PluginConfiguration.new(_local_plugin_path).version 33 | if current_version.compare_to(_latest_update.version) < 0: 34 | if _is_update_downloaded(): 35 | update_ready.emit(_latest_update) 36 | else: 37 | updater_update_available.emit(_latest_update) 38 | else: 39 | updater_up_to_date.emit() 40 | 41 | 42 | 43 | func do_update(): 44 | show_update_loading.emit(true) 45 | var url_path = _latest_update.download_url 46 | _client.client_get(url_path) 47 | 48 | var zip: PackedByteArray = await _client.client_response 49 | var zip_path = _get_zip_path_for_version(_latest_update.version.to_string()) 50 | var tmp_file = FileAccess.open(zip_path, FileAccess.WRITE) 51 | tmp_file.store_buffer(zip) 52 | tmp_file.close() 53 | 54 | show_update_loading.emit(false) 55 | update_ready.emit(_latest_update) 56 | 57 | func apply_update(): 58 | 59 | if FileAccess.open("res://docs/addon_folders.png", FileAccess.READ): 60 | push_error("Trying to update plugin from within a plugin") 61 | return 62 | 63 | show_update_loading.emit(true) 64 | 65 | var zip_path = _get_zip_path_for_version(_latest_update.version.to_string()) 66 | var zip_reader: ZIPReader = ZIPReader.new() 67 | if zip_reader.open(zip_path) != OK: 68 | push_error("Failed to open downloaded ZIP file") 69 | show_update_loading.emit(false) 70 | return 71 | 72 | var files: PackedStringArray = zip_reader.get_files() 73 | 74 | # Move old plugin to trash 75 | OS.move_to_trash(ProjectSettings.globalize_path("res://addons/asset_placer")) 76 | 77 | var base_path: String 78 | for path in files: 79 | if path.ends_with("/addons/"): 80 | base_path = path 81 | break 82 | 83 | if base_path.is_empty(): 84 | push_error("Could not find addons folder in ZIP") 85 | zip_reader.close() 86 | DirAccess.remove_absolute(zip_path) 87 | show_update_loading.emit(false) 88 | return 89 | 90 | for path in files: 91 | if not path.contains(base_path): 92 | continue 93 | 94 | var new_file_path: String = path.replace(base_path, "") 95 | if path.ends_with("/"): 96 | DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path) 97 | else: 98 | var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE) 99 | if file: 100 | file.store_buffer(zip_reader.read_file(path)) 101 | file.close() 102 | else: 103 | push_error("Failed to write file: res://addons/%s" % new_file_path) 104 | 105 | zip_reader.close() 106 | DirAccess.remove_absolute(zip_path) 107 | show_update_loading.emit(false) 108 | EditorInterface.restart_editor(true) 109 | 110 | 111 | 112 | func _is_update_downloaded() -> bool: 113 | if !_latest_update: 114 | return false 115 | var zip_path = _get_zip_path_for_version(_latest_update.version.to_string()) 116 | var tmp_file = FileAccess.open(zip_path, FileAccess.READ) 117 | if not tmp_file: 118 | return false 119 | tmp_file.close() 120 | return true 121 | 122 | func _get_latest_update(channel: AssetPlacerSettings.UpdateChannel = AssetPlacerSettings.UpdateChannel.Stable) -> PluginUpdate: 123 | # Fetch all releases 124 | _client.client_get("https://api.github.com/repos/levinzonr/godot-asset-placer/releases") 125 | var response: PackedByteArray = await _client.client_response 126 | if response.is_empty(): 127 | return null 128 | 129 | var json_string = response.get_string_from_utf8() 130 | var json = JSON.new() 131 | var parse_result = json.parse(json_string) 132 | if parse_result != OK: 133 | return null 134 | 135 | var releases = json.data 136 | if not releases is Array: 137 | return null 138 | 139 | var latest_release = null 140 | var latest_version: Version = null 141 | 142 | for release in releases: 143 | var release_dict = release as Dictionary 144 | if not release_dict: 145 | continue 146 | 147 | var tag_name = release_dict.get("tag_name", "") 148 | if tag_name.is_empty(): 149 | continue 150 | 151 | # Parse version to check channel compatibility 152 | var version = Version.new(tag_name) 153 | 154 | # Filter releases based on channel using Version class 155 | var matches_channel = false 156 | match channel: 157 | AssetPlacerSettings.UpdateChannel.Stable: 158 | matches_channel = version.identifier == null 159 | AssetPlacerSettings.UpdateChannel.Beta: 160 | matches_channel = version.identifier == null or version.identifier.track != Version.Track.Alpha 161 | AssetPlacerSettings.UpdateChannel.Alpha: 162 | # For alpha channel, include all releases (no filtering) 163 | matches_channel = true 164 | 165 | if not matches_channel: 166 | continue 167 | 168 | # Check if this is the latest version for this channel 169 | if latest_version == null or version.compare_to(latest_version) > 0: 170 | latest_version = version 171 | latest_release = release_dict 172 | 173 | if not latest_release: 174 | return null 175 | 176 | var tag_name = latest_release["tag_name"] 177 | var change_log = latest_release["body"] 178 | var download_url = latest_release["zipball_url"] 179 | return PluginUpdate.new(tag_name, change_log, download_url) 180 | 181 | 182 | func _get_zip_path_for_version(version: String) -> String: 183 | return TMP_ZIP_TEMPLATE.format({"version": version}) 184 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/settings/settings_panel.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | var _presenter: SettingsPresenter = SettingsPresenter.new() 5 | @onready var keybinding_option_rotate = %KeybindingOptionRotate 6 | @onready var keybinding_option_scale = %KeybindingOptionScale 7 | @onready var keybinding_option_translate = %KeybindingOptionTranslate 8 | @onready var keybinding_option_grid_snap = %KeybindingOptionGridSnap 9 | @onready var reset_button: Button = %ResetButton 10 | 11 | @onready var material_picker_button = %MaterialPickerButton 12 | @onready var material_clear_button = %MaterialClearButton 13 | @onready var plane_material_picker_button: Button = %PlaneMaterialPickerButton 14 | @onready var keybinding_option_in_place_transform = %KeybindingOptionInPlaceTransform 15 | @onready var trasform_step_spin_box: SpinBox = %TrasformStepSpinBox 16 | @onready var rotation_step_spin_box: SpinBox = %RotationStepSpinBox 17 | @onready var ui_scale_h_slider: HSlider = %UIScaleHSlider 18 | @onready var slider_value = %SliderValue 19 | @onready var keybinding_option_positive_transform = %KeybindingOptionPositiveTransform 20 | @onready var keybinding_option_negative_transform = %KeybindingOptionNegativeTransform 21 | @onready var keybinding_option_axis_x = %KeybindingOptionAxisX 22 | @onready var keybinding_option_axis_y = %KeybindingOptionAxisY 23 | @onready var keybinding_option_axis_z = %KeybindingOptionAxisZ 24 | @onready var keybinding_option_plane_mode = $Panel/MarginContainer/ScrollContainer/VBoxContainer/HBoxContainer/KeyBindings/KeyBindingsOptions/KeybindingOptionPlaneMode 25 | @onready var update_channel_option_button: OptionButton = %UpdateChannelOptionButton 26 | @onready var update_channel_info_button: Button = %UpdateChannelInfoButton 27 | 28 | 29 | func _ready(): 30 | ui_scale_h_slider.drag_ended.connect(func(changed): 31 | _presenter.set_ui_scale(ui_scale_h_slider.value) 32 | ) 33 | ui_scale_h_slider.value_changed.connect(func(value): 34 | slider_value.text = str(value) 35 | ) 36 | trasform_step_spin_box.value_changed.connect(_presenter.set_default_transform_step) 37 | rotation_step_spin_box.value_changed.connect(_presenter.set_rotation_step) 38 | _presenter.show_settings.connect(_show_settings) 39 | update_channel_option_button.item_selected.connect(_presenter.set_update_channel) 40 | keybinding_option_rotate.keybind_changed.connect(func(key): 41 | _presenter.set_binding(AssetPlacerSettings.Bindings.Rotate, key) 42 | ) 43 | 44 | keybinding_option_positive_transform.keybind_changed.connect(func(key): 45 | _presenter.set_binding(AssetPlacerSettings.Bindings.TransformPositive, key) 46 | ) 47 | 48 | keybinding_option_translate.keybind_changed.connect(func(key): 49 | _presenter.set_binding(AssetPlacerSettings.Bindings.Translate, key) 50 | ) 51 | 52 | keybinding_option_plane_mode.keybind_changed.connect(func(key): 53 | _presenter.set_binding(AssetPlacerSettings.Bindings.TogglePlaneMode, key) 54 | ) 55 | 56 | keybinding_option_negative_transform.keybind_changed.connect(func(key): 57 | _presenter.set_binding(AssetPlacerSettings.Bindings.TransformNegative, key) 58 | ) 59 | keybinding_option_scale.keybind_changed.connect(func(key): 60 | _presenter.set_binding(AssetPlacerSettings.Bindings.Scale, key) 61 | ) 62 | keybinding_option_in_place_transform.keybind_changed.connect(func(key): 63 | _presenter.set_binding(AssetPlacerSettings.Bindings.InPlaceTransform, key) 64 | ) 65 | keybinding_option_grid_snap.keybind_changed.connect(func(key): 66 | _presenter.set_binding(AssetPlacerSettings.Bindings.GridSnapping, key) 67 | ) 68 | keybinding_option_axis_x.keybind_changed.connect(func(key): 69 | _presenter.set_binding(AssetPlacerSettings.Bindings.ToggleAxisX, key) 70 | ) 71 | keybinding_option_axis_y.keybind_changed.connect(func(key): 72 | _presenter.set_binding(AssetPlacerSettings.Bindings.ToggleAxisY, key) 73 | ) 74 | keybinding_option_axis_z.keybind_changed.connect(func(key): 75 | _presenter.set_binding(AssetPlacerSettings.Bindings.ToggleAxisZ, key) 76 | ) 77 | reset_button.pressed.connect(_presenter.reset_to_defaults) 78 | material_clear_button.pressed.connect(_presenter.clear_preivew_material) 79 | material_picker_button.pressed.connect(_show_preview_material_picker) 80 | 81 | update_channel_info_button.pressed.connect(func(): 82 | OS.shell_open("https://levinzonr.github.io/godot-asset-placer/development-lifecycle/") 83 | ) 84 | 85 | plane_material_picker_button.pressed.connect(func(): 86 | EditorInterface.popup_quick_open(_presenter.set_plane_material, ["BaseMaterial3D"]) 87 | ) 88 | _presenter.ready() 89 | 90 | func _show_settings(setting: AssetPlacerSettings): 91 | slider_value.text = str(setting.ui_scale) 92 | ui_scale_h_slider.set_value_no_signal(setting.ui_scale) 93 | trasform_step_spin_box.set_value_no_signal(setting.transform_step) 94 | rotation_step_spin_box.set_value_no_signal(setting.rotation_step) 95 | keybinding_option_rotate.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.Rotate]) 96 | keybinding_option_translate.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.Translate]) 97 | keybinding_option_scale.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.Scale]) 98 | keybinding_option_grid_snap.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.GridSnapping]) 99 | keybinding_option_in_place_transform.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.InPlaceTransform]) 100 | keybinding_option_negative_transform.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.TransformNegative]) 101 | keybinding_option_positive_transform.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.TransformPositive]) 102 | keybinding_option_axis_x.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.ToggleAxisX]) 103 | keybinding_option_axis_y.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.ToggleAxisY]) 104 | keybinding_option_axis_z.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.ToggleAxisZ]) 105 | keybinding_option_plane_mode.set_keybind(setting.bindings[AssetPlacerSettings.Bindings.TogglePlaneMode]) 106 | update_channel_option_button.select(setting.update_channel) 107 | plane_material_picker_button.text = setting.plane_material_resource.get_file() 108 | if setting.preview_material_resource.is_empty(): 109 | material_picker_button.text = "No Preview Material" 110 | else: 111 | material_picker_button.text = setting.preview_material_resource.get_file() 112 | 113 | 114 | func _show_preview_material_picker(): 115 | EditorInterface.popup_quick_open(_presenter.set_preview_material, ["BaseMaterial3D"]) 116 | -------------------------------------------------------------------------------- /addons/asset_placer/data/asset_placer_settings_repository.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacerSettingsRepository 3 | 4 | signal settings_changed(settings: AssetPlacerSettings) 5 | 6 | var _editor_settings: EditorSettings 7 | static var instance: AssetPlacerSettingsRepository 8 | 9 | func _init(): 10 | _editor_settings = EditorInterface.get_editor_settings() 11 | instance = self 12 | 13 | 14 | const KEY_BASE = "asset_placer/%s" 15 | const KEY_BINDING_SCALE: String = "bindings/scale_asset" 16 | const KEY_TRANSFORM_STEP: String = "general/transform_step_normal" 17 | const KEY_ROTATION_STEP: String = "general/rotation_step_normal" 18 | const KEY_BINDING_ROTATE: String = "bindings/rotate_asset" 19 | const KEY_BINDING_TRANSLATE: String = "bindings/translate_asset" 20 | const KEY_BINDING_GRID_SNAP: String = "bindings/grid_snapping" 21 | const KEY_GENERAL_PREVIEW_MATERIAL: String = "general/preview_material" 22 | const KEY_GENERAL_PLANE_MATERIAL: String = "general/plane_material" 23 | const KEY_BINDING_IN_PLACE_TRANSFORM: String = "bindings/in_place_transform" 24 | const KEY_BINDING_TRANSFORM_POSITIVE: String = "bindings/positive_transform" 25 | const KEY_BINDING_TRANSFORM_NEGATIVE: String = "bindings/negative_transform" 26 | const KEY_BINDING_TOGGLE_AXIS_X: String = "bindings/toggle_axis_x" 27 | const KEY_BINDING_TOGGLE_AXIS_Y: String = "bindings/toggle_axis_y" 28 | const KEY_BINDING_TOGGLE_AXIS_Z: String = "bindings/toggle_axis_z" 29 | const KEY_BINDING_TOGGLE_PLANE_MODE: String = "bindings/toggle_plane_mode" 30 | const KEY_UI_SCALE: String = "general/ui_scale" 31 | const KEY_ASSET_LIBRARY_PATH: String = "general/asset_library_path" 32 | const KEY_UPDATE_CHANNEL: String = "general/update_channel" 33 | 34 | func _get_binding_storage_key(binding: AssetPlacerSettings.Bindings) -> String: 35 | match binding: 36 | AssetPlacerSettings.Bindings.Rotate: 37 | return KEY_BINDING_ROTATE 38 | AssetPlacerSettings.Bindings.Scale: 39 | return KEY_BINDING_SCALE 40 | AssetPlacerSettings.Bindings.Translate: 41 | return KEY_BINDING_TRANSLATE 42 | AssetPlacerSettings.Bindings.GridSnapping: 43 | return KEY_BINDING_GRID_SNAP 44 | AssetPlacerSettings.Bindings.InPlaceTransform: 45 | return KEY_BINDING_IN_PLACE_TRANSFORM 46 | AssetPlacerSettings.Bindings.TransformPositive: 47 | return KEY_BINDING_TRANSFORM_POSITIVE 48 | AssetPlacerSettings.Bindings.TransformNegative: 49 | return KEY_BINDING_TRANSFORM_NEGATIVE 50 | AssetPlacerSettings.Bindings.ToggleAxisX: 51 | return KEY_BINDING_TOGGLE_AXIS_X 52 | AssetPlacerSettings.Bindings.ToggleAxisY: 53 | return KEY_BINDING_TOGGLE_AXIS_Y 54 | AssetPlacerSettings.Bindings.ToggleAxisZ: 55 | return KEY_BINDING_TOGGLE_AXIS_Z 56 | AssetPlacerSettings.Bindings.TogglePlaneMode: 57 | return KEY_BINDING_TOGGLE_PLANE_MODE 58 | _: 59 | push_error("Unknown binding type: " + str(binding)) 60 | return "" 61 | 62 | 63 | 64 | 65 | func set_settings(settings: AssetPlacerSettings): 66 | var current = get_settings() 67 | 68 | # Validate and resolve keybind conflicts 69 | _validate_and_resolve_conflicts(settings, current) 70 | 71 | # Save all bindings using the mapping function 72 | for binding in settings.bindings.keys(): 73 | var storage_key = _get_binding_storage_key(binding) 74 | if not storage_key.is_empty(): 75 | _set_editor_setting(storage_key, settings.bindings[binding].serialize()) 76 | 77 | _set_project_setting(KEY_GENERAL_PREVIEW_MATERIAL, settings.preview_material_resource) 78 | _set_project_setting(KEY_GENERAL_PLANE_MATERIAL, settings.plane_material_resource) 79 | _set_editor_setting(KEY_TRANSFORM_STEP, settings.transform_step) 80 | _set_editor_setting(KEY_ROTATION_STEP, settings.rotation_step) 81 | _set_editor_setting(KEY_UI_SCALE, settings.ui_scale) 82 | _set_editor_setting(KEY_UPDATE_CHANNEL, settings.update_channel) 83 | _set_project_setting(KEY_ASSET_LIBRARY_PATH, settings.asset_library_path) 84 | settings_changed.emit(get_settings()) 85 | 86 | func get_settings() -> AssetPlacerSettings: 87 | var settings := AssetPlacerSettings.default() 88 | 89 | # Load all bindings using the mapping function 90 | for binding in settings.bindings.keys(): 91 | var storage_key = _get_binding_storage_key(binding) 92 | if not storage_key.is_empty(): 93 | settings.bindings[binding] = _get_binding_settings(storage_key, settings.bindings[binding]) 94 | 95 | settings.preview_material_resource = _get_project_setting(KEY_GENERAL_PREVIEW_MATERIAL, settings.preview_material_resource) 96 | settings.plane_material_resource = _get_project_setting(KEY_GENERAL_PLANE_MATERIAL, settings.plane_material_resource) 97 | settings.transform_step = _get_editor_setting(KEY_TRANSFORM_STEP, settings.transform_step) 98 | settings.rotation_step = _get_editor_setting(KEY_ROTATION_STEP, settings.rotation_step) 99 | settings.ui_scale = _get_editor_setting(KEY_UI_SCALE, settings.ui_scale) 100 | settings.update_channel = _get_editor_setting(KEY_UPDATE_CHANNEL, settings.update_channel) 101 | settings.asset_library_path = _get_project_setting(KEY_ASSET_LIBRARY_PATH, settings.asset_library_path) 102 | return settings 103 | 104 | func _get_binding_settings(key: String, default: APInputOption) -> APInputOption: 105 | var raw = _get_editor_setting(key, default.serialize()) 106 | return APInputOption.desirialize(raw) 107 | 108 | func _set_project_setting(key: String, value: Variant): 109 | ProjectSettings.set_setting(KEY_BASE % key, value) 110 | 111 | func _get_project_setting(key: String, default: Variant): 112 | return ProjectSettings.get_setting(KEY_BASE % key, default) 113 | 114 | func _set_editor_setting(key: String, value): 115 | _editor_settings.set(KEY_BASE % key, value) 116 | 117 | func _get_editor_setting(key: String, default: Variant) -> Variant: 118 | if _editor_settings.has_setting(KEY_BASE % key): 119 | return _editor_settings.get(KEY_BASE % key) 120 | else: 121 | return default 122 | 123 | func _validate_and_resolve_conflicts(new_settings: AssetPlacerSettings, old_settings: AssetPlacerSettings): 124 | for binding in AssetPlacerSettings.Bindings.values(): 125 | var new : APInputOption = new_settings.bindings[binding] 126 | 127 | if new == null: 128 | continue 129 | 130 | if new.equals(APInputOption.none()): 131 | continue 132 | 133 | 134 | for old_binding in AssetPlacerSettings.Bindings.values(): 135 | if binding == old_binding: 136 | continue 137 | 138 | var old = old_settings.bindings[old_binding] 139 | if new.equals(old): 140 | new_settings.bindings[old_binding] = APInputOption.none() 141 | 142 | 143 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_library_window/asset_library_window.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | class_name AssetLibraryWindow 4 | 5 | @onready var presenter = AssetLibraryPresenter.new() 6 | @onready var folder_presenter = FolderPresenter.new() 7 | 8 | @onready var placer_presenter := AssetPlacerPresenter._instance 9 | @onready var grid_container: Container = %GridContainer 10 | @onready var preview_resource = preload("res://addons/asset_placer/ui/components/asset_resource_preview.tscn") 11 | @onready var add_folder_button: Button = %AddFolderButton 12 | @onready var search_field: LineEdit = %SearchField 13 | @onready var filter_button: Button = %FilterButton 14 | @onready var filters_label: Label = %FiltersLabel 15 | @onready var reload_button: Button = %ReloadButton 16 | @onready var progress_bar = %ProgressBar 17 | @onready var empty_content = %EmptyContent 18 | @onready var main_content = %MainContent 19 | @onready var empty_collection_content = %EmptyCollectionContent 20 | @onready var empty_collection_view_add_folder_btn: Button = %EmptyCollectionViewAddFolderBtn 21 | @onready var scroll_container = %ScrollContainer 22 | @onready var empty_search_content = %EmptySearchContent 23 | @onready var empty_view_add_folder_btn = %EmptyViewAddFolderBtn 24 | 25 | signal asset_selected(asset: AssetResource) 26 | 27 | 28 | func _ready(): 29 | presenter.assets_loaded.connect(show_assets) 30 | presenter.show_filter_info.connect(show_filter_info) 31 | presenter.show_sync_active.connect(show_sync_in_progress) 32 | AssetPlacerPresenter._instance.asset_selected.connect(set_selected_asset) 33 | AssetPlacerPresenter._instance.asset_deselected.connect(clear_selected_asset) 34 | empty_collection_view_add_folder_btn.pressed.connect(show_folder_dialog) 35 | empty_view_add_folder_btn.pressed.connect(show_folder_dialog) 36 | presenter.show_empty_view.connect(show_empty_view) 37 | 38 | presenter.on_ready() 39 | add_folder_button.pressed.connect(show_folder_dialog) 40 | search_field.text_changed.connect(presenter.on_query_change) 41 | reload_button.pressed.connect(presenter.sync) 42 | filter_button.pressed.connect(func (): 43 | CollectionPicker.show_in(filter_button, presenter._active_collections, presenter.toggle_collection_filter) 44 | ) 45 | 46 | 47 | 48 | 49 | func show_assets(assets: Array[AssetResource]): 50 | placer_presenter.current_assets = assets 51 | empty_collection_content.hide() 52 | scroll_container.show() 53 | for child in grid_container.get_children(): 54 | child.queue_free() 55 | for asset in assets: 56 | var child: AssetResourcePreview = preview_resource.instantiate() 57 | child.left_clicked.connect(AssetPlacerPresenter._instance.toggle_asset) 58 | child.right_clicked.connect(func(asset): 59 | show_asset_menu(asset, child) 60 | ) 61 | child.set_meta("id", asset.id) 62 | grid_container.add_child(child) 63 | child.set_asset(asset) 64 | 65 | func show_asset_menu(asset: AssetResource, control: Control): 66 | var options_menu := PopupMenu.new() 67 | var mouse_pos = DisplayServer.mouse_get_position() 68 | options_menu.add_icon_item(EditorIconTexture2D.new("Groups"), "Manage collections") 69 | options_menu.add_icon_item(EditorIconTexture2D.new("File"), "Open") 70 | options_menu.add_icon_item(EditorIconTexture2D.new("Remove"), "Remove") 71 | options_menu.index_pressed.connect(func(index): 72 | match index: 73 | 0: CollectionPicker.show_in(control, asset.shallow_collections, func(collection, add): 74 | presenter.toggle_asset_collection(asset, collection, add) 75 | ) 76 | 1: 77 | EditorInterface.open_scene_from_path(asset.scene.resource_path) 78 | EditorInterface.set_main_screen_editor("3D") 79 | 2: 80 | if placer_presenter._selected_asset == asset: 81 | placer_presenter.clear_selection() 82 | presenter.delete_asset(asset) 83 | _: pass 84 | ) 85 | EditorInterface.popup_dialog(options_menu, Rect2(mouse_pos, options_menu.get_contents_minimum_size())) 86 | 87 | func show_folder_dialog(): 88 | var folder_dialog = EditorFileDialog.new() 89 | folder_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_DIR 90 | folder_dialog.access = EditorFileDialog.ACCESS_RESOURCES 91 | folder_dialog.dir_selected.connect(presenter.add_asset_folder) 92 | EditorInterface.popup_dialog_centered(folder_dialog) 93 | 94 | 95 | func clear_selected_asset(): 96 | for child in grid_container.get_children(): 97 | if child is Button: 98 | child.set_pressed_no_signal(false) 99 | 100 | func _can_drop_data(at_position, data): 101 | if data is Dictionary: 102 | var type = data["type"] 103 | var files_or_dirs = type == "files_and_dirs" || type == "files" 104 | return files_or_dirs and data.has("files") 105 | return false 106 | 107 | func _drop_data(at_position, data): 108 | var dirs: PackedStringArray = data["files"] 109 | presenter.add_assets_or_folders(dirs) 110 | 111 | func show_filter_info(size: int): 112 | if size == 0: 113 | filters_label.hide() 114 | else: 115 | filters_label.show() 116 | filters_label.text = str(size) 117 | 118 | func set_selected_asset(asset: AssetResource): 119 | for child in grid_container.get_children(): 120 | if child is Button: 121 | child.set_pressed_no_signal(child.get_meta("id") == asset.id) 122 | 123 | 124 | 125 | 126 | func show_empty_view(type: AssetLibraryPresenter.EmptyType): 127 | match type: 128 | AssetLibraryPresenter.EmptyType.Search: 129 | show_empty_search_content() 130 | AssetLibraryPresenter.EmptyType.Collection: 131 | show_empty_collection_view() 132 | AssetLibraryPresenter.EmptyType.All: 133 | show_onboarding() 134 | AssetLibraryPresenter.EmptyType.None: 135 | show_main_content() 136 | 137 | func show_main_content(): 138 | main_content.show() 139 | empty_content.hide() 140 | scroll_container.show() 141 | empty_collection_content.hide() 142 | empty_search_content.hide() 143 | 144 | func show_onboarding(): 145 | main_content.hide() 146 | empty_collection_content.hide() 147 | empty_search_content.hide() 148 | empty_content.show() 149 | 150 | func show_empty_collection_view(): 151 | main_content.show() 152 | scroll_container.hide() 153 | empty_collection_content.hide() 154 | empty_collection_content.show() 155 | empty_content.hide() 156 | 157 | func show_empty_search_content(): 158 | main_content.show() 159 | scroll_container.hide() 160 | empty_collection_content.hide() 161 | empty_search_content.show() 162 | 163 | func show_sync_in_progress(active: bool): 164 | if active: 165 | reload_button.hide() 166 | progress_bar.show() 167 | else: 168 | reload_button.show() 169 | progress_bar.hide() 170 | -------------------------------------------------------------------------------- /addons/asset_placer/asset_placer_presenter.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacerPresenter 3 | 4 | static var _instance: AssetPlacerPresenter 5 | 6 | static var TRANSFORM_STEP: float = 0.1 7 | 8 | signal asset_deselected 9 | signal parent_changed(parent: NodePath) 10 | signal options_changed(options: AssetPlacerOptions) 11 | signal transform_mode_changed(mode: TransformMode) 12 | signal placement_mode_changed(mode: PlacementMode) 13 | signal preview_transform_axis_changed(axis: Vector3) 14 | signal asset_selected(asset: AssetResource) 15 | signal show_error(message: String) 16 | signal placer_active(value: bool) 17 | signal asset_placed 18 | 19 | var _selected_asset: AssetResource 20 | var options: AssetPlacerOptions 21 | var _parent: NodePath = NodePath("") 22 | var transform_mode: TransformMode = TransformMode.None 23 | var _last_plane_options := PlaneOptions.new(Vector3.UP, Vector3.ZERO) 24 | var _selected_node: Node3D 25 | var current_assets: Array[AssetResource] 26 | 27 | var placement_mode: PlacementMode = PlacementMode.SurfacePlacement.new(): 28 | set(value): 29 | placement_mode = value 30 | placement_mode_changed.emit(value) 31 | 32 | var preview_transform_axis: Vector3 = Vector3.UP 33 | 34 | 35 | enum TransformMode { 36 | None, 37 | Rotate, 38 | Scale, 39 | Move 40 | } 41 | 42 | func _init(): 43 | options = AssetPlacerOptions.new() 44 | self._selected_asset = null 45 | self._instance = self 46 | 47 | func ready(): 48 | options_changed.emit(options) 49 | placement_mode_changed.emit(placement_mode) 50 | 51 | 52 | func plugin_is_active() -> bool: 53 | if _selected_asset != null or _selected_node != null: 54 | return true 55 | else: 56 | return false 57 | 58 | func toggle_plane_placement(): 59 | placement_mode = PlacementMode.PlanePlacement.new(_last_plane_options) 60 | 61 | 62 | func cycle_placement_mode(): 63 | if placement_mode is PlacementMode.SurfacePlacement: 64 | toggle_plane_placement() 65 | elif placement_mode is PlacementMode.PlanePlacement: 66 | toggle_transformation_mode(TransformMode.None) 67 | toggle_surface_placement() 68 | 69 | func toggle_surface_placement(): 70 | placement_mode = PlacementMode.SurfacePlacement.new() 71 | 72 | func toggle_terrain_3d_placement(node_path: NodePath): 73 | if not node_path.is_empty(): 74 | var node = EditorInterface.get_edited_scene_root().get_node(node_path) 75 | self.placement_mode = PlacementMode.Terrain3DPlacement.new(node) 76 | else: 77 | placement_mode_changed.emit(placement_mode) 78 | 79 | func select_placement_mode(mode: PlacementMode): 80 | self.placement_mode = mode 81 | 82 | func select_parent(node: NodePath): 83 | self._parent = node 84 | parent_changed.emit(node) 85 | 86 | func toggle_transformation_mode(mode: TransformMode): 87 | if transform_mode == mode: 88 | transform_mode = TransformMode.None 89 | else: 90 | transform_mode = mode 91 | transform_mode_changed.emit(transform_mode) 92 | 93 | if transform_mode == TransformMode.Move: 94 | select_placement_mode(PlacementMode.PlanePlacement.new(_last_plane_options)) 95 | 96 | if transform_mode == TransformMode.Rotate: 97 | set_random_rotation_enabled(false) 98 | 99 | if transform_mode == TransformMode.Scale: 100 | set_random_scale_enabled(false) 101 | 102 | _select_default_axis(transform_mode) 103 | 104 | func clear_parent(): 105 | self._parent = NodePath("") 106 | parent_changed.emit(_parent) 107 | 108 | func set_unform_scaling(value: bool): 109 | options.uniform_scaling = value 110 | if value: 111 | options.min_scale = uniformV3(options.min_scale.x) 112 | options.max_scale = uniformV3(options.max_scale.x) 113 | options_changed.emit(options) 114 | 115 | func set_grid_snap_value(value: float): 116 | options.snapping_grid_step = value 117 | options_changed.emit(options) 118 | 119 | func set_random_asset_enabled(value: bool): 120 | options.enable_random_placement = value 121 | options_changed.emit(options) 122 | 123 | func toggle_axis(axis: Vector3): 124 | var new := (preview_transform_axis - axis).abs() 125 | select_axis(new) 126 | 127 | func select_axis(axis: Vector3): 128 | if axis == Vector3.ZERO: 129 | show_error.emit("Ignoring Axis selection because it is zero") 130 | return 131 | preview_transform_axis = axis 132 | preview_transform_axis_changed.emit(preview_transform_axis) 133 | 134 | var movement_mode = transform_mode == TransformMode.Move 135 | var idle_mode = transform_mode == TransformMode.None 136 | var plane_placement = placement_mode is PlacementMode.PlanePlacement 137 | if plane_placement and (idle_mode || movement_mode): 138 | _last_plane_options.normal = axis.normalized() 139 | placement_mode = PlacementMode.PlanePlacement.new(_last_plane_options) 140 | 141 | 142 | func set_random_scale_enabled(value: bool): 143 | options.scale_on_placement = value 144 | options_changed.emit(options) 145 | 146 | if value and transform_mode == TransformMode.Scale: 147 | toggle_transformation_mode(TransformMode.None) 148 | 149 | func set_random_rotation_enabled(value: bool): 150 | options.rotate_on_placement = value 151 | options_changed.emit(options) 152 | 153 | if value and transform_mode == TransformMode.Rotate: 154 | toggle_transformation_mode(TransformMode.None) 155 | 156 | func set_align_normals(value: bool): 157 | options.align_normals = value 158 | options_changed.emit(options) 159 | 160 | func set_use_asset_origin(value: bool): 161 | options.use_asset_origin = value 162 | options_changed.emit(options) 163 | 164 | func _select_default_axis(mode: TransformMode): 165 | match mode: 166 | TransformMode.Rotate: 167 | select_axis(Vector3.UP) 168 | TransformMode.Scale: 169 | select_axis(Vector3.ONE) 170 | TransformMode.Move: 171 | select_axis(_last_plane_options.normal) 172 | _: pass 173 | 174 | func uniformV3(value: float) -> Vector3: 175 | return Vector3(value, value, value) 176 | 177 | func set_grid_snapping_enabled(value: bool): 178 | options.snapping_enabled = value 179 | options_changed.emit(options) 180 | 181 | func toggle_grid_snapping(): 182 | set_grid_snapping_enabled(!options.snapping_enabled) 183 | 184 | func set_min_rotation(vector: Vector3): 185 | options.min_rotation = vector 186 | options_changed.emit(options) 187 | 188 | func set_max_scale(vector: Vector3): 189 | options.max_scale = vector 190 | options_changed.emit(options) 191 | 192 | func set_min_scale(vector: Vector3): 193 | options.min_scale = vector 194 | options_changed.emit(options) 195 | 196 | 197 | func set_max_rotation(vector: Vector3): 198 | options.max_rotation = vector 199 | options_changed.emit(options) 200 | 201 | func cancel(): 202 | if transform_mode != TransformMode.None: 203 | toggle_transformation_mode(TransformMode.None) 204 | elif _selected_node != null: 205 | end_node_transform_mode() 206 | else: 207 | clear_selection() 208 | 209 | func clear_selection(): 210 | _selected_asset = null 211 | asset_deselected.emit() 212 | placer_active.emit(false) 213 | 214 | func toggle_asset(asset: AssetResource): 215 | if asset == _selected_asset: 216 | _selected_asset = null 217 | asset_deselected.emit() 218 | placer_active.emit(false) 219 | else: 220 | _selected_asset = asset 221 | asset_selected.emit(asset) 222 | placer_active.emit(true) 223 | 224 | func select_asset(asset: AssetResource): 225 | _selected_asset = asset 226 | asset_selected.emit(asset) 227 | placer_active.emit(true) 228 | 229 | func start_node_transform_mode(node: Node3D): 230 | _selected_node = node 231 | placer_active.emit(true) 232 | 233 | func end_node_transform_mode(): 234 | _selected_node = null 235 | placer_active.emit(false) 236 | 237 | 238 | func on_asset_placed(): 239 | if options.enable_random_placement: 240 | var random = current_assets.pick_random() 241 | select_asset(random) 242 | 243 | 244 | func is_node_transform_mode() -> bool: 245 | return _selected_node != null 246 | 247 | func get_selected_node() -> Node3D: 248 | return _selected_node 249 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/viewport_overlay/viewport_overlay.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=13 format=3 uid="uid://y4168twxqqwr"] 2 | 3 | [ext_resource type="Script" uid="uid://e3l8cnd506a1" path="res://addons/asset_placer/ui/viewport_overlay/viewport_overlay.gd" id="1_3e3nf"] 4 | [ext_resource type="Script" uid="uid://dmicn3kmr620j" path="res://addons/asset_placer/utils/system_icon.gd" id="2_1410h"] 5 | [ext_resource type="Texture2D" uid="uid://be61c5ff55gfo" path="res://addons/asset_placer/icon.png" id="3_h3uaa"] 6 | 7 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_epucf"] 8 | content_margin_left = 16.0 9 | content_margin_top = 16.0 10 | content_margin_right = 16.0 11 | content_margin_bottom = 16.0 12 | bg_color = Color(0.17334509, 0.17334506, 0.17334506, 0.6) 13 | corner_radius_top_left = 16 14 | corner_detail = 5 15 | 16 | [sub_resource type="Texture2D" id="Texture2D_luwev"] 17 | resource_local_to_scene = false 18 | resource_name = "" 19 | script = ExtResource("2_1410h") 20 | icon_name = &"Snap" 21 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 22 | 23 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bu8qk"] 24 | content_margin_left = 16.0 25 | content_margin_top = 16.0 26 | content_margin_right = 16.0 27 | content_margin_bottom = 16.0 28 | bg_color = Color(0.17254902, 0.17254902, 0.17254902, 0.6) 29 | corner_radius_top_right = 16 30 | corner_detail = 5 31 | 32 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_luwev"] 33 | content_margin_left = 8.0 34 | content_margin_top = 8.0 35 | content_margin_right = 8.0 36 | content_margin_bottom = 8.0 37 | bg_color = Color(0.17254902, 0.17254902, 0.17254902, 0.6) 38 | border_width_left = 2 39 | border_width_top = 2 40 | border_width_right = 2 41 | border_width_bottom = 2 42 | border_color = Color(0.64565563, 0.1252135, 0.21010175, 1) 43 | corner_radius_top_right = 16 44 | corner_radius_bottom_right = 16 45 | corner_detail = 5 46 | 47 | [sub_resource type="Texture2D" id="Texture2D_bu8qk"] 48 | resource_local_to_scene = false 49 | resource_name = "" 50 | script = ExtResource("2_1410h") 51 | icon_name = &"StatusError" 52 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 53 | 54 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h3uaa"] 55 | content_margin_left = 24.0 56 | content_margin_top = 16.0 57 | content_margin_right = 24.0 58 | content_margin_bottom = 24.0 59 | bg_color = Color(0.17254902, 0.17254902, 0.17254902, 0.6) 60 | corner_radius_top_left = 16 61 | corner_radius_top_right = 16 62 | corner_radius_bottom_right = 3 63 | corner_radius_bottom_left = 3 64 | corner_detail = 5 65 | 66 | [sub_resource type="Texture2D" id="Texture2D_h3uaa"] 67 | resource_local_to_scene = false 68 | resource_name = "" 69 | script = ExtResource("2_1410h") 70 | icon_name = &"KeyTrackRotation" 71 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 72 | 73 | [sub_resource type="Texture2D" id="Texture2D_1410h"] 74 | resource_local_to_scene = false 75 | resource_name = "" 76 | script = ExtResource("2_1410h") 77 | icon_name = &"KeyTrackScale" 78 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 79 | 80 | [sub_resource type="Texture2D" id="Texture2D_epucf"] 81 | resource_local_to_scene = false 82 | resource_name = "" 83 | script = ExtResource("2_1410h") 84 | icon_name = &"KeyTrackPosition" 85 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 86 | 87 | [node name="ViewportOverlay" type="Control"] 88 | layout_mode = 3 89 | anchors_preset = 15 90 | anchor_right = 1.0 91 | anchor_bottom = 1.0 92 | grow_horizontal = 2 93 | grow_vertical = 2 94 | focus_mode = 2 95 | script = ExtResource("1_3e3nf") 96 | 97 | [node name="MarginContainer2" type="PanelContainer" parent="."] 98 | layout_mode = 1 99 | anchors_preset = -1 100 | anchor_left = 1.0 101 | anchor_top = 1.0 102 | anchor_right = 1.0 103 | anchor_bottom = 1.0 104 | offset_left = -203.0 105 | offset_top = -69.0 106 | grow_horizontal = 0 107 | grow_vertical = 0 108 | theme_override_styles/panel = SubResource("StyleBoxFlat_epucf") 109 | 110 | [node name="SnappingSwitch" type="CheckButton" parent="MarginContainer2"] 111 | unique_name_in_owner = true 112 | layout_mode = 2 113 | text = "S: Grid Snapping" 114 | icon = SubResource("Texture2D_luwev") 115 | 116 | [node name="MarginContainer3" type="PanelContainer" parent="."] 117 | layout_mode = 1 118 | anchors_preset = -1 119 | anchor_top = 1.0 120 | anchor_bottom = 1.0 121 | offset_top = -64.0 122 | offset_right = 239.0 123 | grow_vertical = 0 124 | theme_override_styles/panel = SubResource("StyleBoxFlat_bu8qk") 125 | 126 | [node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer3"] 127 | layout_mode = 2 128 | size_flags_horizontal = 4 129 | size_flags_vertical = 4 130 | 131 | [node name="TextureRect" type="TextureRect" parent="MarginContainer3/HBoxContainer"] 132 | custom_minimum_size = Vector2(32, 32) 133 | layout_mode = 2 134 | size_flags_horizontal = 4 135 | size_flags_vertical = 4 136 | texture = ExtResource("3_h3uaa") 137 | expand_mode = 1 138 | stretch_mode = 5 139 | 140 | [node name="PlacementModeLabel" type="Label" parent="MarginContainer3/HBoxContainer"] 141 | unique_name_in_owner = true 142 | layout_mode = 2 143 | text = "Surface Placement" 144 | 145 | [node name="PlacementShortcutLabel" type="Label" parent="MarginContainer3/HBoxContainer"] 146 | unique_name_in_owner = true 147 | layout_mode = 2 148 | text = "(Q)" 149 | 150 | [node name="ErrorContainer" type="PanelContainer" parent="."] 151 | unique_name_in_owner = true 152 | layout_mode = 1 153 | anchors_preset = -1 154 | anchor_top = 0.745 155 | anchor_bottom = 0.745 156 | offset_left = -2.0 157 | offset_top = -27.5 158 | offset_right = 258.0 159 | offset_bottom = 27.5 160 | grow_vertical = 2 161 | theme_override_styles/panel = SubResource("StyleBoxFlat_luwev") 162 | 163 | [node name="HBoxContainer" type="HBoxContainer" parent="ErrorContainer"] 164 | layout_mode = 2 165 | theme_override_constants/separation = 12 166 | 167 | [node name="TextureRect" type="TextureRect" parent="ErrorContainer/HBoxContainer"] 168 | layout_mode = 2 169 | texture = SubResource("Texture2D_bu8qk") 170 | stretch_mode = 3 171 | 172 | [node name="ErrorLabel" type="Label" parent="ErrorContainer/HBoxContainer"] 173 | unique_name_in_owner = true 174 | layout_mode = 2 175 | theme_override_font_sizes/font_size = 12 176 | text = "No Surface to Collide With" 177 | 178 | [node name="ErrorTimer" type="Timer" parent="."] 179 | unique_name_in_owner = true 180 | wait_time = 3.0 181 | one_shot = true 182 | 183 | [node name="Panel" type="PanelContainer" parent="."] 184 | layout_mode = 1 185 | anchors_preset = -1 186 | anchor_left = 0.5 187 | anchor_top = 1.0 188 | anchor_right = 0.5 189 | anchor_bottom = 1.0 190 | offset_left = -191.5 191 | offset_top = -71.0 192 | offset_right = 191.5 193 | grow_horizontal = 2 194 | grow_vertical = 0 195 | theme_override_styles/panel = SubResource("StyleBoxFlat_h3uaa") 196 | 197 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel"] 198 | layout_mode = 2 199 | 200 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"] 201 | layout_mode = 2 202 | 203 | [node name="RotateCheckButton" type="CheckBox" parent="Panel/VBoxContainer/HBoxContainer"] 204 | unique_name_in_owner = true 205 | layout_mode = 2 206 | text = "E: To Rotate" 207 | icon = SubResource("Texture2D_h3uaa") 208 | 209 | [node name="ScaleCheckButton" type="CheckBox" parent="Panel/VBoxContainer/HBoxContainer"] 210 | unique_name_in_owner = true 211 | layout_mode = 2 212 | text = "R: To Scale" 213 | icon = SubResource("Texture2D_1410h") 214 | 215 | [node name="TranslateCheckButton" type="CheckBox" parent="Panel/VBoxContainer/HBoxContainer"] 216 | unique_name_in_owner = true 217 | layout_mode = 2 218 | text = "W: To Translate" 219 | icon = SubResource("Texture2D_epucf") 220 | 221 | [node name="HBoxContainer2" type="HBoxContainer" parent="Panel/VBoxContainer"] 222 | layout_mode = 2 223 | alignment = 1 224 | 225 | [node name="Label" type="Label" parent="Panel/VBoxContainer/HBoxContainer2"] 226 | unique_name_in_owner = true 227 | layout_mode = 2 228 | text = "Active Axis" 229 | 230 | [node name="XCheckButton" type="CheckButton" parent="Panel/VBoxContainer/HBoxContainer2"] 231 | unique_name_in_owner = true 232 | layout_mode = 2 233 | theme_override_colors/font_color = Color(0.9411765, 0.23529412, 0.37254903, 1) 234 | theme_override_font_sizes/font_size = 20 235 | text = "X" 236 | 237 | [node name="YCheckButton" type="CheckButton" parent="Panel/VBoxContainer/HBoxContainer2"] 238 | unique_name_in_owner = true 239 | layout_mode = 2 240 | theme_override_colors/font_color = Color(0.2, 0.6627451, 0.9607843, 1) 241 | theme_override_font_sizes/font_size = 20 242 | button_pressed = true 243 | text = "Y 244 | " 245 | 246 | [node name="ZCheckButton" type="CheckButton" parent="Panel/VBoxContainer/HBoxContainer2"] 247 | unique_name_in_owner = true 248 | layout_mode = 2 249 | theme_override_colors/font_color = Color(0.6, 0.85490197, 0.047058824, 1) 250 | theme_override_font_sizes/font_size = 20 251 | text = "Z" 252 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/about/about_window.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=3 uid="uid://dynyrwa38r5ag"] 2 | 3 | [ext_resource type="Script" uid="uid://43kdnn7nr2ch" path="res://addons/asset_placer/ui/about/about_window.gd" id="1_namxt"] 4 | [ext_resource type="Script" uid="uid://dmicn3kmr620j" path="res://addons/asset_placer/utils/system_icon.gd" id="2_r5xs8"] 5 | [ext_resource type="Texture2D" uid="uid://be61c5ff55gfo" path="res://addons/asset_placer/icon.png" id="3_vijnu"] 6 | 7 | [sub_resource type="Texture2D" id="Texture2D_4v60v"] 8 | resource_local_to_scene = false 9 | resource_name = "" 10 | script = ExtResource("2_r5xs8") 11 | icon_name = &"NodeInfo" 12 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 13 | 14 | [sub_resource type="Texture2D" id="Texture2D_vijnu"] 15 | resource_local_to_scene = false 16 | resource_name = "" 17 | script = ExtResource("2_r5xs8") 18 | icon_name = &"Favorites" 19 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 20 | 21 | [sub_resource type="Texture2D" id="Texture2D_r5xs8"] 22 | resource_local_to_scene = false 23 | resource_name = "" 24 | script = ExtResource("2_r5xs8") 25 | icon_name = &"Node3D" 26 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 27 | 28 | [sub_resource type="Texture2D" id="Texture2D_tt7kp"] 29 | resource_local_to_scene = false 30 | resource_name = "" 31 | script = ExtResource("2_r5xs8") 32 | icon_name = &"Debug" 33 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 34 | 35 | [node name="AboutWindow" type="Control"] 36 | layout_mode = 3 37 | anchors_preset = 15 38 | anchor_right = 1.0 39 | anchor_bottom = 1.0 40 | grow_horizontal = 2 41 | grow_vertical = 2 42 | script = ExtResource("1_namxt") 43 | 44 | [node name="UpdatePopup" type="PopupPanel" parent="."] 45 | oversampling_override = 1.0 46 | title = "Update" 47 | initial_position = 1 48 | size = Vector2i(552, 475) 49 | borderless = false 50 | 51 | [node name="MarginContainer" type="MarginContainer" parent="UpdatePopup"] 52 | offset_left = 4.0 53 | offset_top = 4.0 54 | offset_right = 548.0 55 | offset_bottom = 471.0 56 | theme_override_constants/margin_left = 16 57 | theme_override_constants/margin_top = 16 58 | theme_override_constants/margin_right = 16 59 | theme_override_constants/margin_bottom = 16 60 | 61 | [node name="Vbox" type="VBoxContainer" parent="UpdatePopup/MarginContainer"] 62 | layout_mode = 2 63 | theme_override_constants/separation = 12 64 | 65 | [node name="TextureRect" type="TextureRect" parent="UpdatePopup/MarginContainer/Vbox"] 66 | custom_minimum_size = Vector2(120, 120) 67 | layout_mode = 2 68 | texture = ExtResource("3_vijnu") 69 | expand_mode = 1 70 | stretch_mode = 5 71 | 72 | [node name="Label" type="Label" parent="UpdatePopup/MarginContainer/Vbox"] 73 | layout_mode = 2 74 | theme_override_font_sizes/font_size = 32 75 | text = "New Version is Available" 76 | horizontal_alignment = 1 77 | 78 | [node name="Spacing" type="Control" parent="UpdatePopup/MarginContainer/Vbox"] 79 | custom_minimum_size = Vector2(0, 32.14) 80 | layout_mode = 2 81 | 82 | [node name="HBoxContainer" type="VBoxContainer" parent="UpdatePopup/MarginContainer/Vbox"] 83 | layout_mode = 2 84 | size_flags_vertical = 4 85 | alignment = 1 86 | 87 | [node name="UpdateVersionLabel" type="Label" parent="UpdatePopup/MarginContainer/Vbox/HBoxContainer"] 88 | unique_name_in_owner = true 89 | layout_mode = 2 90 | size_flags_horizontal = 3 91 | theme_override_font_sizes/font_size = 24 92 | text = "1.1.5" 93 | horizontal_alignment = 1 94 | 95 | [node name="ChangelogLinkButton" type="LinkButton" parent="UpdatePopup/MarginContainer/Vbox/HBoxContainer"] 96 | unique_name_in_owner = true 97 | layout_mode = 2 98 | size_flags_horizontal = 4 99 | theme_override_font_sizes/font_size = 24 100 | text = "View Changelog" 101 | uri = "https://github.com/levinzonr/godot-asset-placer/blob/main/CHANGELOG.md#115" 102 | 103 | [node name="Spacing2" type="Control" parent="UpdatePopup/MarginContainer/Vbox"] 104 | custom_minimum_size = Vector2(0, 32.14) 105 | layout_mode = 2 106 | 107 | [node name="ProgressBar" type="ProgressBar" parent="UpdatePopup/MarginContainer/Vbox"] 108 | visible = false 109 | layout_mode = 2 110 | indeterminate = true 111 | editor_preview_indeterminate = true 112 | 113 | [node name="DownloadUpdateBtn" type="Button" parent="UpdatePopup/MarginContainer/Vbox"] 114 | unique_name_in_owner = true 115 | layout_mode = 2 116 | text = "Download Update" 117 | 118 | [node name="ApplyUpdateAndRestartBtn" type="Button" parent="UpdatePopup/MarginContainer/Vbox"] 119 | unique_name_in_owner = true 120 | layout_mode = 2 121 | text = "Apply Update and Restart" 122 | 123 | [node name="Panel" type="Panel" parent="."] 124 | layout_mode = 1 125 | anchors_preset = 15 126 | anchor_right = 1.0 127 | anchor_bottom = 1.0 128 | grow_horizontal = 2 129 | grow_vertical = 2 130 | 131 | [node name="CenterContainer" type="CenterContainer" parent="Panel"] 132 | layout_mode = 1 133 | anchors_preset = 15 134 | anchor_right = 1.0 135 | anchor_bottom = 1.0 136 | grow_horizontal = 2 137 | grow_vertical = 2 138 | 139 | [node name="HBoxContainer" type="VBoxContainer" parent="Panel/CenterContainer"] 140 | layout_mode = 2 141 | alignment = 1 142 | 143 | [node name="Label" type="Label" parent="Panel/CenterContainer/HBoxContainer"] 144 | layout_mode = 2 145 | size_flags_horizontal = 4 146 | theme_override_font_sizes/font_size = 48 147 | text = "Asset Placer" 148 | 149 | [node name="VersionLabel" type="Label" parent="Panel/CenterContainer/HBoxContainer"] 150 | unique_name_in_owner = true 151 | layout_mode = 2 152 | size_flags_horizontal = 4 153 | text = "Version 1.2.1" 154 | 155 | [node name="UpdateButton" type="Button" parent="Panel/CenterContainer/HBoxContainer"] 156 | unique_name_in_owner = true 157 | visible = false 158 | layout_mode = 2 159 | size_flags_horizontal = 4 160 | theme_override_constants/h_separation = 4 161 | text = "Version 1.1.5 Availalbe" 162 | icon = SubResource("Texture2D_4v60v") 163 | 164 | [node name="Space" type="Control" parent="Panel/CenterContainer/HBoxContainer"] 165 | custom_minimum_size = Vector2(0, 64) 166 | layout_mode = 2 167 | 168 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel/CenterContainer/HBoxContainer"] 169 | layout_mode = 2 170 | theme_override_constants/separation = 16 171 | alignment = 1 172 | 173 | [node name="HBoxContainer3" type="HBoxContainer" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer"] 174 | layout_mode = 2 175 | size_flags_horizontal = 4 176 | 177 | [node name="TextureRect" type="TextureRect" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer3"] 178 | layout_mode = 2 179 | texture = ExtResource("3_vijnu") 180 | expand_mode = 2 181 | stretch_mode = 5 182 | 183 | [node name="DiscordLinkButton" type="LinkButton" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer3"] 184 | unique_name_in_owner = true 185 | layout_mode = 2 186 | size_flags_horizontal = 4 187 | size_flags_vertical = 3 188 | text = "Join our Discord" 189 | uri = "https://discord.gg/UyYCp53Hym" 190 | 191 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer"] 192 | layout_mode = 2 193 | alignment = 1 194 | 195 | [node name="Label2" type="Label" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer"] 196 | layout_mode = 2 197 | text = "Enjoying this Plugin? Please consider leaving a Star on " 198 | 199 | [node name="Label3" type="LinkButton" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer"] 200 | layout_mode = 2 201 | size_flags_horizontal = 4 202 | size_flags_vertical = 4 203 | text = "GitHub" 204 | uri = "https://github.com/levinzonr/godot-asset-placer" 205 | 206 | [node name="TextureRect" type="TextureRect" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer"] 207 | layout_mode = 2 208 | texture = SubResource("Texture2D_vijnu") 209 | stretch_mode = 3 210 | 211 | [node name="Label" type="Label" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer"] 212 | layout_mode = 2 213 | text = "Or" 214 | horizontal_alignment = 1 215 | 216 | [node name="HBoxContainer2" type="HBoxContainer" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer"] 217 | layout_mode = 2 218 | size_flags_vertical = 3 219 | alignment = 1 220 | 221 | [node name="CenterContainer" type="BoxContainer" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer2"] 222 | layout_mode = 2 223 | size_flags_horizontal = 3 224 | 225 | [node name="FeatureRequestButton" type="Button" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer2/CenterContainer"] 226 | unique_name_in_owner = true 227 | layout_mode = 2 228 | text = "Missing Feature?" 229 | icon = SubResource("Texture2D_r5xs8") 230 | alignment = 0 231 | 232 | [node name="CenterContainer2" type="BoxContainer" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer2"] 233 | layout_mode = 2 234 | size_flags_horizontal = 3 235 | alignment = 2 236 | 237 | [node name="IssueButton" type="Button" parent="Panel/CenterContainer/HBoxContainer/VBoxContainer/HBoxContainer2/CenterContainer2"] 238 | unique_name_in_owner = true 239 | layout_mode = 2 240 | text = "Spotted Bug / Issue?" 241 | icon = SubResource("Texture2D_tt7kp") 242 | alignment = 0 243 | 244 | [node name="MarginContainer" type="MarginContainer" parent="Panel"] 245 | layout_mode = 1 246 | anchors_preset = 1 247 | anchor_left = 1.0 248 | anchor_right = 1.0 249 | offset_left = -123.0 250 | offset_bottom = 40.0 251 | grow_horizontal = 0 252 | theme_override_constants/margin_left = 12 253 | theme_override_constants/margin_top = 12 254 | theme_override_constants/margin_right = 12 255 | theme_override_constants/margin_bottom = 12 256 | -------------------------------------------------------------------------------- /addons/asset_placer/asset_placer_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | var _folder_repository: FolderRepository 5 | var _presenter: AssetPlacerPresenter 6 | var _asset_placer: AssetPlacer 7 | var _assets_repository: AssetsRepository 8 | var _collection_repository: AssetCollectionRepository 9 | var synchronizer: Synchronize 10 | var _updater: PluginUpdater 11 | var _async: AssetPlacerAsync 12 | 13 | var _asset_placer_window: AssetLibraryPanel 14 | var _file_system: EditorFileSystem = EditorInterface.get_resource_filesystem() 15 | var _viewport_overlay_res = preload("res://addons/asset_placer/ui/viewport_overlay/viewport_overlay.tscn") 16 | var _plane_preview: Node3D 17 | var _asset_placer_button: Button 18 | var overlay: Control 19 | var plane_placer: PlanePlacer 20 | 21 | var _migration_collection_id = load("res://addons/asset_placer/data/migrations/collection_id_migration.gd") 22 | 23 | var settings_repository: AssetPlacerSettingsRepository 24 | var current_settings: AssetPlacerSettings 25 | var _data_source: AssetLibraryDataSource 26 | 27 | var plugin_path: String: 28 | get(): return get_script().resource_path.get_base_dir() 29 | 30 | 31 | const ADDON_PATH = "res://addons/asset_placer" 32 | 33 | 34 | func _enable_plugin(): 35 | pass 36 | 37 | func _disable_plugin(): 38 | pass 39 | 40 | func _enter_tree(): 41 | _run_migrations() 42 | _initialize_data_layer() 43 | _async = AssetPlacerAsync.new() 44 | _presenter = AssetPlacerPresenter.new() 45 | AssetPlacerDockPresenter.new() 46 | _updater = PluginUpdater.new(ADDON_PATH + "/plugin.cfg", "") 47 | _plane_preview = load("res://addons/asset_placer/ui/plane_preview/plan_preview.tscn").instantiate() 48 | get_tree().root.add_child(_plane_preview) 49 | plane_placer = PlanePlacer.new(_presenter, _plane_preview) 50 | 51 | _asset_placer = AssetPlacer.new(get_undo_redo(), plane_placer) 52 | synchronizer = Synchronize.new(_folder_repository, _assets_repository) 53 | scene_changed.connect(_handle_scene_changed) 54 | _init_parent_scene.call_deferred() 55 | _presenter.asset_selected.connect(start_placement) 56 | _presenter.asset_deselected.connect(_asset_placer.stop_placement) 57 | _asset_placer_window = load("res://addons/asset_placer/ui/asset_library_panel.tscn").instantiate() 58 | _asset_placer_button = add_control_to_bottom_panel(_asset_placer_window, "Asset Placer") 59 | _asset_placer_window.visibility_changed.connect(_on_dock_visibility_changed) 60 | 61 | _presenter.placement_mode_changed.connect(_asset_placer.set_placement_mode) 62 | 63 | synchronizer.sync_complete.connect(func(added, removed, scanned): 64 | var message = "Asset Placer Sync complete\nAdded: %d Removed: %d Scanned total: %d" % [added, removed, scanned] 65 | EditorToasterCompat.toast(message) 66 | ) 67 | 68 | self.overlay = _viewport_overlay_res.instantiate() 69 | get_editor_interface().get_editor_viewport_3d().add_child(overlay) 70 | 71 | _file_system.resources_reimported.connect(_react_to_reimorted_files) 72 | if !_file_system.is_scanning(): 73 | synchronizer.sync_all() 74 | 75 | _updater.updater_update_available.connect(_show_update_available) 76 | _updater.updater_up_to_date.connect(_show_plugin_up_to_date) 77 | _updater.update_ready.connect(_show_update_available) 78 | 79 | func _exit_tree(): 80 | _updater.updater_up_to_date.disconnect(_show_plugin_up_to_date) 81 | _updater.updater_update_available.disconnect(_show_update_available) 82 | _updater.update_ready.disconnect(_show_update_available) 83 | overlay.queue_free() 84 | _plane_preview.queue_free() 85 | settings_repository.settings_changed.disconnect(_react_to_settings_change) 86 | _file_system.resources_reimported.disconnect(_react_to_reimorted_files) 87 | _presenter.asset_selected.disconnect(start_placement) 88 | _presenter.asset_deselected.disconnect(_asset_placer.stop_placement) 89 | _asset_placer_window.visibility_changed.disconnect(_on_dock_visibility_changed) 90 | _asset_placer.stop_placement() 91 | scene_changed.disconnect(_handle_scene_changed) 92 | remove_control_from_bottom_panel(_asset_placer_window) 93 | _asset_placer_window.queue_free() 94 | _async.await_completion() 95 | 96 | func _init_parent_scene(): 97 | var current_scene = get_tree().edited_scene_root 98 | if current_scene and current_scene is Node3D: 99 | _handle_scene_changed(current_scene) 100 | 101 | func _handles(object): 102 | return object is Node3D 103 | 104 | func _handle_scene_changed(scene: Node): 105 | if scene is Node3D: 106 | _presenter.select_parent(scene.get_path()) 107 | else: 108 | _presenter.clear_parent() 109 | 110 | func _run_migrations(): 111 | _migration_collection_id.new().run() 112 | 113 | func _initialize_data_layer(): 114 | settings_repository = AssetPlacerSettingsRepository.new() 115 | current_settings = settings_repository.get_settings() 116 | settings_repository.settings_changed.connect(_react_to_settings_change) 117 | _data_source = AssetLibraryDataSource.new(current_settings.asset_library_path) 118 | _folder_repository = FolderRepository.new(_data_source) 119 | _assets_repository = AssetsRepository.new(_data_source) 120 | _collection_repository = AssetCollectionRepository.new(_data_source) 121 | 122 | func _react_to_settings_change(settings: AssetPlacerSettings): 123 | self.current_settings = settings 124 | _asset_placer.set_plugin_settings(settings) 125 | 126 | func _react_to_reimorted_files(files: PackedStringArray): 127 | synchronizer.sync_all() 128 | 129 | func _on_dock_visibility_changed(): 130 | if not _asset_placer_window.visible: 131 | _presenter.toggle_transformation_mode(AssetPlacerPresenter.TransformMode.None) 132 | _presenter.clear_selection() 133 | 134 | 135 | func start_placement(asset: AssetResource): 136 | EditorInterface.set_main_screen_editor("3D") 137 | AssetPlacerContextUtil.select_context() 138 | _asset_placer.start_placement(get_tree().root, asset, _presenter.placement_mode) 139 | 140 | func _on_node_transform_mode_ended(): 141 | # Node transform mode ended, no special action needed 142 | pass 143 | 144 | func _handle_in_place_transform(): 145 | if _presenter.is_node_transform_mode(): 146 | _presenter.end_node_transform_mode() 147 | _asset_placer.stop_placement() 148 | # Check if a Node3D is selected and we're not already in asset placement mode 149 | elif AssetPlacerContextUtil.is_current_selection_node3d() and not _presenter.plugin_is_active(): 150 | var selection = EditorInterface.get_selection() 151 | var selected_nodes = selection.get_selected_nodes() 152 | if selected_nodes.size() == 1 and selected_nodes[0] is Node3D: 153 | _presenter.start_node_transform_mode(selected_nodes[0]) 154 | _asset_placer.start_node_transform(selected_nodes[0], _presenter.placement_mode) 155 | # If we're in asset placement mode, Tab should also exit it 156 | elif _presenter.plugin_is_active() and not _presenter.is_node_transform_mode(): 157 | _presenter.clear_selection() 158 | _asset_placer.stop_placement() 159 | 160 | func _forward_3d_gui_input(viewport_camera, event): 161 | 162 | if current_settings.bindings[AssetPlacerSettings.Bindings.InPlaceTransform].is_pressed(event): 163 | _handle_in_place_transform() 164 | return _handled() 165 | 166 | # Only process other inputs when plugin is active 167 | if not _presenter.plugin_is_active(): 168 | return EditorPlugin.AFTER_GUI_INPUT_PASS 169 | 170 | if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT): 171 | return false 172 | 173 | if current_settings.bindings[AssetPlacerSettings.Bindings.Rotate].is_pressed(event): 174 | _presenter.toggle_transformation_mode(AssetPlacerPresenter.TransformMode.Rotate) 175 | return _handled() 176 | 177 | if current_settings.bindings[AssetPlacerSettings.Bindings.Scale].is_pressed(event): 178 | _presenter.toggle_transformation_mode(AssetPlacerPresenter.TransformMode.Scale) 179 | return _handled() 180 | if current_settings.bindings[AssetPlacerSettings.Bindings.Translate].is_pressed(event): 181 | _presenter.toggle_transformation_mode(AssetPlacerPresenter.TransformMode.Move) 182 | return _handled() 183 | if current_settings.bindings[AssetPlacerSettings.Bindings.GridSnapping].is_pressed(event): 184 | _presenter.toggle_grid_snapping() 185 | return _handled() 186 | 187 | 188 | if current_settings.binding_positive_transform.is_pressed(event): 189 | var axis := _presenter.preview_transform_axis 190 | if _asset_placer.transform_preview(_presenter.transform_mode, axis, 1): 191 | return _handled() 192 | 193 | elif current_settings.binding_negative_transform.is_pressed(event): 194 | var axis := _presenter.preview_transform_axis 195 | if _asset_placer.transform_preview(_presenter.transform_mode, axis, -1): 196 | return _handled() 197 | 198 | if current_settings.bindings[AssetPlacerSettings.Bindings.ToggleAxisX].is_pressed(event): 199 | _presenter.toggle_axis(Vector3.RIGHT) 200 | return _handled() 201 | if current_settings.bindings[AssetPlacerSettings.Bindings.ToggleAxisY].is_pressed(event): 202 | _presenter.toggle_axis(Vector3.UP) 203 | return _handled() 204 | if current_settings.bindings[AssetPlacerSettings.Bindings.ToggleAxisZ].is_pressed(event): 205 | _presenter.toggle_axis(Vector3.BACK) 206 | return _handled() 207 | 208 | if current_settings.bindings[AssetPlacerSettings.Bindings.TogglePlaneMode].is_pressed(event): 209 | _presenter.cycle_placement_mode() 210 | return _handled() 211 | 212 | if event is InputEventKey and event.is_pressed(): 213 | if event.keycode == KEY_ESCAPE: 214 | _presenter.cancel() 215 | return _handled() 216 | 217 | if event is InputEventMouseMotion: 218 | if event.button_mask == 0: 219 | if _asset_placer.move_preview(event.position, viewport_camera): 220 | return _handled() 221 | 222 | if event is InputEventMouseButton: 223 | if event.button_index == MOUSE_BUTTON_RIGHT: 224 | # Don't handle RMB, let it pass through 225 | pass 226 | elif event.button_index == MOUSE_BUTTON_LEFT and event.pressed: 227 | var handled = _asset_placer.place_asset(Input.is_key_pressed(KEY_SHIFT)) 228 | if handled: 229 | return _handled() 230 | 231 | return EditorPlugin.AFTER_GUI_INPUT_PASS 232 | 233 | func _show_plugin_up_to_date(): 234 | _asset_placer_button.icon = null 235 | 236 | func _show_update_available(update: PluginUpdate): 237 | _asset_placer_button.icon = EditorIconTexture2D.new("MoveUp") 238 | _asset_placer_button.icon_alignment = HORIZONTAL_ALIGNMENT_RIGHT 239 | 240 | func _handled(): 241 | get_viewport().set_input_as_handled() 242 | return EditorPlugin.AFTER_GUI_INPUT_STOP 243 | -------------------------------------------------------------------------------- /addons/asset_placer/asset_placer.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name AssetPlacer 3 | 4 | var preview_node: Node3D 5 | var preview_aabb: AABB 6 | var node_history: Array[String] = [] 7 | var preview_rids = [] 8 | var asset: AssetResource 9 | var _is_node_transform_mode: bool = false 10 | var _original_transform: Transform3D 11 | 12 | var preview_transform_step : float = 0.1 13 | var preview_rotate_step: float = 5 14 | 15 | var undo_redo: EditorUndoRedoManager 16 | var meta_asset_id = &"asset_placer_res_id" 17 | var preview_material = load("res://addons/asset_placer/utils/preview_material.tres") 18 | 19 | var _strategy: AssetPlacementStrategy 20 | var _plane_placer: PlanePlacer 21 | var _presenter: AssetPlacerPresenter: 22 | get: return AssetPlacerPresenter._instance 23 | 24 | func _init(undo_redo: EditorUndoRedoManager, plane_placer: PlanePlacer): 25 | self.undo_redo = undo_redo 26 | self._plane_placer = plane_placer 27 | 28 | 29 | func start_placement(root: Window, asset: AssetResource, placement: PlacementMode): 30 | stop_placement() 31 | self.asset = asset 32 | _is_node_transform_mode = false 33 | preview_node = _instantiate_asset_resource(asset) 34 | root.add_child(preview_node) 35 | preview_rids = get_collision_rids(preview_node) 36 | set_placement_mode(placement) 37 | _apply_preview_material(preview_node) 38 | var scene = EditorInterface.get_selection().get_selected_nodes()[0] 39 | if scene is Node3D: 40 | AssetTransformations.apply_transforms(preview_node, AssetPlacerPresenter._instance.options) 41 | self.preview_aabb = AABBProvider.provide_aabb(preview_node) 42 | 43 | func start_node_transform(node: Node3D, placement: PlacementMode): 44 | stop_placement() 45 | _is_node_transform_mode = true 46 | preview_node = node 47 | _original_transform = node.global_transform 48 | preview_rids = get_collision_rids(preview_node) 49 | set_placement_mode(placement) 50 | self.preview_aabb = AABBProvider.provide_aabb(preview_node) 51 | 52 | func _apply_preview_material(node: Node3D): 53 | if not preview_material: 54 | return 55 | if node is MeshInstance3D: 56 | for i in node.get_surface_override_material_count(): 57 | node.set_surface_override_material(i, preview_material) 58 | 59 | for child in node.get_children(): 60 | if child is MeshInstance3D: 61 | for i in child.get_surface_override_material_count(): 62 | child.set_surface_override_material(i, preview_material) 63 | _apply_preview_material(child) 64 | 65 | 66 | func move_preview(mouse_position: Vector2, camera: Camera3D) -> bool: 67 | if preview_node: 68 | var hit = _strategy.get_placement_point(camera, mouse_position) 69 | var normal = Vector3.UP 70 | 71 | if AssetPlacerPresenter._instance.options.align_normals and hit: 72 | normal = hit.normal 73 | 74 | var snapped_pos = _snap_position(hit.position, normal) 75 | var forward_hint = preview_node.global_transform.basis.z 76 | 77 | var new_basis = get_safe_basis(normal, forward_hint).scaled(preview_node.scale) 78 | var new_transform = Transform3D(new_basis, snapped_pos) 79 | 80 | var local_bottom = Vector3(0, preview_aabb.position.y, 0) 81 | 82 | if _presenter.options.use_asset_origin: 83 | local_bottom = Vector3.ZERO 84 | 85 | var bottom_world = new_transform * local_bottom 86 | var adjust = snapped_pos - bottom_world 87 | new_transform.origin += adjust 88 | preview_node.global_transform = new_transform 89 | 90 | return true 91 | else: 92 | return false 93 | 94 | func place_asset(focus_on_placement: bool): 95 | if preview_node: 96 | if _is_node_transform_mode: 97 | _confirm_node_transform() 98 | return true 99 | else: 100 | _place_instance(preview_node.global_transform, focus_on_placement) 101 | return true 102 | else: 103 | return false 104 | 105 | func set_plugin_settings(settings: AssetPlacerSettings): 106 | preview_rotate_step = settings.rotation_step 107 | preview_transform_step = settings.transform_step 108 | if settings.preview_material_resource.is_empty(): 109 | preview_material = null 110 | else: 111 | preview_material = load(settings.preview_material_resource) 112 | 113 | func transform_preview(mode: AssetPlacerPresenter.TransformMode, axis: Vector3, direction: int) -> bool: 114 | if not preview_node: 115 | return false 116 | 117 | match mode: 118 | AssetPlacerPresenter.TransformMode.None: 119 | return false 120 | AssetPlacerPresenter.TransformMode.Scale: 121 | var factor := 1.0 + preview_transform_step * direction 122 | var min_scale := 0.01 123 | var new_scale := preview_node.scale 124 | if axis.x != 0: 125 | new_scale.x = max(preview_node.scale.x * factor, min_scale) 126 | if axis.y != 0: 127 | new_scale.y = max(preview_node.scale.y * factor, min_scale) 128 | if axis.z != 0: 129 | new_scale.z = max(preview_node.scale.z * factor, min_scale) 130 | preview_node.scale = new_scale 131 | return true 132 | AssetPlacerPresenter.TransformMode.Rotate: 133 | preview_node.rotate(axis.normalized() * direction, deg_to_rad(preview_rotate_step)) # Can be replaced with deg_to_rad(preview_transform_step) however 0.1 deg is realy low. 134 | return true 135 | 136 | AssetPlacerPresenter.TransformMode.Move: 137 | _plane_placer.move_plane_up(direction * 0.2) 138 | return true 139 | _: 140 | return false 141 | 142 | func get_collision_rids(node: Node) -> Array: 143 | var rids = [] 144 | if node is CollisionObject3D: 145 | rids.append(node.get_rid()) 146 | for child in node.get_children(): 147 | rids += get_collision_rids(child) 148 | return rids 149 | 150 | func _snap_position(hit_pos: Vector3, normal: Vector3) -> Vector3: 151 | if !AssetPlacerPresenter._instance.options.snapping_enabled: 152 | return hit_pos 153 | 154 | var grid_step: float = AssetPlacerPresenter._instance.options.snapping_grid_step 155 | 156 | # Build tangent basis aligned to the surface normal 157 | var n := normal.normalized() 158 | var tangent := Vector3.UP.cross(n).normalized() 159 | if tangent.length() < 0.001: 160 | tangent = Vector3.RIGHT.cross(n).normalized() 161 | var bitangent := n.cross(tangent).normalized() 162 | 163 | var local_tangent := tangent.dot(hit_pos) 164 | var local_bitangent := bitangent.dot(hit_pos) 165 | var local_height := n.dot(hit_pos) 166 | 167 | var snapped_tangent = round(local_tangent / grid_step) * grid_step 168 | var snapped_bitangent = round(local_bitangent / grid_step) * grid_step 169 | 170 | var snapped = tangent * snapped_tangent \ 171 | + bitangent * snapped_bitangent \ 172 | + n * local_height 173 | 174 | return snapped 175 | 176 | 177 | func _place_instance(transform: Transform3D, select_after_placement: bool): 178 | var selection = EditorInterface.get_selection() 179 | var scene = EditorInterface.get_edited_scene_root() 180 | var scene_root = scene.get_node(AssetPlacerPresenter._instance._parent) 181 | 182 | if scene_root and asset.scene: 183 | undo_redo.create_action("Place Asset: %s" % asset.name) 184 | undo_redo.add_do_method(self, "_do_placement", scene_root, transform, select_after_placement) 185 | undo_redo.add_undo_method(self, "_undo_placement", scene_root) 186 | undo_redo.commit_action() 187 | AssetTransformations.apply_transforms(preview_node, AssetPlacerPresenter._instance.options) 188 | _presenter.on_asset_placed() 189 | 190 | func _do_placement(root: Node3D, transform: Transform3D, select_after_placement: bool): 191 | var new_node: Node3D = _instantiate_asset_resource(asset) 192 | new_node.global_transform = transform 193 | new_node.transform = root.global_transform.affine_inverse() * transform 194 | new_node.set_meta(meta_asset_id, asset.id) 195 | new_node.name = _pick_name(new_node, root) 196 | root.add_child(new_node) 197 | new_node.owner = EditorInterface.get_edited_scene_root() 198 | node_history.push_front(new_node.name) 199 | if select_after_placement: 200 | AssetPlacerPresenter._instance.clear_selection() 201 | EditorInterface.edit_node(new_node) 202 | 203 | func _undo_placement(root: Node3D): 204 | var last_added = node_history.pop_front() 205 | var children = root.get_children() 206 | var node_index = -1; for a in root.get_child_count(): if children[a].name == last_added: node_index = a; break 207 | var node = root.get_child(node_index) 208 | node.queue_free() 209 | 210 | func _confirm_node_transform(): 211 | if _is_node_transform_mode and preview_node: 212 | # Create undo action for the node transformation 213 | undo_redo.create_action("Transform Node: %s" % preview_node.name) 214 | undo_redo.add_do_method(self, "_do_node_transform", preview_node, preview_node.global_transform) 215 | undo_redo.add_undo_method(self, "_undo_node_transform", preview_node, _original_transform) 216 | undo_redo.commit_action() 217 | 218 | # Exit node transform mode 219 | _presenter.end_node_transform_mode() 220 | stop_placement() 221 | 222 | func _do_node_transform(node: Node3D, new_transform: Transform3D): 223 | node.global_transform = new_transform 224 | 225 | func _undo_node_transform(node: Node3D, original_transform: Transform3D): 226 | node.global_transform = original_transform 227 | 228 | func stop_placement(): 229 | self.asset = null 230 | var was_node_transform_mode = _is_node_transform_mode 231 | _is_node_transform_mode = false 232 | if preview_node and not was_node_transform_mode: 233 | preview_node.queue_free() 234 | preview_node = null 235 | 236 | 237 | func _instantiate_asset_resource(asset: AssetResource) -> Node3D: 238 | var _preview_node: Node3D 239 | if asset.scene is PackedScene: 240 | _preview_node = (asset.scene.instantiate() as Node3D).duplicate() 241 | elif asset.scene is ArrayMesh: 242 | _preview_node = MeshInstance3D.new() 243 | _preview_node.name = asset.name 244 | _preview_node.mesh = asset.scene.duplicate() 245 | else: 246 | push_error("Not supported resource type %s" % str(asset.scene)) 247 | 248 | return _preview_node 249 | 250 | func set_placement_mode(placement_mode: PlacementMode): 251 | if placement_mode is PlacementMode.SurfacePlacement: 252 | _strategy = SurfaceAssetPlacementStrategy.new(preview_rids) 253 | elif placement_mode is PlacementMode.PlanePlacement: 254 | _strategy = PlanePlacementStrategy.new(placement_mode.plane_options) 255 | elif placement_mode is PlacementMode.Terrain3DPlacement: 256 | _strategy = Terrain3DAssetPlacementStrategy.new(placement_mode.terrain3dNode) 257 | else: 258 | push_error("Placement mode %s is not supported" % str(placement_mode)) 259 | 260 | func _pick_name(node: Node3D, parent: Node3D) -> String: 261 | var number_of_same_scenes = 0 262 | for child in parent.get_children(): 263 | if child.has_meta(meta_asset_id) && child.get_meta(meta_asset_id) == asset.id: 264 | number_of_same_scenes += 1 265 | return node.name if number_of_same_scenes == 0 else node.name + " (%s)" % number_of_same_scenes 266 | 267 | 268 | func get_safe_basis(up: Vector3, forward_hint: Vector3) -> Basis: 269 | up = up.normalized() 270 | var forward = forward_hint.normalized() 271 | 272 | if abs(up.dot(forward)) > 0.99: 273 | if abs(up.dot(Vector3.UP)) < 0.9: 274 | forward = Vector3.UP 275 | else: 276 | forward = Vector3.FORWARD 277 | 278 | var right = up.cross(forward).normalized() 279 | 280 | if right.length() < 0.001: 281 | right = up.cross(Vector3.FORWARD).normalized() 282 | if right.length() < 0.001: 283 | right = up.cross(Vector3.RIGHT).normalized() 284 | 285 | forward = right.cross(up).normalized() 286 | 287 | if up.length() < 0.001 or right.length() < 0.001 or forward.length() < 0.001: 288 | return Basis() 289 | 290 | return Basis(right, up, forward).orthonormalized() 291 | -------------------------------------------------------------------------------- /addons/asset_placer/ui/asset_library_window/asset_library_window.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=10 format=3 uid="uid://3qun24bndqll"] 2 | 3 | [ext_resource type="Script" uid="uid://d3hm5bn8vmd2" path="res://addons/asset_placer/ui/asset_library_window/asset_library_window.gd" id="1_l06eo"] 4 | [ext_resource type="PackedScene" uid="uid://dua2vx4bi1gmf" path="res://addons/asset_placer/ui/asset_placer_options/asset_placer_options.tscn" id="2_qx6qk"] 5 | [ext_resource type="Script" uid="uid://dmicn3kmr620j" path="res://addons/asset_placer/utils/system_icon.gd" id="3_gvdec"] 6 | 7 | [sub_resource type="Texture2D" id="Texture2D_gvdec"] 8 | resource_local_to_scene = false 9 | resource_name = "" 10 | script = ExtResource("3_gvdec") 11 | icon_name = &"Search" 12 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 13 | 14 | [sub_resource type="Texture2D" id="Texture2D_yvyyx"] 15 | resource_local_to_scene = false 16 | resource_name = "" 17 | script = ExtResource("3_gvdec") 18 | icon_name = &"Groups" 19 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 20 | 21 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5cqey"] 22 | bg_color = Color(0.25038558, 0.49941733, 6.738305e-07, 1) 23 | corner_radius_top_left = 16 24 | corner_radius_top_right = 16 25 | corner_radius_bottom_right = 16 26 | corner_radius_bottom_left = 16 27 | expand_margin_left = 2.0 28 | expand_margin_right = 2.0 29 | anti_aliasing = false 30 | 31 | [sub_resource type="Texture2D" id="Texture2D_5cqey"] 32 | resource_local_to_scene = false 33 | resource_name = "" 34 | script = ExtResource("3_gvdec") 35 | icon_name = &"FolderCreate" 36 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 37 | 38 | [sub_resource type="Texture2D" id="Texture2D_qx6qk"] 39 | resource_local_to_scene = false 40 | resource_name = "" 41 | script = ExtResource("3_gvdec") 42 | icon_name = &"Reload" 43 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 44 | 45 | [sub_resource type="Texture2D" id="Texture2D_mdpb8"] 46 | resource_local_to_scene = false 47 | resource_name = "" 48 | script = ExtResource("3_gvdec") 49 | icon_name = &"FolderCreate" 50 | metadata/_custom_type_script = "uid://dmicn3kmr620j" 51 | 52 | [node name="AssetLibraryWindow" type="Control"] 53 | layout_mode = 3 54 | anchors_preset = 15 55 | anchor_right = 1.0 56 | anchor_bottom = 1.0 57 | grow_horizontal = 2 58 | grow_vertical = 2 59 | size_flags_vertical = 3 60 | mouse_filter = 1 61 | script = ExtResource("1_l06eo") 62 | 63 | [node name="Panel" type="Panel" parent="."] 64 | layout_mode = 1 65 | anchors_preset = 15 66 | anchor_right = 1.0 67 | anchor_bottom = 1.0 68 | grow_horizontal = 2 69 | grow_vertical = 2 70 | mouse_filter = 1 71 | 72 | [node name="MainContent" type="HSplitContainer" parent="Panel"] 73 | unique_name_in_owner = true 74 | layout_mode = 1 75 | anchors_preset = 15 76 | anchor_right = 1.0 77 | anchor_bottom = 1.0 78 | grow_horizontal = 2 79 | grow_vertical = 2 80 | split_offset = -370 81 | 82 | [node name="ScrollContainer" type="ScrollContainer" parent="Panel/MainContent"] 83 | layout_mode = 2 84 | size_flags_horizontal = 3 85 | 86 | [node name="AssetPlacerOptions" parent="Panel/MainContent/ScrollContainer" instance=ExtResource("2_qx6qk")] 87 | clip_contents = true 88 | layout_mode = 2 89 | size_flags_horizontal = 3 90 | size_flags_vertical = 3 91 | localize_numeral_system = false 92 | 93 | [node name="MarginContainer" type="MarginContainer" parent="Panel/MainContent"] 94 | layout_mode = 2 95 | size_flags_horizontal = 3 96 | theme_override_constants/margin_left = 16 97 | theme_override_constants/margin_top = 16 98 | theme_override_constants/margin_right = 16 99 | theme_override_constants/margin_bottom = 16 100 | 101 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel/MainContent/MarginContainer"] 102 | layout_mode = 2 103 | theme_override_constants/separation = 32 104 | alignment = 2 105 | 106 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer"] 107 | layout_mode = 2 108 | theme_override_constants/separation = 16 109 | alignment = 2 110 | 111 | [node name="Label" type="Label" parent="Panel/MainContent/MarginContainer/VBoxContainer/HBoxContainer"] 112 | layout_mode = 2 113 | size_flags_horizontal = 3 114 | theme_override_font_sizes/font_size = 36 115 | text = "Assets" 116 | 117 | [node name="TextureRect" type="TextureRect" parent="Panel/MainContent/MarginContainer/VBoxContainer/HBoxContainer"] 118 | layout_mode = 2 119 | size_flags_vertical = 4 120 | texture = SubResource("Texture2D_gvdec") 121 | stretch_mode = 5 122 | 123 | [node name="SearchField" type="LineEdit" parent="Panel/MainContent/MarginContainer/VBoxContainer/HBoxContainer"] 124 | unique_name_in_owner = true 125 | custom_minimum_size = Vector2(300, 0) 126 | layout_mode = 2 127 | size_flags_vertical = 4 128 | placeholder_text = "Filter assets by name" 129 | 130 | [node name="FilterButton" type="Button" parent="Panel/MainContent/MarginContainer/VBoxContainer/HBoxContainer"] 131 | unique_name_in_owner = true 132 | layout_mode = 2 133 | size_flags_vertical = 4 134 | tooltip_text = "Edit" 135 | icon = SubResource("Texture2D_yvyyx") 136 | 137 | [node name="FiltersLabel" type="Label" parent="Panel/MainContent/MarginContainer/VBoxContainer/HBoxContainer/FilterButton"] 138 | unique_name_in_owner = true 139 | visible = false 140 | layout_mode = 1 141 | anchors_preset = -1 142 | anchor_left = 1.0 143 | anchor_right = 1.0 144 | offset_left = -18.22 145 | offset_top = -18.02 146 | offset_right = 8.780006 147 | offset_bottom = 12.98 148 | grow_horizontal = 0 149 | size_flags_horizontal = 8 150 | size_flags_vertical = 1 151 | theme_override_font_sizes/font_size = 12 152 | theme_override_styles/normal = SubResource("StyleBoxFlat_5cqey") 153 | text = "0" 154 | horizontal_alignment = 1 155 | vertical_alignment = 1 156 | 157 | [node name="AddFolderButton" type="Button" parent="Panel/MainContent/MarginContainer/VBoxContainer/HBoxContainer"] 158 | unique_name_in_owner = true 159 | layout_mode = 2 160 | size_flags_vertical = 4 161 | tooltip_text = "FolderCreate" 162 | icon = SubResource("Texture2D_5cqey") 163 | 164 | [node name="ReloadButton" type="Button" parent="Panel/MainContent/MarginContainer/VBoxContainer/HBoxContainer"] 165 | unique_name_in_owner = true 166 | layout_mode = 2 167 | size_flags_vertical = 4 168 | text = "Sync Assets" 169 | icon = SubResource("Texture2D_qx6qk") 170 | 171 | [node name="ProgressBar" type="ProgressBar" parent="Panel/MainContent/MarginContainer/VBoxContainer/HBoxContainer"] 172 | unique_name_in_owner = true 173 | visible = false 174 | custom_minimum_size = Vector2(100, 10) 175 | layout_mode = 2 176 | size_flags_horizontal = 4 177 | size_flags_vertical = 4 178 | indeterminate = true 179 | editor_preview_indeterminate = true 180 | 181 | [node name="ScrollContainer" type="ScrollContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer"] 182 | unique_name_in_owner = true 183 | layout_mode = 2 184 | size_flags_vertical = 3 185 | 186 | [node name="GridContainer" type="HFlowContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer/ScrollContainer"] 187 | unique_name_in_owner = true 188 | layout_mode = 2 189 | size_flags_horizontal = 3 190 | size_flags_vertical = 3 191 | 192 | [node name="EmptyCollectionContent" type="CenterContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer"] 193 | unique_name_in_owner = true 194 | visible = false 195 | layout_mode = 2 196 | size_flags_vertical = 3 197 | 198 | [node name="HBoxContainer" type="VBoxContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptyCollectionContent"] 199 | layout_mode = 2 200 | theme_override_constants/separation = 12 201 | alignment = 1 202 | 203 | [node name="Label" type="Label" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptyCollectionContent/HBoxContainer"] 204 | layout_mode = 2 205 | theme_override_font_sizes/font_size = 36 206 | text = "No assets in this collection" 207 | horizontal_alignment = 1 208 | 209 | [node name="Space" type="Control" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptyCollectionContent/HBoxContainer"] 210 | custom_minimum_size = Vector2(0, 32) 211 | layout_mode = 2 212 | 213 | [node name="Label2" type="Label" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptyCollectionContent/HBoxContainer"] 214 | layout_mode = 2 215 | text = "This collection currently has no assets assigned. 216 | You can Drag and Drop assets/folders into this window to fix that!" 217 | horizontal_alignment = 1 218 | 219 | [node name="Label3" type="Label" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptyCollectionContent/HBoxContainer"] 220 | layout_mode = 2 221 | text = "Or" 222 | horizontal_alignment = 1 223 | 224 | [node name="CenterContainer" type="CenterContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptyCollectionContent/HBoxContainer"] 225 | layout_mode = 2 226 | 227 | [node name="EmptyCollectionViewAddFolderBtn" type="Button" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptyCollectionContent/HBoxContainer/CenterContainer"] 228 | unique_name_in_owner = true 229 | layout_mode = 2 230 | text = "Add Folders Using Dialog" 231 | icon = SubResource("Texture2D_mdpb8") 232 | 233 | [node name="EmptySearchContent" type="CenterContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer"] 234 | unique_name_in_owner = true 235 | visible = false 236 | layout_mode = 2 237 | size_flags_vertical = 3 238 | 239 | [node name="HBoxContainer" type="VBoxContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptySearchContent"] 240 | layout_mode = 2 241 | theme_override_constants/separation = 12 242 | alignment = 1 243 | 244 | [node name="Label" type="Label" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptySearchContent/HBoxContainer"] 245 | layout_mode = 2 246 | theme_override_font_sizes/font_size = 36 247 | text = "No assets found" 248 | horizontal_alignment = 1 249 | 250 | [node name="Space" type="Control" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptySearchContent/HBoxContainer"] 251 | custom_minimum_size = Vector2(0, 32) 252 | layout_mode = 2 253 | 254 | [node name="Label2" type="Label" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptySearchContent/HBoxContainer"] 255 | layout_mode = 2 256 | text = "No assets found that would have this name 257 | You can Drag and Drop assets/folders into this window to fix that!" 258 | horizontal_alignment = 1 259 | 260 | [node name="Label3" type="Label" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptySearchContent/HBoxContainer"] 261 | layout_mode = 2 262 | text = "Or" 263 | horizontal_alignment = 1 264 | 265 | [node name="CenterContainer" type="CenterContainer" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptySearchContent/HBoxContainer"] 266 | layout_mode = 2 267 | 268 | [node name="EmptyCollectionViewAddFolderBtn" type="Button" parent="Panel/MainContent/MarginContainer/VBoxContainer/EmptySearchContent/HBoxContainer/CenterContainer"] 269 | layout_mode = 2 270 | text = "Add Folders Using Dialog" 271 | icon = SubResource("Texture2D_mdpb8") 272 | 273 | [node name="EmptyContent" type="CenterContainer" parent="Panel"] 274 | unique_name_in_owner = true 275 | visible = false 276 | layout_mode = 1 277 | anchors_preset = 15 278 | anchor_right = 1.0 279 | anchor_bottom = 1.0 280 | grow_horizontal = 2 281 | grow_vertical = 2 282 | 283 | [node name="HBoxContainer" type="VBoxContainer" parent="Panel/EmptyContent"] 284 | layout_mode = 2 285 | theme_override_constants/separation = 12 286 | alignment = 1 287 | 288 | [node name="Label" type="Label" parent="Panel/EmptyContent/HBoxContainer"] 289 | layout_mode = 2 290 | theme_override_font_sizes/font_size = 36 291 | text = "Welcome to Asset Placer!" 292 | horizontal_alignment = 1 293 | 294 | [node name="Space" type="Control" parent="Panel/EmptyContent/HBoxContainer"] 295 | custom_minimum_size = Vector2(0, 32) 296 | layout_mode = 2 297 | 298 | [node name="Label2" type="Label" parent="Panel/EmptyContent/HBoxContainer"] 299 | layout_mode = 2 300 | text = "Start by draggin and dropping your folder with Assets or Asset files themeselves to get started" 301 | 302 | [node name="Label3" type="Label" parent="Panel/EmptyContent/HBoxContainer"] 303 | layout_mode = 2 304 | text = "Or" 305 | horizontal_alignment = 1 306 | 307 | [node name="CenterContainer" type="CenterContainer" parent="Panel/EmptyContent/HBoxContainer"] 308 | layout_mode = 2 309 | 310 | [node name="EmptyViewAddFolderBtn" type="Button" parent="Panel/EmptyContent/HBoxContainer/CenterContainer"] 311 | unique_name_in_owner = true 312 | layout_mode = 2 313 | text = "Add Folders Using Dialog" 314 | icon = SubResource("Texture2D_mdpb8") 315 | --------------------------------------------------------------------------------