diff options
276 files changed, 4264 insertions, 2424 deletions
diff --git a/.github/workflows/android_builds.yml b/.github/workflows/android_builds.yml index 9a488bd095..f99a31179e 100644 --- a/.github/workflows/android_builds.yml +++ b/.github/workflows/android_builds.yml @@ -13,9 +13,30 @@ concurrency: cancel-in-progress: true jobs: - android-template: + build-android: runs-on: "ubuntu-20.04" - name: Template (target=template_release) + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - name: Editor (target=editor) + cache-name: android-editor + target: editor + tests: false + sconsflags: arch=arm64 production=yes + + - name: Template arm32 (target=template_release, arch=arm32) + cache-name: android-template-arm32 + target: template_release + tests: false + sconsflags: arch=arm32 + + - name: Template arm64 (target=template_release, arch=arm64) + cache-name: android-template-arm64 + target: template_release + tests: false + sconsflags: arch=arm64 steps: - uses: actions/checkout@v4 @@ -30,33 +51,38 @@ jobs: - name: Setup Godot build cache uses: ./.github/actions/godot-cache + with: + cache-name: ${{ matrix.cache-name }} continue-on-error: true - name: Setup Python and SCons uses: ./.github/actions/godot-deps - - name: Compilation (arm32) - uses: ./.github/actions/godot-build - with: - sconsflags: ${{ env.SCONSFLAGS }} arch=arm32 - platform: android - target: template_release - tests: false - - - name: Compilation (arm64) + - name: Compilation uses: ./.github/actions/godot-build with: - sconsflags: ${{ env.SCONSFLAGS }} arch=arm64 + sconsflags: ${{ env.SCONSFLAGS }} ${{ matrix.sconsflags }} platform: android - target: template_release - tests: false + target: ${{ matrix.target }} + tests: ${{ matrix.tests }} - name: Generate Godot templates + if: matrix.target == 'template_release' run: | cd platform/android/java ./gradlew generateGodotTemplates cd ../../.. ls -l bin/ + - name: Generate Godot editor + if: matrix.target == 'editor' + run: | + cd platform/android/java + ./gradlew generateGodotEditor + cd ../../.. + ls -l bin/android_editor_builds/ + - name: Upload artifact uses: ./.github/actions/upload-artifact + with: + name: ${{ matrix.cache-name }} diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 768540a0fa..eac1a66be7 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1515,6 +1515,7 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "display/window/stretch/scale_mode", PROPERTY_HINT_ENUM, "fractional,integer"), "fractional"); GLOBAL_DEF(PropertyInfo(Variant::INT, "debug/settings/profiler/max_functions", PROPERTY_HINT_RANGE, "128,65535,1"), 16384); + GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "debug/settings/profiler/max_timestamp_query_elements", PROPERTY_HINT_RANGE, "256,65535,1"), 256); GLOBAL_DEF(PropertyInfo(Variant::BOOL, "compression/formats/zstd/long_distance_matching"), Compression::zstd_long_distance_matching); GLOBAL_DEF(PropertyInfo(Variant::INT, "compression/formats/zstd/compression_level", PROPERTY_HINT_RANGE, "1,22,1"), Compression::zstd_level); diff --git a/core/input/input.cpp b/core/input/input.cpp index 56f616fac4..ec0303df06 100644 --- a/core/input/input.cpp +++ b/core/input/input.cpp @@ -758,12 +758,13 @@ void Input::_parse_input_event_impl(const Ref<InputEvent> &p_event, bool p_is_em bool was_pressed = action_state.cache.pressed; _update_action_cache(E.key, action_state); + // As input may come in part way through a physics tick, the earliest we can react to it is the next physics tick. if (action_state.cache.pressed && !was_pressed) { - action_state.pressed_physics_frame = Engine::get_singleton()->get_physics_frames(); + action_state.pressed_physics_frame = Engine::get_singleton()->get_physics_frames() + 1; action_state.pressed_process_frame = Engine::get_singleton()->get_process_frames(); } if (!action_state.cache.pressed && was_pressed) { - action_state.released_physics_frame = Engine::get_singleton()->get_physics_frames(); + action_state.released_physics_frame = Engine::get_singleton()->get_physics_frames() + 1; action_state.released_process_frame = Engine::get_singleton()->get_process_frames(); } } diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp index 178d02b987..ddeee9d765 100644 --- a/core/input/input_map.cpp +++ b/core/input/input_map.cpp @@ -636,6 +636,7 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() { inputs = List<Ref<InputEvent>>(); inputs.push_back(InputEventKey::create_reference(Key::A | KeyModifierMask::CTRL)); inputs.push_back(InputEventKey::create_reference(Key::LEFT | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::HOME)); default_builtin_cache.insert("ui_text_caret_line_start.macos", inputs); inputs = List<Ref<InputEvent>>(); @@ -645,6 +646,7 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() { inputs = List<Ref<InputEvent>>(); inputs.push_back(InputEventKey::create_reference(Key::E | KeyModifierMask::CTRL)); inputs.push_back(InputEventKey::create_reference(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::END)); default_builtin_cache.insert("ui_text_caret_line_end.macos", inputs); // Text Caret Movement Page Up/Down @@ -665,6 +667,7 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() { inputs = List<Ref<InputEvent>>(); inputs.push_back(InputEventKey::create_reference(Key::UP | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::HOME | KeyModifierMask::CMD_OR_CTRL)); default_builtin_cache.insert("ui_text_caret_document_start.macos", inputs); inputs = List<Ref<InputEvent>>(); @@ -673,6 +676,7 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() { inputs = List<Ref<InputEvent>>(); inputs.push_back(InputEventKey::create_reference(Key::DOWN | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::END | KeyModifierMask::CMD_OR_CTRL)); default_builtin_cache.insert("ui_text_caret_document_end.macos", inputs); // Text Caret Addition Below/Above diff --git a/core/io/logger.cpp b/core/io/logger.cpp index 1476b8ccac..a24277fe72 100644 --- a/core/io/logger.cpp +++ b/core/io/logger.cpp @@ -212,7 +212,7 @@ void RotatedFileLogger::logv(const char *p_format, va_list p_list, bool p_err) { // Strip ANSI escape codes (such as those inserted by `print_rich()`) // before writing to file, as text editors cannot display those // correctly. - file->store_string(strip_ansi_regex->sub(String(buf), "", true)); + file->store_string(strip_ansi_regex->sub(String::utf8(buf), "", true)); #else file->store_buffer((uint8_t *)buf, len); #endif // MODULE_REGEX_ENABLED diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index ab460c5f4c..f71257fa76 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -749,44 +749,54 @@ Error ResourceLoaderBinary::load() { String t = get_unicode_string(); Ref<Resource> res; + Resource *r = nullptr; - if (cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE && ResourceCache::has(path)) { - //use the existing one - Ref<Resource> cached = ResourceCache::get_ref(path); - if (cached->get_class() == t) { - cached->reset_state(); - res = cached; - } + MissingResource *missing_resource = nullptr; + + if (main) { + res = ResourceLoader::get_resource_ref_override(local_path); + r = res.ptr(); } + if (!r) { + if (cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE && ResourceCache::has(path)) { + //use the existing one + Ref<Resource> cached = ResourceCache::get_ref(path); + if (cached->get_class() == t) { + cached->reset_state(); + res = cached; + } + } - MissingResource *missing_resource = nullptr; + if (res.is_null()) { + //did not replace + + Object *obj = ClassDB::instantiate(t); + if (!obj) { + if (ResourceLoader::is_creating_missing_resources_if_class_unavailable_enabled()) { + //create a missing resource + missing_resource = memnew(MissingResource); + missing_resource->set_original_class(t); + missing_resource->set_recording_properties(true); + obj = missing_resource; + } else { + error = ERR_FILE_CORRUPT; + ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, local_path + ":Resource of unrecognized type in file: " + t + "."); + } + } - if (res.is_null()) { - //did not replace - - Object *obj = ClassDB::instantiate(t); - if (!obj) { - if (ResourceLoader::is_creating_missing_resources_if_class_unavailable_enabled()) { - //create a missing resource - missing_resource = memnew(MissingResource); - missing_resource->set_original_class(t); - missing_resource->set_recording_properties(true); - obj = missing_resource; - } else { + r = Object::cast_to<Resource>(obj); + if (!r) { + String obj_class = obj->get_class(); error = ERR_FILE_CORRUPT; - ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, local_path + ":Resource of unrecognized type in file: " + t + "."); + memdelete(obj); //bye + ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, local_path + ":Resource type in resource field not a resource, type is: " + obj_class + "."); } - } - Resource *r = Object::cast_to<Resource>(obj); - if (!r) { - String obj_class = obj->get_class(); - error = ERR_FILE_CORRUPT; - memdelete(obj); //bye - ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, local_path + ":Resource type in resource field not a resource, type is: " + obj_class + "."); + res = Ref<Resource>(r); } + } - res = Ref<Resource>(r); + if (r) { if (!path.is_empty()) { if (cache_mode != ResourceFormatLoader::CACHE_MODE_IGNORE) { r->set_path(path, cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE); // If got here because the resource with same path has different type, replace it. diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index eb3b117297..20dd192da1 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -40,6 +40,7 @@ #include "core/string/print_string.h" #include "core/string/translation.h" #include "core/variant/variant_parser.h" +#include "servers/rendering_server.h" #ifdef DEBUG_LOAD_THREADED #define print_lt(m_text) print_line(m_text) @@ -272,6 +273,7 @@ Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_origin } load_paths_stack->resize(load_paths_stack->size() - 1); + res_ref_overrides.erase(load_nesting); load_nesting--; if (!res.is_null()) { @@ -302,9 +304,10 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { thread_load_mutex.unlock(); // Thread-safe either if it's the current thread or a brand new one. - bool mq_override_present = false; + thread_local bool mq_override_present = false; CallQueue *own_mq_override = nullptr; if (load_nesting == 0) { + mq_override_present = false; load_paths_stack = memnew(Vector<String>); if (!load_task.dependent_path.is_empty()) { @@ -324,10 +327,6 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { } // -- - if (!Thread::is_main_thread()) { - set_current_thread_safe_for_nodes(true); - } - Ref<Resource> res = _load(load_task.remapped_path, load_task.remapped_path != load_task.local_path ? load_task.local_path : String(), load_task.type_hint, load_task.cache_mode, &load_task.error, load_task.use_sub_threads, &load_task.progress); if (mq_override_present) { MessageQueue::get_singleton()->flush(); @@ -584,6 +583,16 @@ ResourceLoader::ThreadLoadStatus ResourceLoader::load_threaded_get_status(const *r_progress = _dependency_get_progress(local_path); } + // Support userland polling in a loop on the main thread. + if (Thread::is_main_thread() && status == THREAD_LOAD_IN_PROGRESS) { + uint64_t frame = Engine::get_singleton()->get_process_frames(); + if (frame == load_task.last_progress_check_main_thread_frame) { + _ensure_load_progress(); + } else { + load_task.last_progress_check_main_thread_frame = frame; + } + } + return status; } @@ -612,6 +621,21 @@ Ref<Resource> ResourceLoader::load_threaded_get(const String &p_path, Error *r_e } return Ref<Resource>(); } + + // Support userland requesting on the main thread before the load is reported to be complete. + if (Thread::is_main_thread() && !load_token->local_path.is_empty()) { + const ThreadLoadTask &load_task = thread_load_tasks[load_token->local_path]; + while (load_task.status == THREAD_LOAD_IN_PROGRESS) { + if (!_ensure_load_progress()) { + // This local poll loop is not needed. + break; + } + thread_load_lock.~MutexLock(); + OS::get_singleton()->delay_usec(1000); + new (&thread_load_lock) MutexLock(thread_load_mutex); + } + } + res = _load_complete_inner(*load_token, r_error, thread_load_lock); if (load_token->unreference()) { memdelete(load_token); @@ -664,6 +688,7 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro Error wtp_task_err = FAILED; if (loader_is_wtp) { // Loading thread is in the worker pool. + load_task.awaited = true; thread_load_mutex.unlock(); wtp_task_err = WorkerThreadPool::get_singleton()->wait_for_task_completion(load_task.task_id); } @@ -688,7 +713,6 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro } else { DEV_ASSERT(wtp_task_err == OK); thread_load_mutex.lock(); - load_task.awaited = true; } } else { // Loading thread is main or user thread. @@ -730,6 +754,51 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro } } +bool ResourceLoader::_ensure_load_progress() { + // Some servers may need a new engine iteration to allow the load to progress. + // Since the only known one is the rendering server (in single thread mode), let's keep it simple and just sync it. + // This may be refactored in the future to support other servers and have less coupling. + if (OS::get_singleton()->get_render_thread_mode() == OS::RENDER_SEPARATE_THREAD) { + return false; // Not needed. + } + RenderingServer::get_singleton()->sync(); + return true; +} + +Ref<Resource> ResourceLoader::ensure_resource_ref_override_for_outer_load(const String &p_path, const String &p_res_type) { + ERR_FAIL_COND_V(load_nesting == 0, Ref<Resource>()); // It makes no sense to use this from nesting level 0. + const String &local_path = _validate_local_path(p_path); + HashMap<String, Ref<Resource>> &overrides = res_ref_overrides[load_nesting - 1]; + HashMap<String, Ref<Resource>>::Iterator E = overrides.find(local_path); + if (E) { + return E->value; + } else { + Object *obj = ClassDB::instantiate(p_res_type); + ERR_FAIL_NULL_V(obj, Ref<Resource>()); + Ref<Resource> res(obj); + if (!res.is_valid()) { + memdelete(obj); + ERR_FAIL_V(Ref<Resource>()); + } + overrides[local_path] = res; + return res; + } +} + +Ref<Resource> ResourceLoader::get_resource_ref_override(const String &p_path) { + DEV_ASSERT(p_path == _validate_local_path(p_path)); + HashMap<int, HashMap<String, Ref<Resource>>>::Iterator E = res_ref_overrides.find(load_nesting); + if (!E) { + return nullptr; + } + HashMap<String, Ref<Resource>>::Iterator F = E->value.find(p_path); + if (!F) { + return nullptr; + } + + return F->value; +} + bool ResourceLoader::exists(const String &p_path, const String &p_type_hint) { String local_path = _validate_local_path(p_path); @@ -1222,6 +1291,7 @@ bool ResourceLoader::timestamp_on_load = false; thread_local int ResourceLoader::load_nesting = 0; thread_local WorkerThreadPool::TaskID ResourceLoader::caller_task_id = 0; thread_local Vector<String> *ResourceLoader::load_paths_stack; +thread_local HashMap<int, HashMap<String, Ref<Resource>>> ResourceLoader::res_ref_overrides; template <> thread_local uint32_t SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG>::count = 0; diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h index 11abb4dc18..46df79ea22 100644 --- a/core/io/resource_loader.h +++ b/core/io/resource_loader.h @@ -174,6 +174,7 @@ private: String type_hint; float progress = 0.0f; float max_reported_progress = 0.0f; + uint64_t last_progress_check_main_thread_frame = UINT64_MAX; ThreadLoadStatus status = THREAD_LOAD_IN_PROGRESS; ResourceFormatLoader::CacheMode cache_mode = ResourceFormatLoader::CACHE_MODE_REUSE; Error error = OK; @@ -187,6 +188,7 @@ private: static thread_local int load_nesting; static thread_local WorkerThreadPool::TaskID caller_task_id; + static thread_local HashMap<int, HashMap<String, Ref<Resource>>> res_ref_overrides; // Outermost key is nesting level. static thread_local Vector<String> *load_paths_stack; // A pointer to avoid broken TLS implementations from double-running the destructor. static SafeBinaryMutex<BINARY_MUTEX_TAG> thread_load_mutex; static HashMap<String, ThreadLoadTask> thread_load_tasks; @@ -196,6 +198,8 @@ private: static float _dependency_get_progress(const String &p_path); + static bool _ensure_load_progress(); + public: static Error load_threaded_request(const String &p_path, const String &p_type_hint = "", bool p_use_sub_threads = false, ResourceFormatLoader::CacheMode p_cache_mode = ResourceFormatLoader::CACHE_MODE_REUSE); static ThreadLoadStatus load_threaded_get_status(const String &p_path, float *r_progress = nullptr); @@ -272,6 +276,9 @@ public: static void set_create_missing_resources_if_class_unavailable(bool p_enable); _FORCE_INLINE_ static bool is_creating_missing_resources_if_class_unavailable_enabled() { return create_missing_resources_if_class_unavailable; } + static Ref<Resource> ensure_resource_ref_override_for_outer_load(const String &p_path, const String &p_res_type); + static Ref<Resource> get_resource_ref_override(const String &p_path); + static bool is_cleaning_tasks(); static void initialize(); diff --git a/core/math/a_star_grid_2d.cpp b/core/math/a_star_grid_2d.cpp index f272407869..984bb1c9c1 100644 --- a/core/math/a_star_grid_2d.cpp +++ b/core/math/a_star_grid_2d.cpp @@ -122,6 +122,10 @@ AStarGrid2D::CellShape AStarGrid2D::get_cell_shape() const { } void AStarGrid2D::update() { + if (!dirty) { + return; + } + points.clear(); const int32_t end_x = region.get_end().x; diff --git a/core/math/transform_interpolator.cpp b/core/math/transform_interpolator.cpp index 7cfe880b5a..6a564b0ca7 100644 --- a/core/math/transform_interpolator.cpp +++ b/core/math/transform_interpolator.cpp @@ -33,44 +33,14 @@ #include "core/math/transform_2d.h" void TransformInterpolator::interpolate_transform_2d(const Transform2D &p_prev, const Transform2D &p_curr, Transform2D &r_result, real_t p_fraction) { - // Extract parameters. - Vector2 p1 = p_prev.get_origin(); - Vector2 p2 = p_curr.get_origin(); - // Special case for physics interpolation, if flipping, don't interpolate basis. // If the determinant polarity changes, the handedness of the coordinate system changes. if (_sign(p_prev.determinant()) != _sign(p_curr.determinant())) { r_result.columns[0] = p_curr.columns[0]; r_result.columns[1] = p_curr.columns[1]; - r_result.set_origin(p1.lerp(p2, p_fraction)); + r_result.set_origin(p_prev.get_origin().lerp(p_curr.get_origin(), p_fraction)); return; } - real_t r1 = p_prev.get_rotation(); - real_t r2 = p_curr.get_rotation(); - - Size2 s1 = p_prev.get_scale(); - Size2 s2 = p_curr.get_scale(); - - // Slerp rotation. - Vector2 v1(Math::cos(r1), Math::sin(r1)); - Vector2 v2(Math::cos(r2), Math::sin(r2)); - - real_t dot = v1.dot(v2); - - dot = CLAMP(dot, -1, 1); - - Vector2 v; - - if (dot > 0.9995f) { - v = v1.lerp(v2, p_fraction).normalized(); // Linearly interpolate to avoid numerical precision issues. - } else { - real_t angle = p_fraction * Math::acos(dot); - Vector2 v3 = (v2 - v1 * dot).normalized(); - v = v1 * Math::cos(angle) + v3 * Math::sin(angle); - } - - // Construct matrix. - r_result = Transform2D(Math::atan2(v.y, v.x), p1.lerp(p2, p_fraction)); - r_result.scale_basis(s1.lerp(s2, p_fraction)); + r_result = p_prev.interpolate_with(p_curr, p_fraction); } diff --git a/core/object/class_db.cpp b/core/object/class_db.cpp index fe4345aa0d..24cf0a29c5 100644 --- a/core/object/class_db.cpp +++ b/core/object/class_db.cpp @@ -76,6 +76,21 @@ class PlaceholderExtensionInstance { StringName class_name; HashMap<StringName, Variant> properties; + // Checks if a property is from a runtime class, and not a non-runtime base class. + bool is_runtime_property(const StringName &p_property_name) { + StringName current_class_name = class_name; + + while (ClassDB::is_class_runtime(current_class_name)) { + if (ClassDB::has_property(current_class_name, p_property_name, true)) { + return true; + } + + current_class_name = ClassDB::get_parent_class(current_class_name); + } + + return false; + } + public: PlaceholderExtensionInstance(const StringName &p_class_name) { class_name = p_class_name; @@ -83,27 +98,24 @@ public: ~PlaceholderExtensionInstance() {} - void set(const StringName &p_name, const Variant &p_value) { - bool is_default_valid = false; - Variant default_value = ClassDB::class_get_default_property_value(class_name, p_name, &is_default_valid); - - // If there's a default value, then we know it's a valid property. - if (is_default_valid) { + void set(const StringName &p_name, const Variant &p_value, bool &r_valid) { + r_valid = is_runtime_property(p_name); + if (r_valid) { properties[p_name] = p_value; } } - Variant get(const StringName &p_name) { + Variant get(const StringName &p_name, bool &r_valid) { const Variant *value = properties.getptr(p_name); Variant ret; if (value) { ret = *value; + r_valid = true; } else { - bool is_default_valid = false; - Variant default_value = ClassDB::class_get_default_property_value(class_name, p_name, &is_default_valid); - if (is_default_valid) { - ret = default_value; + r_valid = is_runtime_property(p_name); + if (r_valid) { + ret = ClassDB::class_get_default_property_value(class_name, p_name); } } @@ -115,10 +127,10 @@ public: const StringName &name = *(StringName *)p_name; const Variant &value = *(const Variant *)p_value; - self->set(name, value); + bool valid = false; + self->set(name, value, valid); - // We have to return true so Godot doesn't try to call the real setter function. - return true; + return valid; } static GDExtensionBool placeholder_instance_get(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, GDExtensionVariantPtr r_ret) { @@ -126,10 +138,10 @@ public: const StringName &name = *(StringName *)p_name; Variant *value = (Variant *)r_ret; - *value = self->get(name); + bool valid = false; + *value = self->get(name, valid); - // We have to return true so Godot doesn't try to call the real getter function. - return true; + return valid; } static const GDExtensionPropertyInfo *placeholder_instance_get_property_list(GDExtensionClassInstancePtr p_instance, uint32_t *r_count) { @@ -172,9 +184,9 @@ public: static GDExtensionObjectPtr placeholder_class_create_instance(void *p_class_userdata) { ClassDB::ClassInfo *ti = (ClassDB::ClassInfo *)p_class_userdata; - // Find the closest native parent. + // Find the closest native parent, that isn't a runtime class. ClassDB::ClassInfo *native_parent = ti->inherits_ptr; - while (native_parent->gdextension) { + while (native_parent->gdextension || native_parent->is_runtime) { native_parent = native_parent->inherits_ptr; } ERR_FAIL_NULL_V(native_parent->creation_func, nullptr); @@ -1952,6 +1964,14 @@ bool ClassDB::is_class_reloadable(const StringName &p_class) { return ti->reloadable; } +bool ClassDB::is_class_runtime(const StringName &p_class) { + OBJTYPE_RLOCK; + + ClassInfo *ti = classes.getptr(p_class); + ERR_FAIL_NULL_V_MSG(ti, false, "Cannot get class '" + String(p_class) + "'."); + return ti->is_runtime; +} + void ClassDB::add_resource_base_extension(const StringName &p_extension, const StringName &p_class) { if (resource_base_extensions.has(p_extension)) { return; @@ -2063,6 +2083,11 @@ void ClassDB::register_extension_class(ObjectGDExtension *p_extension) { ClassInfo *parent = classes.getptr(p_extension->parent_class_name); +#ifdef TOOLS_ENABLED + // @todo This is a limitation of the current implementation, but it should be possible to remove. + ERR_FAIL_COND_MSG(p_extension->is_runtime && parent->gdextension && !parent->is_runtime, "Extension runtime class " + String(p_extension->class_name) + " cannot descend from " + parent->name + " which isn't also a runtime class"); +#endif + ClassInfo c; c.api = p_extension->editor_class ? API_EDITOR_EXTENSION : API_EXTENSION; c.gdextension = p_extension; diff --git a/core/object/class_db.h b/core/object/class_db.h index 37a864c109..fb671bdc84 100644 --- a/core/object/class_db.h +++ b/core/object/class_db.h @@ -460,6 +460,7 @@ public: static bool is_class_exposed(const StringName &p_class); static bool is_class_reloadable(const StringName &p_class); + static bool is_class_runtime(const StringName &p_class); static void add_resource_base_extension(const StringName &p_extension, const StringName &p_class); static void get_resource_base_extensions(List<String> *p_extensions); diff --git a/core/object/worker_thread_pool.cpp b/core/object/worker_thread_pool.cpp index a7c0a0353e..caf4ed3835 100644 --- a/core/object/worker_thread_pool.cpp +++ b/core/object/worker_thread_pool.cpp @@ -33,7 +33,6 @@ #include "core/object/script_language.h" #include "core/os/os.h" #include "core/os/thread_safe.h" -#include "core/templates/command_queue_mt.h" WorkerThreadPool::Task *const WorkerThreadPool::ThreadData::YIELDING = (Task *)1; @@ -46,7 +45,9 @@ void WorkerThreadPool::Task::free_template_userdata() { WorkerThreadPool *WorkerThreadPool::singleton = nullptr; -thread_local CommandQueueMT *WorkerThreadPool::flushing_cmd_queue = nullptr; +#ifdef THREADS_ENABLED +thread_local uintptr_t WorkerThreadPool::unlockable_mutexes[MAX_UNLOCKABLE_MUTEXES] = {}; +#endif void WorkerThreadPool::_process_task(Task *p_task) { #ifdef THREADS_ENABLED @@ -419,6 +420,34 @@ Error WorkerThreadPool::wait_for_task_completion(TaskID p_task_id) { return OK; } +void WorkerThreadPool::_lock_unlockable_mutexes() { +#ifdef THREADS_ENABLED + for (uint32_t i = 0; i < MAX_UNLOCKABLE_MUTEXES; i++) { + if (unlockable_mutexes[i]) { + if ((((uintptr_t)unlockable_mutexes[i]) & 1) == 0) { + ((Mutex *)unlockable_mutexes[i])->lock(); + } else { + ((BinaryMutex *)(unlockable_mutexes[i] & ~1))->lock(); + } + } + } +#endif +} + +void WorkerThreadPool::_unlock_unlockable_mutexes() { +#ifdef THREADS_ENABLED + for (uint32_t i = 0; i < MAX_UNLOCKABLE_MUTEXES; i++) { + if (unlockable_mutexes[i]) { + if ((((uintptr_t)unlockable_mutexes[i]) & 1) == 0) { + ((Mutex *)unlockable_mutexes[i])->unlock(); + } else { + ((BinaryMutex *)(unlockable_mutexes[i] & ~1))->unlock(); + } + } + } +#endif +} + void WorkerThreadPool::_wait_collaboratively(ThreadData *p_caller_pool_thread, Task *p_task) { // Keep processing tasks until the condition to stop waiting is met. @@ -426,6 +455,7 @@ void WorkerThreadPool::_wait_collaboratively(ThreadData *p_caller_pool_thread, T while (true) { Task *task_to_process = nullptr; + bool relock_unlockables = false; { MutexLock lock(task_mutex); bool was_signaled = p_caller_pool_thread->signaled; @@ -463,13 +493,9 @@ void WorkerThreadPool::_wait_collaboratively(ThreadData *p_caller_pool_thread, T if (!task_to_process) { p_caller_pool_thread->awaited_task = p_task; - if (flushing_cmd_queue) { - flushing_cmd_queue->unlock(); - } + _unlock_unlockable_mutexes(); + relock_unlockables = true; p_caller_pool_thread->cond_var.wait(lock); - if (flushing_cmd_queue) { - flushing_cmd_queue->lock(); - } DEV_ASSERT(exit_threads || p_caller_pool_thread->signaled || IS_WAIT_OVER); p_caller_pool_thread->awaited_task = nullptr; @@ -477,6 +503,10 @@ void WorkerThreadPool::_wait_collaboratively(ThreadData *p_caller_pool_thread, T } } + if (relock_unlockables) { + _lock_unlockable_mutexes(); + } + if (task_to_process) { _process_task(task_to_process); } @@ -603,13 +633,9 @@ void WorkerThreadPool::wait_for_group_task_completion(GroupID p_group) { { Group *group = *groupp; - if (flushing_cmd_queue) { - flushing_cmd_queue->unlock(); - } + _unlock_unlockable_mutexes(); group->done_semaphore.wait(); - if (flushing_cmd_queue) { - flushing_cmd_queue->lock(); - } + _lock_unlockable_mutexes(); uint32_t max_users = group->tasks_used + 1; // Add 1 because the thread waiting for it is also user. Read before to avoid another thread freeing task after increment. uint32_t finished_users = group->finished.increment(); // fetch happens before inc, so increment later. @@ -633,16 +659,41 @@ int WorkerThreadPool::get_thread_index() { return singleton->thread_ids.has(tid) ? singleton->thread_ids[tid] : -1; } -void WorkerThreadPool::thread_enter_command_queue_mt_flush(CommandQueueMT *p_queue) { - ERR_FAIL_COND(flushing_cmd_queue != nullptr); - flushing_cmd_queue = p_queue; +#ifdef THREADS_ENABLED +uint32_t WorkerThreadPool::thread_enter_unlock_allowance_zone(Mutex *p_mutex) { + return _thread_enter_unlock_allowance_zone(p_mutex, false); +} + +uint32_t WorkerThreadPool::thread_enter_unlock_allowance_zone(BinaryMutex *p_mutex) { + return _thread_enter_unlock_allowance_zone(p_mutex, true); } -void WorkerThreadPool::thread_exit_command_queue_mt_flush() { - ERR_FAIL_NULL(flushing_cmd_queue); - flushing_cmd_queue = nullptr; +uint32_t WorkerThreadPool::_thread_enter_unlock_allowance_zone(void *p_mutex, bool p_is_binary) { + for (uint32_t i = 0; i < MAX_UNLOCKABLE_MUTEXES; i++) { + if (unlikely(unlockable_mutexes[i] == (uintptr_t)p_mutex)) { + // Already registered in the current thread. + return UINT32_MAX; + } + if (!unlockable_mutexes[i]) { + unlockable_mutexes[i] = (uintptr_t)p_mutex; + if (p_is_binary) { + unlockable_mutexes[i] |= 1; + } + return i; + } + } + ERR_FAIL_V_MSG(UINT32_MAX, "No more unlockable mutex slots available. Engine bug."); } +void WorkerThreadPool::thread_exit_unlock_allowance_zone(uint32_t p_zone_id) { + if (p_zone_id == UINT32_MAX) { + return; + } + DEV_ASSERT(unlockable_mutexes[p_zone_id]); + unlockable_mutexes[p_zone_id] = 0; +} +#endif + void WorkerThreadPool::init(int p_thread_count, float p_low_priority_task_ratio) { ERR_FAIL_COND(threads.size() > 0); if (p_thread_count < 0) { diff --git a/core/object/worker_thread_pool.h b/core/object/worker_thread_pool.h index a9cf260a0f..8774143abf 100644 --- a/core/object/worker_thread_pool.h +++ b/core/object/worker_thread_pool.h @@ -41,8 +41,6 @@ #include "core/templates/rid.h" #include "core/templates/safe_refcount.h" -class CommandQueueMT; - class WorkerThreadPool : public Object { GDCLASS(WorkerThreadPool, Object) public: @@ -163,7 +161,10 @@ private: static WorkerThreadPool *singleton; - static thread_local CommandQueueMT *flushing_cmd_queue; +#ifdef THREADS_ENABLED + static const uint32_t MAX_UNLOCKABLE_MUTEXES = 2; + static thread_local uintptr_t unlockable_mutexes[MAX_UNLOCKABLE_MUTEXES]; +#endif TaskID _add_task(const Callable &p_callable, void (*p_func)(void *), void *p_userdata, BaseTemplateUserdata *p_template_userdata, bool p_high_priority, const String &p_description); GroupID _add_group_task(const Callable &p_callable, void (*p_func)(void *, uint32_t), void *p_userdata, BaseTemplateUserdata *p_template_userdata, int p_elements, int p_tasks, bool p_high_priority, const String &p_description); @@ -190,6 +191,13 @@ private: void _wait_collaboratively(ThreadData *p_caller_pool_thread, Task *p_task); +#ifdef THREADS_ENABLED + static uint32_t _thread_enter_unlock_allowance_zone(void *p_mutex, bool p_is_binary); +#endif + + void _lock_unlockable_mutexes(); + void _unlock_unlockable_mutexes(); + protected: static void _bind_methods(); @@ -232,8 +240,14 @@ public: static WorkerThreadPool *get_singleton() { return singleton; } static int get_thread_index(); - static void thread_enter_command_queue_mt_flush(CommandQueueMT *p_queue); - static void thread_exit_command_queue_mt_flush(); +#ifdef THREADS_ENABLED + static uint32_t thread_enter_unlock_allowance_zone(Mutex *p_mutex); + static uint32_t thread_enter_unlock_allowance_zone(BinaryMutex *p_mutex); + static void thread_exit_unlock_allowance_zone(uint32_t p_zone_id); +#else + static uint32_t thread_enter_unlock_allowance_zone(void *p_mutex) { return UINT32_MAX; } + static void thread_exit_unlock_allowance_zone(uint32_t p_zone_id) {} +#endif void init(int p_thread_count = -1, float p_low_priority_task_ratio = 0.3); void finish(); diff --git a/core/os/os.cpp b/core/os/os.cpp index 40d8601af3..642de11a9f 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -247,7 +247,10 @@ String OS::get_safe_dir_name(const String &p_dir_name, bool p_allow_paths) const for (int i = 0; i < invalid_chars.size(); i++) { safe_dir_name = safe_dir_name.replace(invalid_chars[i], "-"); } - return safe_dir_name; + + // Trim trailing periods from the returned value as it's not valid for folder names on Windows. + // This check is still applied on non-Windows platforms so the returned value is consistent across platforms. + return safe_dir_name.rstrip("."); } // Path to data, config, cache, etc. OS-specific folders diff --git a/core/templates/command_queue_mt.h b/core/templates/command_queue_mt.h index 349404d75b..0748e9cb83 100644 --- a/core/templates/command_queue_mt.h +++ b/core/templates/command_queue_mt.h @@ -364,7 +364,7 @@ class CommandQueueMT { lock(); - WorkerThreadPool::thread_enter_command_queue_mt_flush(this); + uint32_t allowance_id = WorkerThreadPool::thread_enter_unlock_allowance_zone(&mutex); while (flush_read_ptr < command_mem.size()) { uint64_t size = *(uint64_t *)&command_mem[flush_read_ptr]; flush_read_ptr += 8; @@ -383,7 +383,7 @@ class CommandQueueMT { flush_read_ptr += size; } - WorkerThreadPool::thread_exit_command_queue_mt_flush(); + WorkerThreadPool::thread_exit_unlock_allowance_zone(allowance_id); command_mem.clear(); flush_read_ptr = 0; diff --git a/core/variant/typed_array.h b/core/variant/typed_array.h index e00947ed1e..07bf8afa7b 100644 --- a/core/variant/typed_array.h +++ b/core/variant/typed_array.h @@ -46,10 +46,15 @@ public: _ref(p_array); } _FORCE_INLINE_ TypedArray(const Variant &p_variant) : - Array(Array(p_variant), Variant::OBJECT, T::get_class_static(), Variant()) { + TypedArray(Array(p_variant)) { } - _FORCE_INLINE_ TypedArray(const Array &p_array) : - Array(p_array, Variant::OBJECT, T::get_class_static(), Variant()) { + _FORCE_INLINE_ TypedArray(const Array &p_array) { + set_typed(Variant::OBJECT, T::get_class_static(), Variant()); + if (is_same_typed(p_array)) { + _ref(p_array); + } else { + assign(p_array); + } } _FORCE_INLINE_ TypedArray() { set_typed(Variant::OBJECT, T::get_class_static(), Variant()); @@ -78,10 +83,15 @@ struct VariantInternalAccessor<const TypedArray<T> &> { _ref(p_array); \ } \ _FORCE_INLINE_ TypedArray(const Variant &p_variant) : \ - Array(Array(p_variant), m_variant_type, StringName(), Variant()) { \ + TypedArray(Array(p_variant)) { \ } \ - _FORCE_INLINE_ TypedArray(const Array &p_array) : \ - Array(p_array, m_variant_type, StringName(), Variant()) { \ + _FORCE_INLINE_ TypedArray(const Array &p_array) { \ + set_typed(m_variant_type, StringName(), Variant()); \ + if (is_same_typed(p_array)) { \ + _ref(p_array); \ + } else { \ + assign(p_array); \ + } \ } \ _FORCE_INLINE_ TypedArray() { \ set_typed(m_variant_type, StringName(), Variant()); \ diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp index 30a8facd67..c1ef31c784 100644 --- a/core/variant/variant.cpp +++ b/core/variant/variant.cpp @@ -951,7 +951,7 @@ bool Variant::is_zero() const { return *reinterpret_cast<const ::RID *>(_data._mem) == ::RID(); } case OBJECT: { - return get_validated_object() == nullptr; + return _get_obj().obj == nullptr; } case CALLABLE: { return reinterpret_cast<const Callable *>(_data._mem)->is_null(); diff --git a/core/variant/variant_op.h b/core/variant/variant_op.h index 0b94d79a97..ac39a4135f 100644 --- a/core/variant/variant_op.h +++ b/core/variant/variant_op.h @@ -548,14 +548,14 @@ public: class OperatorEvaluatorEqualObject { public: static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) { - const ObjectID &a = VariantInternal::get_object_id(&p_left); - const ObjectID &b = VariantInternal::get_object_id(&p_right); + const Object *a = p_left.get_validated_object(); + const Object *b = p_right.get_validated_object(); *r_ret = a == b; r_valid = true; } static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) { - const ObjectID &a = VariantInternal::get_object_id(left); - const ObjectID &b = VariantInternal::get_object_id(right); + const Object *a = left->get_validated_object(); + const Object *b = right->get_validated_object(); *VariantGetInternalPtr<bool>::get_ptr(r_ret) = a == b; } static void ptr_evaluate(const void *left, const void *right, void *r_ret) { @@ -567,12 +567,12 @@ public: class OperatorEvaluatorEqualObjectNil { public: static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) { - const Object *a = p_left.operator Object *(); + const Object *a = p_left.get_validated_object(); *r_ret = a == nullptr; r_valid = true; } static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) { - const Object *a = left->operator Object *(); + const Object *a = left->get_validated_object(); *VariantGetInternalPtr<bool>::get_ptr(r_ret) = a == nullptr; } static void ptr_evaluate(const void *left, const void *right, void *r_ret) { @@ -584,12 +584,12 @@ public: class OperatorEvaluatorEqualNilObject { public: static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) { - const Object *b = p_right.operator Object *(); + const Object *b = p_right.get_validated_object(); *r_ret = nullptr == b; r_valid = true; } static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) { - const Object *b = right->operator Object *(); + const Object *b = right->get_validated_object(); *VariantGetInternalPtr<bool>::get_ptr(r_ret) = nullptr == b; } static void ptr_evaluate(const void *left, const void *right, void *r_ret) { @@ -619,14 +619,14 @@ public: class OperatorEvaluatorNotEqualObject { public: static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) { - const ObjectID &a = VariantInternal::get_object_id(&p_left); - const ObjectID &b = VariantInternal::get_object_id(&p_right); + Object *a = p_left.get_validated_object(); + Object *b = p_right.get_validated_object(); *r_ret = a != b; r_valid = true; } static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) { - const ObjectID &a = VariantInternal::get_object_id(left); - const ObjectID &b = VariantInternal::get_object_id(right); + Object *a = left->get_validated_object(); + Object *b = right->get_validated_object(); *VariantGetInternalPtr<bool>::get_ptr(r_ret) = a != b; } static void ptr_evaluate(const void *left, const void *right, void *r_ret) { @@ -638,12 +638,12 @@ public: class OperatorEvaluatorNotEqualObjectNil { public: static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) { - Object *a = p_left.operator Object *(); + Object *a = p_left.get_validated_object(); *r_ret = a != nullptr; r_valid = true; } static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) { - Object *a = left->operator Object *(); + Object *a = left->get_validated_object(); *VariantGetInternalPtr<bool>::get_ptr(r_ret) = a != nullptr; } static void ptr_evaluate(const void *left, const void *right, void *r_ret) { @@ -655,12 +655,12 @@ public: class OperatorEvaluatorNotEqualNilObject { public: static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) { - Object *b = p_right.operator Object *(); + Object *b = p_right.get_validated_object(); *r_ret = nullptr != b; r_valid = true; } static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) { - Object *b = right->operator Object *(); + Object *b = right->get_validated_object(); *VariantGetInternalPtr<bool>::get_ptr(r_ret) = nullptr != b; } static void ptr_evaluate(const void *left, const void *right, void *r_ret) { diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml index 3b7a6e66fe..80fd52c684 100644 --- a/doc/classes/Animation.xml +++ b/doc/classes/Animation.xml @@ -560,7 +560,7 @@ <param index="0" name="track_idx" type="int" /> <param index="1" name="path" type="NodePath" /> <description> - Sets the path of a track. Paths must be valid scene-tree paths to a node and must be specified starting from the parent node of the node that will reproduce the animation. Tracks that control properties or bones must append their name after the path, separated by [code]":"[/code]. + Sets the path of a track. Paths must be valid scene-tree paths to a node and must be specified starting from the [member AnimationMixer.root_node] that will reproduce the animation. Tracks that control properties or bones must append their name after the path, separated by [code]":"[/code]. For example, [code]"character/skeleton:ankle"[/code] or [code]"character/mesh:transform/local"[/code]. </description> </method> diff --git a/doc/classes/AnimationMixer.xml b/doc/classes/AnimationMixer.xml index c635eba2ab..58ef118e46 100644 --- a/doc/classes/AnimationMixer.xml +++ b/doc/classes/AnimationMixer.xml @@ -27,6 +27,13 @@ <param index="1" name="library" type="AnimationLibrary" /> <description> Adds [param library] to the animation player, under the key [param name]. + AnimationMixer has a global library by default with an empty string as key. For adding an animation to the global library: + [codeblocks] + [gdscript] + var global_library = mixer.get_animation_library("") + global_library.add_animation("animation_name", animation_resource) + [/gdscript] + [/codeblocks] </description> </method> <method name="advance"> @@ -182,10 +189,10 @@ func _process(delta): if Input.is_action_just_pressed("animate"): state_machine.travel("Animate") - var current_root_motion_rotation_accumulator: Quaternion = animation_tree.get_root_motion_Quaternion_accumulator() + var current_root_motion_rotation_accumulator: Quaternion = animation_tree.get_root_motion_rotation_accumulator() var difference: Quaternion = prev_root_motion_rotation_accumulator.inverse() * current_root_motion_rotation_accumulator prev_root_motion_rotation_accumulator = current_root_motion_rotation_accumulator - transform.basis *= difference + transform.basis *= Basis(difference) [/gdscript] [/codeblocks] However, if the animation loops, an unintended discrete change may occur, so this is only useful for some simple use cases. @@ -297,8 +304,8 @@ This makes it more convenient to preview and edit animations in the editor, as changes to the scene will not be saved as long as they are set in the reset animation. </member> <member name="root_motion_track" type="NodePath" setter="set_root_motion_track" getter="get_root_motion_track" default="NodePath("")"> - The path to the Animation track used for root motion. Paths must be valid scene-tree paths to a node, and must be specified starting from the parent node of the node that will reproduce the animation. To specify a track that controls properties or bones, append its name after the path, separated by [code]":"[/code]. For example, [code]"character/skeleton:ankle"[/code] or [code]"character/mesh:transform/local"[/code]. - If the track has type [constant Animation.TYPE_POSITION_3D], [constant Animation.TYPE_ROTATION_3D] or [constant Animation.TYPE_SCALE_3D] the transformation will be canceled visually, and the animation will appear to stay in place. See also [method get_root_motion_position], [method get_root_motion_rotation], [method get_root_motion_scale] and [RootMotionView]. + The path to the Animation track used for root motion. Paths must be valid scene-tree paths to a node, and must be specified starting from the parent node of the node that will reproduce the animation. The [member root_motion_track] uses the same format as [method Animation.track_set_path], but note that a bone must be specified. + If the track has type [constant Animation.TYPE_POSITION_3D], [constant Animation.TYPE_ROTATION_3D], or [constant Animation.TYPE_SCALE_3D] the transformation will be canceled visually, and the animation will appear to stay in place. See also [method get_root_motion_position], [method get_root_motion_rotation], [method get_root_motion_scale], and [RootMotionView]. </member> <member name="root_node" type="NodePath" setter="set_root_node" getter="get_root_node" default="NodePath("..")"> The node which node path references will travel from. diff --git a/doc/classes/Array.xml b/doc/classes/Array.xml index 326b71c588..3731b8dcf1 100644 --- a/doc/classes/Array.xml +++ b/doc/classes/Array.xml @@ -4,41 +4,31 @@ A built-in data structure that holds a sequence of elements. </brief_description> <description> - An array data structure that can contain a sequence of elements of any type. Elements are accessed by a numerical index starting at 0. Negative indices are used to count from the back (-1 is the last element, -2 is the second to last, etc.). + An array data structure that can contain a sequence of elements of any [Variant] type. Elements are accessed by a numerical index starting at 0. Negative indices are used to count from the back (-1 is the last element, -2 is the second to last, etc.). [b]Example:[/b] [codeblocks] [gdscript] - var array = ["One", 2, 3, "Four"] - print(array[0]) # One. - print(array[2]) # 3. - print(array[-1]) # Four. - array[2] = "Three" - print(array[-2]) # Three. - [/gdscript] - [csharp] - var array = new Godot.Collections.Array{"One", 2, 3, "Four"}; - GD.Print(array[0]); // One. - GD.Print(array[2]); // 3. - GD.Print(array[array.Count - 1]); // Four. - array[2] = "Three"; - GD.Print(array[array.Count - 2]); // Three. - [/csharp] - [/codeblocks] - Arrays can be concatenated using the [code]+[/code] operator: - [codeblocks] - [gdscript] - var array1 = ["One", 2] - var array2 = [3, "Four"] - print(array1 + array2) # ["One", 2, 3, "Four"] + var array = ["First", 2, 3, "Last"] + print(array[0]) # Prints "First" + print(array[2]) # Prints 3 + print(array[-1]) # Prints "Last" + + array[1] = "Second" + print(array[1]) # Prints "Second" + print(array[-3]) # Prints "Second" [/gdscript] [csharp] - // Array concatenation is not possible with C# arrays, but is with Godot.Collections.Array. - var array1 = new Godot.Collections.Array{"One", 2}; - var array2 = new Godot.Collections.Array{3, "Four"}; - GD.Print(array1 + array2); // Prints [One, 2, 3, Four] + var array = new Godot.Collections.Array{"First", 2, 3, "Last"}; + GD.Print(array[0]); // Prints "First" + GD.Print(array[2]); // Prints 3 + GD.Print(array[array.Count - 1]); // Prints "Last" + + array[2] = "Second"; + GD.Print(array[1]); // Prints "Second" + GD.Print(array[array.Count - 3]); // Prints "Second" [/csharp] [/codeblocks] - [b]Note:[/b] Arrays are always passed by reference. To get a copy of an array that can be modified independently of the original array, use [method duplicate]. + [b]Note:[/b] Arrays are always passed by [b]reference[/b]. To get a copy of an array that can be modified independently of the original array, use [method duplicate]. [b]Note:[/b] Erasing elements while iterating over arrays is [b]not[/b] supported and will result in unpredictable behavior. [b]Differences between packed arrays, typed arrays, and untyped arrays:[/b] Packed arrays are generally faster to iterate on and modify compared to a typed array of the same type (e.g. [PackedInt64Array] versus [code]Array[int][/code]). Also, packed arrays consume less memory. As a downside, packed arrays are less flexible as they don't offer as many convenience methods such as [method Array.map]. Typed arrays are in turn faster to iterate on and modify than untyped arrays. </description> @@ -58,29 +48,32 @@ <param index="2" name="class_name" type="StringName" /> <param index="3" name="script" type="Variant" /> <description> - Creates a typed array from the [param base] array. All arguments are required. - - [param type] is the built-in type as a [enum Variant.Type] constant, for example [constant TYPE_INT]. - - [param class_name] is the [b]native[/b] class name, for example [Node]. If [param type] is not [constant TYPE_OBJECT], must be an empty string. - - [param script] is the associated script. Must be a [Script] instance or [code]null[/code]. - Examples: + Creates a typed array from the [param base] array. A typed array can only contain elements of the given type, or that inherit from the given class, as described by this constructor's parameters: + - [param type] is the built-in [Variant] type, as one the [enum Variant.Type] constants. + - [param class_name] is the built-in class name (see [method Object.get_class]). + - [param script] is the associated script. It must be a [Script] instance or [code]null[/code]. + If [param type] is not [constant TYPE_OBJECT], [param class_name] must be an empty [StringName] and [param script] must be [code]null[/code]. [codeblock] - class_name MyNode + class_name Sword extends Node - class MyClass: + class Stats: pass func _ready(): - var a = Array([], TYPE_INT, &"", null) # Array[int] - var b = Array([], TYPE_OBJECT, &"Node", null) # Array[Node] - var c = Array([], TYPE_OBJECT, &"Node", MyNode) # Array[MyNode] - var d = Array([], TYPE_OBJECT, &"RefCounted", MyClass) # Array[MyClass] + var a = Array([], TYPE_INT, "", null) # Array[int] + var b = Array([], TYPE_OBJECT, "Node", null) # Array[Node] + var c = Array([], TYPE_OBJECT, "Node", Sword) # Array[Sword] + var d = Array([], TYPE_OBJECT, "RefCounted", Stats) # Array[Stats] [/codeblock] - [b]Note:[/b] This constructor can be useful if you want to create a typed array on the fly, but you are not required to use it. In GDScript you can use a temporary variable with the static type you need and then pass it: + The [param base] array's elements are converted when necessary. If this is not possible or [param base] is already typed, this constructor fails and returns an empty [Array]. + In GDScript, this constructor is usually not necessary, as it is possible to create a typed array through static typing: [codeblock] - func _ready(): - var a: Array[int] = [] - some_func(a) + var numbers: Array[float] = [] + var children: Array[Node] = [$Node, $Sprite2D, $RigidBody3D] + + var integers: Array[int] = [0.2, 4.5, -2.0] + print(integers) # Prints [0, 4, -2] [/codeblock] </description> </constructor> @@ -167,20 +160,44 @@ <return type="bool" /> <param index="0" name="method" type="Callable" /> <description> - Calls the provided [Callable] on each element in the array and returns [code]true[/code] if the [Callable] returns [code]true[/code] for [i]all[/i] elements in the array. If the [Callable] returns [code]false[/code] for one array element or more, this method returns [code]false[/code]. - The callable's method should take one [Variant] parameter (the current array element) and return a boolean value. - [codeblock] + Calls the given [Callable] on each element in the array and returns [code]true[/code] if the [Callable] returns [code]true[/code] for [i]all[/i] elements in the array. If the [Callable] returns [code]false[/code] for one array element or more, this method returns [code]false[/code]. + The [param method] should take one [Variant] parameter (the current array element) and return a [bool]. + [codeblocks] + [gdscript] + func greater_than_5(number): + return number > 5 + func _ready(): - print([6, 10, 6].all(greater_than_5)) # Prints True (3/3 elements evaluate to `true`). - print([4, 10, 4].all(greater_than_5)) # Prints False (1/3 elements evaluate to `true`). - print([4, 4, 4].all(greater_than_5)) # Prints False (0/3 elements evaluate to `true`). - print([].all(greater_than_5)) # Prints True (0/0 elements evaluate to `true`). + print([6, 10, 6].all(greater_than_5)) # Prints true (3/3 elements evaluate to true). + print([4, 10, 4].all(greater_than_5)) # Prints false (1/3 elements evaluate to true). + print([4, 4, 4].all(greater_than_5)) # Prints false (0/3 elements evaluate to true). + print([].all(greater_than_5)) # Prints true (0/0 elements evaluate to true). + + # Same as the first line above, but using a lambda function. + print([6, 10, 6].all(func(element): return element > 5)) # Prints true + [/gdscript] + [csharp] + private static bool GreaterThan5(int number) + { + return number > 5; + } - print([6, 10, 6].all(func(number): return number > 5)) # Prints True. Same as the first line above, but using lambda function. + public override void _Ready() + { + // Prints true (3/3 elements evaluate to true). + GD.Print(new Godot.Collections.Array>int< { 6, 10, 6 }.All(GreaterThan5)); + // Prints false (1/3 elements evaluate to true). + GD.Print(new Godot.Collections.Array>int< { 4, 10, 4 }.All(GreaterThan5)); + // Prints false (0/3 elements evaluate to true). + GD.Print(new Godot.Collections.Array>int< { 4, 4, 4 }.All(GreaterThan5)); + // Prints true (0/0 elements evaluate to true). + GD.Print(new Godot.Collections.Array>int< { }.All(GreaterThan5)); - func greater_than_5(number): - return number > 5 - [/codeblock] + // Same as the first line above, but using a lambda function. + GD.Print(new Godot.Collections.Array>int< { 6, 10, 6 }.All(element => element > 5)); // Prints true + } + [/csharp] + [/codeblocks] See also [method any], [method filter], [method map] and [method reduce]. [b]Note:[/b] Unlike relying on the size of an array returned by [method filter], this method will return as early as possible to improve performance (especially with large arrays). [b]Note:[/b] For an empty array, this method [url=https://en.wikipedia.org/wiki/Vacuous_truth]always[/url] returns [code]true[/code]. @@ -190,19 +207,20 @@ <return type="bool" /> <param index="0" name="method" type="Callable" /> <description> - Calls the provided [Callable] on each element in the array and returns [code]true[/code] if the [Callable] returns [code]true[/code] for [i]one or more[/i] elements in the array. If the [Callable] returns [code]false[/code] for all elements in the array, this method returns [code]false[/code]. - The callable's method should take one [Variant] parameter (the current array element) and return a boolean value. + Calls the given [Callable] on each element in the array and returns [code]true[/code] if the [Callable] returns [code]true[/code] for [i]one or more[/i] elements in the array. If the [Callable] returns [code]false[/code] for all elements in the array, this method returns [code]false[/code]. + The [param method] should take one [Variant] parameter (the current array element) and return a [bool]. [codeblock] - func _ready(): - print([6, 10, 6].any(greater_than_5)) # Prints True (3 elements evaluate to `true`). - print([4, 10, 4].any(greater_than_5)) # Prints True (1 elements evaluate to `true`). - print([4, 4, 4].any(greater_than_5)) # Prints False (0 elements evaluate to `true`). - print([].any(greater_than_5)) # Prints False (0 elements evaluate to `true`). - - print([6, 10, 6].any(func(number): return number > 5)) # Prints True. Same as the first line above, but using lambda function. - func greater_than_5(number): return number > 5 + + func _ready(): + print([6, 10, 6].any(greater_than_5)) # Prints true (3 elements evaluate to true). + print([4, 10, 4].any(greater_than_5)) # Prints true (1 elements evaluate to true). + print([4, 4, 4].any(greater_than_5)) # Prints false (0 elements evaluate to true). + print([].any(greater_than_5)) # Prints false (0 elements evaluate to true). + + # Same as the first line above, but using a lambda function. + print([6, 10, 6].any(func(number): return number > 5)) # Prints true [/codeblock] See also [method all], [method filter], [method map] and [method reduce]. [b]Note:[/b] Unlike relying on the size of an array returned by [method filter], this method will return as early as possible to improve performance (especially with large arrays). @@ -213,19 +231,19 @@ <return type="void" /> <param index="0" name="value" type="Variant" /> <description> - Appends an element at the end of the array (alias of [method push_back]). + Appends [param value] at the end of the array (alias of [method push_back]). </description> </method> <method name="append_array"> <return type="void" /> <param index="0" name="array" type="Array" /> <description> - Appends another array at the end of this array. + Appends another [param array] at the end of this array. [codeblock] - var array1 = [1, 2, 3] - var array2 = [4, 5, 6] - array1.append_array(array2) - print(array1) # Prints [1, 2, 3, 4, 5, 6]. + var numbers = [1, 2, 3] + var extra = [4, 5, 6] + numbers.append_array(extra) + print(nums) # Prints [1, 2, 3, 4, 5, 6] [/codeblock] </description> </method> @@ -239,8 +257,8 @@ <method name="back" qualifiers="const"> <return type="Variant" /> <description> - Returns the last element of the array. Prints an error and returns [code]null[/code] if the array is empty. - [b]Note:[/b] Calling this function is not the same as writing [code]array[-1][/code]. If the array is empty, accessing by index will pause project execution when running from the editor. + Returns the last element of the array. If the array is empty, fails and returns [code]null[/code]. See also [method front]. + [b]Note:[/b] Unlike with the [code][][/code] operator ([code]array[-1][/code]), an error is generated without stopping project execution. </description> </method> <method name="bsearch" qualifiers="const"> @@ -248,13 +266,20 @@ <param index="0" name="value" type="Variant" /> <param index="1" name="before" type="bool" default="true" /> <description> - Finds the index of an existing value (or the insertion index that maintains sorting order, if the value is not yet present in the array) using binary search. Optionally, a [param before] specifier can be passed. If [code]false[/code], the returned index comes after all existing entries of the value in the array. + Returns the index of [param value] in the sorted array. If it cannot be found, returns where [param value] should be inserted to keep the array sorted. The algorithm used is [url=https://en.wikipedia.org/wiki/Binary_search_algorithm]binary search[/url]. + If [param before] is [code]true[/code] (as by default), the returned index comes before all existing elements equal to [param value] in the array. [codeblock] - var array = ["a", "b", "c", "c", "d", "e"] - print(array.bsearch("c", true)) # Prints 2, at the first matching element. - print(array.bsearch("c", false)) # Prints 4, after the last matching element, pointing to "d". + var numbers = [2, 4, 8, 10] + var idx = numbers.bsearch(7) + + numbers.insert(idx, 7) + print(numbers) # Prints [2, 4, 7, 8, 10] + + var fruits = ["Apple", "Lemon", "Lemon", "Orange"] + print(fruits.bsearch("Lemon", true)) # Prints 1, points at the first "Lemon". + print(fruits.bsearch("Lemon", false)) # Prints 3, points at "Orange". [/codeblock] - [b]Note:[/b] Calling [method bsearch] on an unsorted array results in unexpected behavior. + [b]Note:[/b] Calling [method bsearch] on an [i]unsorted[/i] array will result in unexpected behavior. Use [method sort] before calling this method. </description> </method> <method name="bsearch_custom" qualifiers="const"> @@ -263,15 +288,36 @@ <param index="1" name="func" type="Callable" /> <param index="2" name="before" type="bool" default="true" /> <description> - Finds the index of an existing value (or the insertion index that maintains sorting order, if the value is not yet present in the array) using binary search and a custom comparison method. Optionally, a [param before] specifier can be passed. If [code]false[/code], the returned index comes after all existing entries of the value in the array. The custom method receives two arguments (an element from the array and the value searched for) and must return [code]true[/code] if the first argument is less than the second, and return [code]false[/code] otherwise. - [b]Note:[/b] The custom method must accept the two arguments in any order, you cannot rely on that the first argument will always be from the array. - [b]Note:[/b] Calling [method bsearch_custom] on an unsorted array results in unexpected behavior. + Returns the index of [param value] in the sorted array. If it cannot be found, returns where [param value] should be inserted to keep the array sorted (using [param func] for the comparisons). The algorithm used is [url=https://en.wikipedia.org/wiki/Binary_search_algorithm]binary search[/url]. + Similar to [method sort_custom], [param func] is called as many times as necessary, receiving one array element and [param value] as arguments. The function should return [code]true[/code] if the array element should be [i]behind[/i] [param value], otherwise it should return [code]false[/code]. + If [param before] is [code]true[/code] (as by default), the returned index comes before all existing elements equal to [param value] in the array. + [codeblock] + func sort_by_amount(a, b): + if a[1] < b[1]: + return true + return false + + func _ready(): + var my_items = [["Tomato", 2], ["Kiwi", 5], ["Rice", 9]] + + var apple = ["Apple", 5] + # "Apple" is inserted before "Kiwi". + my_items.insert(my_items.bsearch_custom(apple, sort_by_amount, true), apple) + + var banana = ["Banana", 5] + # "Banana" is inserted after "Kiwi". + my_items.insert(my_items.bsearch_custom(banana, sort_by_amount, false), banana) + + # Prints [["Tomato", 2], ["Apple", 5], ["Kiwi", 5], ["Banana", 5], ["Rice", 9]] + print(my_items) + [/codeblock] + [b]Note:[/b] Calling [method bsearch_custom] on an [i]unsorted[/i] array will result in unexpected behavior. Use [method sort_custom] with [param func] before calling this method. </description> </method> <method name="clear"> <return type="void" /> <description> - Clears the array. This is equivalent to using [method resize] with a size of [code]0[/code]. + Removes all elements from the array. This is equivalent to using [method resize] with a size of [code]0[/code]. </description> </method> <method name="count" qualifiers="const"> @@ -285,53 +331,57 @@ <return type="Array" /> <param index="0" name="deep" type="bool" default="false" /> <description> - Returns a copy of the array. - If [param deep] is [code]true[/code], a deep copy is performed: all nested arrays and dictionaries are duplicated and will not be shared with the original array. If [code]false[/code], a shallow copy is made and references to the original nested arrays and dictionaries are kept, so that modifying a sub-array or dictionary in the copy will also impact those referenced in the source array. Note that any [Object]-derived elements will be shallow copied regardless of the [param deep] setting. + Returns a new copy of the array. + By default, a [b]shallow[/b] copy is returned: all nested [Array] and [Dictionary] elements are shared with the original array. Modifying them in one array will also affect them in the other.[br]If [param deep] is [code]true[/code], a [b]deep[/b] copy is returned: all nested arrays and dictionaries are also duplicated (recursively). </description> </method> <method name="erase"> <return type="void" /> <param index="0" name="value" type="Variant" /> <description> - Removes the first occurrence of a value from the array. If the value does not exist in the array, nothing happens. To remove an element by index, use [method remove_at] instead. - [b]Note:[/b] This method acts in-place and doesn't return a modified array. - [b]Note:[/b] On large arrays, this method will be slower if the removed element is close to the beginning of the array (index 0). This is because all elements placed after the removed element have to be reindexed. - [b]Note:[/b] Do not erase entries while iterating over the array. + Finds and removes the first occurrence of [param value] from the array. If [param value] does not exist in the array, nothing happens. To remove an element by index, use [method remove_at] instead. + [b]Note:[/b] This method shifts every element's index after the removed [param value] back, which may have a noticeable performance cost, especially on larger arrays. + [b]Note:[/b] Erasing elements while iterating over arrays is [b]not[/b] supported and will result in unpredictable behavior. </description> </method> <method name="fill"> <return type="void" /> <param index="0" name="value" type="Variant" /> <description> - Assigns the given value to all elements in the array. This can typically be used together with [method resize] to create an array with a given size and initialized elements: + Assigns the given [param value] to all elements in the array. + This method can often be combined with [method resize] to create an array with a given size and initialized elements: [codeblocks] [gdscript] var array = [] - array.resize(10) - array.fill(0) # Initialize the 10 elements to 0. + array.resize(5) + array.fill(2) + print(array) # Prints [2, 2, 2, 2, 2] [/gdscript] [csharp] var array = new Godot.Collections.Array(); - array.Resize(10); - array.Fill(0); // Initialize the 10 elements to 0. + array.Resize(5); + array.Fill(2); + GD.Print(array); // Prints [2, 2, 2, 2, 2] [/csharp] [/codeblocks] - [b]Note:[/b] If [param value] is of a reference type ([Object]-derived, [Array], [Dictionary], etc.) then the array is filled with the references to the same object, i.e. no duplicates are created. + [b]Note:[/b] If [param value] is a [Variant] passed by reference ([Object]-derived, [Array], [Dictionary], etc.), the array will be filled with references to the same [param value], which are not duplicates. </description> </method> <method name="filter" qualifiers="const"> <return type="Array" /> <param index="0" name="method" type="Callable" /> <description> - Calls the provided [Callable] on each element in the array and returns a new array with the elements for which the method returned [code]true[/code]. - The callable's method should take one [Variant] parameter (the current array element) and return a boolean value. + Calls the given [Callable] on each element in the array and returns a new, filtered [Array]. + The [param method] receives one of the array elements as an argument, and should return [code]true[/code] to add the element to the filtered array, or [code]false[/code] to exclude it. [codeblock] + func is_even(number): + return number % 2 == 0 + func _ready(): - print([1, 2, 3].filter(remove_1)) # Prints [2, 3]. - print([1, 2, 3].filter(func(number): return number != 1)) # Same as above, but using lambda function. + print([1, 4, 5, 8].filter(is_even)) # Prints [4, 8] - func remove_1(number): - return number != 1 + # Same as above, but using a lambda function. + print([1, 4, 5, 8].filter(func(number): return number % 2 == 0)) [/codeblock] See also [method any], [method all], [method map] and [method reduce]. </description> @@ -341,78 +391,70 @@ <param index="0" name="what" type="Variant" /> <param index="1" name="from" type="int" default="0" /> <description> - Searches the array for a value and returns its index or [code]-1[/code] if not found. Optionally, the initial search index can be passed. + Returns the index of the [b]first[/b] occurrence of [param what] in this array, or [code]-1[/code] if there are none. The search's start can be specified with [param from], continuing to the end of the array. + [b]Note:[/b] If you just want to know whether the array contains [param what], use [method has] ([code]Contains[/code] in C#). In GDScript, you may also use the [code]in[/code] operator. + [b]Note:[/b] For performance reasons, the search is affected by [param what]'s [enum Variant.Type]. For example, [code]7[/code] ([int]) and [code]7.0[/code] ([float]) are not considered equal for this method. </description> </method> <method name="front" qualifiers="const"> <return type="Variant" /> <description> - Returns the first element of the array. Prints an error and returns [code]null[/code] if the array is empty. - [b]Note:[/b] Calling this function is not the same as writing [code]array[0][/code]. If the array is empty, accessing by index will pause project execution when running from the editor. + Returns the first element of the array. If the array is empty, fails and returns [code]null[/code]. See also [method back]. + [b]Note:[/b] Unlike with the [code][][/code] operator ([code]array[0][/code]), an error is generated without stopping project execution. </description> </method> <method name="get_typed_builtin" qualifiers="const"> <return type="int" /> <description> - Returns the built-in type of the typed array as a [enum Variant.Type] constant. If the array is not typed, returns [constant TYPE_NIL]. + Returns the built-in [Variant] type of the typed array as a [enum Variant.Type] constant. If the array is not typed, returns [constant TYPE_NIL]. See also [method is_typed]. </description> </method> <method name="get_typed_class_name" qualifiers="const"> <return type="StringName" /> <description> - Returns the [b]native[/b] class name of the typed array if the built-in type is [constant TYPE_OBJECT]. Otherwise, this method returns an empty string. + Returns the [b]built-in[/b] class name of the typed array, if the built-in [Variant] type [constant TYPE_OBJECT]. Otherwise, returns an empty [StringName]. See also [method is_typed] and [method Object.get_class]. </description> </method> <method name="get_typed_script" qualifiers="const"> <return type="Variant" /> <description> - Returns the script associated with the typed array. This method returns a [Script] instance or [code]null[/code]. + Returns the [Script] instance associated with this typed array, or [code]null[/code] if it does not exist. See also [method is_typed]. </description> </method> <method name="has" qualifiers="const" keywords="includes, contains"> <return type="bool" /> <param index="0" name="value" type="Variant" /> <description> - Returns [code]true[/code] if the array contains the given value. + Returns [code]true[/code] if the array contains the given [param value]. [codeblocks] [gdscript] - print(["inside", 7].has("inside")) # True - print(["inside", 7].has("outside")) # False - print(["inside", 7].has(7)) # True - print(["inside", 7].has("7")) # False + print(["inside", 7].has("inside")) # Prints true + print(["inside", 7].has("outside")) # Prints false + print(["inside", 7].has(7)) # Prints true + print(["inside", 7].has("7")) # Prints false [/gdscript] [csharp] var arr = new Godot.Collections.Array { "inside", 7 }; - // has is renamed to Contains - GD.Print(arr.Contains("inside")); // True - GD.Print(arr.Contains("outside")); // False - GD.Print(arr.Contains(7)); // True - GD.Print(arr.Contains("7")); // False - [/csharp] - [/codeblocks] - [b]Note:[/b] This is equivalent to using the [code]in[/code] operator as follows: - [codeblocks] - [gdscript] - # Will evaluate to `true`. - if 2 in [2, 4, 6, 8]: - print("Contains!") - [/gdscript] - [csharp] - // As there is no "in" keyword in C#, you have to use Contains - var array = new Godot.Collections.Array { 2, 4, 6, 8 }; - if (array.Contains(2)) - { - GD.Print("Contains!"); - } + // By C# convention, this method is renamed to `Contains`. + GD.Print(arr.Contains("inside")); // Prints true + GD.Print(arr.Contains("outside")); // Prints false + GD.Print(arr.Contains(7)); // Prints true + GD.Print(arr.Contains("7")); // Prints false [/csharp] [/codeblocks] + In GDScript, this is equivalent to the [code]in[/code] operator: + [codeblock] + if 4 in [2, 4, 6, 8]: + print("4 is here!") # Will be printed. + [/codeblock] + [b]Note:[/b] For performance reasons, the search is affected by the [param value]'s [enum Variant.Type]. For example, [code]7[/code] ([int]) and [code]7.0[/code] ([float]) are not considered equal for this method. </description> </method> <method name="hash" qualifiers="const"> <return type="int" /> <description> Returns a hashed 32-bit integer value representing the array and its contents. - [b]Note:[/b] [Array]s with equal content will always produce identical hash values. However, the reverse is not true. Returning identical hash values does [i]not[/i] imply the arrays are equal, because different arrays can have identical hash values due to hash collisions. + [b]Note:[/b] Arrays with equal hash values are [i]not[/i] guaranteed to be the same, as a result of hash collisions. On the countrary, arrays with different hash values are guaranteed to be different. </description> </method> <method name="insert"> @@ -420,55 +462,64 @@ <param index="0" name="position" type="int" /> <param index="1" name="value" type="Variant" /> <description> - Inserts a new element at a given position in the array. The position must be valid, or at the end of the array ([code]pos == size()[/code]). Returns [constant OK] on success, or one of the other [enum Error] values if the operation failed. - [b]Note:[/b] This method acts in-place and doesn't return a modified array. - [b]Note:[/b] On large arrays, this method will be slower if the inserted element is close to the beginning of the array (index 0). This is because all elements placed after the newly inserted element have to be reindexed. + Inserts a new element ([param value]) at a given index ([param position]) in the array. [param position] should be between [code]0[/code] and the array's [method size]. + Returns [constant OK] on success, or one of the other [enum Error] constants if this method fails. + [b]Note:[/b] Every element's index after [param position] needs to be shifted forward, which may have a noticeable performance cost, especially on larger arrays. </description> </method> <method name="is_empty" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the array is empty. + Returns [code]true[/code] if the array is empty ([code][][/code]). See also [method size]. </description> </method> <method name="is_read_only" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the array is read-only. See [method make_read_only]. Arrays are automatically read-only if declared with [code]const[/code] keyword. + Returns [code]true[/code] if the array is read-only. See [method make_read_only]. + In GDScript, arrays are automatically read-only if declared with the [code]const[/code] keyword. </description> </method> <method name="is_same_typed" qualifiers="const"> <return type="bool" /> <param index="0" name="array" type="Array" /> <description> - Returns [code]true[/code] if the array is typed the same as [param array]. + Returns [code]true[/code] if this array is typed the same as the given [param array]. See also [method is_typed]. </description> </method> <method name="is_typed" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the array is typed. Typed arrays can only store elements of their associated type and provide type safety for the [code][][/code] operator. Methods of typed array still return [Variant]. + Returns [code]true[/code] if the array is typed. Typed arrays can only contain elements of a specific type, as defined by the typed array constructor. The methods of a typed array are still expected to return a generic [Variant]. + In GDScript, it is possible to define a typed array with static typing: + [codeblock] + var numbers: Array[float] = [0.2, 4.2, -2.0] + print(numbers.is_typed()) # Prints true + [/codeblock] </description> </method> <method name="make_read_only"> <return type="void" /> <description> - Makes the array read-only, i.e. disabled modifying of the array's elements. Does not apply to nested content, e.g. content of nested arrays. + Makes the array read-only. The array's elements cannot be overridden with different values, and their order cannot change. Does not apply to nested elements, such as dictionaries. + In GDScript, arrays are automatically read-only if declared with the [code]const[/code] keyword. </description> </method> <method name="map" qualifiers="const"> <return type="Array" /> <param index="0" name="method" type="Callable" /> <description> - Calls the provided [Callable] for each element in the array and returns a new array filled with values returned by the method. - The callable's method should take one [Variant] parameter (the current array element) and can return any [Variant]. + Calls the given [Callable] for each element in the array and returns a new array filled with values returned by the [param method]. + The [param method] should take one [Variant] parameter (the current array element) and can return any [Variant]. [codeblock] + func double(number): + return number * 2 + func _ready(): - print([1, 2, 3].map(negate)) # Prints [-1, -2, -3]. - print([1, 2, 3].map(func(number): return -number)) # Same as above, but using lambda function. + print([1, 2, 3].map(double)) # Prints [2, 4, 6] - func negate(number): - return -number + # Same as above, but using a lambda function. + print([1, 2, 3].map(func(element): return element * 2)) [/codeblock] See also [method filter], [method reduce], [method any] and [method all]. </description> @@ -476,61 +527,52 @@ <method name="max" qualifiers="const"> <return type="Variant" /> <description> - Returns the maximum value contained in the array if all elements are of comparable types. If the elements can't be compared, [code]null[/code] is returned. - To find the maximum value using a custom comparator, you can use [method reduce]. In this example every array element is checked and the first maximum value is returned: - [codeblock] - func _ready(): - var arr = [Vector2(0, 1), Vector2(2, 0), Vector2(1, 1), Vector2(1, 0), Vector2(0, 2)] - # In this example we compare the lengths. - print(arr.reduce(func(max, val): return val if is_length_greater(val, max) else max)) - - func is_length_greater(a, b): - return a.length() > b.length() - [/codeblock] + Returns the maximum value contained in the array, if all elements can be compared. Otherwise, returns [code]null[/code]. See also [method min]. + To find the maximum value using a custom comparator, you can use [method reduce]. </description> </method> <method name="min" qualifiers="const"> <return type="Variant" /> <description> - Returns the minimum value contained in the array if all elements are of comparable types. If the elements can't be compared, [code]null[/code] is returned. - See also [method max] for an example of using a custom comparator. + Returns the minimum value contained in the array, if all elements can be compared. Otherwise, returns [code]null[/code]. See also [method max]. </description> </method> <method name="pick_random" qualifiers="const"> <return type="Variant" /> <description> - Returns a random value from the target array. Prints an error and returns [code]null[/code] if the array is empty. + Returns a random element from the array. Generates an error and returns [code]null[/code] if the array is empty. [codeblocks] [gdscript] - var array: Array[int] = [1, 2, 3, 4] - print(array.pick_random()) # Prints either of the four numbers. + # May print 1, 2, 3.25, or "Hi". + print([1, 2, 3.25, "Hi"].pick_random()) [/gdscript] [csharp] - var array = new Godot.Collections.Array { 1, 2, 3, 4 }; - GD.Print(array.PickRandom()); // Prints either of the four numbers. + var array = new Godot.Collections.Array { 1, 2, 3.25f, "Hi" }; + GD.Print(array.PickRandom()); // May print 1, 2, 3.25, or "Hi". [/csharp] [/codeblocks] + [b]Note:[/b] Like many similar functions in the engine (such as [method @GlobalScope.randi] or [method shuffle]), this method uses a common, global random seed. To get a predictable outcome from this method, see [method @GlobalScope.seed]. </description> </method> <method name="pop_at"> <return type="Variant" /> <param index="0" name="position" type="int" /> <description> - Removes and returns the element of the array at index [param position]. If negative, [param position] is considered relative to the end of the array. Leaves the array unchanged and returns [code]null[/code] if the array is empty or if it's accessed out of bounds. An error message is printed when the array is accessed out of bounds, but not when the array is empty. - [b]Note:[/b] On large arrays, this method can be slower than [method pop_back] as it will reindex the array's elements that are located after the removed element. The larger the array and the lower the index of the removed element, the slower [method pop_at] will be. + Removes and returns the element of the array at index [param position]. If negative, [param position] is considered relative to the end of the array. Returns [code]null[/code] if the array is empty. If [param position] is out of bounds, an error message is also generated. + [b]Note:[/b] This method shifts every element's index after [param position] back, which may have a noticeable performance cost, especially on larger arrays. </description> </method> <method name="pop_back"> <return type="Variant" /> <description> - Removes and returns the last element of the array. Returns [code]null[/code] if the array is empty, without printing an error message. See also [method pop_front]. + Removes and returns the last element of the array. Returns [code]null[/code] if the array is empty, without generating an error. See also [method pop_front]. </description> </method> <method name="pop_front"> <return type="Variant" /> <description> - Removes and returns the first element of the array. Returns [code]null[/code] if the array is empty, without printing an error message. See also [method pop_back]. - [b]Note:[/b] On large arrays, this method is much slower than [method pop_back] as it will reindex all the array's elements every time it's called. The larger the array, the slower [method pop_front] will be. + Removes and returns the first element of the array. Returns [code]null[/code] if the array is empty, without generating an error. See also [method pop_back]. + [b]Note:[/b] This method shifts every other element's index back, which may have a noticeable performance cost, especially on larger arrays. </description> </method> <method name="push_back"> @@ -545,7 +587,7 @@ <param index="0" name="value" type="Variant" /> <description> Adds an element at the beginning of the array. See also [method push_back]. - [b]Note:[/b] On large arrays, this method is much slower than [method push_back] as it will reindex all the array's elements every time it's called. The larger the array, the slower [method push_front] will be. + [b]Note:[/b] This method shifts every other element's index forward, which may have a noticeable performance cost, especially on larger arrays. </description> </method> <method name="reduce" qualifiers="const"> @@ -553,15 +595,29 @@ <param index="0" name="method" type="Callable" /> <param index="1" name="accum" type="Variant" default="null" /> <description> - Calls the provided [Callable] for each element in array and accumulates the result in [param accum]. - The callable's method takes two arguments: the current value of [param accum] and the current array element. If [param accum] is [code]null[/code] (default value), the iteration will start from the second element, with the first one used as initial value of [param accum]. + Calls the given [Callable] for each element in array, accumulates the result in [param accum], then returns it. + The [param method] takes two arguments: the current value of [param accum] and the current array element. If [param accum] is [code]null[/code] (as by default), the iteration will start from the second element, with the first one used as initial value of [param accum]. [codeblock] - func _ready(): - print([1, 2, 3].reduce(sum, 10)) # Prints 16. - print([1, 2, 3].reduce(func(accum, number): return accum + number, 10)) # Same as above, but using lambda function. - func sum(accum, number): return accum + number + + func _ready(): + print([1, 2, 3].reduce(sum, 0)) # Prints 6 + print([1, 2, 3].reduce(sum, 10)) # Prints 16 + + # Same as above, but using a lambda function. + print([1, 2, 3].reduce(func(accum, number): return accum + number, 10)) + [/codeblock] + If [method max] is not desirable, this method may also be used to implement a custom comparator: + [codeblock] + func _ready(): + var arr = [Vector2(5, 0), Vector2(3, 4), Vector2(1, 2)] + + var longest_vec = arr.reduce(func(max, vec): return vec if is_length_greater(vec, max) else max) + print(longest_vec) # Prints Vector2(3, 4). + + func is_length_greater(a, b): + return a.length() > b.length() [/codeblock] See also [method map], [method filter], [method any] and [method all]. </description> @@ -570,25 +626,25 @@ <return type="void" /> <param index="0" name="position" type="int" /> <description> - Removes an element from the array by index. If the index does not exist in the array, nothing happens. To remove an element by searching for its value, use [method erase] instead. - [b]Note:[/b] This method acts in-place and doesn't return a modified array. - [b]Note:[/b] On large arrays, this method will be slower if the removed element is close to the beginning of the array (index 0). This is because all elements placed after the removed element have to be reindexed. - [b]Note:[/b] [param position] cannot be negative. To remove an element relative to the end of the array, use [code]arr.remove_at(arr.size() - (i + 1))[/code]. To remove the last element from the array without returning the value, use [code]arr.resize(arr.size() - 1)[/code]. + Removes the element from the array at the given index ([param position]). If the index is out of bounds, this method fails. + If you need to return the removed element, use [method pop_at]. To remove an element by value, use [method erase] instead. + [b]Note:[/b] This method shifts every element's index after [param position] back, which may have a noticeable performance cost, especially on larger arrays. + [b]Note:[/b] The [param position] cannot be negative. To remove an element relative to the end of the array, use [code]arr.remove_at(arr.size() - (i + 1))[/code]. To remove the last element from the array, use [code]arr.resize(arr.size() - 1)[/code]. </description> </method> <method name="resize"> <return type="int" /> <param index="0" name="size" type="int" /> <description> - Resizes the array to contain a different number of elements. If the array size is smaller, elements are cleared, if bigger, new elements are [code]null[/code]. Returns [constant OK] on success, or one of the other [enum Error] values if the operation failed. - Calling [method resize] once and assigning the new values is faster than adding new elements one by one. - [b]Note:[/b] This method acts in-place and doesn't return a modified array. + Sets the array's number of elements to [param size]. If [param size] is smaller than the array's current size, the elements at the end are removed. If [param size] is greater, new default elements (usually [code]null[/code]) are added, depending on the array's type. + Returns [constant OK] on success, or one of the other [enum Error] constants if this method fails. + [b]Note:[/b] Calling this method once and assigning the new values is faster than calling [method append] for every new element. </description> </method> <method name="reverse"> <return type="void" /> <description> - Reverses the order of the elements in the array. + Reverses the order of all elements in the array. </description> </method> <method name="rfind" qualifiers="const"> @@ -596,19 +652,20 @@ <param index="0" name="what" type="Variant" /> <param index="1" name="from" type="int" default="-1" /> <description> - Searches the array in reverse order. Optionally, a start search index can be passed. If negative, the start index is considered relative to the end of the array. + Returns the index of the [b]last[/b] occurrence of [param what] in this array, or [code]-1[/code] if there are none. The search's start can be specified with [param from], continuing to the beginning of the array. This method is the reverse of [method find]. </description> </method> <method name="shuffle"> <return type="void" /> <description> - Shuffles the array such that the items will have a random order. This method uses the global random number generator common to methods such as [method @GlobalScope.randi]. Call [method @GlobalScope.randomize] to ensure that a new seed will be used each time if you want non-reproducible shuffling. + Shuffles all elements of the array in a random order. + [b]Note:[/b] Like many similar functions in the engine (such as [method @GlobalScope.randi] or [method pick_random]), this method uses a common, global random seed. To get a predictable outcome from this method, see [method @GlobalScope.seed]. </description> </method> <method name="size" qualifiers="const"> <return type="int" /> <description> - Returns the number of elements in the array. + Returns the number of elements in the array. Empty arrays ([code][][/code]) always return [code]0[/code]. See also [method is_empty]. </description> </method> <method name="slice" qualifiers="const"> @@ -618,67 +675,71 @@ <param index="2" name="step" type="int" default="1" /> <param index="3" name="deep" type="bool" default="false" /> <description> - Returns the slice of the [Array], from [param begin] (inclusive) to [param end] (exclusive), as a new [Array]. - The absolute value of [param begin] and [param end] will be clamped to the array size, so the default value for [param end] makes it slice to the size of the array by default (i.e. [code]arr.slice(1)[/code] is a shorthand for [code]arr.slice(1, arr.size())[/code]). - If either [param begin] or [param end] are negative, they will be relative to the end of the array (i.e. [code]arr.slice(0, -2)[/code] is a shorthand for [code]arr.slice(0, arr.size() - 2)[/code]). - If specified, [param step] is the relative index between source elements. It can be negative, then [param begin] must be higher than [param end]. For example, [code][0, 1, 2, 3, 4, 5].slice(5, 1, -2)[/code] returns [code][5, 3][/code]. - If [param deep] is true, each element will be copied by value rather than by reference. - [b]Note:[/b] To include the first element when [param step] is negative, use [code]arr.slice(begin, -arr.size() - 1, step)[/code] (i.e. [code][0, 1, 2].slice(1, -4, -1)[/code] returns [code][1, 0][/code]). + Returns a new [Array] containing this array's elements, from index [param begin] (inclusive) to [param end] (exclusive), every [param step] elements. + If either [param begin] or [param end] are negative, their value is relative to the end of the array. + If [param step] is negative, this method iterates through the array in reverse, returning a slice ordered backwards. For this to work, [param begin] must be greater than [param end]. + If [param deep] is [code]true[/code], all nested [Array] and [Dictionary] elements in the slice are duplicated from the original, recursively. See also [method duplicate]). + [codeblock] + var letters = ["A", "B", "C", "D", "E", "F"] + + print(letters.slice(0, 2)) # Prints ["A", "B"] + print(letters.slice(2, -2)) # Prints ["C", "D"] + print(letters.slice(-2, 6)) # Prints ["E", "F"] + + print(letters.slice(0, 6, 2)) # Prints ["A", "C", "E"] + print(letters.slice(4, 1, -1)) # Prints ["E", "D", "C"] + [/codeblock] </description> </method> <method name="sort"> <return type="void" /> <description> - Sorts the array. - [b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that values considered equal may have their order changed when using [method sort]. - [b]Note:[/b] Strings are sorted in alphabetical order (as opposed to natural order). This may lead to unexpected behavior when sorting an array of strings ending with a sequence of numbers. Consider the following example: + Sorts the array in ascending order. The final order is dependent on the "less than" ([code]>[/code]) comparison between elements. [codeblocks] [gdscript] - var strings = ["string1", "string2", "string10", "string11"] - strings.sort() - print(strings) # Prints [string1, string10, string11, string2] + var numbers = [10, 5, 2.5, 8] + numbers.sort() + print(numbers) # Prints [2.5, 5, 8, 10] [/gdscript] [csharp] - var strings = new Godot.Collections.Array { "string1", "string2", "string10", "string11" }; - strings.Sort(); - GD.Print(strings); // Prints [string1, string10, string11, string2] + var numbers = new Godot.Collections.Array { 10, 5, 2.5, 8 }; + numbers.Sort(); + GD.Print(numbers); // Prints [2.5, 5, 8, 10] [/csharp] [/codeblocks] - To perform natural order sorting, you can use [method sort_custom] with [method String.naturalnocasecmp_to] as follows: - [codeblock] - var strings = ["string1", "string2", "string10", "string11"] - strings.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0) - print(strings) # Prints [string1, string2, string10, string11] - [/codeblock] + [b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that equivalent elements (such as [code]2[/code] and [code]2.0[/code]) may have their order changed when calling [method sort]. </description> </method> <method name="sort_custom"> <return type="void" /> <param index="0" name="func" type="Callable" /> <description> - Sorts the array using a custom method. The custom method receives two arguments (a pair of elements from the array) and must return either [code]true[/code] or [code]false[/code]. For two elements [code]a[/code] and [code]b[/code], if the given method returns [code]true[/code], element [code]b[/code] will be after element [code]a[/code] in the array. - [b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that values considered equal may have their order changed when using [method sort_custom]. - [b]Note:[/b] You cannot randomize the return value as the heapsort algorithm expects a deterministic result. Randomizing the return value will result in unexpected behavior. - [codeblocks] - [gdscript] + Sorts the array using a custom [Callable]. + [param func] is called as many times as necessary, receiving two array elements as arguments. The function should return [code]true[/code] if the first element should be moved [i]behind[/i] the second one, otherwise it should return [code]false[/code]. + [codeblock] func sort_ascending(a, b): - if a[0] < b[0]: + if a[1] < b[1]: return true return false func _ready(): - var my_items = [[5, "Potato"], [9, "Rice"], [4, "Tomato"]] + var my_items = [["Tomato", 5], ["Apple", 9], ["Rice", 4]] my_items.sort_custom(sort_ascending) - print(my_items) # Prints [[4, Tomato], [5, Potato], [9, Rice]]. + print(my_items) # Prints [["Rice", 4], ["Tomato", 5], ["Apple", 9]] - # Descending, lambda version. + # Sort descending, using a lambda function. my_items.sort_custom(func(a, b): return a[0] > b[0]) - print(my_items) # Prints [[9, Rice], [5, Potato], [4, Tomato]]. - [/gdscript] - [csharp] - // There is no custom sort support for Godot.Collections.Array - [/csharp] - [/codeblocks] + print(my_items) # Prints [["Apple", 9], ["Tomato", 5], ["Rice", 4]] + [/codeblock] + It may also be necessary to use this method to sort strings by natural order, with [method String.naturalnocasecmp_to], as in the following example: + [codeblock] + var files = ["newfile1", "newfile2", "newfile10", "newfile11"] + files.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0) + print(files) # Prints ["newfile1", "newfile2", "newfile10", "newfile11"] + [/codeblock] + [b]Note:[/b] In C#, this method is not supported. + [b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that values considered equal may have their order changed when calling this method. + [b]Note:[/b] You should not randomize the return value of [param func], as the heapsort algorithm expects a consistent result. Randomizing the return value will result in unexpected behavior. </description> </method> </methods> @@ -687,28 +748,44 @@ <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Compares the left operand [Array] against the [param right] [Array]. Returns [code]true[/code] if the sizes or contents of the arrays are [i]not[/i] equal, [code]false[/code] otherwise. + Returns [code]true[/code] if the array's size or its elements are different than [param right]'s. </description> </operator> <operator name="operator +"> <return type="Array" /> <param index="0" name="right" type="Array" /> <description> - Concatenates two [Array]s together, with the [param right] [Array] being added to the end of the [Array] specified in the left operand. For example, [code][1, 2] + [3, 4][/code] results in [code][1, 2, 3, 4][/code]. + Appends the [param right] array to the left operand, creating a new [Array]. This is also known as an array concatenation. + [codeblocks] + [gdscript] + var array1 = ["One", 2] + var array2 = [3, "Four"] + print(array1 + array2) # Prints ["One", 2, 3, "Four"] + [/gdscript] + [csharp] + // Note that concatenation is not possible with C#'s native Array type. + var array1 = new Godot.Collections.Array{"One", 2}; + var array2 = new Godot.Collections.Array{3, "Four"}; + GD.Print(array1 + array2); // Prints ["One", 2, 3, "Four"] + [/csharp] + [/codeblocks] + [b]Note:[/b] For existing arrays, [method append_array] is much more efficient than concatenation and assignment with the [code]+=[/code] operator. </description> </operator> <operator name="operator <"> <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Performs a comparison for each index between the left operand [Array] and the [param right] [Array], considering the highest common index of both arrays for this comparison: Returns [code]true[/code] on the first occurrence of an element that is less, or [code]false[/code] if the element is greater. Note that depending on the type of data stored, this function may be recursive. If all elements are equal, it compares the length of both arrays and returns [code]false[/code] if the left operand [Array] has fewer elements, otherwise it returns [code]true[/code]. + Compares the elements of both arrays in order, starting from index [code]0[/code] and ending on the last index in common between both arrays. For each pair of elements, returns [code]true[/code] if this array's element is less than [param right]'s, [code]false[/code] if this element is greater. Otherwise, continues to the next pair. + If all searched elements are equal, returns [code]true[/code] if this array's size is less than [param right]'s, otherwise returns [code]false[/code]. </description> </operator> <operator name="operator <="> <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Performs a comparison for each index between the left operand [Array] and the [param right] [Array], considering the highest common index of both arrays for this comparison: Returns [code]true[/code] on the first occurrence of an element that is less, or [code]false[/code] if the element is greater. Note that depending on the type of data stored, this function may be recursive. If all elements are equal, it compares the length of both arrays and returns [code]true[/code] if the left operand [Array] has the same number of elements or fewer, otherwise it returns [code]false[/code]. + Compares the elements of both arrays in order, starting from index [code]0[/code] and ending on the last index in common between both arrays. For each pair of elements, returns [code]true[/code] if this array's element is less than [param right]'s, [code]false[/code] if this element is greater. Otherwise, continues to the next pair. + If all searched elements are equal, returns [code]true[/code] if this array's size is less or equal to [param right]'s, otherwise returns [code]false[/code]. </description> </operator> <operator name="operator =="> @@ -722,21 +799,23 @@ <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Performs a comparison for each index between the left operand [Array] and the [param right] [Array], considering the highest common index of both arrays for this comparison: Returns [code]true[/code] on the first occurrence of an element that is greater, or [code]false[/code] if the element is less. Note that depending on the type of data stored, this function may be recursive. If all elements are equal, it compares the length of both arrays and returns [code]true[/code] if the [param right] [Array] has more elements, otherwise it returns [code]false[/code]. + Compares the elements of both arrays in order, starting from index [code]0[/code] and ending on the last index in common between both arrays. For each pair of elements, returns [code]true[/code] if this array's element is greater than [param right]'s, [code]false[/code] if this element is less. Otherwise, continues to the next pair. + If all searched elements are equal, returns [code]true[/code] if this array's size is greater than [param right]'s, otherwise returns [code]false[/code]. </description> </operator> <operator name="operator >="> <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Performs a comparison for each index between the left operand [Array] and the [param right] [Array], considering the highest common index of both arrays for this comparison: Returns [code]true[/code] on the first occurrence of an element that is greater, or [code]false[/code] if the element is less. Note that depending on the type of data stored, this function may be recursive. If all elements are equal, it compares the length of both arrays and returns [code]true[/code] if the [param right] [Array] has more or the same number of elements, otherwise it returns [code]false[/code]. + Compares the elements of both arrays in order, starting from index [code]0[/code] and ending on the last index in common between both arrays. For each pair of elements, returns [code]true[/code] if this array's element is greater than [param right]'s, [code]false[/code] if this element is less. Otherwise, continues to the next pair. + If all searched elements are equal, returns [code]true[/code] if this array's size is greater or equal to [param right]'s, otherwise returns [code]false[/code]. </description> </operator> <operator name="operator []"> <return type="Variant" /> <param index="0" name="index" type="int" /> <description> - Returns a reference to the element of type [Variant] at the specified location. Arrays start at index 0. [param index] can be a zero or positive value to start from the beginning, or a negative value to start from the end. Out-of-bounds array access causes a run-time error, which will result in an error being printed and the project execution pausing if run from the editor. + Returns the [Variant] element at the specified [param index]. Arrays start at index 0. If [param index] is greater or equal to [code]0[/code], the element is fetched starting from the beginning of the array. If [param index] is a negative value, the element is fetched starting from the end. Accessing an array out-of-bounds will cause a run-time error, pausing the project execution if run from the editor. </description> </operator> </operators> diff --git a/doc/classes/AudioEffectSpectrumAnalyzer.xml b/doc/classes/AudioEffectSpectrumAnalyzer.xml index fbc0c2275f..b90f87ef5b 100644 --- a/doc/classes/AudioEffectSpectrumAnalyzer.xml +++ b/doc/classes/AudioEffectSpectrumAnalyzer.xml @@ -5,6 +5,7 @@ </brief_description> <description> This audio effect does not affect sound output, but can be used for real-time audio visualizations. + This resource configures an [AudioEffectSpectrumAnalyzerInstance], which performs the actual analysis at runtime. An instance can be acquired with [method AudioServer.get_bus_effect_instance]. See also [AudioStreamGenerator] for procedurally generating sounds. </description> <tutorials> diff --git a/doc/classes/AudioEffectSpectrumAnalyzerInstance.xml b/doc/classes/AudioEffectSpectrumAnalyzerInstance.xml index e8c6394073..184f80db2e 100644 --- a/doc/classes/AudioEffectSpectrumAnalyzerInstance.xml +++ b/doc/classes/AudioEffectSpectrumAnalyzerInstance.xml @@ -1,10 +1,14 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="AudioEffectSpectrumAnalyzerInstance" inherits="AudioEffectInstance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> <brief_description> + Queryable instance of an [AudioEffectSpectrumAnalyzer]. </brief_description> <description> + The runtime part of an [AudioEffectSpectrumAnalyzer], which can be used to query the magnitude of a frequency range on its host bus. + An instance of this class can be acquired with [method AudioServer.get_bus_effect_instance]. </description> <tutorials> + <link title="Audio Spectrum Visualizer Demo">https://godotengine.org/asset-library/asset/2762</link> </tutorials> <methods> <method name="get_magnitude_for_frequency_range" qualifiers="const"> @@ -13,15 +17,17 @@ <param index="1" name="to_hz" type="float" /> <param index="2" name="mode" type="int" enum="AudioEffectSpectrumAnalyzerInstance.MagnitudeMode" default="1" /> <description> + Returns the magnitude of the frequencies from [param from_hz] to [param to_hz] in linear energy as a Vector2. The [code]x[/code] component of the return value represents the left stereo channel, and [code]y[/code] represents the right channel. + [param mode] determines how the frequency range will be processed. See [enum MagnitudeMode]. </description> </method> </methods> <constants> <constant name="MAGNITUDE_AVERAGE" value="0" enum="MagnitudeMode"> - Use the average value as magnitude. + Use the average value across the frequency range as magnitude. </constant> <constant name="MAGNITUDE_MAX" value="1" enum="MagnitudeMode"> - Use the maximum value as magnitude. + Use the maximum value of the frequency range as magnitude. </constant> </constants> </class> diff --git a/doc/classes/Basis.xml b/doc/classes/Basis.xml index 41bda1033d..322d2ab9d4 100644 --- a/doc/classes/Basis.xml +++ b/doc/classes/Basis.xml @@ -8,7 +8,7 @@ A [Basis] is composed by 3 axis vectors, each representing a column of the matrix: [member x], [member y], and [member z]. The length of each axis ([method Vector3.length]) influences the basis's scale, while the direction of all axes influence the rotation. Usually, these axes are perpendicular to one another. However, when you rotate any axis individually, the basis becomes sheared. Applying a sheared basis to a 3D model will make the model appear distorted. A [Basis] is [b]orthogonal[/b] if its axes are perpendicular to each other. A basis is [b]normalized[/b] if the length of every axis is [code]1[/code]. A basis is [b]uniform[/b] if all axes share the same length (see [method get_scale]). A basis is [b]orthonormal[/b] if it is both orthogonal and normalized, which allows it to only represent rotations. A basis is [b]conformal[/b] if it is both orthogonal and uniform, which ensures it is not distorted. For a general introduction, see the [url=$DOCS_URL/tutorials/math/matrices_and_transforms.html]Matrices and transforms[/url] tutorial. - [b]Note:[/b] Godot uses a [url=https://en.wikipedia.org/wiki/Right-hand_rule]right-handed coordinate system[/url], which is a common standard. For directions, the convention for built-in types like [Camera3D] is for -Z to point forward (+X is right, +Y is up, and +Z is back). Other objects may use different direction conventions. For more information, see the [url=$DOCS_URL/tutorials/assets_pipeline/importing_scenes.html#d-asset-direction-conventions]Importing 3D Scenes[/url] tutorial. + [b]Note:[/b] Godot uses a [url=https://en.wikipedia.org/wiki/Right-hand_rule]right-handed coordinate system[/url], which is a common standard. For directions, the convention for built-in types like [Camera3D] is for -Z to point forward (+X is right, +Y is up, and +Z is back). Other objects may use different direction conventions. For more information, see the [url=$DOCS_URL/tutorials/assets_pipeline/importing_3d_scenes/model_export_considerations.html#d-asset-direction-conventions]3D asset direction conventions[/url] tutorial. [b]Note:[/b] The basis matrices are exposed as [url=https://www.mindcontrol.org/~hplus/graphics/matrix-layout.html]column-major[/url] order, which is the same as OpenGL. However, they are stored internally in row-major order, which is the same as DirectX. </description> <tutorials> diff --git a/doc/classes/Button.xml b/doc/classes/Button.xml index 98f25ed573..68fb918904 100644 --- a/doc/classes/Button.xml +++ b/doc/classes/Button.xml @@ -119,7 +119,7 @@ Icon modulate [Color] used when the [Button] is being pressed. </theme_item> <theme_item name="align_to_largest_stylebox" data_type="constant" type="int" default="0"> - This constant acts as a boolean. If [code]true[/code], text and icon are always aligned to the largest stylebox margins, otherwise it's aligned to the current button state stylebox margins. + This constant acts as a boolean. If [code]true[/code], the minimum size of the button and text/icon alignment is always based on the largest stylebox margins, otherwise it's based on the current button state stylebox margins. </theme_item> <theme_item name="h_separation" data_type="constant" type="int" default="4"> The horizontal space between [Button]'s icon and text. Negative values will be treated as [code]0[/code] when used. diff --git a/doc/classes/CanvasItem.xml b/doc/classes/CanvasItem.xml index 207045b065..8bee6c3470 100644 --- a/doc/classes/CanvasItem.xml +++ b/doc/classes/CanvasItem.xml @@ -623,8 +623,8 @@ [b]Note:[/b] For controls that inherit [Popup], the correct way to make them visible is to call one of the multiple [code]popup*()[/code] functions instead. </member> <member name="y_sort_enabled" type="bool" setter="set_y_sort_enabled" getter="is_y_sort_enabled" default="false"> - If [code]true[/code], this and child [CanvasItem] nodes with a lower Y position are rendered in front of nodes with a higher Y position. If [code]false[/code], this and child [CanvasItem] nodes are rendered normally in scene tree order. - With Y-sorting enabled on a parent node ('A') but disabled on a child node ('B'), the child node ('B') is sorted but its children ('C1', 'C2', etc) render together on the same Y position as the child node 'B'. This allows you to organize the render order of a scene without changing the scene tree. + If [code]true[/code], this and child [CanvasItem] nodes with a higher Y position are rendered in front of nodes with a lower Y position. If [code]false[/code], this and child [CanvasItem] nodes are rendered normally in scene tree order. + With Y-sorting enabled on a parent node ('A') but disabled on a child node ('B'), the child node ('B') is sorted but its children ('C1', 'C2', etc) render together on the same Y position as the child node ('B'). This allows you to organize the render order of a scene without changing the scene tree. Nodes sort relative to each other only if they are on the same [member z_index]. </member> <member name="z_as_relative" type="bool" setter="set_z_as_relative" getter="is_z_relative" default="true"> diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml index 3e2b3ea111..9ef911a68d 100644 --- a/doc/classes/EditorExportPlugin.xml +++ b/doc/classes/EditorExportPlugin.xml @@ -17,7 +17,7 @@ <param index="1" name="features" type="PackedStringArray" /> <description> Return [code]true[/code] if this plugin will customize resources based on the platform and features used. - When enabled, [method _get_customization_configuration_hash], [method _customize_resource] and [method _customize_scene] will be called and must be implemented. + When enabled, [method _get_customization_configuration_hash] and [method _customize_resource] will be called and must be implemented. </description> </method> <method name="_begin_customize_scenes" qualifiers="virtual const"> @@ -25,7 +25,8 @@ <param index="0" name="platform" type="EditorExportPlatform" /> <param index="1" name="features" type="PackedStringArray" /> <description> - Return true if this plugin will customize scenes based on the platform and features used. + Return [code]true[/code] if this plugin will customize scenes based on the platform and features used. + When enabled, [method _get_customization_configuration_hash] and [method _customize_scene] will be called and must be implemented. </description> </method> <method name="_customize_resource" qualifiers="virtual"> @@ -81,7 +82,7 @@ <param index="1" name="type" type="String" /> <param index="2" name="features" type="PackedStringArray" /> <description> - Virtual method to be overridden by the user. Called for each exported file, providing arguments that can be used to identify the file. [param path] is the path of the file, [param type] is the [Resource] represented by the file (e.g. [PackedScene]) and [param features] is the list of features for the export. + Virtual method to be overridden by the user. Called for each exported file before [method _customize_resource] and [method _customize_scene]. The arguments can be used to identify the file. [param path] is the path of the file, [param type] is the [Resource] represented by the file (e.g. [PackedScene]), and [param features] is the list of features for the export. Calling [method skip] inside this callback will make the file not included in the export. </description> </method> @@ -231,6 +232,7 @@ <description> Adds a custom file to be exported. [param path] is the virtual path that can be used to load the file, [param file] is the binary data of the file. When called inside [method _export_file] and [param remap] is [code]true[/code], the current file will not be exported, but instead remapped to this custom file. [param remap] is ignored when called in other places. + [param file] will not be imported, so consider using [method _customize_resource] to remap imported resources. </description> </method> <method name="add_ios_bundle_file"> @@ -313,7 +315,7 @@ <method name="skip"> <return type="void" /> <description> - To be called inside [method _export_file], [method _customize_resource], or [method _customize_scene]. Skips the current file, so it's not included in the export. + To be called inside [method _export_file]. Skips the current file, so it's not included in the export. </description> </method> </methods> diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml index 89b9e7d6c2..c3ea440aea 100644 --- a/doc/classes/EditorPlugin.xml +++ b/doc/classes/EditorPlugin.xml @@ -444,7 +444,7 @@ <param index="2" name="script" type="Script" /> <param index="3" name="icon" type="Texture2D" /> <description> - Adds a custom type, which will appear in the list of nodes or resources. An icon can be optionally passed. + Adds a custom type, which will appear in the list of nodes or resources. When a given node or resource is selected, the base type will be instantiated (e.g. "Node3D", "Control", "Resource"), then the script will be loaded and set to this object. [b]Note:[/b] The base type is the base engine class which this type's class hierarchy inherits, not any custom type parent classes. You can use the virtual method [method _handles] to check if your custom object is being edited by checking the script or using the [code]is[/code] keyword. diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index c5737b235f..c7ff543b66 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -396,6 +396,10 @@ <member name="editors/animation/autorename_animation_tracks" type="bool" setter="" getter=""> If [code]true[/code], automatically updates animation tracks' target paths when renaming or reparenting nodes in the Scene tree dock. </member> + <member name="editors/animation/confirm_insert_track" type="bool" setter="" getter=""> + If [code]true[/code], display a confirmation dialog when adding a new track to an animation by pressing the "key" icon next to a property. Holding Shift will bypass the dialog. + If [code]false[/code], the behavior is reversed, i.e. the dialog only appears when Shift is held. + </member> <member name="editors/animation/default_create_bezier_tracks" type="bool" setter="" getter=""> If [code]true[/code], create a Bezier track instead of a standard track when pressing the "key" icon next to a property. Bezier tracks provide more control over animation curves, but are more difficult to adjust quickly. </member> @@ -1044,7 +1048,7 @@ [b]Note:[/b] The [url=$DOCS_URL/tutorials/scripting/gdscript/gdscript_styleguide.html]GDScript style guide[/url] recommends using tabs for indentation. It is advised to change this setting only if you need to work on a project that currently uses spaces for indentation. </member> <member name="text_editor/behavior/navigation/custom_word_separators" type="String" setter="" getter=""> - The characters to consider as word delimiters if [member text_editor/behavior/navigation/use_custom_word_separators] is [code]true[/code]. The characters should be defined without separation, for example [code]#_![/code]. + The characters to consider as word delimiters if [member text_editor/behavior/navigation/use_custom_word_separators] is [code]true[/code]. This is in addition to default characters if [member text_editor/behavior/navigation/use_default_word_separators] is [code]true[/code]. The characters should be defined without separation, for example [code]_♥=[/code]. </member> <member name="text_editor/behavior/navigation/drag_and_drop_selection" type="bool" setter="" getter=""> If [code]true[/code], allows drag-and-dropping text in the script editor to move text. Disable this if you find yourself accidentally drag-and-dropping text in the script editor. @@ -1066,10 +1070,10 @@ If [code]true[/code], prevents automatically switching between the Script and 2D/3D screens when selecting a node in the Scene tree dock. </member> <member name="text_editor/behavior/navigation/use_custom_word_separators" type="bool" setter="" getter=""> - If [code]false[/code], using [kbd]Ctrl + Left[/kbd] or [kbd]Ctrl + Right[/kbd] ([kbd]Cmd + Left[/kbd] or [kbd]Cmd + Right[/kbd] on macOS) bindings will use the behavior of [member text_editor/behavior/navigation/use_default_word_separators]. If [code]true[/code], it will also stop the caret if a character within [member text_editor/behavior/navigation/custom_word_separators] is detected. Useful for subword moving. This behavior also will be applied to the behavior of text selection. + If [code]true[/code], uses the characters in [member text_editor/behavior/navigation/custom_word_separators] as word separators for word navigation and operations. This is in addition to the default characters if [member text_editor/behavior/navigation/use_default_word_separators] is also enabled. Word navigation and operations include double-clicking on a word or holding [kbd]Ctrl[/kbd] ([kbd]Cmd[/kbd] on macOS) while pressing [kbd]left[/kbd], [kbd]right[/kbd], [kbd]backspace[/kbd], or [kbd]delete[/kbd]. </member> <member name="text_editor/behavior/navigation/use_default_word_separators" type="bool" setter="" getter=""> - If [code]false[/code], using [kbd]Ctrl + Left[/kbd] or [kbd]Ctrl + Right[/kbd] ([kbd]Cmd + Left[/kbd] or [kbd]Cmd + Right[/kbd] on macOS) bindings will stop moving caret only if a space or punctuation is detected. If [code]true[/code], it will also stop the caret if a character is [code]´`~$^=+|<>[/code], a General Punctuation, or CJK Punctuation. Useful for subword moving. This behavior also will be applied to the behavior of text selection. + If [code]true[/code], uses the characters in [code]`!"#$%&'()*+,-./:;<=>?@[\]^`{|}~[/code], the Unicode General Punctuation table, and the Unicode CJK Punctuation table as word separators for word navigation and operations. If [code]false[/code], a subset of these characters are used and does not include the characters [code]<>$~^=+|[/code]. This is in addition to custom characters if [member text_editor/behavior/navigation/use_custom_word_separators] is also enabled. These characters are used to determine where a word stops. Word navigation and operations include double-clicking on a word or holding [kbd]Ctrl[/kbd] ([kbd]Cmd[/kbd] on macOS) while pressing [kbd]left[/kbd], [kbd]right[/kbd], [kbd]backspace[/kbd], or [kbd]delete[/kbd]. </member> <member name="text_editor/behavior/navigation/v_scroll_speed" type="int" setter="" getter=""> The number of pixels to scroll with every mouse wheel increment. Higher values make the script scroll by faster when using the mouse wheel. @@ -1091,7 +1095,7 @@ The delay in seconds after which autocompletion suggestions should be displayed when the user stops typing. </member> <member name="text_editor/completion/code_complete_enabled" type="bool" setter="" getter=""> - If [code]true[/code], code completion will be triggered automatically after [member text_editor/completion/code_complete_delay]. If [code]false[/code], you can still trigger completion manually by pressing [kbd]Ctrl + Space[/kbd] ([kbd]Cmd + Space[/kbd] on macOS). + If [code]true[/code], code completion will be triggered automatically after [member text_editor/completion/code_complete_delay]. Even if [code]false[/code], code completion can be triggered manually with the [code]ui_text_completion_query[/code] action (by default [kbd]Ctrl + Space[/kbd] or [kbd]Cmd + Space[/kbd] on macOS). </member> <member name="text_editor/completion/colorize_suggestions" type="bool" setter="" getter=""> If [code]true[/code] enables the coloring for some items in the autocompletion suggestions, like vector components. diff --git a/doc/classes/EditorUndoRedoManager.xml b/doc/classes/EditorUndoRedoManager.xml index 26580bbf06..5ac0d790a2 100644 --- a/doc/classes/EditorUndoRedoManager.xml +++ b/doc/classes/EditorUndoRedoManager.xml @@ -88,6 +88,13 @@ The way undo operation are ordered in actions is dictated by [param backward_undo_ops]. When [param backward_undo_ops] is [code]false[/code] undo option are ordered in the same order they were added. Which means the first operation to be added will be the first to be undone. </description> </method> + <method name="force_fixed_history"> + <return type="void" /> + <description> + Forces the next operation (e.g. [method add_do_method]) to use the action's history rather than guessing it from the object. This is sometimes needed when a history can't be correctly determined, like for a nested resource that doesn't have a path yet. + This method should only be used when absolutely necessary, otherwise it might cause invalid history state. For most of complex cases, the [code]custom_context[/code] parameter of [method create_action] is sufficient. + </description> + </method> <method name="get_history_undo_redo" qualifiers="const"> <return type="UndoRedo" /> <param index="0" name="id" type="int" /> diff --git a/doc/classes/GraphEdit.xml b/doc/classes/GraphEdit.xml index 001839d745..670df10a89 100644 --- a/doc/classes/GraphEdit.xml +++ b/doc/classes/GraphEdit.xml @@ -7,6 +7,7 @@ [GraphEdit] provides tools for creation, manipulation, and display of various graphs. Its main purpose in the engine is to power the visual programming systems, such as visual shaders, but it is also available for use in user projects. [GraphEdit] by itself is only an empty container, representing an infinite grid where [GraphNode]s can be placed. Each [GraphNode] represents a node in the graph, a single unit of data in the connected scheme. [GraphEdit], in turn, helps to control various interactions with nodes and between nodes. When the user attempts to connect, disconnect, or delete a [GraphNode], a signal is emitted in the [GraphEdit], but no action is taken by default. It is the responsibility of the programmer utilizing this control to implement the necessary logic to determine how each request should be handled. [b]Performance:[/b] It is greatly advised to enable low-processor usage mode (see [member OS.low_processor_usage_mode]) when using GraphEdits. + [b]Note:[/b] Keep in mind that [method Node.get_children] will also return the connection layer node named [code]_connection_layer[/code] due to technical limitations. This behavior may change in future releases. </description> <tutorials> </tutorials> diff --git a/doc/classes/HashingContext.xml b/doc/classes/HashingContext.xml index f2681ae7b3..b42acb2b99 100644 --- a/doc/classes/HashingContext.xml +++ b/doc/classes/HashingContext.xml @@ -20,8 +20,9 @@ # Open the file to hash. var file = FileAccess.open(path, FileAccess.READ) # Update the context after reading each chunk. - while not file.eof_reached(): - ctx.update(file.get_buffer(CHUNK_SIZE)) + while file.get_position() < file.get_length(): + var remaining = file.get_length() - file.get_position() + ctx.update(file.get_buffer(min(remaining, CHUNK_SIZE))) # Get the computed hash. var res = ctx.finish() # Print the result as hex string and array. @@ -43,9 +44,10 @@ // Open the file to hash. using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); // Update the context after reading each chunk. - while (!file.EofReached()) + while (file.GetPosition() < file.GetLength()) { - ctx.Update(file.GetBuffer(ChunkSize)); + int remaining = (int)(file.GetLength() - file.GetPosition()); + ctx.Update(file.GetBuffer(Mathf.Min(remaining, ChunkSize))); } // Get the computed hash. byte[] res = ctx.Finish(); diff --git a/doc/classes/Mesh.xml b/doc/classes/Mesh.xml index 6b5a50d97b..7f4dd5af37 100644 --- a/doc/classes/Mesh.xml +++ b/doc/classes/Mesh.xml @@ -223,6 +223,7 @@ </constant> <constant name="ARRAY_NORMAL" value="1" enum="ArrayType"> [PackedVector3Array] of vertex normals. + [b]Note:[/b] The array has to consist of normal vectors, otherwise they will be normalized by the engine, potentially causing visual discrepancies. </constant> <constant name="ARRAY_TANGENT" value="2" enum="ArrayType"> [PackedFloat32Array] of vertex tangents. Each element in groups of 4 floats, first 3 floats determine the tangent, and the last the binormal direction as -1 or 1. diff --git a/doc/classes/NodePath.xml b/doc/classes/NodePath.xml index b4969b8906..d0ec81ab45 100644 --- a/doc/classes/NodePath.xml +++ b/doc/classes/NodePath.xml @@ -23,11 +23,12 @@ [/codeblock] Despite their name, node paths may also point to a property: [codeblock] - ^"position" # Points to this object's position. - ^"position:x" # Points to this object's position in the x axis. + ^":position" # Points to this object's position. + ^":position:x" # Points to this object's position in the x axis. ^"Camera3D:rotation:y" # Points to the child Camera3D and its y rotation. ^"/root:size:x" # Points to the root Window and its width. [/codeblock] + In some situations, it's possible to omit the leading [code]:[/code] when pointing to an object's property. As an example, this is the case with [method Object.set_indexed] and [method Tween.tween_property], as those methods call [method NodePath.get_as_property_path] under the hood. However, it's generally recommended to keep the [code]:[/code] prefix. Node paths cannot check whether they are valid and may point to nodes or properties that do not exist. Their meaning depends entirely on the context in which they're used. You usually do not have to worry about the [NodePath] type, as strings are automatically converted to the type when necessary. There are still times when defining node paths is useful. For example, exported [NodePath] properties allow you to easily select any node within the currently edited scene. They are also automatically updated when moving, renaming or deleting nodes in the scene tree editor. See also [annotation @GDScript.@export_node_path]. See also [StringName], which is a similar type designed for optimized strings. diff --git a/doc/classes/OS.xml b/doc/classes/OS.xml index 3d048e2f63..77caea9745 100644 --- a/doc/classes/OS.xml +++ b/doc/classes/OS.xml @@ -283,7 +283,7 @@ <return type="String" /> <description> Returns the file path to the current engine executable. - [b]Note:[/b] On macOS, always use [method create_instance] instead of relying on executable path. + [b]Note:[/b] On macOS, if you want to launch another instance of Godot, always use [method create_instance] instead of relying on the executable path. </description> </method> <method name="get_granted_permissions" qualifiers="const"> diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml index 0f5687f091..004bbe2286 100644 --- a/doc/classes/PopupMenu.xml +++ b/doc/classes/PopupMenu.xml @@ -395,6 +395,12 @@ Returns [code]true[/code] if the specified item's shortcut is disabled. </description> </method> + <method name="is_native_menu" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if the system native menu is supported and currently used by this [PopupMenu]. + </description> + </method> <method name="is_system_menu" qualifiers="const"> <return type="bool" /> <description> @@ -636,6 +642,7 @@ </member> <member name="prefer_native_menu" type="bool" setter="set_prefer_native_menu" getter="is_prefer_native_menu" default="false"> If [code]true[/code], [MenuBar] will use native menu when supported. + [b]Note:[/b] If [PopupMenu] is linked to [StatusIndicator], [MenuBar], or another [PopupMenu] item it can use native menu regardless of this property, use [method is_native_menu] to check it. </member> <member name="submenu_popup_delay" type="float" setter="set_submenu_popup_delay" getter="get_submenu_popup_delay" default="0.3"> Sets the delay time in seconds for the submenu item to popup on mouse hovering. If the popup menu is added as a child of another (acting as a submenu), it will inherit the delay time of the parent menu item. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 405e8340f3..b0f421e932 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -271,6 +271,7 @@ <member name="application/config/custom_user_dir_name" type="String" setter="" getter="" default=""""> This user directory is used for storing persistent data ([code]user://[/code] filesystem). If a custom directory name is defined, this name will be appended to the system-specific user data directory (same parent folder as the Godot configuration folder documented in [method OS.get_user_data_dir]). The [member application/config/use_custom_user_dir] setting must be enabled for this to take effect. + [b]Note:[/b] If [member application/config/custom_user_dir_name] contains trailing periods, they will be stripped as folder names ending with a period are not allowed on Windows. </member> <member name="application/config/description" type="String" setter="" getter="" default=""""> The project's description, displayed as a tooltip in the Project Manager when hovering the project. @@ -476,6 +477,9 @@ <member name="debug/gdscript/warnings/assert_always_true" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an [code]assert[/code] call always evaluates to true. </member> + <member name="debug/gdscript/warnings/confusable_capture_reassignment" type="int" setter="" getter="" default="1"> + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local variable captured by a lambda is reassigned, since this does not modify the outer local variable. + </member> <member name="debug/gdscript/warnings/confusable_identifier" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an identifier contains characters that can be confused with something else, like when mixing different alphabets. </member> @@ -629,6 +633,9 @@ <member name="debug/settings/profiler/max_functions" type="int" setter="" getter="" default="16384"> Maximum number of functions per frame allowed when profiling. </member> + <member name="debug/settings/profiler/max_timestamp_query_elements" type="int" setter="" getter="" default="256"> + Maximum number of timestamp query elements allowed per frame for visual profiling. + </member> <member name="debug/settings/stdout/print_fps" type="bool" setter="" getter="" default="false"> Print frames per second to standard output every second. </member> @@ -893,8 +900,8 @@ <member name="display/window/stretch/mode" type="String" setter="" getter="" default=""disabled""> Defines how the base size is stretched to fit the resolution of the window or screen. [b]"disabled"[/b]: No stretching happens. One unit in the scene corresponds to one pixel on the screen. In this mode, [member display/window/stretch/aspect] has no effect. Recommended for non-game applications. - [b]"canvas_items"[/b]: The base size specified in width and height in the project settings is stretched to cover the whole screen (taking [member display/window/stretch/aspect] into account). This means that everything is rendered directly at the target resolution. 3D is unaffected, while in 2D, there is no longer a 1:1 correspondence between sprite pixels and screen pixels, which may result in scaling artifacts. Recommended for most games that don't use a pixel art esthetic, although it is possible to use this stretch mode for pixel art games too (especially in 3D). - [b]"viewport"[/b]: The size of the root [Viewport] is set precisely to the base size specified in the Project Settings' Display section. The scene is rendered to this viewport first. Finally, this viewport is scaled to fit the screen (taking [member display/window/stretch/aspect] into account). Recommended for games that use a pixel art esthetic. + [b]"canvas_items"[/b]: The base size specified in width and height in the project settings is stretched to cover the whole screen (taking [member display/window/stretch/aspect] into account). This means that everything is rendered directly at the target resolution. 3D is unaffected, while in 2D, there is no longer a 1:1 correspondence between sprite pixels and screen pixels, which may result in scaling artifacts. Recommended for most games that don't use a pixel art aesthetic, although it is possible to use this stretch mode for pixel art games too (especially in 3D). + [b]"viewport"[/b]: The size of the root [Viewport] is set precisely to the base size specified in the Project Settings' Display section. The scene is rendered to this viewport first. Finally, this viewport is scaled to fit the screen (taking [member display/window/stretch/aspect] into account). Recommended for games that use a pixel art aesthetic. </member> <member name="display/window/stretch/scale" type="float" setter="" getter="" default="1.0"> The scale factor multiplier to use for 2D elements. This multiplies the final scale factor determined by [member display/window/stretch/mode]. If using the [b]Disabled[/b] stretch mode, this scale factor is applied as-is. This can be adjusted to make the UI easier to read on certain displays. @@ -927,8 +934,9 @@ Changing this value allows setting up a multi-project scenario where there are multiple [code].csproj[/code]. Keep in mind that the Godot project is considered one of the C# projects in the workspace and it's root directory should contain the [code]project.godot[/code] and [code].csproj[/code] next to each other. </member> <member name="editor/export/convert_text_resources_to_binary" type="bool" setter="" getter="" default="true"> - If [code]true[/code], text resources are converted to a binary format on export. This decreases file sizes and speeds up loading slightly. - [b]Note:[/b] If [member editor/export/convert_text_resources_to_binary] is [code]true[/code], [method @GDScript.load] will not be able to return the converted files in an exported project. Some file paths within the exported PCK will also change, such as [code]project.godot[/code] becoming [code]project.binary[/code]. If you rely on run-time loading of files present within the PCK, set [member editor/export/convert_text_resources_to_binary] to [code]false[/code]. + If [code]true[/code], text resource ([code]tres[/code]) and text scene ([code]tscn[/code]) files are converted to their corresponding binary format on export. This decreases file sizes and speeds up loading slightly. + [b]Note:[/b] Because a resource's file extension may change in an exported project, it is heavily recommended to use [method @GDScript.load] or [ResourceLoader] instead of [FileAccess] to load resources dynamically. + [b]Note:[/b] The project settings file ([code]project.godot[/code]) will always be converted to binary on export, regardless of this setting. </member> <member name="editor/import/atlas_max_width" type="int" setter="" getter="" default="2048"> The maximum width to use when importing textures as an atlas. The value will be rounded to the nearest power of two when used. Use this to prevent imported textures from growing too large in the other direction. @@ -1387,6 +1395,12 @@ Enabling this can greatly improve the responsiveness to input, specially in devices that need to run multiple physics frames per visible (process) frame, because they can't run at the target frame rate. [b]Note:[/b] Currently implemented only on Android. </member> + <member name="input_devices/buffering/android/use_accumulated_input" type="bool" setter="" getter="" default="true"> + If [code]true[/code], multiple input events will be accumulated into a single input event when possible. + </member> + <member name="input_devices/buffering/android/use_input_buffering" type="bool" setter="" getter="" default="true"> + If [code]true[/code], input events will be buffered prior to being dispatched. + </member> <member name="input_devices/compatibility/legacy_just_pressed_behavior" type="bool" setter="" getter="" default="false"> If [code]true[/code], [method Input.is_action_just_pressed] and [method Input.is_action_just_released] will only return [code]true[/code] if the action is still in the respective state, i.e. an action that is pressed [i]and[/i] released on the same frame will be missed. If [code]false[/code], no input will be lost. @@ -2351,7 +2365,6 @@ </member> <member name="rendering/anti_aliasing/quality/msaa_3d" type="int" setter="" getter="" default="0"> Sets the number of MSAA samples to use for 3D rendering (as a power of two). MSAA is used to reduce aliasing around the edges of polygons. A higher MSAA value results in smoother edges but can be significantly slower on some hardware, especially integrated graphics due to their limited memory bandwidth. See also [member rendering/scaling_3d/mode] for supersampling, which provides higher quality but is much more expensive. This has no effect on shader-induced aliasing or texture aliasing. - [b]Note:[/b] MSAA is only supported in the Forward+ and Mobile rendering methods, not Compatibility. </member> <member name="rendering/anti_aliasing/quality/screen_space_aa" type="int" setter="" getter="" default="0"> Sets the screen-space antialiasing mode for the default screen [Viewport]. Screen-space antialiasing works by selectively blurring edges in a post-process shader. It differs from MSAA which takes multiple coverage samples while rendering objects. Screen-space AA methods are typically faster than MSAA and will smooth out specular aliasing, but tend to make scenes appear blurry. The blurriness is partially counteracted by automatically using a negative mipmap LOD bias (see [member rendering/textures/default_filters/texture_mipmap_bias]). @@ -2923,11 +2936,11 @@ </member> <member name="xr/openxr/foveation_dynamic" type="bool" setter="" getter="" default="false"> If true and foveation is supported, will automatically adjust foveation level based on framerate up to the level set on [member xr/openxr/foveation_level]. - [b]Note:[/b] Only works on compatibility renderer. + [b]Note:[/b] Only works on the Compatibility rendering method. </member> <member name="xr/openxr/foveation_level" type="int" setter="" getter="" default=""0""> Applied foveation level if supported: 0 = off, 1 = low, 2 = medium, 3 = high. - [b]Note:[/b] Only works on compatibility renderer. + [b]Note:[/b] Only works on the Compatibility rendering method. On platforms other than Android, if [member rendering/anti_aliasing/quality/msaa_3d] is enabled, this feature will be disabled. </member> <member name="xr/openxr/reference_space" type="int" setter="" getter="" default=""1""> Specify the default reference space. diff --git a/doc/classes/ResourceLoader.xml b/doc/classes/ResourceLoader.xml index 1961ca2b0e..cb0db46595 100644 --- a/doc/classes/ResourceLoader.xml +++ b/doc/classes/ResourceLoader.xml @@ -87,7 +87,7 @@ <param index="0" name="path" type="String" /> <description> Returns the resource loaded by [method load_threaded_request]. - If this is called before the loading thread is done (i.e. [method load_threaded_get_status] is not [constant THREAD_LOAD_LOADED]), the calling thread will be blocked until the resource has finished loading. + If this is called before the loading thread is done (i.e. [method load_threaded_get_status] is not [constant THREAD_LOAD_LOADED]), the calling thread will be blocked until the resource has finished loading. However, it's recommended to use [method load_threaded_get_status] to known when the load has actually completed. </description> </method> <method name="load_threaded_get_status"> @@ -97,6 +97,7 @@ <description> Returns the status of a threaded loading operation started with [method load_threaded_request] for the resource at [param path]. See [enum ThreadLoadStatus] for possible return values. An array variable can optionally be passed via [param progress], and will return a one-element array containing the percentage of completion of the threaded loading. + [b]Note:[/b] The recommended way of using this method is to call it during different frames (e.g., in [method Node._process], instead of a loop). </description> </method> <method name="load_threaded_request"> diff --git a/doc/classes/ScriptEditorBase.xml b/doc/classes/ScriptEditorBase.xml index 403608355a..638bc921d6 100644 --- a/doc/classes/ScriptEditorBase.xml +++ b/doc/classes/ScriptEditorBase.xml @@ -72,7 +72,7 @@ </description> </signal> <signal name="request_save_previous_state"> - <param index="0" name="line" type="int" /> + <param index="0" name="state" type="Dictionary" /> <description> Emitted when the user changes current script or moves caret by 10 or more columns within the same script. </description> diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index f82671b533..f71601f9ae 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -1370,7 +1370,7 @@ If [code]false[/code], using [kbd]Ctrl + Left[/kbd] or [kbd]Ctrl + Right[/kbd] ([kbd]Cmd + Left[/kbd] or [kbd]Cmd + Right[/kbd] on macOS) bindings will use the behavior of [member use_default_word_separators]. If [code]true[/code], it will also stop the caret if a character within [member custom_word_separators] is detected. Useful for subword moving. This behavior also will be applied to the behavior of text selection. </member> <member name="use_default_word_separators" type="bool" setter="set_use_default_word_separators" getter="is_default_word_separators_enabled" default="true"> - If [code]false[/code], using [kbd]Ctrl + Left[/kbd] or [kbd]Ctrl + Right[/kbd] ([kbd]Cmd + Left[/kbd] or [kbd]Cmd + Right[/kbd] on macOS) bindings will stop moving caret only if a space or punctuation is detected. If [code]true[/code], it will also stop the caret if a character is [code]´`~$^=+|<>[/code], a General Punctuation, or CJK Punctuation. Useful for subword moving. This behavior also will be applied to the behavior of text selection. + If [code]false[/code], using [kbd]Ctrl + Left[/kbd] or [kbd]Ctrl + Right[/kbd] ([kbd]Cmd + Left[/kbd] or [kbd]Cmd + Right[/kbd] on macOS) bindings will stop moving caret only if a space or punctuation is detected. If [code]true[/code], it will also stop the caret if a character is part of [code]!"#$%&'()*+,-./:;<=>?@[\]^`{|}~[/code], the Unicode General Punctuation table, or the Unicode CJK Punctuation table. Useful for subword moving. This behavior also will be applied to the behavior of text selection. </member> <member name="virtual_keyboard_enabled" type="bool" setter="set_virtual_keyboard_enabled" getter="is_virtual_keyboard_enabled" default="true"> If [code]true[/code], the native virtual keyboard is shown when focused on platforms that support it. diff --git a/doc/classes/Transform3D.xml b/doc/classes/Transform3D.xml index 2a0bbc46af..30c141659a 100644 --- a/doc/classes/Transform3D.xml +++ b/doc/classes/Transform3D.xml @@ -6,7 +6,7 @@ <description> The [Transform3D] built-in [Variant] type is a 3×4 matrix representing a transformation in 3D space. It contains a [Basis], which on its own can represent rotation, scale, and shear. Additionally, combined with its own [member origin], the transform can also represent a translation. For a general introduction, see the [url=$DOCS_URL/tutorials/math/matrices_and_transforms.html]Matrices and transforms[/url] tutorial. - [b]Note:[/b] Godot uses a [url=https://en.wikipedia.org/wiki/Right-hand_rule]right-handed coordinate system[/url], which is a common standard. For directions, the convention for built-in types like [Camera3D] is for -Z to point forward (+X is right, +Y is up, and +Z is back). Other objects may use different direction conventions. For more information, see the [url=$DOCS_URL/tutorials/assets_pipeline/importing_scenes.html#d-asset-direction-conventions]Importing 3D Scenes[/url] tutorial. + [b]Note:[/b] Godot uses a [url=https://en.wikipedia.org/wiki/Right-hand_rule]right-handed coordinate system[/url], which is a common standard. For directions, the convention for built-in types like [Camera3D] is for -Z to point forward (+X is right, +Y is up, and +Z is back). Other objects may use different direction conventions. For more information, see the [url=$DOCS_URL/tutorials/assets_pipeline/importing_3d_scenes/model_export_considerations.html#d-asset-direction-conventions]3D asset direction conventions[/url] tutorial. </description> <tutorials> <link title="Math documentation index">$DOCS_URL/tutorials/math/index.html</link> diff --git a/drivers/d3d12/rendering_device_driver_d3d12.cpp b/drivers/d3d12/rendering_device_driver_d3d12.cpp index ca2b392e66..a6bce1d79a 100644 --- a/drivers/d3d12/rendering_device_driver_d3d12.cpp +++ b/drivers/d3d12/rendering_device_driver_d3d12.cpp @@ -872,6 +872,7 @@ RDD::BufferID RenderingDeviceDriverD3D12::buffer_create(uint64_t p_size, BitFiel D3D12MA::ALLOCATION_DESC allocation_desc = {}; allocation_desc.HeapType = D3D12_HEAP_TYPE_DEFAULT; + D3D12_RESOURCE_STATES initial_state = D3D12_RESOURCE_STATE_COMMON; switch (p_allocation_type) { case MEMORY_ALLOCATION_TYPE_CPU: { bool is_src = p_usage.has_flag(BUFFER_USAGE_TRANSFER_FROM_BIT); @@ -879,10 +880,12 @@ RDD::BufferID RenderingDeviceDriverD3D12::buffer_create(uint64_t p_size, BitFiel if (is_src && !is_dst) { // Looks like a staging buffer: CPU maps, writes sequentially, then GPU copies to VRAM. allocation_desc.HeapType = D3D12_HEAP_TYPE_UPLOAD; + initial_state = D3D12_RESOURCE_STATE_GENERIC_READ; } if (is_dst && !is_src) { // Looks like a readback buffer: GPU copies from VRAM, then CPU maps and reads. allocation_desc.HeapType = D3D12_HEAP_TYPE_READBACK; + initial_state = D3D12_RESOURCE_STATE_COPY_DEST; } } break; case MEMORY_ALLOCATION_TYPE_GPU: { @@ -911,7 +914,7 @@ RDD::BufferID RenderingDeviceDriverD3D12::buffer_create(uint64_t p_size, BitFiel res = allocator->CreateResource( &allocation_desc, reinterpret_cast<const D3D12_RESOURCE_DESC *>(&resource_desc), - D3D12_RESOURCE_STATE_COMMON, + initial_state, nullptr, allocation.GetAddressOf(), IID_PPV_ARGS(buffer.GetAddressOf())); @@ -925,7 +928,7 @@ RDD::BufferID RenderingDeviceDriverD3D12::buffer_create(uint64_t p_size, BitFiel buf_info->resource = buffer.Get(); buf_info->owner_info.resource = buffer; buf_info->owner_info.allocation = allocation; - buf_info->owner_info.states.subresource_states.push_back(D3D12_RESOURCE_STATE_COMMON); + buf_info->owner_info.states.subresource_states.push_back(initial_state); buf_info->states_ptr = &buf_info->owner_info.states; buf_info->size = p_size; buf_info->flags.usable_as_uav = (resource_desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp index bacf607c66..21790d29b5 100644 --- a/drivers/gles3/storage/material_storage.cpp +++ b/drivers/gles3/storage/material_storage.cpp @@ -1111,15 +1111,15 @@ MaterialStorage::MaterialStorage() { global_shader_uniforms.buffer_size = MAX(16, (int)GLOBAL_GET("rendering/limits/global_shader_variables/buffer_size")); if (global_shader_uniforms.buffer_size * sizeof(GlobalShaderUniforms::Value) > uint32_t(Config::get_singleton()->max_uniform_buffer_size)) { + // Limit to maximum support UBO size. global_shader_uniforms.buffer_size = uint32_t(Config::get_singleton()->max_uniform_buffer_size) / sizeof(GlobalShaderUniforms::Value); - WARN_PRINT("Project setting \"rendering/limits/global_shader_variables/buffer_size\" exceeds maximum uniform buffer size of: " + itos(Config::get_singleton()->max_uniform_buffer_size / sizeof(GlobalShaderUniforms::Value)) + ". Falling back on maximum buffer size."); } global_shader_uniforms.buffer_values = memnew_arr(GlobalShaderUniforms::Value, global_shader_uniforms.buffer_size); memset(global_shader_uniforms.buffer_values, 0, sizeof(GlobalShaderUniforms::Value) * global_shader_uniforms.buffer_size); global_shader_uniforms.buffer_usage = memnew_arr(GlobalShaderUniforms::ValueUsage, global_shader_uniforms.buffer_size); - global_shader_uniforms.buffer_dirty_regions = memnew_arr(bool, global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE); - memset(global_shader_uniforms.buffer_dirty_regions, 0, sizeof(bool) * global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE); + global_shader_uniforms.buffer_dirty_regions = memnew_arr(bool, 1 + (global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE)); + memset(global_shader_uniforms.buffer_dirty_regions, 0, sizeof(bool) * (1 + (global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE))); glGenBuffers(1, &global_shader_uniforms.buffer); glBindBuffer(GL_UNIFORM_BUFFER, global_shader_uniforms.buffer); glBufferData(GL_UNIFORM_BUFFER, sizeof(GlobalShaderUniforms::Value) * global_shader_uniforms.buffer_size, nullptr, GL_DYNAMIC_DRAW); @@ -1788,7 +1788,7 @@ void MaterialStorage::global_shader_parameter_add(const StringName &p_name, RS:: //is vector, allocate in buffer and update index gv.buffer_index = _global_shader_uniform_allocate(gv.buffer_elements); - ERR_FAIL_COND_MSG(gv.buffer_index < 0, vformat("Failed allocating global variable '%s' out of buffer memory. Consider increasing it in the Project Settings.", String(p_name))); + ERR_FAIL_COND_MSG(gv.buffer_index < 0, vformat("Failed allocating global variable '%s' out of buffer memory. Consider increasing rendering/limits/global_shader_variables/buffer_size in the Project Settings. Maximum items supported by this hardware is: %d.", String(p_name), Config::get_singleton()->max_uniform_buffer_size / sizeof(GlobalShaderUniforms::Value))); global_shader_uniforms.buffer_usage[gv.buffer_index].elements = gv.buffer_elements; _global_shader_uniform_store_in_buffer(gv.buffer_index, gv.type, gv.value); _global_shader_uniform_mark_buffer_dirty(gv.buffer_index, gv.buffer_elements); @@ -1999,7 +1999,7 @@ int32_t MaterialStorage::global_shader_parameters_instance_allocate(RID p_instan ERR_FAIL_COND_V(global_shader_uniforms.instance_buffer_pos.has(p_instance), -1); int32_t pos = _global_shader_uniform_allocate(ShaderLanguage::MAX_INSTANCE_UNIFORM_INDICES); global_shader_uniforms.instance_buffer_pos[p_instance] = pos; //save anyway - ERR_FAIL_COND_V_MSG(pos < 0, -1, "Too many instances using shader instance variables. Increase buffer size in Project Settings."); + ERR_FAIL_COND_V_MSG(pos < 0, -1, vformat("Too many instances using shader instance variables. Consider increasing rendering/limits/global_shader_variables/buffer_size in the Project Settings. Maximum items supported by this hardware is: %d.", Config::get_singleton()->max_uniform_buffer_size / sizeof(GlobalShaderUniforms::Value))); global_shader_uniforms.buffer_usage[pos].elements = ShaderLanguage::MAX_INSTANCE_UNIFORM_INDICES; return pos; } @@ -2079,7 +2079,7 @@ void MaterialStorage::global_shader_parameters_instance_update(RID p_instance, i void MaterialStorage::_update_global_shader_uniforms() { MaterialStorage *material_storage = MaterialStorage::get_singleton(); if (global_shader_uniforms.buffer_dirty_region_count > 0) { - uint32_t total_regions = global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE; + uint32_t total_regions = 1 + (global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE); if (total_regions / global_shader_uniforms.buffer_dirty_region_count <= 4) { // 25% of regions dirty, just update all buffer glBindBuffer(GL_UNIFORM_BUFFER, global_shader_uniforms.buffer); diff --git a/drivers/vulkan/SCsub b/drivers/vulkan/SCsub index 80d5f35305..1efef5ad77 100644 --- a/drivers/vulkan/SCsub +++ b/drivers/vulkan/SCsub @@ -16,14 +16,14 @@ if env["use_volk"]: if env["platform"] == "android": env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_ANDROID_KHR"]) elif env["platform"] == "ios": - env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_IOS_MVK"]) + env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_IOS_MVK", "VK_USE_PLATFORM_METAL_EXT"]) elif env["platform"] == "linuxbsd": if env["x11"]: env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_XLIB_KHR"]) if env["wayland"]: env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_WAYLAND_KHR"]) elif env["platform"] == "macos": - env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_MACOS_MVK"]) + env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_MACOS_MVK", "VK_USE_PLATFORM_METAL_EXT"]) elif env["platform"] == "windows": env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_WIN32_KHR"]) diff --git a/drivers/vulkan/rendering_context_driver_vulkan.cpp b/drivers/vulkan/rendering_context_driver_vulkan.cpp index 7cba820978..fe2ff5e0da 100644 --- a/drivers/vulkan/rendering_context_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_context_driver_vulkan.cpp @@ -102,6 +102,10 @@ Error RenderingContextDriverVulkan::_initialize_instance_extensions() { // This extension allows us to use the properties2 features to query additional device capabilities. _register_requested_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, false); +#if defined(USE_VOLK) && (defined(MACOS_ENABLED) || defined(IOS_ENABLED)) + _register_requested_instance_extension(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, true); +#endif + // Only enable debug utils in verbose mode or DEV_ENABLED. // End users would get spammed with messages of varying verbosity due to the // mess that thirdparty layers/extensions and drivers seem to leave in their @@ -360,6 +364,11 @@ Error RenderingContextDriverVulkan::_initialize_instance() { VkInstanceCreateInfo instance_info = {}; instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + +#if defined(USE_VOLK) && (defined(MACOS_ENABLED) || defined(IOS_ENABLED)) + instance_info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +#endif + instance_info.pApplicationInfo = &app_info; instance_info.enabledExtensionCount = enabled_extension_names.size(); instance_info.ppEnabledExtensionNames = enabled_extension_names.ptr(); diff --git a/editor/animation_bezier_editor.cpp b/editor/animation_bezier_editor.cpp index ffd609d925..48b9e01fd8 100644 --- a/editor/animation_bezier_editor.cpp +++ b/editor/animation_bezier_editor.cpp @@ -682,6 +682,7 @@ void AnimationBezierTrackEdit::set_editor(AnimationTrackEditor *p_editor) { editor = p_editor; connect("clear_selection", callable_mp(editor, &AnimationTrackEditor::_clear_selection).bind(false)); connect("select_key", callable_mp(editor, &AnimationTrackEditor::_key_selected), CONNECT_DEFERRED); + connect("deselect_key", callable_mp(editor, &AnimationTrackEditor::_key_deselected), CONNECT_DEFERRED); } void AnimationBezierTrackEdit::_play_position_draw() { @@ -871,14 +872,14 @@ void AnimationBezierTrackEdit::_change_selected_keys_handle_mode(Animation::Hand } void AnimationBezierTrackEdit::_clear_selection_for_anim(const Ref<Animation> &p_anim) { - if (!(animation == p_anim)) { + if (!(animation == p_anim) || !is_visible()) { return; } _clear_selection(); } -void AnimationBezierTrackEdit::_select_at_anim(const Ref<Animation> &p_anim, int p_track, real_t p_pos) { - if (!(animation == p_anim)) { +void AnimationBezierTrackEdit::_select_at_anim(const Ref<Animation> &p_anim, int p_track, real_t p_pos, bool p_single) { + if (!(animation == p_anim) || !is_visible()) { return; } @@ -886,7 +887,7 @@ void AnimationBezierTrackEdit::_select_at_anim(const Ref<Animation> &p_anim, int ERR_FAIL_COND(idx < 0); selection.insert(IntPair(p_track, idx)); - emit_signal(SNAME("select_key"), idx, true, p_track); + emit_signal(SNAME("select_key"), idx, p_single, p_track); queue_redraw(); } @@ -964,7 +965,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { real_t minimum_value = INFINITY; real_t maximum_value = -INFINITY; - for (const IntPair &E : selection) { + for (const IntPair &E : focused_keys) { IntPair key_pair = E; real_t time = animation->track_get_key_time(key_pair.first, key_pair.second); @@ -1002,7 +1003,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { return; } else if (ED_IS_SHORTCUT("animation_bezier_editor/select_all_keys", p_event)) { for (int i = 0; i < edit_points.size(); ++i) { - selection.insert(IntPair(edit_points[i].track, edit_points[i].key)); + _select_at_anim(animation, edit_points[i].track, animation->track_get_key_time(edit_points[i].track, edit_points[i].key), i == 0); } queue_redraw(); @@ -1010,6 +1011,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { return; } else if (ED_IS_SHORTCUT("animation_bezier_editor/deselect_all_keys", p_event)) { selection.clear(); + emit_signal(SNAME("clear_selection")); queue_redraw(); accept_event(); @@ -1094,7 +1096,8 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { for (int i = 0; i < animation->track_get_key_count(track); ++i) { undo_redo->add_undo_method( this, - "_bezier_track_insert_key", + "_bezier_track_insert_key_at_anim", + animation, track, animation->track_get_key_time(track, i), animation->bezier_track_get_key_value(track, i), @@ -1218,7 +1221,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { //insert new point if (mb->get_position().x >= limit && mb->get_position().x < get_size().width && mb->is_command_or_control_pressed()) { float h = (get_size().height / 2.0 - mb->get_position().y) * timeline_v_zoom + timeline_v_scroll; - Array new_point = make_default_bezier_key(h); + Array new_point = animation->make_default_bezier_key(h); real_t time = ((mb->get_position().x - limit) / timeline->get_zoom_scale()) + timeline->get_value(); while (animation->track_find_key(selected_track, time, Animation::FIND_MODE_APPROX) != -1) { @@ -1235,7 +1238,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { int index = animation->track_find_key(selected_track, time, Animation::FIND_MODE_APPROX); ERR_FAIL_COND(index == -1); _clear_selection(); - selection.insert(IntPair(selected_track, index)); + _select_at_anim(animation, selected_track, animation->track_get_key_time(selected_track, index), true); moving_selection_attempt = true; moving_selection = false; @@ -1277,13 +1280,15 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { Rect2 selection_rect(bs_from, bs_to - bs_from); bool track_set = false; + int j = 0; for (int i = 0; i < edit_points.size(); i++) { if (edit_points[i].point_rect.intersects(selection_rect)) { - selection.insert(IntPair(edit_points[i].track, edit_points[i].key)); + _select_at_anim(animation, edit_points[i].track, animation->track_get_key_time(edit_points[i].track, edit_points[i].key), j == 0 && !box_selecting_add); if (!track_set) { track_set = true; set_animation_and_track(animation, edit_points[i].track, read_only); } + j++; } } } else { @@ -1366,7 +1371,8 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { key[0] = h; undo_redo->add_do_method( this, - "_bezier_track_insert_key", + "_bezier_track_insert_key_at_anim", + animation, E->get().first, newpos, key[0], @@ -1387,7 +1393,8 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { Array key = animation->track_get_key_value(E->get().first, E->get().second); undo_redo->add_undo_method( this, - "_bezier_track_insert_key", + "_bezier_track_insert_key_at_anim", + animation, E->get().first, oldpos, key[0], @@ -1405,7 +1412,8 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, 1); undo_redo->add_undo_method( this, - "_bezier_track_insert_key", + "_bezier_track_insert_key_at_anim", + animation, amr.track, amr.time, key[0], @@ -1418,21 +1426,22 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); // 7-reselect - + int i = 0; for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { real_t oldpos = animation->track_get_key_time(E->get().first, E->get().second); real_t newpos = oldpos + moving_selection_offset.x; - undo_redo->add_do_method(this, "_select_at_anim", animation, E->get().first, newpos); - undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, oldpos); + undo_redo->add_do_method(this, "_select_at_anim", animation, E->get().first, newpos, i == 0); + undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, oldpos, i == 0); + i++; } undo_redo->commit_action(); } else if (select_single_attempt != IntPair(-1, -1)) { selection.clear(); - selection.insert(select_single_attempt); set_animation_and_track(animation, select_single_attempt.first, read_only); + _select_at_anim(animation, select_single_attempt.first, animation->track_get_key_time(select_single_attempt.first, select_single_attempt.second), true); } moving_selection = false; @@ -1567,9 +1576,10 @@ bool AnimationBezierTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p if (selection.has(pair)) { if (p_deselectable) { selection.erase(pair); + emit_signal(SNAME("deselect_key"), edit_points[i].key, edit_points[i].track); } } else { - selection.insert(pair); + _select_at_anim(animation, edit_points[i].track, animation->track_get_key_time(edit_points[i].track, edit_points[i].key), false); } queue_redraw(); select_single_attempt = IntPair(-1, -1); @@ -1595,7 +1605,7 @@ bool AnimationBezierTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p set_animation_and_track(animation, pair.first, read_only); if (!selection.has(pair)) { selection.clear(); - selection.insert(pair); + _select_at_anim(animation, edit_points[i].track, animation->track_get_key_time(edit_points[i].track, edit_points[i].key), true); } } return true; @@ -1637,19 +1647,6 @@ void AnimationBezierTrackEdit::_zoom_callback(float p_zoom_factor, Vector2 p_ori queue_redraw(); } -Array AnimationBezierTrackEdit::make_default_bezier_key(float p_value) { - Array new_point; - new_point.resize(5); - - new_point[0] = p_value; - new_point[1] = -0.25; - new_point[2] = 0; - new_point[3] = 0.25; - new_point[4] = 0; - - return new_point; -} - float AnimationBezierTrackEdit::get_bezier_key_value(Array p_bezier_key_array) { return p_bezier_key_array[0]; } @@ -1669,7 +1666,7 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) { time += 0.001; } float h = (get_size().height / 2.0 - menu_insert_key.y) * timeline_v_zoom + timeline_v_scroll; - Array new_point = make_default_bezier_key(h); + Array new_point = animation->make_default_bezier_key(h); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Add Bezier Point")); undo_redo->add_do_method(animation.ptr(), "track_insert_key", selected_track, time, new_point); @@ -1763,12 +1760,16 @@ void AnimationBezierTrackEdit::duplicate_selected_keys(real_t p_ofs, bool p_ofs_ undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); // Reselect duplicated. + int i = 0; for (const Pair<int, real_t> &E : new_selection_values) { - undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second); + undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second, i == 0); + i++; } + i = 0; for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { real_t time = animation->track_get_key_time(E->get().first, E->get().second); - undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, time); + undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, time, i == 0); + i++; } undo_redo->add_do_method(this, "queue_redraw"); @@ -1805,16 +1806,20 @@ void AnimationBezierTrackEdit::copy_selected_keys(bool p_cut) { undo_redo->create_action(TTR("Animation Cut Keys"), UndoRedo::MERGE_DISABLE, animation.ptr()); undo_redo->add_do_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); + int i = 0; for (RBMap<AnimationTrackEditor::SelectedKey, AnimationTrackEditor::KeyInfo>::Element *E = keys.back(); E; E = E->prev()) { int track_idx = E->key().track; int key_idx = E->key().key; float time = E->value().pos; undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", track_idx, time); undo_redo->add_undo_method(animation.ptr(), "track_insert_key", track_idx, time, animation->track_get_key_value(track_idx, key_idx), animation->track_get_key_transition(track_idx, key_idx)); - undo_redo->add_undo_method(this, "_select_at_anim", animation, track_idx, time); + undo_redo->add_undo_method(this, "_select_at_anim", animation, track_idx, time, i == 0); + i++; } + i = 0; for (RBMap<AnimationTrackEditor::SelectedKey, AnimationTrackEditor::KeyInfo>::Element *E = keys.back(); E; E = E->prev()) { - undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->value().pos); + undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->value().pos, i == 0); + i++; } undo_redo->commit_action(); } @@ -1863,7 +1868,7 @@ void AnimationBezierTrackEdit::paste_keys(real_t p_ofs, bool p_ofs_valid) { Variant value = key.value; if (key.track_type != Animation::TYPE_BEZIER) { - value = make_default_bezier_key(key.value); + value = animation->make_default_bezier_key(key.value); } undo_redo->add_do_method(animation.ptr(), "track_insert_key", selected_track, dst_time, value, key.transition); @@ -1883,11 +1888,15 @@ void AnimationBezierTrackEdit::paste_keys(real_t p_ofs, bool p_ofs_valid) { undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); // Reselect pasted. + int i = 0; for (const Pair<int, float> &E : new_selection_values) { - undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second); + undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second, i == 0); + i++; } + i = 0; for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { - undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, animation->track_get_key_time(E->get().first, E->get().second)); + undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, animation->track_get_key_time(E->get().first, E->get().second), i == 0); + i++; } undo_redo->commit_action(); @@ -1913,10 +1922,9 @@ void AnimationBezierTrackEdit::delete_selection() { } } -void AnimationBezierTrackEdit::_bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode) { - ERR_FAIL_COND(animation.is_null()); - int idx = animation->bezier_track_insert_key(p_track, p_time, p_value, p_in_handle, p_out_handle); - animation->bezier_track_set_key_handle_mode(p_track, idx, p_handle_mode); +void AnimationBezierTrackEdit::_bezier_track_insert_key_at_anim(const Ref<Animation> &p_anim, int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode) { + int idx = p_anim->bezier_track_insert_key(p_track, p_time, p_value, p_in_handle, p_out_handle); + p_anim->bezier_track_set_key_handle_mode(p_track, idx, p_handle_mode); } void AnimationBezierTrackEdit::_bind_methods() { @@ -1925,9 +1933,10 @@ void AnimationBezierTrackEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("_select_at_anim"), &AnimationBezierTrackEdit::_select_at_anim); ClassDB::bind_method(D_METHOD("_update_hidden_tracks_after"), &AnimationBezierTrackEdit::_update_hidden_tracks_after); ClassDB::bind_method(D_METHOD("_update_locked_tracks_after"), &AnimationBezierTrackEdit::_update_locked_tracks_after); - ClassDB::bind_method(D_METHOD("_bezier_track_insert_key"), &AnimationBezierTrackEdit::_bezier_track_insert_key); + ClassDB::bind_method(D_METHOD("_bezier_track_insert_key_at_anim"), &AnimationBezierTrackEdit::_bezier_track_insert_key_at_anim); ADD_SIGNAL(MethodInfo("select_key", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::BOOL, "single"), PropertyInfo(Variant::INT, "track"))); + ADD_SIGNAL(MethodInfo("deselect_key", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::INT, "track"))); ADD_SIGNAL(MethodInfo("clear_selection")); } diff --git a/editor/animation_bezier_editor.h b/editor/animation_bezier_editor.h index ab667421f3..dd7e8758f3 100644 --- a/editor/animation_bezier_editor.h +++ b/editor/animation_bezier_editor.h @@ -144,7 +144,7 @@ class AnimationBezierTrackEdit : public Control { void _clear_selection(); void _clear_selection_for_anim(const Ref<Animation> &p_anim); - void _select_at_anim(const Ref<Animation> &p_anim, int p_track, real_t p_pos); + void _select_at_anim(const Ref<Animation> &p_anim, int p_track, real_t p_pos, bool p_single); bool _try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable); void _change_selected_keys_handle_mode(Animation::HandleMode p_mode, bool p_auto = false); @@ -198,7 +198,6 @@ protected: void _notification(int p_what); public: - static Array make_default_bezier_key(float p_value); static float get_bezier_key_value(Array p_bezier_key_array); virtual String get_tooltip(const Point2 &p_pos) const override; @@ -222,7 +221,7 @@ public: void paste_keys(real_t p_ofs, bool p_ofs_valid); void delete_selection(); - void _bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode); + void _bezier_track_insert_key_at_anim(const Ref<Animation> &p_anim, int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode); AnimationBezierTrackEdit(); }; diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index b80a8d77f4..b6636ca576 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -874,15 +874,15 @@ bool AnimationMultiTrackKeyEdit::_set(const StringName &p_name, const Variant &p undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); } Vector2 prev = animation->bezier_track_get_key_out_handle(track, key); - undo_redo->add_do_method(this, "_bezier_track_set_key_out_handle", track, key, value); - undo_redo->add_undo_method(this, "_bezier_track_set_key_out_handle", track, key, prev); + undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, value); + undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev); update_obj = true; } else if (name == "handle_mode") { const Variant &value = p_value; if (!setting) { setting = true; - undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS); + undo_redo->create_action(TTR("Animation Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS, animation.ptr()); } int prev_mode = animation->bezier_track_get_key_handle_mode(track, key); Vector2 prev_in_handle = animation->bezier_track_get_key_in_handle(track, key); @@ -3690,7 +3690,7 @@ void AnimationTrackEditor::_name_limit_changed() { } void AnimationTrackEditor::_timeline_changed(float p_new_pos, bool p_timeline_only) { - emit_signal(SNAME("timeline_changed"), p_new_pos, p_timeline_only); + emit_signal(SNAME("timeline_changed"), p_new_pos, p_timeline_only, false); } void AnimationTrackEditor::_track_remove_request(int p_track) { @@ -3787,6 +3787,7 @@ void AnimationTrackEditor::set_anim_pos(float p_pos) { } _redraw_groups(); bezier_edit->set_play_position(p_pos); + emit_signal(SNAME("timeline_changed"), p_pos, true, true); } static bool track_type_is_resettable(Animation::TrackType p_type) { @@ -3867,15 +3868,23 @@ void AnimationTrackEditor::commit_insert_queue() { } // Skip the confirmation dialog if the user holds Shift while clicking the key icon. - if (!Input::get_singleton()->is_key_pressed(Key::SHIFT) && num_tracks > 0) { - String shortcut_hint = TTR("Hold Shift when clicking the key icon to skip this dialog."); + // If `confirm_insert_track` editor setting is disabled, the behavior is reversed. + bool confirm_insert = EDITOR_GET("editors/animation/confirm_insert_track"); + if ((Input::get_singleton()->is_key_pressed(Key::SHIFT) != confirm_insert) && num_tracks > 0) { + String dialog_text; + // Potentially a new key, does not exist. if (num_tracks == 1) { // TRANSLATORS: %s will be replaced by a phrase describing the target of track. - insert_confirm_text->set_text(vformat(TTR("Create new track for %s and insert key?") + "\n\n" + shortcut_hint, last_track_query)); + dialog_text = vformat(TTR("Create new track for %s and insert key?"), last_track_query); } else { - insert_confirm_text->set_text(vformat(TTR("Create %d new tracks and insert keys?") + "\n\n" + shortcut_hint, num_tracks)); + dialog_text = vformat(TTR("Create %d new tracks and insert keys?"), num_tracks); + } + + if (confirm_insert) { + dialog_text += +"\n\n" + TTR("Hold Shift when clicking the key icon to skip this dialog."); } + insert_confirm_text->set_text(dialog_text); insert_confirm_bezier->set_visible(all_bezier); insert_confirm_reset->set_visible(reset_allowed); @@ -4420,7 +4429,7 @@ AnimationTrackEditor::TrackIndices AnimationTrackEditor::_confirm_insert(InsertD for (int i = 0; i < subindices.size(); i++) { InsertData id = p_id; id.type = Animation::TYPE_BEZIER; - id.value = p_id.value.get(subindices[i].substr(1, subindices[i].length())); + id.value = subindices[i].is_empty() ? p_id.value : p_id.value.get(subindices[i].substr(1, subindices[i].length())); id.path = String(p_id.path) + subindices[i]; p_next_tracks = _confirm_insert(id, p_next_tracks, p_reset_wanted, p_reset_anim, false); } @@ -4456,16 +4465,8 @@ AnimationTrackEditor::TrackIndices AnimationTrackEditor::_confirm_insert(InsertD } break; case Animation::TYPE_BEZIER: { - Array array; - array.resize(5); - array[0] = p_id.value; - array[1] = -0.25; - array[2] = 0; - array[3] = 0.25; - array[4] = 0; - value = array; + value = animation->make_default_bezier_key(p_id.value); bezier_edit_icon->set_disabled(false); - } break; default: { // Other track types shouldn't use this code path. @@ -5084,17 +5085,7 @@ void AnimationTrackEditor::_fetch_value_track_options(const NodePath &p_path, An PropertyInfo h = _find_hint_for_track(animation->get_track_count() - 1, np); animation->remove_track(animation->get_track_count() - 1); // Hack. switch (h.type) { - case Variant::FLOAT: { -#ifdef DISABLE_DEPRECATED - bool is_angle = h.type == Variant::FLOAT && h.hint_string.contains("radians_as_degrees"); -#else - bool is_angle = h.type == Variant::FLOAT && h.hint_string.contains("radians"); -#endif // DISABLE_DEPRECATED - if (is_angle) { - *r_interpolation_type = Animation::INTERPOLATION_LINEAR_ANGLE; - } - [[fallthrough]]; - } + case Variant::FLOAT: case Variant::VECTOR2: case Variant::RECT2: case Variant::VECTOR3: @@ -5277,15 +5268,7 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { NodePath bp; Variant value; _find_hint_for_track(p_track, bp, &value); - Array arr; - arr.resize(5); - arr[0] = value; - arr[1] = -0.25; - arr[2] = 0; - arr[3] = 0.25; - arr[4] = 0; - - id.value = arr; + id.value = animation->make_default_bezier_key(value); } break; case Animation::TYPE_AUDIO: { Dictionary ak; @@ -5829,7 +5812,7 @@ void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, bool p_ofs_valid, i if (key_is_bezier && !track_is_bezier) { value = AnimationBezierTrackEdit::get_bezier_key_value(value); } else if (!key_is_bezier && track_is_bezier) { - value = AnimationBezierTrackEdit::make_default_bezier_key(value); + value = animation->make_default_bezier_key(value); } undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, animation->track_get_key_transition(E->key().track, E->key().key)); @@ -5973,7 +5956,7 @@ void AnimationTrackEditor::_anim_paste_keys(float p_ofs, bool p_ofs_valid, int p if (key_is_bezier && !track_is_bezier) { value = AnimationBezierTrackEdit::get_bezier_key_value(value); } else if (!key_is_bezier && track_is_bezier) { - value = AnimationBezierTrackEdit::make_default_bezier_key(value); + value = animation->make_default_bezier_key(value); } undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, key.transition); diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 7e07e626b1..a9d37a6a99 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -347,18 +347,18 @@ void FindReplaceBar::_replace_all() { needs_to_count_results = true; } -void FindReplaceBar::_get_search_from(int &r_line, int &r_col, bool p_is_searching_next) { +void FindReplaceBar::_get_search_from(int &r_line, int &r_col, SearchMode p_search_mode) { if (!text_editor->has_selection(0) || is_selection_only()) { r_line = text_editor->get_caret_line(0); r_col = text_editor->get_caret_column(0); - if (!p_is_searching_next && r_line == result_line && r_col >= result_col && r_col <= result_col + get_search_text().length()) { + if (p_search_mode == SEARCH_PREV && r_line == result_line && r_col >= result_col && r_col <= result_col + get_search_text().length()) { r_col = result_col; } return; } - if (p_is_searching_next) { + if (p_search_mode == SEARCH_NEXT) { r_line = text_editor->get_selection_to_line(); r_col = text_editor->get_selection_to_column(); } else { @@ -368,7 +368,11 @@ void FindReplaceBar::_get_search_from(int &r_line, int &r_col, bool p_is_searchi } void FindReplaceBar::_update_results_count() { - if (!needs_to_count_results && (result_line != -1) && results_count_to_current > 0) { + int caret_line, caret_column; + _get_search_from(caret_line, caret_column, SEARCH_CURRENT); + bool match_selected = caret_line == result_line && caret_column == result_col && !is_selection_only() && text_editor->has_selection(0); + + if (match_selected && !needs_to_count_results && result_line != -1 && results_count_to_current > 0) { results_count_to_current += (flags & TextEdit::SEARCH_BACKWARDS) ? -1 : 1; if (results_count_to_current > results_count) { @@ -385,9 +389,10 @@ void FindReplaceBar::_update_results_count() { return; } - needs_to_count_results = false; + needs_to_count_results = !match_selected; results_count = 0; + results_count_to_current = 0; for (int i = 0; i < text_editor->get_line_count(); i++) { String line_text = text_editor->get_line(i); @@ -417,18 +422,26 @@ void FindReplaceBar::_update_results_count() { results_count++; - if (i == result_line) { - if (col_pos == result_col) { - results_count_to_current = results_count; - } else if (col_pos < result_col && col_pos + searched.length() > result_col) { - col_pos = result_col; - results_count_to_current = results_count; - } + if (i <= result_line && col_pos <= result_col) { + results_count_to_current = results_count; + } + if (i == result_line && col_pos < result_col && col_pos + searched.length() > result_col) { + // Searching forwards and backwards with repeating text can lead to different matches. + col_pos = result_col; } - col_pos += searched.length(); } } + if (!match_selected) { + // Current result should refer to the match before the caret, if the caret is not on a match. + if (caret_line != result_line || caret_column != result_col) { + results_count_to_current -= 1; + } + if (results_count_to_current == 0 && (caret_line > result_line || (caret_line == result_line && caret_column > result_col))) { + // Caret is after all matches. + results_count_to_current = results_count; + } + } } void FindReplaceBar::_update_matches_display() { @@ -457,7 +470,7 @@ bool FindReplaceBar::search_current() { _update_flags(false); int line, col; - _get_search_from(line, col); + _get_search_from(line, col, SEARCH_CURRENT); return _search(flags, line, col); } @@ -473,10 +486,14 @@ bool FindReplaceBar::search_prev() { String text = get_search_text(); + if ((flags & TextEdit::SEARCH_BACKWARDS) == 0) { + needs_to_count_results = true; + } + _update_flags(true); int line, col; - _get_search_from(line, col); + _get_search_from(line, col, SEARCH_PREV); col -= text.length(); if (col < 0) { @@ -499,10 +516,14 @@ bool FindReplaceBar::search_next() { popup_search(true); } + if (flags & TextEdit::SEARCH_BACKWARDS) { + needs_to_count_results = true; + } + _update_flags(false); int line, col; - _get_search_from(line, col, true); + _get_search_from(line, col, SEARCH_NEXT); return _search(flags, line, col); } @@ -653,6 +674,7 @@ void FindReplaceBar::set_text_edit(CodeTextEditor *p_text_editor) { } if (base_text_editor) { + text_editor->set_search_text(String()); base_text_editor->remove_find_replace_bar(); base_text_editor = nullptr; text_editor->disconnect(SceneStringName(text_changed), callable_mp(this, &FindReplaceBar::_editor_text_changed)); @@ -670,8 +692,7 @@ void FindReplaceBar::set_text_edit(CodeTextEditor *p_text_editor) { text_editor = base_text_editor->get_text_editor(); text_editor->connect(SceneStringName(text_changed), callable_mp(this, &FindReplaceBar::_editor_text_changed)); - _update_results_count(); - _update_matches_display(); + _editor_text_changed(); } void FindReplaceBar::_bind_methods() { diff --git a/editor/code_editor.h b/editor/code_editor.h index af33a3fac8..28f6944b66 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -64,6 +64,12 @@ class CodeTextEditor; class FindReplaceBar : public HBoxContainer { GDCLASS(FindReplaceBar, HBoxContainer); + enum SearchMode { + SEARCH_CURRENT, + SEARCH_NEXT, + SEARCH_PREV, + }; + LineEdit *search_text = nullptr; Label *matches_label = nullptr; Button *find_prev = nullptr; @@ -94,7 +100,7 @@ class FindReplaceBar : public HBoxContainer { bool replace_all_mode = false; bool preserve_cursor = false; - void _get_search_from(int &r_line, int &r_col, bool p_is_searching_next = false); + void _get_search_from(int &r_line, int &r_col, SearchMode p_search_mode); void _update_results_count(); void _update_matches_display(); diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index bfc4d91af7..eb0ab1174b 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -1377,6 +1377,8 @@ void ConnectionsDock::_notification(int p_what) { slot_menu->set_item_icon(slot_menu->get_item_index(SLOT_MENU_DISCONNECT), get_editor_theme_icon(SNAME("Unlinked"))); tree->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor))); + + update_tree(); } break; case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { diff --git a/editor/editor_autoload_settings.cpp b/editor/editor_autoload_settings.cpp index 545404ec8e..b62351256e 100644 --- a/editor/editor_autoload_settings.cpp +++ b/editor/editor_autoload_settings.cpp @@ -55,11 +55,6 @@ void EditorAutoloadSettings::_notification(int p_what) { file_dialog->add_filter("*." + E); } - for (const AutoloadInfo &info : autoload_cache) { - if (info.node && info.in_editor) { - callable_mp((Node *)get_tree()->get_root(), &Node::add_child).call_deferred(info.node, false, Node::INTERNAL_MODE_DISABLED); - } - } browse_button->set_icon(get_editor_theme_icon(SNAME("Folder"))); } break; @@ -419,6 +414,8 @@ Node *EditorAutoloadSettings::_create_autoload(const String &p_path) { Ref<Script> scr = res; if (scr.is_valid()) { + ERR_FAIL_COND_V_MSG(!scr->is_valid(), nullptr, vformat("Failed to create an autoload, script '%s' is not compiling.", p_path)); + StringName ibt = scr->get_instance_base_type(); bool valid_type = ClassDB::is_parent_class(ibt, "Node"); ERR_FAIL_COND_V_MSG(!valid_type, nullptr, vformat("Failed to create an autoload, script '%s' does not inherit from 'Node'.", p_path)); @@ -436,6 +433,35 @@ Node *EditorAutoloadSettings::_create_autoload(const String &p_path) { return n; } +void EditorAutoloadSettings::init_autoloads() { + for (AutoloadInfo &info : autoload_cache) { + info.node = _create_autoload(info.path); + + if (info.node) { + Ref<Script> scr = info.node->get_script(); + info.in_editor = scr.is_valid() && scr->is_tool(); + info.node->set_name(info.name); + } + + if (info.is_singleton) { + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + ScriptServer::get_language(i)->add_named_global_constant(info.name, info.node); + } + } + + if (!info.is_singleton && !info.in_editor && info.node != nullptr) { + memdelete(info.node); + info.node = nullptr; + } + } + + for (const AutoloadInfo &info : autoload_cache) { + if (info.node && info.in_editor) { + callable_mp((Node *)get_tree()->get_root(), &Node::add_child).call_deferred(info.node, false, Node::INTERNAL_MODE_DISABLED); + } + } +} + void EditorAutoloadSettings::update_autoload() { if (updating_autoload) { return; @@ -857,27 +883,6 @@ EditorAutoloadSettings::EditorAutoloadSettings() { autoload_cache.push_back(info); } - for (AutoloadInfo &info : autoload_cache) { - info.node = _create_autoload(info.path); - - if (info.node) { - Ref<Script> scr = info.node->get_script(); - info.in_editor = scr.is_valid() && scr->is_tool(); - info.node->set_name(info.name); - } - - if (info.is_singleton) { - for (int i = 0; i < ScriptServer::get_language_count(); i++) { - ScriptServer::get_language(i)->add_named_global_constant(info.name, info.node); - } - } - - if (!info.is_singleton && !info.in_editor && info.node != nullptr) { - memdelete(info.node); - info.node = nullptr; - } - } - HBoxContainer *hbc = memnew(HBoxContainer); add_child(hbc); diff --git a/editor/editor_autoload_settings.h b/editor/editor_autoload_settings.h index e4ac62e700..2ab969eb59 100644 --- a/editor/editor_autoload_settings.h +++ b/editor/editor_autoload_settings.h @@ -104,6 +104,7 @@ protected: static void _bind_methods(); public: + void init_autoloads(); void update_autoload(); bool autoload_add(const String &p_name, const String &p_path); void autoload_remove(const String &p_name); diff --git a/editor/editor_dock_manager.cpp b/editor/editor_dock_manager.cpp index ccb47220db..75135532aa 100644 --- a/editor/editor_dock_manager.cpp +++ b/editor/editor_dock_manager.cpp @@ -277,7 +277,7 @@ void EditorDockManager::_restore_dock_to_saved_window(Control *p_dock, const Dic p_window_dump.get("window_screen_rect", Rect2i())); } -void EditorDockManager::_dock_move_to_bottom(Control *p_dock) { +void EditorDockManager::_dock_move_to_bottom(Control *p_dock, bool p_visible) { _move_dock(p_dock, nullptr); all_docks[p_dock].at_bottom = true; @@ -288,7 +288,7 @@ void EditorDockManager::_dock_move_to_bottom(Control *p_dock) { // Force docks moved to the bottom to appear first in the list, and give them their associated shortcut to toggle their bottom panel. Button *bottom_button = EditorNode::get_bottom_panel()->add_item(all_docks[p_dock].title, p_dock, all_docks[p_dock].shortcut, true); bottom_button->connect(SceneStringName(gui_input), callable_mp(this, &EditorDockManager::_bottom_dock_button_gui_input).bind(bottom_button).bind(p_dock)); - EditorNode::get_bottom_panel()->make_item_visible(p_dock); + EditorNode::get_bottom_panel()->make_item_visible(p_dock, p_visible); } void EditorDockManager::_dock_remove_from_bottom(Control *p_dock) { @@ -548,11 +548,13 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S // Don't open disabled docks. continue; } + bool at_bottom = false; if (restore_window_on_load && floating_docks_dump.has(name)) { all_docks[dock].previous_at_bottom = dock_bottom.has(name); _restore_dock_to_saved_window(dock, floating_docks_dump[name]); } else if (dock_bottom.has(name)) { - _dock_move_to_bottom(dock); + _dock_move_to_bottom(dock, false); + at_bottom = true; } else if (i >= 0) { _move_dock(dock, dock_slot[i], 0); } @@ -564,7 +566,11 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S } else { // Make sure it is open. all_docks[dock].open = true; - dock->show(); + // It's important to not update the visibility of bottom panels. + // Visibility of bottom panels are managed in EditorBottomPanel. + if (!at_bottom) { + dock->show(); + } } all_docks[dock].dock_slot_index = i; @@ -668,7 +674,7 @@ void EditorDockManager::open_dock(Control *p_dock, bool p_set_current) { // Open dock to its previous location. if (all_docks[p_dock].previous_at_bottom) { - _dock_move_to_bottom(p_dock); + _dock_move_to_bottom(p_dock, true); } else if (all_docks[p_dock].dock_slot_index != DOCK_SLOT_NONE) { TabContainer *slot = dock_slot[all_docks[p_dock].dock_slot_index]; int tab_index = all_docks[p_dock].previous_tab_index; @@ -899,7 +905,7 @@ void DockContextPopup::_float_dock() { void DockContextPopup::_move_dock_to_bottom() { hide(); - dock_manager->_dock_move_to_bottom(context_dock); + dock_manager->_dock_move_to_bottom(context_dock, true); dock_manager->_update_layout(); } diff --git a/editor/editor_dock_manager.h b/editor/editor_dock_manager.h index 226222c55a..1e6b413d14 100644 --- a/editor/editor_dock_manager.h +++ b/editor/editor_dock_manager.h @@ -121,7 +121,7 @@ private: void _open_dock_in_window(Control *p_dock, bool p_show_window = true, bool p_reset_size = false); void _restore_dock_to_saved_window(Control *p_dock, const Dictionary &p_window_dump); - void _dock_move_to_bottom(Control *p_dock); + void _dock_move_to_bottom(Control *p_dock, bool p_visible); void _dock_remove_from_bottom(Control *p_dock); bool _is_dock_at_bottom(Control *p_dock); diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index d84ccb0c03..4664defa59 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -44,6 +44,7 @@ #include "editor/editor_paths.h" #include "editor/editor_resource_preview.h" #include "editor/editor_settings.h" +#include "editor/project_settings_editor.h" #include "scene/resources/packed_scene.h" EditorFileSystem *EditorFileSystem::singleton = nullptr; @@ -149,6 +150,11 @@ uint64_t EditorFileSystemDirectory::get_file_modified_time(int p_idx) const { return files[p_idx]->modified_time; } +uint64_t EditorFileSystemDirectory::get_file_import_modified_time(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, files.size(), 0); + return files[p_idx]->import_modified_time; +} + String EditorFileSystemDirectory::get_file_script_class_name(int p_idx) const { return files[p_idx]->script_class_name; } @@ -206,17 +212,68 @@ EditorFileSystemDirectory::EditorFileSystemDirectory() { } EditorFileSystemDirectory::~EditorFileSystemDirectory() { - for (int i = 0; i < files.size(); i++) { - memdelete(files[i]); + for (EditorFileSystemDirectory::FileInfo *fi : files) { + memdelete(fi); } - for (int i = 0; i < subdirs.size(); i++) { - memdelete(subdirs[i]); + for (EditorFileSystemDirectory *dir : subdirs) { + memdelete(dir); + } +} + +EditorFileSystem::ScannedDirectory::~ScannedDirectory() { + for (ScannedDirectory *dir : subdirs) { + memdelete(dir); + } +} + +void EditorFileSystem::_first_scan_filesystem() { + Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES); + first_scan_root_dir = memnew(ScannedDirectory); + first_scan_root_dir->full_path = "res://"; + HashSet<String> existing_class_names; + + nb_files_total = _scan_new_dir(first_scan_root_dir, d); + + // This loads the global class names from the scripts and ensures that even if the + // global_script_class_cache.cfg was missing or invalid, the global class names are valid in ScriptServer. + _first_scan_process_scripts(first_scan_root_dir, existing_class_names); + + // Removing invalid global class to prevent having invalid paths in ScriptServer. + _remove_invalid_global_class_names(existing_class_names); + + // Now that all the global class names should be loaded, create autoloads and plugins. + // This is done after loading the global class names because autoloads and plugins can use + // global class names. + ProjectSettingsEditor::get_singleton()->init_autoloads(); + EditorNode::get_singleton()->init_plugins(); +} + +void EditorFileSystem::_first_scan_process_scripts(const ScannedDirectory *p_scan_dir, HashSet<String> &p_existing_class_names) { + for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) { + _first_scan_process_scripts(scan_sub_dir, p_existing_class_names); + } + + for (const String &scan_file : p_scan_dir->files) { + String path = p_scan_dir->full_path.path_join(scan_file); + String type = ResourceLoader::get_resource_type(path); + + if (ClassDB::is_parent_class(type, SNAME("Script"))) { + String script_class_extends; + String script_class_icon_path; + String script_class_name = _get_global_script_class(type, path, &script_class_extends, &script_class_icon_path); + _register_global_class_script(path, path, type, script_class_name, script_class_extends, script_class_icon_path); + + if (!script_class_name.is_empty()) { + p_existing_class_names.insert(script_class_name); + } + } } } void EditorFileSystem::_scan_filesystem() { - ERR_FAIL_COND(!scanning || new_filesystem); + // On the first scan, the first_scan_root_dir is created in _first_scan_filesystem. + ERR_FAIL_COND(!scanning || new_filesystem || (first_scan && !first_scan_root_dir)); //read .fscache String cpath; @@ -274,8 +331,8 @@ void EditorFileSystem::_scan_filesystem() { FileCache fc; fc.type = split[1]; if (fc.type.contains("/")) { - fc.type = fc.type.get_slice("/", 0); - fc.resource_script_class = fc.type.get_slice("/", 1); + fc.type = split[1].get_slice("/", 0); + fc.resource_script_class = split[1].get_slice("/", 1); } fc.uid = split[2].to_int(); fc.modification_time = split[3].to_int(); @@ -318,23 +375,33 @@ void EditorFileSystem::_scan_filesystem() { } EditorProgressBG scan_progress("efs", "ScanFS", 1000); - ScanProgress sp; - sp.low = 0; - sp.hi = 1; + sp.hi = nb_files_total; sp.progress = &scan_progress; new_filesystem = memnew(EditorFileSystemDirectory); new_filesystem->parent = nullptr; - Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES); - d->change_dir("res://"); - _scan_new_dir(new_filesystem, d, sp); - dep_update_list.clear(); + ScannedDirectory *sd; + // On the first scan, the first_scan_root_dir is created in _first_scan_filesystem. + if (first_scan) { + sd = first_scan_root_dir; + } else { + Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES); + sd = memnew(ScannedDirectory); + sd->full_path = "res://"; + nb_files_total = _scan_new_dir(sd, d); + } + _process_file_system(sd, new_filesystem, sp); + + dep_update_list.clear(); file_cache.clear(); //clear caches, no longer needed - if (!first_scan) { + if (first_scan) { + memdelete(first_scan_root_dir); + first_scan_root_dir = nullptr; + } else { //on the first scan this is done from the main thread after re-importing _save_filesystem_cache(); } @@ -567,6 +634,10 @@ bool EditorFileSystem::_scan_import_support(const Vector<String> &reimports) { bool EditorFileSystem::_update_scan_actions() { sources_changed.clear(); + // We need to update the script global class names before the reimports to be sure that + // all the importer classes that depends on class names will work. + _update_script_classes(); + bool fs_changed = false; Vector<String> reimports; @@ -615,7 +686,7 @@ bool EditorFileSystem::_update_scan_actions() { fs_changed = true; if (ClassDB::is_parent_class(ia.new_file->type, SNAME("Script"))) { - _queue_update_script_class(ia.dir->get_file_path(idx)); + _queue_update_script_class(ia.dir->get_file_path(idx), ia.new_file->type, ia.new_file->script_class_name, ia.new_file->script_class_extends, ia.new_file->script_class_icon_path); } if (ia.new_file->type == SNAME("PackedScene")) { _queue_update_scene_groups(ia.dir->get_file_path(idx)); @@ -626,8 +697,9 @@ bool EditorFileSystem::_update_scan_actions() { int idx = ia.dir->find_file_index(ia.file); ERR_CONTINUE(idx == -1); + String script_class_name = ia.dir->files[idx]->script_class_name; if (ClassDB::is_parent_class(ia.dir->files[idx]->type, SNAME("Script"))) { - _queue_update_script_class(ia.dir->get_file_path(idx)); + _queue_update_script_class(ia.dir->get_file_path(idx), "", "", "", ""); } if (ia.dir->files[idx]->type == SNAME("PackedScene")) { _queue_update_scene_groups(ia.dir->get_file_path(idx)); @@ -637,6 +709,15 @@ bool EditorFileSystem::_update_scan_actions() { memdelete(ia.dir->files[idx]); ia.dir->files.remove_at(idx); + // Restore another script with the same global class name if it exists. + if (!script_class_name.is_empty()) { + EditorFileSystemDirectory::FileInfo *old_fi = nullptr; + String old_file = _get_file_by_class_name(filesystem, script_class_name, old_fi); + if (!old_file.is_empty() && old_fi) { + _queue_update_script_class(old_file, old_fi->type, old_fi->script_class_name, old_fi->script_class_extends, old_fi->script_class_icon_path); + } + } + fs_changed = true; } break; @@ -644,12 +725,22 @@ bool EditorFileSystem::_update_scan_actions() { int idx = ia.dir->find_file_index(ia.file); ERR_CONTINUE(idx == -1); String full_path = ia.dir->get_file_path(idx); - if (_test_for_reimport(full_path, false)) { + + bool need_reimport = _test_for_reimport(full_path, false); + if (!need_reimport && FileAccess::exists(full_path + ".import")) { + uint64_t import_mt = ia.dir->get_file_import_modified_time(idx); + if (import_mt != FileAccess::get_modified_time(full_path + ".import")) { + need_reimport = true; + } + } + + if (need_reimport) { //must reimport reimports.push_back(full_path); Vector<String> dependencies = _get_dependencies(full_path); - for (const String &dependency_path : dependencies) { - if (import_extensions.has(dependency_path.get_extension())) { + for (const String &dep : dependencies) { + const String &dependency_path = dep.contains("::") ? dep.get_slice("::", 0) : dep; + if (import_extensions.has(dep.get_extension())) { reimports.push_back(dependency_path); } } @@ -667,10 +758,11 @@ bool EditorFileSystem::_update_scan_actions() { ERR_CONTINUE(idx == -1); String full_path = ia.dir->get_file_path(idx); - if (ClassDB::is_parent_class(ia.dir->files[idx]->type, SNAME("Script"))) { - _queue_update_script_class(full_path); + const EditorFileSystemDirectory::FileInfo *fi = ia.dir->files[idx]; + if (ClassDB::is_parent_class(fi->type, SNAME("Script"))) { + _queue_update_script_class(full_path, fi->type, fi->script_class_name, fi->script_class_extends, fi->script_class_icon_path); } - if (ia.dir->files[idx]->type == SNAME("PackedScene")) { + if (fi->type == SNAME("PackedScene")) { _queue_update_scene_groups(full_path); } @@ -711,6 +803,10 @@ bool EditorFileSystem::_update_scan_actions() { _save_filesystem_cache(); } + // Moving the processing of pending updates before the resources_reload event to be sure all global class names + // are updated. Script.cpp listens on resources_reload and reloads updated scripts. + _process_update_pending(); + if (reloads.size()) { emit_signal(SNAME("resources_reload"), reloads); } @@ -728,6 +824,14 @@ void EditorFileSystem::scan() { return; } + // The first scan must be on the main thread because, after the first scan and update + // of global class names, we load the plugins and autoloads. These need to + // be added on the main thread because they are nodes, and we need to wait for them + // to be loaded to continue the scan and reimportations. + if (first_scan) { + _first_scan_filesystem(); + } + _update_extensions(); if (!use_threads) { @@ -741,14 +845,14 @@ void EditorFileSystem::scan() { filesystem = new_filesystem; new_filesystem = nullptr; _update_scan_actions(); - scanning = false; - _update_pending_script_classes(); - _update_pending_scene_groups(); // Update all icons so they are loaded for the FileSystemDock. _update_files_icon_path(); + scanning = false; + // Set first_scan to false before the signals so the function doing_first_scan can return false + // in editor_node to start the export if needed. + first_scan = false; emit_signal(SNAME("filesystem_changed")); emit_signal(SNAME("sources_changed"), sources_changed.size() > 0); - first_scan = false; } else { ERR_FAIL_COND(thread.is_started()); set_process(true); @@ -762,28 +866,19 @@ void EditorFileSystem::scan() { } } -void EditorFileSystem::ScanProgress::update(int p_current, int p_total) const { - float ratio = low + ((hi - low) / p_total) * p_current; - progress->step(ratio * 1000); +void EditorFileSystem::ScanProgress::increment() { + current++; + float ratio = current / MAX(hi, 1.0f); + progress->step(ratio * 1000.0f); EditorFileSystem::singleton->scan_total = ratio; } -EditorFileSystem::ScanProgress EditorFileSystem::ScanProgress::get_sub(int p_current, int p_total) const { - ScanProgress sp = *this; - float slice = (sp.hi - sp.low) / p_total; - sp.low += slice * p_current; - sp.hi = slice; - return sp; -} - -void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAccess> &da, const ScanProgress &p_progress) { +int EditorFileSystem::_scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da) { List<String> dirs; List<String> files; String cd = da->get_current_dir(); - p_dir->modified_time = FileAccess::get_modified_time(cd); - da->list_dir_begin(); while (true) { String f = da->get_next(); @@ -816,55 +911,59 @@ void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAc dirs.sort_custom<FileNoCaseComparator>(); files.sort_custom<FileNoCaseComparator>(); - int total = dirs.size() + files.size(); - int idx = 0; + int nb_files_total_scan = 0; - for (List<String>::Element *E = dirs.front(); E; E = E->next(), idx++) { + for (List<String>::Element *E = dirs.front(); E; E = E->next()) { if (da->change_dir(E->get()) == OK) { String d = da->get_current_dir(); if (d == cd || !d.begins_with(cd)) { da->change_dir(cd); //avoid recursion } else { - EditorFileSystemDirectory *efd = memnew(EditorFileSystemDirectory); + ScannedDirectory *sd = memnew(ScannedDirectory); + sd->name = E->get(); + sd->full_path = p_dir->full_path.path_join(sd->name); - efd->parent = p_dir; - efd->name = E->get(); + nb_files_total_scan += _scan_new_dir(sd, da); - _scan_new_dir(efd, da, p_progress.get_sub(idx, total)); - - int idx2 = 0; - for (int i = 0; i < p_dir->subdirs.size(); i++) { - if (efd->name.filenocasecmp_to(p_dir->subdirs[i]->name) < 0) { - break; - } - idx2++; - } - if (idx2 == p_dir->subdirs.size()) { - p_dir->subdirs.push_back(efd); - } else { - p_dir->subdirs.insert(idx2, efd); - } + p_dir->subdirs.push_back(sd); da->change_dir(".."); } } else { ERR_PRINT("Cannot go into subdir '" + E->get() + "'."); } + } - p_progress.update(idx, total); + p_dir->files = files; + nb_files_total_scan += files.size(); + + return nb_files_total_scan; +} + +void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress) { + p_dir->modified_time = FileAccess::get_modified_time(p_scan_dir->full_path); + + for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) { + EditorFileSystemDirectory *sub_dir = memnew(EditorFileSystemDirectory); + sub_dir->parent = p_dir; + sub_dir->name = scan_sub_dir->name; + p_dir->subdirs.push_back(sub_dir); + _process_file_system(scan_sub_dir, sub_dir, p_progress); } - for (List<String>::Element *E = files.front(); E; E = E->next(), idx++) { - String ext = E->get().get_extension().to_lower(); + for (const String &scan_file : p_scan_dir->files) { + String ext = scan_file.get_extension().to_lower(); if (!valid_extensions.has(ext)) { + p_progress.increment(); continue; //invalid } - EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo); - fi->file = E->get(); + String path = p_scan_dir->full_path.path_join(scan_file); - String path = cd.path_join(fi->file); + EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo); + fi->file = scan_file; + p_dir->files.push_back(fi); FileCache *fc = file_cache.getptr(path); uint64_t mt = FileAccess::get_modified_time(path); @@ -894,7 +993,7 @@ void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAc ItemAction ia; ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT; ia.dir = p_dir; - ia.file = E->get(); + ia.file = fi->file; scan_actions.push_back(ia); } @@ -923,7 +1022,7 @@ void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAc ItemAction ia; ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT; ia.dir = p_dir; - ia.file = E->get(); + ia.file = fi->file; scan_actions.push_back(ia); } } else { @@ -939,6 +1038,21 @@ void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAc fi->script_class_name = fc->script_class_name; fi->script_class_extends = fc->script_class_extends; fi->script_class_icon_path = fc->script_class_icon_path; + + if (first_scan && ClassDB::is_parent_class(fi->type, SNAME("Script"))) { + bool update_script = false; + String old_class_name = fi->script_class_name; + fi->script_class_name = _get_global_script_class(fi->type, path, &fi->script_class_extends, &fi->script_class_icon_path); + if (old_class_name != fi->script_class_name) { + update_script = true; + } else if (!fi->script_class_name.is_empty() && (!ScriptServer::is_global_class(fi->script_class_name) || ScriptServer::get_global_class_path(fi->script_class_name) != path)) { + // This script has a class name but is not in the global class names or the path of the class has changed. + update_script = true; + } + if (update_script) { + _queue_update_script_class(path, fi->type, fi->script_class_name, fi->script_class_extends, fi->script_class_icon_path); + } + } } else { //new or modified time fi->type = ResourceLoader::get_resource_type(path); @@ -956,7 +1070,7 @@ void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAc // Files in dep_update_list are forced for rescan to update dependencies. They don't need other updates. if (!dep_update_list.has(path)) { if (ClassDB::is_parent_class(fi->type, SNAME("Script"))) { - _queue_update_script_class(path); + _queue_update_script_class(path, fi->type, fi->script_class_name, fi->script_class_extends, fi->script_class_icon_path); } if (fi->type == SNAME("PackedScene")) { _queue_update_scene_groups(path); @@ -973,16 +1087,16 @@ void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAc } } - p_dir->files.push_back(fi); - p_progress.update(idx, total); + p_progress.increment(); } } -void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, const ScanProgress &p_progress) { +void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanProgress &p_progress) { uint64_t current_mtime = FileAccess::get_modified_time(p_dir->get_path()); bool updated_dir = false; String cd = p_dir->get_path(); + int diff_nb_files = 0; if (current_mtime != p_dir->modified_time || using_fat32_or_exfat) { updated_dir = true; @@ -999,6 +1113,8 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, const p_dir->get_subdir(i)->verified = false; } + diff_nb_files -= p_dir->files.size(); + //then scan files and directories and check what's different Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); @@ -1024,17 +1140,25 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, const int idx = p_dir->find_dir_index(f); if (idx == -1) { - if (_should_skip_directory(cd.path_join(f))) { + String dir_path = cd.path_join(f); + if (_should_skip_directory(dir_path)) { continue; } - EditorFileSystemDirectory *efd = memnew(EditorFileSystemDirectory); + ScannedDirectory sd; + sd.name = f; + sd.full_path = dir_path; + EditorFileSystemDirectory *efd = memnew(EditorFileSystemDirectory); efd->parent = p_dir; efd->name = f; + Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES); - d->change_dir(cd.path_join(f)); - _scan_new_dir(efd, d, p_progress.get_sub(1, 1)); + d->change_dir(dir_path); + int nb_files_dir = _scan_new_dir(&sd, d); + p_progress.hi += nb_files_dir; + diff_nb_files += nb_files_dir; + _process_file_system(&sd, efd, p_progress); ItemAction ia; ia.action = ItemAction::ACTION_DIR_ADD; @@ -1088,7 +1212,7 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, const ia.file = f; scan_actions.push_back(ia); } - + diff_nb_files++; } else { p_dir->files[idx]->verified = true; } @@ -1106,6 +1230,7 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, const ia.dir = p_dir; ia.file = p_dir->files[i]->file; scan_actions.push_back(ia); + diff_nb_files--; continue; } @@ -1152,10 +1277,16 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, const scan_actions.push_back(ia); } } + + p_progress.increment(); } for (int i = 0; i < p_dir->subdirs.size(); i++) { if ((updated_dir && !p_dir->subdirs[i]->verified) || _should_skip_directory(p_dir->subdirs[i]->get_path())) { + // Add all the files of the folder to be sure _update_scan_actions process the removed files + // for global class names. + diff_nb_files += _insert_actions_delete_files_directory(p_dir->subdirs[i]); + //this directory was removed or ignored, add action to remove it ItemAction ia; ia.action = ItemAction::ACTION_DIR_REMOVE; @@ -1165,6 +1296,8 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, const } _scan_fs_changes(p_dir->get_subdir(i), p_progress); } + + nb_files_total = MAX(nb_files_total + diff_nb_files, 0); } void EditorFileSystem::_delete_internal_files(const String &p_file) { @@ -1179,19 +1312,64 @@ void EditorFileSystem::_delete_internal_files(const String &p_file) { } } +int EditorFileSystem::_insert_actions_delete_files_directory(EditorFileSystemDirectory *p_dir) { + int nb_files = 0; + for (EditorFileSystemDirectory::FileInfo *fi : p_dir->files) { + ItemAction ia; + ia.action = ItemAction::ACTION_FILE_REMOVE; + ia.dir = p_dir; + ia.file = fi->file; + scan_actions.push_back(ia); + nb_files++; + } + + for (EditorFileSystemDirectory *sub_dir : p_dir->subdirs) { + nb_files += _insert_actions_delete_files_directory(sub_dir); + } + + return nb_files; +} + void EditorFileSystem::_thread_func_sources(void *_userdata) { EditorFileSystem *efs = (EditorFileSystem *)_userdata; if (efs->filesystem) { EditorProgressBG pr("sources", TTR("ScanSources"), 1000); ScanProgress sp; sp.progress = ≺ - sp.hi = 1; - sp.low = 0; + sp.hi = efs->nb_files_total; efs->_scan_fs_changes(efs->filesystem, sp); } efs->scanning_changes_done.set(); } +void EditorFileSystem::_remove_invalid_global_class_names(const HashSet<String> &p_existing_class_names) { + List<StringName> global_classes; + ScriptServer::get_global_class_list(&global_classes); + for (const StringName &class_name : global_classes) { + if (!p_existing_class_names.has(class_name)) { + ScriptServer::remove_global_class(class_name); + } + } +} + +String EditorFileSystem::_get_file_by_class_name(EditorFileSystemDirectory *p_dir, const String &p_class_name, EditorFileSystemDirectory::FileInfo *&r_file_info) { + for (EditorFileSystemDirectory::FileInfo *fi : p_dir->files) { + if (fi->script_class_name == p_class_name) { + r_file_info = fi; + return p_dir->get_path().path_join(fi->file); + } + } + + for (EditorFileSystemDirectory *sub_dir : p_dir->subdirs) { + String file = _get_file_by_class_name(sub_dir, p_class_name, r_file_info); + if (!file.is_empty()) { + return file; + } + } + r_file_info = nullptr; + return ""; +} + void EditorFileSystem::scan_changes() { if (first_scan || // Prevent a premature changes scan from inhibiting the first full scan scanning || scanning_changes || thread.is_started()) { @@ -1210,14 +1388,10 @@ void EditorFileSystem::scan_changes() { EditorProgressBG pr("sources", TTR("ScanSources"), 1000); ScanProgress sp; sp.progress = ≺ - sp.hi = 1; - sp.low = 0; + sp.hi = nb_files_total; scan_total = 0; _scan_fs_changes(filesystem, sp); - bool changed = _update_scan_actions(); - _update_pending_script_classes(); - _update_pending_scene_groups(); - if (changed) { + if (_update_scan_actions()) { emit_signal(SNAME("filesystem_changed")); } } @@ -1282,13 +1456,13 @@ void EditorFileSystem::_notification(int p_what) { thread_sources.wait_to_finish(); } bool changed = _update_scan_actions(); - _update_pending_script_classes(); - _update_pending_scene_groups(); + // Set first_scan to false before the signals so the function doing_first_scan can return false + // in editor_node to start the export if needed. + first_scan = false; if (changed) { emit_signal(SNAME("filesystem_changed")); } emit_signal(SNAME("sources_changed"), sources_changed.size() > 0); - first_scan = false; scanning_changes = false; // Changed to false here to prevent recursive triggering of scan thread. done_importing = true; } @@ -1302,13 +1476,13 @@ void EditorFileSystem::_notification(int p_what) { new_filesystem = nullptr; thread.wait_to_finish(); _update_scan_actions(); - _update_pending_script_classes(); - _update_pending_scene_groups(); // Update all icons so they are loaded for the FileSystemDock. _update_files_icon_path(); + // Set first_scan to false before the signals so the function doing_first_scan can return false + // in editor_node to start the export if needed. + first_scan = false; emit_signal(SNAME("filesystem_changed")); emit_signal(SNAME("sources_changed"), sources_changed.size() > 0); - first_scan = false; } if (done_importing && scan_changes_pending) { @@ -1323,7 +1497,7 @@ void EditorFileSystem::_notification(int p_what) { } bool EditorFileSystem::is_scanning() const { - return scanning || scanning_changes; + return scanning || scanning_changes || first_scan; } float EditorFileSystem::get_scanning_progress() const { @@ -1589,7 +1763,8 @@ String EditorFileSystem::_get_global_script_class(const String &p_type, const St void EditorFileSystem::_update_file_icon_path(EditorFileSystemDirectory::FileInfo *file_info) { String icon_path; if (file_info->script_class_icon_path.is_empty() && !file_info->deps.is_empty()) { - const String &script_path = file_info->deps[0]; // Assuming the first dependency is a script. + const String &script_dep = file_info->deps[0]; // Assuming the first dependency is a script. + const String &script_path = script_dep.contains("::") ? script_dep.get_slice("::", 2) : script_dep; if (!script_path.is_empty()) { String *cached = file_icon_cache.getptr(script_path); if (cached) { @@ -1624,14 +1799,41 @@ void EditorFileSystem::_update_files_icon_path(EditorFileSystemDirectory *edp) { } void EditorFileSystem::_update_script_classes() { + if (update_script_paths.is_empty()) { + return; + } + update_script_mutex.lock(); - for (const String &path : update_script_paths) { - EditorFileSystem::get_singleton()->register_global_class_script(path, path); + for (const KeyValue<String, ScriptInfo> &E : update_script_paths) { + _register_global_class_script(E.key, E.key, E.value.type, E.value.script_class_name, E.value.script_class_extends, E.value.script_class_icon_path); } - // Parse documentation second, as it requires the class names to be correct and registered - for (const String &path : update_script_paths) { + update_script_paths.clear(); + update_script_mutex.unlock(); + + ScriptServer::save_global_classes(); + EditorNode::get_editor_data().script_class_save_icon_paths(); + + emit_signal("script_classes_updated"); + + // Rescan custom loaders and savers. + // Doing the following here because the `filesystem_changed` signal fires multiple times and isn't always followed by script classes update. + // So I thought it's better to do this when script classes really get updated + ResourceLoader::remove_custom_loaders(); + ResourceLoader::add_custom_loaders(); + ResourceSaver::remove_custom_savers(); + ResourceSaver::add_custom_savers(); +} + +void EditorFileSystem::_update_script_documentation() { + if (update_script_paths_documentation.is_empty()) { + return; + } + + update_script_mutex.lock(); + + for (const String &path : update_script_paths_documentation) { int index = -1; EditorFileSystemDirectory *efd = find_file(path, &index); @@ -1655,40 +1857,38 @@ void EditorFileSystem::_update_script_classes() { } } - update_script_paths.clear(); + update_script_paths_documentation.clear(); update_script_mutex.unlock(); - - ScriptServer::save_global_classes(); - EditorNode::get_editor_data().script_class_save_icon_paths(); - emit_signal("script_classes_updated"); - - // Rescan custom loaders and savers. - // Doing the following here because the `filesystem_changed` signal fires multiple times and isn't always followed by script classes update. - // So I thought it's better to do this when script classes really get updated - ResourceLoader::remove_custom_loaders(); - ResourceLoader::add_custom_loaders(); - ResourceSaver::remove_custom_savers(); - ResourceSaver::add_custom_savers(); } -void EditorFileSystem::_update_pending_script_classes() { - if (!update_script_paths.is_empty()) { - _update_script_classes(); - } else { - // In case the class cache file was removed somehow, regenerate it. - if (!FileAccess::exists(ScriptServer::get_global_class_cache_file_path())) { - ScriptServer::save_global_classes(); - } - } +void EditorFileSystem::_process_update_pending() { + _update_script_classes(); + // Parse documentation second, as it requires the class names to be loaded + // because _update_script_documentation loads the scripts completely. + _update_script_documentation(); + _update_pending_scene_groups(); } -void EditorFileSystem::_queue_update_script_class(const String &p_path) { +void EditorFileSystem::_queue_update_script_class(const String &p_path, const String &p_type, const String &p_script_class_name, const String &p_script_class_extends, const String &p_script_class_icon_path) { update_script_mutex.lock(); - update_script_paths.insert(p_path); + + ScriptInfo si; + si.type = p_type; + si.script_class_name = p_script_class_name; + si.script_class_extends = p_script_class_extends; + si.script_class_icon_path = p_script_class_icon_path; + update_script_paths.insert(p_path, si); + + update_script_paths_documentation.insert(p_path); + update_script_mutex.unlock(); } void EditorFileSystem::_update_scene_groups() { + if (update_scene_paths.is_empty()) { + return; + } + EditorProgress *ep = nullptr; if (update_scene_paths.size() > 1) { ep = memnew(EditorProgress("update_scene_groups", TTR("Update Scene Groups"), update_scene_paths.size())); @@ -1707,7 +1907,7 @@ void EditorFileSystem::_update_scene_groups() { continue; } - const HashSet<StringName> scene_groups = _get_scene_groups(path); + const HashSet<StringName> scene_groups = PackedScene::get_scene_groups(path); if (!scene_groups.is_empty()) { ProjectSettings::get_singleton()->add_scene_groups_cache(path, scene_groups); } @@ -1751,12 +1951,6 @@ void EditorFileSystem::_get_all_scenes(EditorFileSystemDirectory *p_dir, HashSet } } -HashSet<StringName> EditorFileSystem::_get_scene_groups(const String &p_path) { - Ref<PackedScene> packed_scene = ResourceLoader::load(p_path); - ERR_FAIL_COND_V(packed_scene.is_null(), HashSet<StringName>()); - return packed_scene->get_state()->get_all_groups(); -} - void EditorFileSystem::update_file(const String &p_file) { ERR_FAIL_COND(p_file.is_empty()); update_files({ p_file }); @@ -1787,7 +1981,7 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) { } } if (ClassDB::is_parent_class(fs->files[cpos]->type, SNAME("Script"))) { - _queue_update_script_class(file); + _queue_update_script_class(file, fs->files[cpos]->type, "", "", ""); if (!fs->files[cpos]->script_class_icon_path.is_empty()) { update_files_icon_cache = true; } @@ -1840,6 +2034,7 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) { } const String old_script_class_icon_path = fs->files[cpos]->script_class_icon_path; + const String old_class_name = fs->files[cpos]->script_class_name; fs->files[cpos]->type = type; fs->files[cpos]->resource_script_class = script_class; fs->files[cpos]->uid = uid; @@ -1862,23 +2057,31 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) { EditorResourcePreview::get_singleton()->check_for_invalidation(file); if (ClassDB::is_parent_class(fs->files[cpos]->type, SNAME("Script"))) { - _queue_update_script_class(file); + _queue_update_script_class(file, fs->files[cpos]->type, fs->files[cpos]->script_class_name, fs->files[cpos]->script_class_extends, fs->files[cpos]->script_class_icon_path); } if (fs->files[cpos]->type == SNAME("PackedScene")) { _queue_update_scene_groups(file); } + if (fs->files[cpos]->type == SNAME("Resource")) { files_to_update_icon_path.push_back(fs->files[cpos]); } else if (old_script_class_icon_path != fs->files[cpos]->script_class_icon_path) { update_files_icon_cache = true; } + + // Restore another script as the global class name if multiple scripts had the same old class name. + if (!old_class_name.is_empty() && fs->files[cpos]->script_class_name != old_class_name && ClassDB::is_parent_class(type, SNAME("Script"))) { + EditorFileSystemDirectory::FileInfo *old_fi = nullptr; + String old_file = _get_file_by_class_name(filesystem, old_class_name, old_fi); + if (!old_file.is_empty() && old_fi) { + _queue_update_script_class(old_file, old_fi->type, old_fi->script_class_name, old_fi->script_class_extends, old_fi->script_class_icon_path); + } + } updated = true; } } if (updated) { - _update_pending_script_classes(); - _update_pending_scene_groups(); if (update_files_icon_cache) { _update_files_icon_path(); } else { @@ -1886,7 +2089,10 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) { _update_file_icon_path(fi); } } - call_deferred(SNAME("emit_signal"), "filesystem_changed"); //update later + if (!is_scanning()) { + _process_update_pending(); + call_deferred(SNAME("emit_signal"), "filesystem_changed"); //update later + } } } @@ -1894,31 +2100,37 @@ HashSet<String> EditorFileSystem::get_valid_extensions() const { return valid_extensions; } -void EditorFileSystem::register_global_class_script(const String &p_search_path, const String &p_target_path) { +void EditorFileSystem::_register_global_class_script(const String &p_search_path, const String &p_target_path, const String &p_type, const String &p_script_class_name, const String &p_script_class_extends, const String &p_script_class_icon_path) { ScriptServer::remove_global_class_by_path(p_search_path); // First remove, just in case it changed - int index = -1; - EditorFileSystemDirectory *efd = find_file(p_search_path, &index); - - if (!efd || index < 0) { - // The file was removed + if (p_script_class_name.is_empty()) { return; } - if (!efd->files[index]->script_class_name.is_empty()) { - String lang; - for (int j = 0; j < ScriptServer::get_language_count(); j++) { - if (ScriptServer::get_language(j)->handles_global_class_type(efd->files[index]->type)) { - lang = ScriptServer::get_language(j)->get_name(); - } - } - if (lang.is_empty()) { - return; // No lang found that can handle this global class + String lang; + for (int j = 0; j < ScriptServer::get_language_count(); j++) { + if (ScriptServer::get_language(j)->handles_global_class_type(p_type)) { + lang = ScriptServer::get_language(j)->get_name(); + break; } + } + if (lang.is_empty()) { + return; // No lang found that can handle this global class + } - ScriptServer::add_global_class(efd->files[index]->script_class_name, efd->files[index]->script_class_extends, lang, p_target_path); - EditorNode::get_editor_data().script_class_set_icon_path(efd->files[index]->script_class_name, efd->files[index]->script_class_icon_path); - EditorNode::get_editor_data().script_class_set_name(p_target_path, efd->files[index]->script_class_name); + ScriptServer::add_global_class(p_script_class_name, p_script_class_extends, lang, p_target_path); + EditorNode::get_editor_data().script_class_set_icon_path(p_script_class_name, p_script_class_icon_path); + EditorNode::get_editor_data().script_class_set_name(p_target_path, p_script_class_name); +} + +void EditorFileSystem::register_global_class_script(const String &p_search_path, const String &p_target_path) { + int index_file; + EditorFileSystemDirectory *efsd = find_file(p_search_path, &index_file); + if (efsd) { + const EditorFileSystemDirectory::FileInfo *fi = efsd->files[index_file]; + EditorFileSystem::get_singleton()->_register_global_class_script(p_search_path, p_target_path, fi->type, fi->script_class_name, fi->script_class_extends, fi->script_class_icon_path); + } else { + ScriptServer::remove_global_class_by_path(p_search_path); } } @@ -2542,8 +2754,7 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { ResourceUID::get_singleton()->update_cache(); // After reimporting, update the cache. _save_filesystem_cache(); - _update_pending_script_classes(); - _update_pending_scene_groups(); + _process_update_pending(); importing = false; if (!is_scanning()) { emit_signal(SNAME("filesystem_changed")); diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h index 88cd67296b..1bc24416eb 100644 --- a/editor/editor_file_system.h +++ b/editor/editor_file_system.h @@ -89,6 +89,7 @@ public: Vector<String> get_file_deps(int p_idx) const; bool get_file_import_is_valid(int p_idx) const; uint64_t get_file_modified_time(int p_idx) const; + uint64_t get_file_import_modified_time(int p_idx) const; String get_file_script_class_name(int p_idx) const; //used for scripts String get_file_script_class_extends(int p_idx) const; //used for scripts String get_file_script_class_icon_path(int p_idx) const; //used for scripts @@ -159,11 +160,21 @@ class EditorFileSystem : public Node { EditorFileSystemDirectory::FileInfo *new_file = nullptr; }; + struct ScannedDirectory { + String name; + String full_path; + Vector<ScannedDirectory *> subdirs; + List<String> files; + + ~ScannedDirectory(); + }; + bool use_threads = false; Thread thread; static void _thread_func(void *_userdata); EditorFileSystemDirectory *new_filesystem = nullptr; + ScannedDirectory *first_scan_root_dir = nullptr; bool scanning = false; bool importing = false; @@ -172,8 +183,11 @@ class EditorFileSystem : public Node { float scan_total; String filesystem_settings_version_for_import; bool revalidate_import_files = false; + int nb_files_total = 0; void _scan_filesystem(); + void _first_scan_filesystem(); + void _first_scan_process_scripts(const ScannedDirectory *p_scan_dir, HashSet<String> &p_existing_class_names); HashSet<String> late_update_files; @@ -202,11 +216,10 @@ class EditorFileSystem : public Node { HashSet<String> dep_update_list; struct ScanProgress { - float low = 0; float hi = 0; - mutable EditorProgressBG *progress = nullptr; - void update(int p_current, int p_total) const; - ScanProgress get_sub(int p_current, int p_total) const; + int current = 0; + EditorProgressBG *progress = nullptr; + void increment(); }; void _save_filesystem_cache(); @@ -214,15 +227,17 @@ class EditorFileSystem : public Node { bool _find_file(const String &p_file, EditorFileSystemDirectory **r_d, int &r_file_pos) const; - void _scan_fs_changes(EditorFileSystemDirectory *p_dir, const ScanProgress &p_progress); + void _scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanProgress &p_progress); void _delete_internal_files(const String &p_file); + int _insert_actions_delete_files_directory(EditorFileSystemDirectory *p_dir); HashSet<String> textfile_extensions; HashSet<String> valid_extensions; HashSet<String> import_extensions; - void _scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAccess> &da, const ScanProgress &p_progress); + int _scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da); + void _process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress); Thread thread_sources; bool scanning_changes = false; @@ -256,18 +271,26 @@ class EditorFileSystem : public Node { } }; + struct ScriptInfo { + String type; + String script_class_name; + String script_class_extends; + String script_class_icon_path; + }; + Mutex update_script_mutex; - HashSet<String> update_script_paths; - void _queue_update_script_class(const String &p_path); + HashMap<String, ScriptInfo> update_script_paths; + HashSet<String> update_script_paths_documentation; + void _queue_update_script_class(const String &p_path, const String &p_type, const String &p_script_class_name, const String &p_script_class_extends, const String &p_script_class_icon_path); void _update_script_classes(); - void _update_pending_script_classes(); + void _update_script_documentation(); + void _process_update_pending(); Mutex update_scene_mutex; HashSet<String> update_scene_paths; void _queue_update_scene_groups(const String &p_path); void _update_scene_groups(); void _update_pending_scene_groups(); - HashSet<StringName> _get_scene_groups(const String &p_path); void _get_all_scenes(EditorFileSystemDirectory *p_dir, HashSet<String> &r_list); String _get_global_script_class(const String &p_type, const String &p_path, String *r_extends, String *r_icon_path) const; @@ -300,6 +323,10 @@ class EditorFileSystem : public Node { void _update_file_icon_path(EditorFileSystemDirectory::FileInfo *file_info); void _update_files_icon_path(EditorFileSystemDirectory *edp = nullptr); + void _remove_invalid_global_class_names(const HashSet<String> &p_existing_class_names); + String _get_file_by_class_name(EditorFileSystemDirectory *p_dir, const String &p_class_name, EditorFileSystemDirectory::FileInfo *&r_file_info); + + void _register_global_class_script(const String &p_search_path, const String &p_target_path, const String &p_type, const String &p_script_class_name, const String &p_script_class_extends, const String &p_script_class_icon_path); protected: void _notification(int p_what); diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 00ac1c7c6f..5725129f65 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -3761,6 +3761,12 @@ EditorHelpBit::EditorHelpBit(const String &p_symbol) { /// EditorHelpBitTooltip /// +void EditorHelpBitTooltip::_start_timer() { + if (timer->is_inside_tree() && timer->is_stopped()) { + timer->start(); + } +} + void EditorHelpBitTooltip::_safe_queue_free() { if (_pushing_input > 0) { _need_free = true; @@ -3769,13 +3775,20 @@ void EditorHelpBitTooltip::_safe_queue_free() { } } +void EditorHelpBitTooltip::_target_gui_input(const Ref<InputEvent> &p_event) { + const Ref<InputEventMouse> mouse_event = p_event; + if (mouse_event.is_valid()) { + _start_timer(); + } +} + void EditorHelpBitTooltip::_notification(int p_what) { switch (p_what) { case NOTIFICATION_WM_MOUSE_ENTER: timer->stop(); break; case NOTIFICATION_WM_MOUSE_EXIT: - timer->start(); + _start_timer(); break; } } @@ -3783,7 +3796,7 @@ void EditorHelpBitTooltip::_notification(int p_what) { // Forwards non-mouse input to the parent viewport. void EditorHelpBitTooltip::_input_from_window(const Ref<InputEvent> &p_event) { if (p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) { - hide(); // Will be deleted on its timer. + _safe_queue_free(); } else { const Ref<InputEventMouse> mouse_event = p_event; if (mouse_event.is_null()) { @@ -3801,7 +3814,7 @@ void EditorHelpBitTooltip::_input_from_window(const Ref<InputEvent> &p_event) { void EditorHelpBitTooltip::show_tooltip(EditorHelpBit *p_help_bit, Control *p_target) { ERR_FAIL_NULL(p_help_bit); EditorHelpBitTooltip *tooltip = memnew(EditorHelpBitTooltip(p_target)); - p_help_bit->connect("request_hide", callable_mp(static_cast<Window *>(tooltip), &Window::hide)); // Will be deleted on its timer. + p_help_bit->connect("request_hide", callable_mp(tooltip, &EditorHelpBitTooltip::_safe_queue_free)); tooltip->add_child(p_help_bit); p_target->get_viewport()->add_child(tooltip); p_help_bit->update_content_height(); @@ -3858,8 +3871,8 @@ EditorHelpBitTooltip::EditorHelpBitTooltip(Control *p_target) { add_child(timer); ERR_FAIL_NULL(p_target); - p_target->connect(SceneStringName(mouse_entered), callable_mp(timer, &Timer::stop)); - p_target->connect(SceneStringName(mouse_exited), callable_mp(timer, &Timer::start).bind(-1)); + p_target->connect(SceneStringName(mouse_exited), callable_mp(this, &EditorHelpBitTooltip::_start_timer)); + p_target->connect(SceneStringName(gui_input), callable_mp(this, &EditorHelpBitTooltip::_target_gui_input)); } #if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) diff --git a/editor/editor_help.h b/editor/editor_help.h index 8d1fec713e..93f74cb2c1 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -329,7 +329,9 @@ class EditorHelpBitTooltip : public PopupPanel { int _pushing_input = 0; bool _need_free = false; + void _start_timer(); void _safe_queue_free(); + void _target_gui_input(const Ref<InputEvent> &p_event); protected: void _notification(int p_what); diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 6c26231a4b..467a19ebb1 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -2598,6 +2598,10 @@ int EditorInspector::inspector_plugin_count = 0; EditorProperty *EditorInspector::instantiate_property_editor(Object *p_object, const Variant::Type p_type, const String &p_path, PropertyHint p_hint, const String &p_hint_text, const uint32_t p_usage, const bool p_wide) { for (int i = inspector_plugin_count - 1; i >= 0; i--) { + if (!inspector_plugins[i]->can_handle(p_object)) { + continue; + } + inspector_plugins[i]->parse_property(p_object, p_type, p_path, p_hint, p_hint_text, p_usage, p_wide); if (inspector_plugins[i]->added_editors.size()) { for (List<EditorInspectorPlugin::AddedEditor>::Element *E = inspector_plugins[i]->added_editors.front()->next(); E; E = E->next()) { //only keep first one @@ -3133,6 +3137,7 @@ void EditorInspector::update_tree() { // Recreate the category vbox if it was reset. if (category_vbox == nullptr) { category_vbox = memnew(VBoxContainer); + category_vbox->hide(); main_vbox->add_child(category_vbox); } @@ -3205,6 +3210,7 @@ void EditorInspector::update_tree() { // If we did not find a section to add the property to, add it to the category vbox instead (the category vbox handles margins correctly). if (current_vbox == main_vbox) { + category_vbox->show(); current_vbox = category_vbox; } diff --git a/editor/editor_log.cpp b/editor/editor_log.cpp index 6a016c217a..0dfbcd0e0d 100644 --- a/editor/editor_log.cpp +++ b/editor/editor_log.cpp @@ -58,8 +58,8 @@ void EditorLog::_error_handler(void *p_self, const char *p_func, const char *p_f MessageType message_type = p_type == ERR_HANDLER_WARNING ? MSG_TYPE_WARNING : MSG_TYPE_ERROR; - if (self->current != Thread::get_caller_id()) { - callable_mp(self, &EditorLog::add_message).call_deferred(err_str, message_type); + if (!Thread::is_main_thread()) { + MessageQueue::get_main_singleton()->push_callable(callable_mp(self, &EditorLog::add_message), err_str, message_type); } else { self->add_message(err_str, message_type); } @@ -557,8 +557,6 @@ EditorLog::EditorLog() { eh.errfunc = _error_handler; eh.userdata = this; add_error_handler(&eh); - - current = Thread::get_caller_id(); } void EditorLog::deinit() { diff --git a/editor/editor_log.h b/editor/editor_log.h index 7012a2a43c..9c652e912a 100644 --- a/editor/editor_log.h +++ b/editor/editor_log.h @@ -156,8 +156,6 @@ private: ErrorHandlerList eh; - Thread::ID current; - //void _dragged(const Point2& p_ofs); void _meta_clicked(const String &p_meta); void _clear_request(); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 55487f7896..fd49920c6b 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -317,14 +317,10 @@ void EditorNode::shortcut_input(const Ref<InputEvent> &p_event) { Ref<InputEventKey> k = p_event; if ((k.is_valid() && k->is_pressed() && !k->is_echo()) || Object::cast_to<InputEventShortcut>(*p_event)) { - EditorPlugin *old_editor = editor_plugin_screen; - + bool is_handled = true; if (ED_IS_SHORTCUT("editor/filter_files", p_event)) { FileSystemDock::get_singleton()->focus_on_filter(); - get_tree()->get_root()->set_input_as_handled(); - } - - if (ED_IS_SHORTCUT("editor/editor_2d", p_event)) { + } else if (ED_IS_SHORTCUT("editor/editor_2d", p_event)) { editor_select(EDITOR_2D); } else if (ED_IS_SHORTCUT("editor/editor_3d", p_event)) { editor_select(EDITOR_3D); @@ -343,9 +339,10 @@ void EditorNode::shortcut_input(const Ref<InputEvent> &p_event) { } else if (ED_IS_SHORTCUT("editor/toggle_last_opened_bottom_panel", p_event)) { bottom_panel->toggle_last_opened_bottom_panel(); } else { + is_handled = false; } - if (old_editor != editor_plugin_screen) { + if (is_handled) { get_tree()->get_root()->set_input_as_handled(); } } @@ -706,23 +703,7 @@ void EditorNode::_notification(int p_what) { } break; case NOTIFICATION_READY: { - { - started_timestamp = Time::get_singleton()->get_unix_time_from_system(); - _initializing_plugins = true; - Vector<String> addons; - if (ProjectSettings::get_singleton()->has_setting("editor_plugins/enabled")) { - addons = GLOBAL_GET("editor_plugins/enabled"); - } - - for (int i = 0; i < addons.size(); i++) { - set_addon_plugin_enabled(addons[i], true); - } - _initializing_plugins = false; - - if (!pending_addons.is_empty()) { - EditorFileSystem::get_singleton()->connect("script_classes_updated", callable_mp(this, &EditorNode::_enable_pending_addons)); - } - } + started_timestamp = Time::get_singleton()->get_unix_time_from_system(); RenderingServer::get_singleton()->viewport_set_disable_2d(get_scene_root()->get_viewport_rid(), true); RenderingServer::get_singleton()->viewport_set_environment_mode(get_viewport()->get_viewport_rid(), RenderingServer::VIEWPORT_ENVIRONMENT_DISABLED); @@ -860,6 +841,23 @@ void EditorNode::_update_update_spinner() { OS::get_singleton()->set_low_processor_usage_mode(!update_continuously); } +void EditorNode::init_plugins() { + _initializing_plugins = true; + Vector<String> addons; + if (ProjectSettings::get_singleton()->has_setting("editor_plugins/enabled")) { + addons = GLOBAL_GET("editor_plugins/enabled"); + } + + for (const String &addon : addons) { + set_addon_plugin_enabled(addon, true); + } + _initializing_plugins = false; + + if (!pending_addons.is_empty()) { + EditorFileSystem::get_singleton()->connect("script_classes_updated", callable_mp(this, &EditorNode::_enable_pending_addons), CONNECT_ONE_SHOT); + } +} + void EditorNode::_on_plugin_ready(Object *p_script, const String &p_activate_name) { Ref<Script> scr = Object::cast_to<Script>(p_script); if (scr.is_null()) { @@ -954,6 +952,7 @@ void EditorNode::_fs_changed() { // FIXME: Move this to a cleaner location, it's hacky to do this in _fs_changed. String export_error; Error err = OK; + // It's important to wait for the first scan to finish; otherwise, scripts or resources might not be imported. if (!export_defer.preset.is_empty() && !EditorFileSystem::get_singleton()->is_scanning()) { String preset_name = export_defer.preset; // Ensures export_project does not loop infinitely, because notifications may @@ -1737,7 +1736,7 @@ bool EditorNode::_validate_scene_recursive(const String &p_filename, Node *p_nod return false; } -int EditorNode::_save_external_resources() { +int EditorNode::_save_external_resources(bool p_also_save_external_data) { // Save external resources and its subresources if any was modified. int flg = 0; @@ -1783,6 +1782,16 @@ int EditorNode::_save_external_resources() { saved++; } + if (p_also_save_external_data) { + for (int i = 0; i < editor_data.get_editor_plugin_count(); i++) { + EditorPlugin *plugin = editor_data.get_editor_plugin(i); + if (!plugin->get_unsaved_status().is_empty()) { + plugin->save_external_data(); + saved++; + } + } + } + EditorUndoRedoManager::get_singleton()->set_history_as_saved(EditorUndoRedoManager::GLOBAL_HISTORY); return saved; @@ -1812,9 +1821,7 @@ static void _reset_animation_mixers(Node *p_node, List<Pair<AnimationMixer *, Re } void EditorNode::_save_scene(String p_file, int idx) { - if (!saving_scene.is_empty() && saving_scene == p_file) { - return; - } + ERR_FAIL_COND_MSG(!saving_scene.is_empty() && saving_scene == p_file, "Scene saved while already being saved!"); Node *scene = editor_data.get_edited_scene_root(idx); @@ -2728,10 +2735,10 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) { ScriptEditor::get_singleton()->save_current_script(); } - const int saved = _save_external_resources(); + const int saved = _save_external_resources(true); if (saved > 0) { show_accept( - vformat(TTR("The current scene has no root node, but %d modified external resource(s) were saved anyway."), saved), + vformat(TTR("The current scene has no root node, but %d modified external resource(s) and/or plugin data were saved anyway."), saved), TTR("OK")); } else if (p_option == FILE_SAVE_AS_SCENE) { // Don't show this dialog when pressing Ctrl + S to avoid interfering with script saving. @@ -5251,6 +5258,14 @@ bool EditorNode::has_scenes_in_session() { return !scenes.is_empty(); } +void EditorNode::undo() { + trigger_menu_option(EDIT_UNDO, true); +} + +void EditorNode::redo() { + trigger_menu_option(EDIT_REDO, true); +} + bool EditorNode::ensure_main_scene(bool p_from_native) { pick_main_scene->set_meta("from_native", p_from_native); // Whether from play button or native run. String main_scene = GLOBAL_GET("application/run/main_scene"); @@ -5963,9 +5978,6 @@ void EditorNode::reload_instances_with_path_in_edited_scenes(const String &p_ins is_editable = owner->is_editable_instance(original_node); } - // For clear instance state for path recaching. - instantiated_node->set_scene_instance_state(Ref<SceneState>()); - bool original_node_is_displayed_folded = original_node->is_displayed_folded(); bool original_node_scene_instance_load_placeholder = original_node->get_scene_instance_load_placeholder(); @@ -6799,7 +6811,7 @@ EditorNode::EditorNode() { distraction_free = memnew(Button); distraction_free->set_theme_type_variation("FlatMenuButton"); ED_SHORTCUT_AND_COMMAND("editor/distraction_free_mode", TTR("Distraction Free Mode"), KeyModifierMask::CTRL | KeyModifierMask::SHIFT | Key::F11); - ED_SHORTCUT_OVERRIDE("editor/distraction_free_mode", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::D); + ED_SHORTCUT_OVERRIDE("editor/distraction_free_mode", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::D); ED_SHORTCUT_AND_COMMAND("editor/toggle_last_opened_bottom_panel", TTR("Toggle Last Opened Bottom Panel"), KeyModifierMask::CMD_OR_CTRL | Key::J); distraction_free->set_shortcut(ED_GET_SHORTCUT("editor/distraction_free_mode")); distraction_free->set_tooltip_text(TTR("Toggle distraction-free mode.")); diff --git a/editor/editor_node.h b/editor/editor_node.h index b7b4dff74e..7a26156ab8 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -573,7 +573,7 @@ private: void _update_undo_redo_allowed(); - int _save_external_resources(); + int _save_external_resources(bool p_also_save_external_data = false); void _set_current_scene(int p_idx); void _set_current_scene_nocheck(int p_idx); @@ -683,6 +683,7 @@ protected: public: // Public for use with callable_mp. + void init_plugins(); void _on_plugin_ready(Object *p_script, const String &p_activate_name); void editor_select(int p_which); @@ -915,6 +916,9 @@ public: bool has_scenes_in_session(); + void undo(); + void redo(); + int execute_and_show_output(const String &p_title, const String &p_path, const List<String> &p_arguments, bool p_close_on_ok = true, bool p_close_on_errors = false, String *r_output = nullptr); EditorNode(); diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp index 127bca9bbf..633f6abad9 100644 --- a/editor/editor_properties_array_dict.cpp +++ b/editor/editor_properties_array_dict.cpp @@ -435,7 +435,7 @@ void EditorPropertyArray::update_property() { editor->setup("Object"); new_prop = editor; } else { - new_prop = EditorInspector::instantiate_property_editor(nullptr, value_type, "", subtype_hint, subtype_hint_string, PROPERTY_USAGE_NONE); + new_prop = EditorInspector::instantiate_property_editor(this, value_type, "", subtype_hint, subtype_hint_string, PROPERTY_USAGE_NONE); } new_prop->set_selectable(false); new_prop->set_use_folding(is_using_folding()); @@ -1064,7 +1064,7 @@ void EditorPropertyDictionary::update_property() { editor->setup("Object"); new_prop = editor; } else { - new_prop = EditorInspector::instantiate_property_editor(nullptr, value_type, "", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE); + new_prop = EditorInspector::instantiate_property_editor(this, value_type, "", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE); } new_prop->set_selectable(false); new_prop->set_use_folding(is_using_folding()); diff --git a/editor/editor_quick_open.cpp b/editor/editor_quick_open.cpp index 0621e5f8d1..dfb87f43da 100644 --- a/editor/editor_quick_open.cpp +++ b/editor/editor_quick_open.cpp @@ -32,6 +32,7 @@ #include "core/os/keyboard.h" #include "editor/editor_node.h" +#include "editor/editor_string_names.h" #include "editor/themes/editor_scale.h" Rect2i EditorQuickOpen::prev_rect = Rect2i(); @@ -119,10 +120,12 @@ void EditorQuickOpen::_update_search() { sorter.sort(entries.ptrw(), entries.size()); } + const int class_icon_size = search_options->get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)); const int entry_limit = MIN(entries.size(), 300); for (int i = 0; i < entry_limit; i++) { TreeItem *ti = search_options->create_item(root); ti->set_text(0, entries[i].path); + ti->set_icon_max_width(0, class_icon_size); ti->set_icon(0, *icons.lookup_ptr(entries[i].path.get_extension())); } diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index a895219e6f..4cd44e3020 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -1065,7 +1065,7 @@ EditorResourcePicker::EditorResourcePicker(bool p_hide_assign_button_controls) { } edit_button = memnew(Button); - edit_button->set_flat(true); + edit_button->set_flat(false); edit_button->set_toggle_mode(true); edit_button->connect(SceneStringName(pressed), callable_mp(this, &EditorResourcePicker::_update_menu)); add_child(edit_button); @@ -1268,8 +1268,6 @@ void EditorAudioStreamPicker::_preview_draw() { if (audio_stream->get_length() > 0 && size.width > 0) { rect.size.height *= 0.5; - stream_preview_rect->draw_rect(rect, Color(0, 0, 0, 1)); - Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(audio_stream); float preview_len = preview->get_length(); @@ -1325,8 +1323,8 @@ void EditorAudioStreamPicker::_preview_draw() { text = audio_stream->get_class().replace_first("AudioStream", ""); } - stream_preview_rect->draw_texture(icon, Point2i(EDSCALE * 2, rect.position.y + (rect.size.height - icon->get_height()) / 2), icon_modulate); - stream_preview_rect->draw_string(font, Point2i(EDSCALE * 2 + icon->get_width(), rect.position.y + font->get_ascent(font_size) + (rect.size.height - font->get_height(font_size)) / 2), text, HORIZONTAL_ALIGNMENT_CENTER, size.width - 4 * EDSCALE - icon->get_width()); + stream_preview_rect->draw_texture(icon, Point2i(EDSCALE * 4, rect.position.y + (rect.size.height - icon->get_height()) / 2), icon_modulate); + stream_preview_rect->draw_string(font, Point2i(EDSCALE * 4 + icon->get_width(), rect.position.y + font->get_ascent(font_size) + (rect.size.height - font->get_height(font_size)) / 2), text, HORIZONTAL_ALIGNMENT_CENTER, size.width - 4 * EDSCALE - icon->get_width(), font_size, get_theme_color(SNAME("font_color"), EditorStringName(Editor))); } EditorAudioStreamPicker::EditorAudioStreamPicker() : diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index e021be9668..5d3cc80da9 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -474,6 +474,11 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/vsync_mode", 1, "Disabled,Enabled,Adaptive,Mailbox") EDITOR_SETTING(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/update_continuously", false, "") +#ifdef ANDROID_ENABLED + EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/android/use_accumulated_input", true, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) + EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/android/use_input_buffering", true, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) +#endif + // Inspector EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "interface/inspector/max_array_dictionary_items_per_page", 20, "10,100,1") EDITOR_SETTING(Variant::BOOL, PROPERTY_HINT_NONE, "interface/inspector/show_low_level_opentype_features", false, "") @@ -781,6 +786,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { // Animation _initial_set("editors/animation/autorename_animation_tracks", true); + _initial_set("editors/animation/confirm_insert_track", true); _initial_set("editors/animation/default_create_bezier_tracks", false); _initial_set("editors/animation/default_create_reset_tracks", true); _initial_set("editors/animation/onion_layers_past_color", Color(1, 0, 0)); diff --git a/editor/editor_settings_dialog.cpp b/editor/editor_settings_dialog.cpp index 6fd6a7103f..a71d43ad51 100644 --- a/editor/editor_settings_dialog.cpp +++ b/editor/editor_settings_dialog.cpp @@ -168,28 +168,17 @@ void EditorSettingsDialog::_notification(int p_what) { } void EditorSettingsDialog::shortcut_input(const Ref<InputEvent> &p_event) { - ERR_FAIL_COND(p_event.is_null()); - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - const Ref<InputEventKey> k = p_event; if (k.is_valid() && k->is_pressed()) { bool handled = false; if (ED_IS_SHORTCUT("ui_undo", p_event)) { - String action = undo_redo->get_current_action_name(); - if (!action.is_empty()) { - EditorNode::get_log()->add_message(vformat(TTR("Undo: %s"), action), EditorLog::MSG_TYPE_EDITOR); - } - undo_redo->undo(); + EditorNode::get_singleton()->undo(); handled = true; } if (ED_IS_SHORTCUT("ui_redo", p_event)) { - undo_redo->redo(); - String action = undo_redo->get_current_action_name(); - if (!action.is_empty()) { - EditorNode::get_log()->add_message(vformat(TTR("Redo: %s"), action), EditorLog::MSG_TYPE_EDITOR); - } + EditorNode::get_singleton()->redo(); handled = true; } diff --git a/editor/editor_undo_redo_manager.cpp b/editor/editor_undo_redo_manager.cpp index 94f76dbc41..55bc198dfb 100644 --- a/editor/editor_undo_redo_manager.cpp +++ b/editor/editor_undo_redo_manager.cpp @@ -104,8 +104,13 @@ int EditorUndoRedoManager::get_history_id_for_object(Object *p_object) const { } EditorUndoRedoManager::History &EditorUndoRedoManager::get_history_for_object(Object *p_object) { - int history_id = get_history_id_for_object(p_object); - ERR_FAIL_COND_V_MSG(pending_action.history_id != INVALID_HISTORY && history_id != pending_action.history_id, get_or_create_history(pending_action.history_id), vformat("UndoRedo history mismatch: expected %d, got %d.", pending_action.history_id, history_id)); + int history_id; + if (!forced_history) { + history_id = get_history_id_for_object(p_object); + ERR_FAIL_COND_V_MSG(pending_action.history_id != INVALID_HISTORY && history_id != pending_action.history_id, get_or_create_history(pending_action.history_id), vformat("UndoRedo history mismatch: expected %d, got %d.", pending_action.history_id, history_id)); + } else { + history_id = pending_action.history_id; + } History &history = get_or_create_history(history_id); if (pending_action.history_id == INVALID_HISTORY) { @@ -116,6 +121,11 @@ EditorUndoRedoManager::History &EditorUndoRedoManager::get_history_for_object(Ob return history; } +void EditorUndoRedoManager::force_fixed_history() { + ERR_FAIL_COND_MSG(pending_action.history_id == INVALID_HISTORY, "The current action has no valid history assigned."); + forced_history = true; +} + void EditorUndoRedoManager::create_action_for_history(const String &p_name, int p_history_id, UndoRedo::MergeMode p_mode, bool p_backward_undo_ops) { if (pending_action.history_id != INVALID_HISTORY) { // Nested action. @@ -236,6 +246,7 @@ void EditorUndoRedoManager::commit_action(bool p_execute) { return; // Empty action, do nothing. } + forced_history = false; is_committing = true; History &history = get_or_create_history(pending_action.history_id); @@ -469,6 +480,7 @@ void EditorUndoRedoManager::_bind_methods() { ClassDB::bind_method(D_METHOD("create_action", "name", "merge_mode", "custom_context", "backward_undo_ops"), &EditorUndoRedoManager::create_action, DEFVAL(UndoRedo::MERGE_DISABLE), DEFVAL((Object *)nullptr), DEFVAL(false)); ClassDB::bind_method(D_METHOD("commit_action", "execute"), &EditorUndoRedoManager::commit_action, DEFVAL(true)); ClassDB::bind_method(D_METHOD("is_committing_action"), &EditorUndoRedoManager::is_committing_action); + ClassDB::bind_method(D_METHOD("force_fixed_history"), &EditorUndoRedoManager::force_fixed_history); { MethodInfo mi; diff --git a/editor/editor_undo_redo_manager.h b/editor/editor_undo_redo_manager.h index e8c782871c..219d5e0702 100644 --- a/editor/editor_undo_redo_manager.h +++ b/editor/editor_undo_redo_manager.h @@ -67,6 +67,7 @@ private: HashMap<int, History> history_map; Action pending_action; + bool forced_history = false; bool is_committing = false; History *_get_newest_undo(); @@ -79,6 +80,7 @@ public: UndoRedo *get_history_undo_redo(int p_idx) const; int get_history_id_for_object(Object *p_object) const; History &get_history_for_object(Object *p_object); + void force_fixed_history(); void create_action_for_history(const String &p_name, int p_history_id, UndoRedo::MergeMode p_mode = UndoRedo::MERGE_DISABLE, bool p_backward_undo_ops = false); void create_action(const String &p_name = "", UndoRedo::MergeMode p_mode = UndoRedo::MERGE_DISABLE, Object *p_custom_context = nullptr, bool p_backward_undo_ops = false); diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index 527544fea3..c0646dc572 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -797,10 +797,6 @@ String EditorExportPlatform::_export_customize(const String &p_path, LocalVector if (!customize_scenes_plugins.is_empty()) { for (Ref<EditorExportPlugin> &plugin : customize_scenes_plugins) { Node *customized = plugin->_customize_scene(node, p_path); - if (plugin->skipped) { - plugin->_clear(); - return String(); - } if (customized != nullptr) { node = customized; modified = true; @@ -834,10 +830,6 @@ String EditorExportPlatform::_export_customize(const String &p_path, LocalVector if (!customize_resources_plugins.is_empty()) { for (Ref<EditorExportPlugin> &plugin : customize_resources_plugins) { Ref<Resource> new_res = plugin->_customize_resource(res, p_path); - if (plugin->skipped) { - plugin->_clear(); - return String(); - } if (new_res.is_valid()) { modified = true; if (new_res != res) { @@ -1132,33 +1124,97 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & } //store everything in the export medium - int idx = 0; int total = paths.size(); + // idx is incremented at the beginning of the paths loop to easily allow + // for continue statements without accidentally skipping an increment. + int idx = total > 0 ? -1 : 0; for (const String &E : paths) { + idx++; String path = E; String type = ResourceLoader::get_resource_type(path); - if (FileAccess::exists(path + ".import")) { - // Before doing this, try to see if it can be customized. + bool has_import_file = FileAccess::exists(path + ".import"); + Ref<ConfigFile> config; + if (has_import_file) { + config.instantiate(); + err = config->load(path + ".import"); + if (err != OK) { + ERR_PRINT("Could not parse: '" + path + "', not exported."); + continue; + } + + String importer_type = config->get_value("remap", "importer"); + + if (importer_type == "skip") { + // Skip file. + continue; + } + } + + bool do_export = true; + for (int i = 0; i < export_plugins.size(); i++) { + if (GDVIRTUAL_IS_OVERRIDDEN_PTR(export_plugins[i], _export_file)) { + export_plugins.write[i]->_export_file_script(path, type, features_psa); + } else { + export_plugins.write[i]->_export_file(path, type, features); + } + if (p_so_func) { + for (int j = 0; j < export_plugins[i]->shared_objects.size(); j++) { + err = p_so_func(p_udata, export_plugins[i]->shared_objects[j]); + if (err != OK) { + return err; + } + } + } + + for (int j = 0; j < export_plugins[i]->extra_files.size(); j++) { + err = p_func(p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, idx, total, enc_in_filters, enc_ex_filters, key); + if (err != OK) { + return err; + } + if (export_plugins[i]->extra_files[j].remap) { + do_export = false; // If remap, do not. + path_remaps.push_back(path); + path_remaps.push_back(export_plugins[i]->extra_files[j].path); + } + } + + if (export_plugins[i]->skipped) { + do_export = false; + } + export_plugins.write[i]->_clear(); + + if (!do_export) { + break; + } + } + if (!do_export) { + continue; + } + + if (has_import_file) { + String importer_type = config->get_value("remap", "importer"); + + if (importer_type == "keep") { + // Just keep file as-is. + Vector<uint8_t> array = FileAccess::get_file_as_bytes(path); + err = p_func(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key); + + if (err != OK) { + return err; + } - String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, false); - if (export_path.is_empty()) { - // Skipped from plugin. continue; } + // Before doing this, try to see if it can be customized. + String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, false); + if (export_path != path) { // It was actually customized. // Since the original file is likely not recognized, just use the import system. - Ref<ConfigFile> config; - config.instantiate(); - err = config->load(path + ".import"); - if (err != OK) { - ERR_PRINT("Could not parse: '" + path + "', not exported."); - continue; - } config->set_value("remap", "type", ResourceLoader::get_resource_type(export_path)); // Erase all Paths. @@ -1194,33 +1250,6 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & } } else { // File is imported and not customized, replace by what it imports. - Ref<ConfigFile> config; - config.instantiate(); - err = config->load(path + ".import"); - if (err != OK) { - ERR_PRINT("Could not parse: '" + path + "', not exported."); - continue; - } - - String importer_type = config->get_value("remap", "importer"); - - if (importer_type == "skip") { - // Skip file. - continue; - } - - if (importer_type == "keep") { - // Just keep file as-is. - Vector<uint8_t> array = FileAccess::get_file_as_bytes(path); - err = p_func(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key); - - if (err != OK) { - return err; - } - - continue; - } - List<String> remaps; config->get_section_keys("remap", &remaps); @@ -1282,66 +1311,24 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & } } else { - // Customize. + // Just store it as it comes. - bool do_export = true; - for (int i = 0; i < export_plugins.size(); i++) { - if (GDVIRTUAL_IS_OVERRIDDEN_PTR(export_plugins[i], _export_file)) { - export_plugins.write[i]->_export_file_script(path, type, features_psa); - } else { - export_plugins.write[i]->_export_file(path, type, features); - } - if (p_so_func) { - for (int j = 0; j < export_plugins[i]->shared_objects.size(); j++) { - err = p_so_func(p_udata, export_plugins[i]->shared_objects[j]); - if (err != OK) { - return err; - } - } - } - - for (int j = 0; j < export_plugins[i]->extra_files.size(); j++) { - err = p_func(p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, idx, total, enc_in_filters, enc_ex_filters, key); - if (err != OK) { - return err; - } - if (export_plugins[i]->extra_files[j].remap) { - do_export = false; //if remap, do not - path_remaps.push_back(path); - path_remaps.push_back(export_plugins[i]->extra_files[j].path); - } - } + // Customization only happens if plugins did not take care of it before. + bool force_binary = convert_text_to_binary && (path.get_extension().to_lower() == "tres" || path.get_extension().to_lower() == "tscn"); + String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, force_binary); - if (export_plugins[i]->skipped) { - do_export = false; - } - export_plugins.write[i]->_clear(); - - if (!do_export) { - break; //apologies, not exporting - } + if (export_path != path) { + // Add a remap entry. + path_remaps.push_back(path); + path_remaps.push_back(export_path); } - //just store it as it comes - if (do_export) { - // Customization only happens if plugins did not take care of it before - bool force_binary = convert_text_to_binary && (path.get_extension().to_lower() == "tres" || path.get_extension().to_lower() == "tscn"); - String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, force_binary); - - if (export_path != path) { - // Add a remap entry - path_remaps.push_back(path); - path_remaps.push_back(export_path); - } - Vector<uint8_t> array = FileAccess::get_file_as_bytes(export_path); - err = p_func(p_udata, export_path, array, idx, total, enc_in_filters, enc_ex_filters, key); - if (err != OK) { - return err; - } + Vector<uint8_t> array = FileAccess::get_file_as_bytes(export_path); + err = p_func(p_udata, export_path, array, idx, total, enc_in_filters, enc_ex_filters, key); + if (err != OK) { + return err; } } - - idx++; } if (convert_text_to_binary || !customize_resources_plugins.is_empty() || !customize_scenes_plugins.is_empty()) { diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index 44ff95b0f9..d4bd97a393 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -546,7 +546,7 @@ void FileSystemDock::_notification(int p_what) { case NOTIFICATION_PROCESS: { if (EditorFileSystem::get_singleton()->is_scanning()) { - scanning_progress->set_value(EditorFileSystem::get_singleton()->get_scanning_progress() * 100); + scanning_progress->set_value(EditorFileSystem::get_singleton()->get_scanning_progress() * 100.0f); } } break; @@ -758,6 +758,8 @@ void FileSystemDock::navigate_to_path(const String &p_path) { // Ensure that the FileSystem dock is visible. EditorDockManager::get_singleton()->focus_dock(this); + import_dock_needs_update = true; + _update_import_dock(); } void FileSystemDock::_file_list_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, const Variant &p_udata) { diff --git a/editor/groups_editor.cpp b/editor/groups_editor.cpp index 18ac4074da..6cfc035fc9 100644 --- a/editor/groups_editor.cpp +++ b/editor/groups_editor.cpp @@ -369,6 +369,7 @@ void GroupsEditor::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: { filter->set_right_icon(get_editor_theme_icon("Search")); add->set_icon(get_editor_theme_icon("Add")); + _update_tree(); } break; case NOTIFICATION_VISIBILITY_CHANGED: { if (groups_dirty && is_visible_in_tree()) { diff --git a/editor/gui/editor_bottom_panel.cpp b/editor/gui/editor_bottom_panel.cpp index f2c4a13e05..3e74a3c94e 100644 --- a/editor/gui/editor_bottom_panel.cpp +++ b/editor/gui/editor_bottom_panel.cpp @@ -178,7 +178,11 @@ Button *EditorBottomPanel::add_item(String p_text, Control *p_item, const Ref<Sh bpi.button = tb; bpi.control = p_item; bpi.name = p_text; - items.push_back(bpi); + if (p_at_front) { + items.insert(0, bpi); + } else { + items.push_back(bpi); + } return tb; } diff --git a/editor/icons/AudioStreamPlayer.svg b/editor/icons/AudioStreamPlayer.svg index 98a1650f6f..a3c4ad8e35 100644 --- a/editor/icons/AudioStreamPlayer.svg +++ b/editor/icons/AudioStreamPlayer.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16" version="1.0" viewBox="0 0 2.4 2.4"><path fill="#e0e0e0" stroke="#e0e0e0" stroke-linejoin="round" stroke-width=".176" d="M1.382.335.777.858H.204v.673h.564l.614.531Z"/><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-width=".176" d="M1.718.572c.275.374.275.882 0 1.256M1.947.343c.402.5.402 1.213 0 1.714"/></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16" version="1.0" viewBox="0 0 2.4 2.4"><path fill="#e0e0e0" stroke="#e0e0e0" stroke-linejoin="round" stroke-width=".176" d="M1.382.335.777.858H.204v.673h.564l.614.531Z"/><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-width=".176" d="M1.718.572a1.06 1.06 0 0 1 0 1.256M1.947.343c.402.5.402 1.213 0 1.714"/></svg>
\ No newline at end of file diff --git a/editor/icons/AudioStreamPlayer2D.svg b/editor/icons/AudioStreamPlayer2D.svg index 3a5c5c29af..fa60e30238 100644 --- a/editor/icons/AudioStreamPlayer2D.svg +++ b/editor/icons/AudioStreamPlayer2D.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16" version="1.0" viewBox="0 0 2.4 2.4"><path fill="#8da5f3" stroke="#8da5f3" stroke-linejoin="round" stroke-width=".176" d="M1.382.335.777.858H.204v.673h.564l.614.531Z"/><path fill="none" stroke="#8da5f3" stroke-linecap="round" stroke-width=".176" d="M1.718.572c.275.374.275.882 0 1.256M1.947.343c.402.5.402 1.213 0 1.714"/></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16" version="1.0" viewBox="0 0 2.4 2.4"><path fill="#8da5f3" stroke="#8da5f3" stroke-linejoin="round" stroke-width=".176" d="M1.382.335.777.858H.204v.673h.564l.614.531Z"/><path fill="none" stroke="#8da5f3" stroke-linecap="round" stroke-width=".176" d="M1.718.572a1.06 1.06 0 0 1 0 1.256M1.947.343c.402.5.402 1.213 0 1.714"/></svg>
\ No newline at end of file diff --git a/editor/icons/AudioStreamPlayer3D.svg b/editor/icons/AudioStreamPlayer3D.svg index c0480d0c7d..f6be929e61 100644 --- a/editor/icons/AudioStreamPlayer3D.svg +++ b/editor/icons/AudioStreamPlayer3D.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16" version="1.0" viewBox="0 0 2.4 2.4"><path fill="#fc7f7f" stroke="#fc7f7f" stroke-linejoin="round" stroke-width=".176" d="M1.382.335.777.858H.204v.673h.564l.614.531Z"/><path fill="none" stroke="#fc7f7f" stroke-linecap="round" stroke-width=".176" d="M1.718.572c.275.374.275.882 0 1.256M1.947.343c.402.5.402 1.213 0 1.714"/></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16" version="1.0" viewBox="0 0 2.4 2.4"><path fill="#fc7f7f" stroke="#fc7f7f" stroke-linejoin="round" stroke-width=".176" d="M1.382.335.777.858H.204v.673h.564l.614.531Z"/><path fill="none" stroke="#fc7f7f" stroke-linecap="round" stroke-width=".176" d="M1.718.572a1.06 1.06 0 0 1 0 1.256M1.947.343c.402.5.402 1.213 0 1.714"/></svg>
\ No newline at end of file diff --git a/editor/import/3d/resource_importer_scene.cpp b/editor/import/3d/resource_importer_scene.cpp index 55a7208169..2db3fb1f07 100644 --- a/editor/import/3d/resource_importer_scene.cpp +++ b/editor/import/3d/resource_importer_scene.cpp @@ -1959,7 +1959,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "generate/physics", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/navmesh", PROPERTY_HINT_ENUM, "Disabled,Mesh + NavMesh,NavMesh Only"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "physics/body_type", PROPERTY_HINT_ENUM, "Static,Dynamic,Area"), 0)); - r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "physics/shape_type", PROPERTY_HINT_ENUM, "Decompose Convex,Simple Convex,Trimesh,Box,Sphere,Cylinder,Capsule", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 2)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "physics/shape_type", PROPERTY_HINT_ENUM, "Decompose Convex,Simple Convex,Trimesh,Box,Sphere,Cylinder,Capsule,Automatic", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 7)); r_options->push_back(ImportOption(PropertyInfo(Variant::OBJECT, "physics/physics_material_override", PROPERTY_HINT_RESOURCE_TYPE, "PhysicsMaterial"), Variant())); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "physics/layer", PROPERTY_HINT_LAYERS_3D_PHYSICS), 1)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "physics/mask", PROPERTY_HINT_LAYERS_3D_PHYSICS), 1)); diff --git a/editor/import/3d/resource_importer_scene.h b/editor/import/3d/resource_importer_scene.h index cd6a725231..f9124ad289 100644 --- a/editor/import/3d/resource_importer_scene.h +++ b/editor/import/3d/resource_importer_scene.h @@ -212,6 +212,7 @@ class ResourceImporterScene : public ResourceImporter { SHAPE_TYPE_SPHERE, SHAPE_TYPE_CYLINDER, SHAPE_TYPE_CAPSULE, + SHAPE_TYPE_AUTOMATIC, }; static Error _check_resource_save_paths(const Dictionary &p_data); @@ -324,11 +325,21 @@ public: template <typename M> Vector<Ref<Shape3D>> ResourceImporterScene::get_collision_shapes(const Ref<ImporterMesh> &p_mesh, const M &p_options, float p_applied_root_scale) { ERR_FAIL_COND_V(p_mesh.is_null(), Vector<Ref<Shape3D>>()); - ShapeType generate_shape_type = SHAPE_TYPE_TRIMESH; + + ShapeType generate_shape_type = SHAPE_TYPE_AUTOMATIC; if (p_options.has(SNAME("physics/shape_type"))) { generate_shape_type = (ShapeType)p_options[SNAME("physics/shape_type")].operator int(); } + if (generate_shape_type == SHAPE_TYPE_AUTOMATIC) { + BodyType body_type = BODY_TYPE_STATIC; + if (p_options.has(SNAME("physics/body_type"))) { + body_type = (BodyType)p_options[SNAME("physics/body_type")].operator int(); + } + + generate_shape_type = body_type == BODY_TYPE_DYNAMIC ? SHAPE_TYPE_DECOMPOSE_CONVEX : SHAPE_TYPE_TRIMESH; + } + if (generate_shape_type == SHAPE_TYPE_DECOMPOSE_CONVEX) { Ref<MeshConvexDecompositionSettings> decomposition_settings = Ref<MeshConvexDecompositionSettings>(); decomposition_settings.instantiate(); @@ -482,11 +493,20 @@ template <typename M> Transform3D ResourceImporterScene::get_collision_shapes_transform(const M &p_options) { Transform3D transform; - ShapeType generate_shape_type = SHAPE_TYPE_TRIMESH; + ShapeType generate_shape_type = SHAPE_TYPE_AUTOMATIC; if (p_options.has(SNAME("physics/shape_type"))) { generate_shape_type = (ShapeType)p_options[SNAME("physics/shape_type")].operator int(); } + if (generate_shape_type == SHAPE_TYPE_AUTOMATIC) { + BodyType body_type = BODY_TYPE_STATIC; + if (p_options.has(SNAME("physics/body_type"))) { + body_type = (BodyType)p_options[SNAME("physics/body_type")].operator int(); + } + + generate_shape_type = body_type == BODY_TYPE_DYNAMIC ? SHAPE_TYPE_DECOMPOSE_CONVEX : SHAPE_TYPE_TRIMESH; + } + if (generate_shape_type == SHAPE_TYPE_BOX || generate_shape_type == SHAPE_TYPE_SPHERE || generate_shape_type == SHAPE_TYPE_CYLINDER || diff --git a/editor/plugins/abstract_polygon_2d_editor.cpp b/editor/plugins/abstract_polygon_2d_editor.cpp index 97f628c4a4..f73c494b25 100644 --- a/editor/plugins/abstract_polygon_2d_editor.cpp +++ b/editor/plugins/abstract_polygon_2d_editor.cpp @@ -570,7 +570,7 @@ void AbstractPolygon2DEditor::forward_canvas_draw_over_viewport(Control *p_overl const Vector2 p = (vertex == edited_point) ? edited_point.pos : (points[i] + offset); const Vector2 point = xform.xform(p); - const Color overlay_modulate = vertex == active_point ? Color(0.5, 1, 2) : Color(1, 1, 1); + const Color overlay_modulate = vertex == active_point ? Color(0.4, 1, 1) : Color(1, 1, 1); p_overlay->draw_texture(handle, point - handle->get_size() * 0.5, overlay_modulate); if (vertex == hover_point) { diff --git a/editor/plugins/animation_library_editor.cpp b/editor/plugins/animation_library_editor.cpp index afe7ea83d8..b07db993ba 100644 --- a/editor/plugins/animation_library_editor.cpp +++ b/editor/plugins/animation_library_editor.cpp @@ -781,6 +781,27 @@ void AnimationLibraryEditor::_update_editor(Object *p_mixer) { emit_signal("update_editor", p_mixer); } +void AnimationLibraryEditor::shortcut_input(const Ref<InputEvent> &p_event) { + const Ref<InputEventKey> k = p_event; + if (k.is_valid() && k->is_pressed()) { + bool handled = false; + + if (ED_IS_SHORTCUT("ui_undo", p_event)) { + EditorNode::get_singleton()->undo(); + handled = true; + } + + if (ED_IS_SHORTCUT("ui_redo", p_event)) { + EditorNode::get_singleton()->redo(); + handled = true; + } + + if (handled) { + set_input_as_handled(); + } + } +} + void AnimationLibraryEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("_update_editor", "mixer"), &AnimationLibraryEditor::_update_editor); ADD_SIGNAL(MethodInfo("update_editor")); @@ -788,6 +809,7 @@ void AnimationLibraryEditor::_bind_methods() { AnimationLibraryEditor::AnimationLibraryEditor() { set_title(TTR("Edit Animation Libraries")); + set_process_shortcut_input(true); file_dialog = memnew(EditorFileDialog); add_child(file_dialog); diff --git a/editor/plugins/animation_library_editor.h b/editor/plugins/animation_library_editor.h index c8d9274f4f..beb34c6343 100644 --- a/editor/plugins/animation_library_editor.h +++ b/editor/plugins/animation_library_editor.h @@ -113,6 +113,7 @@ class AnimationLibraryEditor : public AcceptDialog { protected: void _notification(int p_what); void _update_editor(Object *p_mixer); + virtual void shortcut_input(const Ref<InputEvent> &p_event) override; static void _bind_methods(); public: diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp index 484d2b1fff..02fa582da4 100644 --- a/editor/plugins/animation_player_editor_plugin.cpp +++ b/editor/plugins/animation_player_editor_plugin.cpp @@ -1395,23 +1395,14 @@ void AnimationPlayerEditor::_current_animation_changed(const String &p_name) { void AnimationPlayerEditor::_animation_key_editor_anim_len_changed(float p_len) { frame->set_max(p_len); } - -void AnimationPlayerEditor::_animation_key_editor_seek(float p_pos, bool p_timeline_only) { +void AnimationPlayerEditor::_animation_key_editor_seek(float p_pos, bool p_timeline_only, bool p_update_position_only) { timeline_position = p_pos; - if (!is_visible_in_tree()) { - return; - } - - if (!player) { - return; - } - - if (player->is_playing()) { - return; - } - - if (!player->has_animation(player->get_assigned_animation())) { + if (!is_visible_in_tree() || + p_update_position_only || + !player || + player->is_playing() || + !player->has_animation(player->get_assigned_animation())) { return; } diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h index e624522566..4a3b1f37ab 100644 --- a/editor/plugins/animation_player_editor_plugin.h +++ b/editor/plugins/animation_player_editor_plugin.h @@ -214,7 +214,7 @@ class AnimationPlayerEditor : public VBoxContainer { void _animation_player_changed(Object *p_pl); void _animation_libraries_updated(); - void _animation_key_editor_seek(float p_pos, bool p_timeline_only = false); + void _animation_key_editor_seek(float p_pos, bool p_timeline_only = false, bool p_update_position_only = false); void _animation_key_editor_anim_len_changed(float p_len); virtual void shortcut_input(const Ref<InputEvent> &p_ev) override; diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index ee5d90b1f9..294df95874 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -780,19 +780,24 @@ bool CanvasItemEditor::_select_click_on_item(CanvasItem *item, Point2 p_click_po return still_selected; } -List<CanvasItem *> CanvasItemEditor::_get_edited_canvas_items(bool retrieve_locked, bool remove_canvas_item_if_parent_in_selection) const { +List<CanvasItem *> CanvasItemEditor::_get_edited_canvas_items(bool p_retrieve_locked, bool p_remove_canvas_item_if_parent_in_selection, bool *r_has_locked_items) const { List<CanvasItem *> selection; for (const KeyValue<Node *, Object *> &E : editor_selection->get_selection()) { CanvasItem *ci = Object::cast_to<CanvasItem>(E.key); - if (ci && ci->is_visible_in_tree() && ci->get_viewport() == EditorNode::get_singleton()->get_scene_root() && (retrieve_locked || !_is_node_locked(ci))) { - CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data<CanvasItemEditorSelectedItem>(ci); - if (se) { - selection.push_back(ci); + if (ci) { + if (ci->is_visible_in_tree() && ci->get_viewport() == EditorNode::get_singleton()->get_scene_root() && (p_retrieve_locked || !_is_node_locked(ci))) { + CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data<CanvasItemEditorSelectedItem>(ci); + if (se) { + selection.push_back(ci); + } + } else if (r_has_locked_items) { + // CanvasItem is selected, but can't be interacted with. + *r_has_locked_items = true; } } } - if (remove_canvas_item_if_parent_in_selection) { + if (p_remove_canvas_item_if_parent_in_selection) { List<CanvasItem *> filtered_selection; for (CanvasItem *E : selection) { if (!selection.find(E->get_parent())) { @@ -1454,7 +1459,8 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref<InputEvent> &p_event) { if (drag_type == DRAG_NONE) { if (b.is_valid() && b->get_button_index() == MouseButton::LEFT && b->is_pressed()) { if ((b->is_command_or_control_pressed() && !b->is_alt_pressed() && tool == TOOL_SELECT) || tool == TOOL_ROTATE) { - List<CanvasItem *> selection = _get_edited_canvas_items(); + bool has_locked_items = false; + List<CanvasItem *> selection = _get_edited_canvas_items(false, true, &has_locked_items); // Remove not movable nodes for (CanvasItem *E : selection) { @@ -1477,6 +1483,11 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref<InputEvent> &p_event) { } _save_canvas_item_state(drag_selection); return true; + } else { + if (has_locked_items) { + EditorToaster::get_singleton()->popup_str(TTR(locked_transform_warning), EditorToaster::SEVERITY_WARNING); + } + return has_locked_items; } } } @@ -1687,13 +1698,18 @@ bool CanvasItemEditor::_gui_input_anchors(const Ref<InputEvent> &p_event) { _commit_canvas_item_state( drag_selection, vformat(TTR("Move CanvasItem \"%s\" Anchor"), drag_selection.front()->get()->get_name())); + snap_target[0] = SNAP_TARGET_NONE; + snap_target[1] = SNAP_TARGET_NONE; _reset_drag(); + viewport->queue_redraw(); return true; } // Cancel a drag if (ED_IS_SHORTCUT("canvas_item_editor/cancel_transform", p_event) || (b.is_valid() && b->get_button_index() == MouseButton::RIGHT && b->is_pressed())) { _restore_canvas_item_state(drag_selection); + snap_target[0] = SNAP_TARGET_NONE; + snap_target[1] = SNAP_TARGET_NONE; _reset_drag(); viewport->queue_redraw(); return true; @@ -1912,7 +1928,8 @@ bool CanvasItemEditor::_gui_input_scale(const Ref<InputEvent> &p_event) { // Drag resize handles if (drag_type == DRAG_NONE) { if (b.is_valid() && b->get_button_index() == MouseButton::LEFT && b->is_pressed() && ((b->is_alt_pressed() && b->is_command_or_control_pressed()) || tool == TOOL_SCALE)) { - List<CanvasItem *> selection = _get_edited_canvas_items(); + bool has_locked_items = false; + List<CanvasItem *> selection = _get_edited_canvas_items(false, true, &has_locked_items); if (selection.size() == 1) { CanvasItem *ci = selection.front()->get(); @@ -1941,6 +1958,11 @@ bool CanvasItemEditor::_gui_input_scale(const Ref<InputEvent> &p_event) { _save_canvas_item_state(drag_selection); return true; } + } else { + if (has_locked_items) { + EditorToaster::get_singleton()->popup_str(TTR(locked_transform_warning), EditorToaster::SEVERITY_WARNING); + } + return has_locked_items; } } } @@ -2051,7 +2073,8 @@ bool CanvasItemEditor::_gui_input_move(const Ref<InputEvent> &p_event) { //Start moving the nodes if (b.is_valid() && b->get_button_index() == MouseButton::LEFT && b->is_pressed()) { if ((b->is_alt_pressed() && !b->is_command_or_control_pressed()) || tool == TOOL_MOVE) { - List<CanvasItem *> selection = _get_edited_canvas_items(); + bool has_locked_items = false; + List<CanvasItem *> selection = _get_edited_canvas_items(false, true, &has_locked_items); if (selection.size() > 0) { drag_selection.clear(); @@ -2084,6 +2107,11 @@ bool CanvasItemEditor::_gui_input_move(const Ref<InputEvent> &p_event) { _save_canvas_item_state(drag_selection); return true; + } else { + if (has_locked_items) { + EditorToaster::get_singleton()->popup_str(TTR(locked_transform_warning), EditorToaster::SEVERITY_WARNING); + } + return has_locked_items; } } } @@ -5171,7 +5199,7 @@ CanvasItemEditor::CanvasItemEditor() { SceneTreeDock::get_singleton()->connect("node_created", callable_mp(this, &CanvasItemEditor::_adjust_new_node_position)); SceneTreeDock::get_singleton()->connect("add_node_used", callable_mp(this, &CanvasItemEditor::_reset_create_position)); - // Add some margin to the sides for better esthetics. + // Add some margin to the sides for better aesthetics. // This prevents the first button's hover/pressed effect from "touching" the panel's border, // which looks ugly. MarginContainer *toolbar_margin = memnew(MarginContainer); diff --git a/editor/plugins/canvas_item_editor_plugin.h b/editor/plugins/canvas_item_editor_plugin.h index a9de5e9a0b..bae9efebc9 100644 --- a/editor/plugins/canvas_item_editor_plugin.h +++ b/editor/plugins/canvas_item_editor_plugin.h @@ -188,6 +188,8 @@ private: GRID_VISIBILITY_HIDE, }; + const String locked_transform_warning = TTRC("All selected CanvasItems are either invisible or locked in some way and can't be transformed."); + bool selection_menu_additive_selection = false; Tool tool = TOOL_SELECT; @@ -430,7 +432,7 @@ private: ThemePreviewMode theme_preview = THEME_PREVIEW_PROJECT; void _switch_theme_preview(int p_mode); - List<CanvasItem *> _get_edited_canvas_items(bool retrieve_locked = false, bool remove_canvas_item_if_parent_in_selection = true) const; + List<CanvasItem *> _get_edited_canvas_items(bool p_retrieve_locked = false, bool p_remove_canvas_item_if_parent_in_selection = true, bool *r_has_locked_items = nullptr) const; Rect2 _get_encompassing_rect_from_list(const List<CanvasItem *> &p_list); void _expand_encompassing_rect_using_children(Rect2 &r_rect, const Node *p_node, bool &r_first, const Transform2D &p_parent_xform = Transform2D(), const Transform2D &p_canvas_xform = Transform2D(), bool include_locked_nodes = true); Rect2 _get_encompassing_rect(const Node *p_node); diff --git a/editor/plugins/cast_2d_editor_plugin.cpp b/editor/plugins/cast_2d_editor_plugin.cpp index c9d7ff8e08..3da7d4a7dc 100644 --- a/editor/plugins/cast_2d_editor_plugin.cpp +++ b/editor/plugins/cast_2d_editor_plugin.cpp @@ -102,7 +102,6 @@ bool Cast2DEditor::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { node->set("target_position", point); canvas_item_editor->update_viewport(); - node->notify_property_list_changed(); return true; } diff --git a/editor/plugins/editor_plugin.cpp b/editor/plugins/editor_plugin.cpp index 005407e188..d9f60e155d 100644 --- a/editor/plugins/editor_plugin.cpp +++ b/editor/plugins/editor_plugin.cpp @@ -414,13 +414,19 @@ void EditorPlugin::remove_translation_parser_plugin(const Ref<EditorTranslationP void EditorPlugin::add_import_plugin(const Ref<EditorImportPlugin> &p_importer, bool p_first_priority) { ERR_FAIL_COND(!p_importer.is_valid()); ResourceFormatImporter::get_singleton()->add_importer(p_importer, p_first_priority); - callable_mp(EditorFileSystem::get_singleton(), &EditorFileSystem::scan).call_deferred(); + // Plugins are now loaded during the first scan. It's important not to start another scan, + // even a deferred one, as it would cause a scan during a scan at the next main thread iteration. + if (!EditorFileSystem::get_singleton()->doing_first_scan()) { + callable_mp(EditorFileSystem::get_singleton(), &EditorFileSystem::scan).call_deferred(); + } } void EditorPlugin::remove_import_plugin(const Ref<EditorImportPlugin> &p_importer) { ERR_FAIL_COND(!p_importer.is_valid()); ResourceFormatImporter::get_singleton()->remove_importer(p_importer); - if (!EditorNode::get_singleton()->is_exiting()) { + // Plugins are now loaded during the first scan. It's important not to start another scan, + // even a deferred one, as it would cause a scan during a scan at the next main thread iteration. + if (!EditorNode::get_singleton()->is_exiting() && !EditorFileSystem::get_singleton()->doing_first_scan()) { callable_mp(EditorFileSystem::get_singleton(), &EditorFileSystem::scan).call_deferred(); } } diff --git a/editor/plugins/font_config_plugin.cpp b/editor/plugins/font_config_plugin.cpp index 15b268337f..e6ce63fe36 100644 --- a/editor/plugins/font_config_plugin.cpp +++ b/editor/plugins/font_config_plugin.cpp @@ -386,15 +386,8 @@ EditorPropertyFontMetaOverride::EditorPropertyFontMetaOverride(bool p_script) { void EditorPropertyOTVariation::_property_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { if (p_property.begins_with("keys")) { Dictionary dict = object->get_dict(); - Dictionary defaults_dict = object->get_defaults(); int key = p_property.get_slice("/", 1).to_int(); dict[key] = (int)p_value; - if (defaults_dict.has(key)) { - Vector3i range = defaults_dict[key]; - if (range.z == (int)p_value) { - dict.erase(key); - } - } emit_changed(get_edited_property(), dict, "", true); @@ -422,6 +415,14 @@ void EditorPropertyOTVariation::update_property() { Dictionary supported = (fd.is_valid()) ? fd->get_supported_variation_list() : Dictionary(); + for (int i = 0; i < supported.size(); i++) { + int name_tag = supported.get_key_at_index(i); + Vector3i range = supported.get_value_at_index(i); + if ((dict.has(name_tag) && dict[name_tag].get_type() == Variant::NIL) || !dict.has(name_tag)) { + dict[name_tag] = range.z; + } + } + edit->set_text(vformat(TTR("Variation Coordinates (%d)"), supported.size())); bool unfolded = get_edited_object()->editor_is_section_unfolded(get_edited_property()); @@ -481,7 +482,21 @@ void EditorPropertyOTVariation::update_property() { prop->set_object_and_property(object.ptr(), "keys/" + itos(name_tag)); String name = TS->tag_to_name(name_tag); - prop->set_label(name.capitalize()); + String name_cap; + { + String aux = name.replace("_", " ").strip_edges(); + for (int j = 0; j < aux.get_slice_count(" "); j++) { + String slice = aux.get_slicec(' ', j); + if (slice.length() > 0) { + slice[0] = String::char_uppercase(slice[0]); + if (i > 0) { + name_cap += " "; + } + name_cap += slice; + } + } + } + prop->set_label(name_cap); prop->set_tooltip_text(name); prop->set_selectable(false); @@ -935,6 +950,12 @@ void FontPreview::_notification(int p_what) { font->draw_string(get_canvas_item(), Point2(0, font->get_height(font_size) + 2 * EDSCALE), TTR("Unable to preview font"), HORIZONTAL_ALIGNMENT_CENTER, get_size().x, font_size, text_color); } } break; + + case NOTIFICATION_EXIT_TREE: { + if (prev_font.is_valid()) { + prev_font->disconnect_changed(callable_mp(this, &FontPreview::_preview_changed)); + } + } break; } } @@ -945,7 +966,17 @@ Size2 FontPreview::get_minimum_size() const { } void FontPreview::set_data(const Ref<Font> &p_f) { + if (prev_font.is_valid()) { + prev_font->disconnect_changed(callable_mp(this, &FontPreview::_preview_changed)); + } prev_font = p_f; + if (prev_font.is_valid()) { + prev_font->connect_changed(callable_mp(this, &FontPreview::_preview_changed)); + } + queue_redraw(); +} + +void FontPreview::_preview_changed() { queue_redraw(); } diff --git a/editor/plugins/font_config_plugin.h b/editor/plugins/font_config_plugin.h index 7b2d26da4a..4e798fc3e8 100644 --- a/editor/plugins/font_config_plugin.h +++ b/editor/plugins/font_config_plugin.h @@ -225,6 +225,8 @@ protected: Ref<Font> prev_font; + void _preview_changed(); + public: virtual Size2 get_minimum_size() const override; diff --git a/editor/plugins/gradient_texture_2d_editor_plugin.cpp b/editor/plugins/gradient_texture_2d_editor_plugin.cpp index 9ccbea4c3c..7e22e1209c 100644 --- a/editor/plugins/gradient_texture_2d_editor_plugin.cpp +++ b/editor/plugins/gradient_texture_2d_editor_plugin.cpp @@ -213,7 +213,7 @@ void GradientTexture2DEdit::_draw() { } // Draw handles. - const Color focus_modulate = Color(0.5, 1, 2); + const Color focus_modulate = Color(0.4, 1, 1); bool modulate_handle_from = grabbed == HANDLE_FROM || hovered == HANDLE_FROM; bool modulate_handle_to = grabbed == HANDLE_TO || hovered == HANDLE_TO; draw_texture(fill_from_icon, (_get_handle_pos(HANDLE_FROM) - handle_size / 2).round(), modulate_handle_from ? focus_modulate : Color(1, 1, 1)); diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index d7d51d6a04..72eea8a27e 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -325,11 +325,11 @@ void ViewportRotationControl::_notification(int p_what) { } void ViewportRotationControl::_draw() { - const Vector2i center = get_size() / 2.0; + const Vector2 center = get_size() / 2.0; const real_t radius = get_size().x / 2.0; if (focused_axis > -2 || orbiting_index != -1) { - draw_circle(center, radius, Color(0.5, 0.5, 0.5, 0.25)); + draw_circle(center, radius, Color(0.5, 0.5, 0.5, 0.25), true, -1.0, true); } Vector<Axis2D> axis_to_draw; @@ -345,34 +345,42 @@ void ViewportRotationControl::_draw_axis(const Axis2D &p_axis) { const int direction = p_axis.axis % 3; const Color axis_color = axis_colors[direction]; - const double alpha = focused ? 1.0 : ((p_axis.z_axis + 1.0) / 2.0) * 0.5 + 0.5; - const Color c = focused ? Color(0.9, 0.9, 0.9) : Color(axis_color, alpha); + const double min_alpha = 0.35; + const double alpha = focused ? 1.0 : Math::remap((p_axis.z_axis + 1.0) / 2.0, 0, 0.5, min_alpha, 1.0); + const Color c = focused ? Color(axis_color.lightened(0.75), 1.0) : Color(axis_color, alpha); if (positive) { // Draw axis lines for the positive axes. - const Vector2i center = get_size() / 2.0; - draw_line(center, p_axis.screen_point, c, 1.5 * EDSCALE); + const Vector2 center = get_size() / 2.0; + const Vector2 diff = p_axis.screen_point - center; + const float line_length = MAX(diff.length() - AXIS_CIRCLE_RADIUS - 0.5 * EDSCALE, 0); - draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS, c); + draw_line(center + diff.limit_length(0.5 * EDSCALE), center + diff.limit_length(line_length), c, 1.5 * EDSCALE, true); + + draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS, c, true, -1.0, true); // Draw the axis letter for the positive axes. const String axis_name = direction == 0 ? "X" : (direction == 1 ? "Y" : "Z"); - draw_char(get_theme_font(SNAME("rotation_control"), EditorStringName(EditorFonts)), p_axis.screen_point + Vector2i(Math::round(-4.0 * EDSCALE), Math::round(5.0 * EDSCALE)), axis_name, get_theme_font_size(SNAME("rotation_control_size"), EditorStringName(EditorFonts)), Color(0.0, 0.0, 0.0, alpha)); + const Ref<Font> &font = get_theme_font(SNAME("rotation_control"), EditorStringName(EditorFonts)); + const int font_size = get_theme_font_size(SNAME("rotation_control_size"), EditorStringName(EditorFonts)); + const Size2 char_size = font->get_char_size(axis_name[0], font_size); + const Vector2 char_offset = Vector2(-char_size.width / 2.0, char_size.height * 0.25); + draw_char(font, p_axis.screen_point + char_offset, axis_name, font_size, Color(0.0, 0.0, 0.0, alpha * 0.6)); } else { // Draw an outline around the negative axes. - draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS, c); - draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS * 0.8, c.darkened(0.4)); + draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS, c, true, -1.0, true); + draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS * 0.8, c.darkened(0.4), true, -1.0, true); } } void ViewportRotationControl::_get_sorted_axis(Vector<Axis2D> &r_axis) { - const Vector2i center = get_size() / 2.0; + const Vector2 center = get_size() / 2.0; const real_t radius = get_size().x / 2.0 - AXIS_CIRCLE_RADIUS - 2.0 * EDSCALE; const Basis camera_basis = viewport->to_camera_transform(viewport->cursor).get_basis().inverse(); for (int i = 0; i < 3; ++i) { Vector3 axis_3d = camera_basis.get_column(i); - Vector2i axis_vector = Vector2(axis_3d.x, -axis_3d.y) * radius; + Vector2 axis_vector = Vector2(axis_3d.x, -axis_3d.y) * radius; if (Math::abs(axis_3d.z) <= 1.0) { Axis2D pos_axis; @@ -2895,8 +2903,8 @@ void Node3DEditorViewport::_notification(int p_what) { } bool show_info = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_INFORMATION)); - if (show_info != info_label->is_visible()) { - info_label->set_visible(show_info); + if (show_info != info_panel->is_visible()) { + info_panel->set_visible(show_info); } Camera3D *current_camera; @@ -3079,7 +3087,7 @@ void Node3DEditorViewport::_notification(int p_what) { frame_time_gradient->set_color(1, get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); frame_time_gradient->set_color(2, get_theme_color(SNAME("error_color"), EditorStringName(Editor))); - info_label->add_theme_style_override(CoreStringName(normal), information_3d_stylebox); + info_panel->add_theme_style_override(SceneStringName(panel), information_3d_stylebox); frame_time_panel->add_theme_style_override(SceneStringName(panel), information_3d_stylebox); // Set a minimum width to prevent the width from changing all the time @@ -5374,15 +5382,19 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p bottom_center_vbox->set_v_grow_direction(GROW_DIRECTION_BEGIN); surface->add_child(bottom_center_vbox); + info_panel = memnew(PanelContainer); + info_panel->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -90 * EDSCALE); + info_panel->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -90 * EDSCALE); + info_panel->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, -10 * EDSCALE); + info_panel->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -10 * EDSCALE); + info_panel->set_h_grow_direction(GROW_DIRECTION_BEGIN); + info_panel->set_v_grow_direction(GROW_DIRECTION_BEGIN); + info_panel->set_mouse_filter(MOUSE_FILTER_IGNORE); + surface->add_child(info_panel); + info_panel->hide(); + info_label = memnew(Label); - info_label->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -90 * EDSCALE); - info_label->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -90 * EDSCALE); - info_label->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, -10 * EDSCALE); - info_label->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -10 * EDSCALE); - info_label->set_h_grow_direction(GROW_DIRECTION_BEGIN); - info_label->set_v_grow_direction(GROW_DIRECTION_BEGIN); - surface->add_child(info_label); - info_label->hide(); + info_panel->add_child(info_label); cinema_label = memnew(Label); cinema_label->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 10 * EDSCALE); @@ -5432,6 +5444,7 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p frame_time_gradient->add_point(0.5, Color()); top_right_vbox = memnew(VBoxContainer); + top_right_vbox->add_theme_constant_override("separation", 10.0 * EDSCALE); top_right_vbox->set_anchors_and_offsets_preset(PRESET_TOP_RIGHT, PRESET_MODE_MINSIZE, 10.0 * EDSCALE); top_right_vbox->set_h_grow_direction(GROW_DIRECTION_BEGIN); @@ -5466,6 +5479,7 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p top_right_vbox->add_child(rotation_control); frame_time_panel = memnew(PanelContainer); + frame_time_panel->set_mouse_filter(MOUSE_FILTER_IGNORE); top_right_vbox->add_child(frame_time_panel); frame_time_panel->hide(); @@ -8460,7 +8474,7 @@ Node3DEditor::Node3DEditor() { camera_override_viewport_id = 0; - // Add some margin to the sides for better esthetics. + // Add some margin to the sides for better aesthetics. // This prevents the first button's hover/pressed effect from "touching" the panel's border, // which looks ugly. MarginContainer *toolbar_margin = memnew(MarginContainer); @@ -8635,7 +8649,8 @@ Node3DEditor::Node3DEditor() { ED_SHORTCUT("spatial_editor/insert_anim_key", TTR("Insert Animation Key"), Key::K); ED_SHORTCUT("spatial_editor/focus_origin", TTR("Focus Origin"), Key::O); ED_SHORTCUT("spatial_editor/focus_selection", TTR("Focus Selection"), Key::F); - ED_SHORTCUT("spatial_editor/align_transform_with_view", TTR("Align Transform with View"), KeyModifierMask::ALT + KeyModifierMask::CMD_OR_CTRL + Key::M); + ED_SHORTCUT_ARRAY("spatial_editor/align_transform_with_view", TTR("Align Transform with View"), + { int32_t(KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL | Key::KP_0), int32_t(KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL | Key::M) }); ED_SHORTCUT("spatial_editor/align_rotation_with_view", TTR("Align Rotation with View"), KeyModifierMask::ALT + KeyModifierMask::CMD_OR_CTRL + Key::F); ED_SHORTCUT("spatial_editor/freelook_toggle", TTR("Toggle Freelook"), KeyModifierMask::SHIFT + Key::F); ED_SHORTCUT("spatial_editor/decrease_fov", TTR("Decrease Field of View"), KeyModifierMask::CMD_OR_CTRL + Key::EQUAL); // Usually direct access key for `KEY_PLUS`. diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h index 859d075732..5bd14748c0 100644 --- a/editor/plugins/node_3d_editor_plugin.h +++ b/editor/plugins/node_3d_editor_plugin.h @@ -65,7 +65,7 @@ class ViewportRotationControl : public Control { GDCLASS(ViewportRotationControl, Control); struct Axis2D { - Vector2i screen_point; + Vector2 screen_point; float z_axis = -99.0; int axis = -1; }; @@ -241,6 +241,7 @@ private: real_t freelook_speed; Vector2 previous_mouse_position; + PanelContainer *info_panel = nullptr; Label *info_label = nullptr; Label *cinema_label = nullptr; Label *locked_label = nullptr; diff --git a/editor/plugins/polygon_2d_editor_plugin.cpp b/editor/plugins/polygon_2d_editor_plugin.cpp index b5db7bef70..e442c37edd 100644 --- a/editor/plugins/polygon_2d_editor_plugin.cpp +++ b/editor/plugins/polygon_2d_editor_plugin.cpp @@ -52,6 +52,31 @@ #include "scene/gui/texture_rect.h" #include "scene/gui/view_panner.h" +class UVEditDialog : public AcceptDialog { + GDCLASS(UVEditDialog, AcceptDialog); + + void shortcut_input(const Ref<InputEvent> &p_event) override { + const Ref<InputEventKey> k = p_event; + if (k.is_valid() && k->is_pressed()) { + bool handled = false; + + if (ED_IS_SHORTCUT("ui_undo", p_event)) { + EditorNode::get_singleton()->undo(); + handled = true; + } + + if (ED_IS_SHORTCUT("ui_redo", p_event)) { + EditorNode::get_singleton()->redo(); + handled = true; + } + + if (handled) { + set_input_as_handled(); + } + } + } +}; + Node2D *Polygon2DEditor::_get_node() const { return node; } @@ -1305,9 +1330,10 @@ Polygon2DEditor::Polygon2DEditor() { button_uv->connect(SceneStringName(pressed), callable_mp(this, &Polygon2DEditor::_menu_option).bind(MODE_EDIT_UV)); uv_mode = UV_MODE_EDIT_POINT; - uv_edit = memnew(AcceptDialog); - add_child(uv_edit); + uv_edit = memnew(UVEditDialog); uv_edit->set_title(TTR("Polygon 2D UV Editor")); + uv_edit->set_process_shortcut_input(true); + add_child(uv_edit); uv_edit->connect(SceneStringName(confirmed), callable_mp(this, &Polygon2DEditor::_uv_edit_popup_hide)); uv_edit->connect("canceled", callable_mp(this, &Polygon2DEditor::_uv_edit_popup_hide)); diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index 3d043909b2..c51eb44aee 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -272,7 +272,7 @@ void ScriptEditorBase::_bind_methods() { ADD_SIGNAL(MethodInfo("request_help", PropertyInfo(Variant::STRING, "topic"))); ADD_SIGNAL(MethodInfo("request_open_script_at_line", PropertyInfo(Variant::OBJECT, "script"), PropertyInfo(Variant::INT, "line"))); ADD_SIGNAL(MethodInfo("request_save_history")); - ADD_SIGNAL(MethodInfo("request_save_previous_state", PropertyInfo(Variant::INT, "line"))); + ADD_SIGNAL(MethodInfo("request_save_previous_state", PropertyInfo(Variant::DICTIONARY, "state"))); ADD_SIGNAL(MethodInfo("go_to_help", PropertyInfo(Variant::STRING, "what"))); ADD_SIGNAL(MethodInfo("search_in_files_requested", PropertyInfo(Variant::STRING, "text"))); ADD_SIGNAL(MethodInfo("replace_in_files_requested", PropertyInfo(Variant::STRING, "text"))); @@ -1012,8 +1012,6 @@ void ScriptEditor::_resave_scripts(const String &p_str) { se->trim_final_newlines(); } - se->insert_final_newline(); - if (convert_indent_on_save) { se->convert_indent(); } @@ -1410,8 +1408,6 @@ void ScriptEditor::_menu_option(int p_option) { current->trim_final_newlines(); } - current->insert_final_newline(); - if (convert_indent_on_save) { current->convert_indent(); } @@ -2614,8 +2610,6 @@ void ScriptEditor::save_current_script() { current->trim_final_newlines(); } - current->insert_final_newline(); - if (convert_indent_on_save) { current->convert_indent(); } @@ -2662,8 +2656,6 @@ void ScriptEditor::save_all_scripts() { se->trim_final_newlines(); } - se->insert_final_newline(); - if (!se->is_unsaved()) { continue; } @@ -2713,6 +2705,7 @@ void ScriptEditor::apply_scripts() const { if (!se) { continue; } + se->insert_final_newline(); se->apply_code(); } } @@ -4056,6 +4049,7 @@ ScriptEditor::ScriptEditor(WindowWrapper *p_wrapper) { filename = memnew(Label); filename->set_clip_text(true); filename->set_h_size_flags(SIZE_EXPAND_FILL); + filename->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); filename->add_theme_style_override(CoreStringName(normal), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(CoreStringName(normal), SNAME("LineEdit"))); buttons_hbox->add_child(filename); diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 059e177874..96127ec93e 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -1819,15 +1819,25 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data CodeEdit *te = code_editor->get_text_editor(); Point2i pos = te->get_line_column_at_pos(p_point); - int row = pos.y; - int col = pos.x; + int drop_at_line = pos.y; + int drop_at_column = pos.x; + int selection_index = te->get_selection_at_line_column(drop_at_line, drop_at_column); + + bool line_will_be_empty = false; + if (selection_index >= 0) { + // Dropped on a selection, it will be replaced. + drop_at_line = te->get_selection_from_line(selection_index); + drop_at_column = te->get_selection_from_column(selection_index); + line_will_be_empty = drop_at_column <= te->get_first_non_whitespace_column(drop_at_line) && te->get_selection_to_column(selection_index) == te->get_line(te->get_selection_to_line(selection_index)).length(); + } + + String text_to_drop; const bool drop_modifier_pressed = Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL); - const String &line = te->get_line(row); - const bool is_empty_line = line.is_empty() || te->get_first_non_whitespace_column(row) == line.length(); + const String &line = te->get_line(drop_at_line); + const bool is_empty_line = line_will_be_empty || line.is_empty() || te->get_first_non_whitespace_column(drop_at_line) == line.length(); if (d.has("type") && String(d["type"]) == "resource") { - te->remove_secondary_carets(); Ref<Resource> resource = d["resource"]; if (resource.is_null()) { return; @@ -1840,7 +1850,6 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data return; } - String text_to_drop; if (drop_modifier_pressed) { if (resource->is_built_in()) { String warning = TTR("Preloading internal resources is not supported."); @@ -1851,19 +1860,10 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data } else { text_to_drop = _quote_drop_data(path); } - - te->set_caret_line(row); - te->set_caret_column(col); - te->insert_text_at_caret(text_to_drop); - te->grab_focus(); } if (d.has("type") && (String(d["type"]) == "files" || String(d["type"]) == "files_and_dirs")) { - te->remove_secondary_carets(); - Array files = d["files"]; - String text_to_drop; - for (int i = 0; i < files.size(); i++) { const String &path = String(files[i]); @@ -1883,15 +1883,9 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data text_to_drop += is_empty_line ? "\n" : ", "; } } - - te->set_caret_line(row); - te->set_caret_column(col); - te->insert_text_at_caret(text_to_drop); - te->grab_focus(); } if (d.has("type") && String(d["type"]) == "nodes") { - te->remove_secondary_carets(); Node *scene_root = get_tree()->get_edited_scene_root(); if (!scene_root) { EditorNode::get_singleton()->show_warning(TTR("Can't drop nodes without an open scene.")); @@ -1909,7 +1903,6 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data } Array nodes = d["nodes"]; - String text_to_drop; if (drop_modifier_pressed) { const bool use_type = EDITOR_GET("text_editor/completion/add_type_hints"); @@ -1981,27 +1974,33 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data text_to_drop += (is_unique ? "%" : "$") + path; } } - - te->set_caret_line(row); - te->set_caret_column(col); - te->insert_text_at_caret(text_to_drop); - te->grab_focus(); } if (d.has("type") && String(d["type"]) == "obj_property") { - te->remove_secondary_carets(); - bool add_literal = EDITOR_GET("text_editor/completion/add_node_path_literals"); - String text_to_drop = add_literal ? "^" : ""; + text_to_drop = add_literal ? "^" : ""; // It is unclear whether properties may contain single or double quotes. // Assume here that double-quotes may not exist. We are escaping single-quotes if necessary. text_to_drop += _quote_drop_data(String(d["property"])); + } + + if (text_to_drop.is_empty()) { + return; + } - te->set_caret_line(row); - te->set_caret_column(col); - te->insert_text_at_caret(text_to_drop); - te->grab_focus(); + // Remove drag caret before any actions so it is not included in undo. + te->remove_drag_caret(); + te->begin_complex_operation(); + if (selection_index >= 0) { + te->delete_selection(selection_index); } + te->remove_secondary_carets(); + te->deselect(); + te->set_caret_line(drop_at_line); + te->set_caret_column(drop_at_column); + te->insert_text_at_caret(text_to_drop); + te->end_complex_operation(); + te->grab_focus(); } void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { diff --git a/editor/plugins/sprite_frames_editor_plugin.cpp b/editor/plugins/sprite_frames_editor_plugin.cpp index b33250bcb5..48087e3166 100644 --- a/editor/plugins/sprite_frames_editor_plugin.cpp +++ b/editor/plugins/sprite_frames_editor_plugin.cpp @@ -953,7 +953,7 @@ void SpriteFramesEditor::_sync_animation() { } void SpriteFramesEditor::_select_animation(const String &p_name, bool p_update_node) { - if (!frames->has_animation(p_name)) { + if (frames.is_null() || !frames->has_animation(p_name)) { return; } edited_anim = p_name; @@ -1056,10 +1056,12 @@ void SpriteFramesEditor::_rename_node_animation(EditorUndoRedoManager *undo_redo for (Node *E : nodes) { String current_name = E->call("get_animation"); if (current_name == p_filter) { + undo_redo->force_fixed_history(); // Fixes corner-case when editing SpriteFrames stored as separate file. undo_redo->add_undo_method(E, "set_animation", p_new_animation); } String autoplay_name = E->call("get_autoplay"); if (autoplay_name == p_filter) { + undo_redo->force_fixed_history(); undo_redo->add_undo_method(E, "set_autoplay", p_new_autoplay); } } @@ -1067,10 +1069,12 @@ void SpriteFramesEditor::_rename_node_animation(EditorUndoRedoManager *undo_redo for (Node *E : nodes) { String current_name = E->call("get_animation"); if (current_name == p_filter) { + undo_redo->force_fixed_history(); undo_redo->add_do_method(E, "set_animation", p_new_animation); } String autoplay_name = E->call("get_autoplay"); if (autoplay_name == p_filter) { + undo_redo->force_fixed_history(); undo_redo->add_do_method(E, "set_autoplay", p_new_autoplay); } } diff --git a/editor/plugins/tiles/tile_data_editors.cpp b/editor/plugins/tiles/tile_data_editors.cpp index 62090a5d17..f985bbc629 100644 --- a/editor/plugins/tiles/tile_data_editors.cpp +++ b/editor/plugins/tiles/tile_data_editors.cpp @@ -138,6 +138,19 @@ void GenericTilePolygonEditor::_base_control_draw() { const Ref<Texture2D> add_handle = get_editor_theme_icon(SNAME("EditorHandleAdd")); const Ref<StyleBox> focus_stylebox = get_theme_stylebox(SNAME("Focus"), EditorStringName(EditorStyles)); + // Get the background data. + Rect2 background_region; + TileData *tile_data = nullptr; + + if (background_atlas_source.is_valid()) { + tile_data = background_atlas_source->get_tile_data(background_atlas_coords, background_alternative_id); + ERR_FAIL_NULL(tile_data); + background_region = background_atlas_source->get_tile_texture_region(background_atlas_coords); + } else { + // If no tile was selected yet, use default size. + background_region.size = tile_set->get_tile_size(); + } + // Draw the focus rectangle. if (base_control->has_focus()) { base_control->draw_style_box(focus_stylebox, Rect2(Vector2(), base_control->get_size())); @@ -145,42 +158,57 @@ void GenericTilePolygonEditor::_base_control_draw() { // Draw tile-related things. const Size2 base_tile_size = tile_set->get_tile_size(); - const Size2 tile_size = background_region.size; Transform2D xform; xform.set_origin(base_control->get_size() / 2 + panning); xform.set_scale(Vector2(editor_zoom_widget->get_zoom(), editor_zoom_widget->get_zoom())); base_control->draw_set_transform_matrix(xform); - // Draw the tile shape filled. - Transform2D tile_xform; - tile_xform.set_scale(tile_size); - tile_set->draw_tile_shape(base_control, tile_xform, Color(1.0, 1.0, 1.0, 0.3), true); + // Draw fill rect under texture region. + Rect2 texture_rect(-background_region.size / 2, background_region.size); + if (tile_data) { + texture_rect.position -= tile_data->get_texture_origin(); + } + base_control->draw_rect(texture_rect, Color(1, 1, 1, 0.3)); // Draw the background. - if (background_texture.is_valid()) { + if (tile_data && background_atlas_source->get_texture().is_valid()) { Size2 region_size = background_region.size; - if (background_h_flip) { + if (tile_data->get_flip_h()) { region_size.x = -region_size.x; } - if (background_v_flip) { + if (tile_data->get_flip_v()) { region_size.y = -region_size.y; } - base_control->draw_texture_rect_region(background_texture, Rect2(-background_region.size / 2 - background_offset, region_size), background_region, background_modulate, background_transpose); + base_control->draw_texture_rect_region(background_atlas_source->get_texture(), Rect2(-background_region.size / 2 - tile_data->get_texture_origin(), region_size), background_region, tile_data->get_modulate(), tile_data->get_transpose()); } + // Compute and draw the grid area. + Rect2 grid_area = Rect2(-base_tile_size / 2, base_tile_size); + if (tile_data) { + grid_area.expand_to(-background_region.get_size() / 2 - tile_data->get_texture_origin()); + grid_area.expand_to(background_region.get_size() / 2 - tile_data->get_texture_origin()); + } else { + grid_area.expand_to(-background_region.get_size() / 2); + grid_area.expand_to(background_region.get_size() / 2); + } + base_control->draw_rect(grid_area, Color(1, 1, 1, 0.3), false); + // Draw grid. if (current_snap_option == SNAP_GRID) { Vector2 spacing = base_tile_size / snap_subdivision->get_value(); - Vector2 offset = -tile_size / 2; - int w = snap_subdivision->get_value() * (tile_size / base_tile_size).x; - int h = snap_subdivision->get_value() * (tile_size / base_tile_size).y; - - for (int y = 1; y < h; y++) { - for (int x = 1; x < w; x++) { - base_control->draw_line(Vector2(spacing.x * x, 0) + offset, Vector2(spacing.x * x, tile_size.y) + offset, Color(1, 1, 1, 0.33)); - base_control->draw_line(Vector2(0, spacing.y * y) + offset, Vector2(tile_size.x, spacing.y * y) + offset, Color(1, 1, 1, 0.33)); - } + Vector2 origin = -base_tile_size / 2; + for (real_t y = origin.y; y < grid_area.get_end().y; y += spacing.y) { + base_control->draw_line(Vector2(grid_area.get_position().x, y), Vector2(grid_area.get_end().x, y), Color(1, 1, 1, 0.33)); + } + for (real_t y = origin.y - spacing.y; y > grid_area.get_position().y; y -= spacing.y) { + base_control->draw_line(Vector2(grid_area.get_position().x, y), Vector2(grid_area.get_end().x, y), Color(1, 1, 1, 0.33)); + } + for (real_t x = origin.x; x < grid_area.get_end().x; x += spacing.x) { + base_control->draw_line(Vector2(x, grid_area.get_position().y), Vector2(x, grid_area.get_end().y), Color(1, 1, 1, 0.33)); + } + for (real_t x = origin.x - spacing.x; x > grid_area.get_position().x; x -= spacing.x) { + base_control->draw_line(Vector2(x, grid_area.get_position().y), Vector2(x, grid_area.get_end().y), Color(1, 1, 1, 0.33)); } } @@ -237,7 +265,7 @@ void GenericTilePolygonEditor::_base_control_draw() { for (int i = 0; i < (int)polygons.size(); i++) { const Vector<Vector2> &polygon = polygons[i]; for (int j = 0; j < polygon.size(); j++) { - const Color poly_modulate = (tinted_polygon_index == i && tinted_point_index == j) ? Color(0.5, 1, 2) : Color(1, 1, 1); + const Color poly_modulate = (tinted_polygon_index == i && tinted_point_index == j) ? Color(0.4, 1, 1) : Color(1, 1, 1); base_control->draw_texture(handle, xform.xform(polygon[j]) - handle->get_size() / 2, poly_modulate); } } @@ -253,7 +281,7 @@ void GenericTilePolygonEditor::_base_control_draw() { } if (drag_type == DRAG_TYPE_CREATE_POINT) { - base_control->draw_texture(handle, xform.xform(in_creation_point) - handle->get_size() / 2, Color(0.5, 1, 2)); + base_control->draw_texture(handle, xform.xform(in_creation_point) - handle->get_size() / 2, Color(0.4, 1, 1)); } // Draw the point creation preview in edit mode. @@ -263,6 +291,8 @@ void GenericTilePolygonEditor::_base_control_draw() { // Draw the tile shape line. base_control->draw_set_transform_matrix(xform); + Transform2D tile_xform; + tile_xform.set_scale(base_tile_size); tile_set->draw_tile_shape(base_control, tile_xform, grid_color, false); base_control->draw_set_transform_matrix(Transform2D()); } @@ -721,8 +751,18 @@ void GenericTilePolygonEditor::set_tile_set(Ref<TileSet> p_tile_set) { add_polygon(polygon); } + // Trigger a redraw on tile_set change. + Callable callable = callable_mp((CanvasItem *)base_control, &CanvasItem::queue_redraw); + if (tile_set.is_valid()) { + tile_set->disconnect_changed(callable); + } + tile_set = p_tile_set; + if (tile_set.is_valid()) { + tile_set->connect_changed(callable); + } + // Set the default zoom value. int default_control_y_size = 200 * EDSCALE; Vector2 zoomed_tile = editor_zoom_widget->get_zoom() * tile_set->get_tile_size(); @@ -746,14 +786,11 @@ void GenericTilePolygonEditor::set_tile_set(Ref<TileSet> p_tile_set) { _zoom_changed(); } -void GenericTilePolygonEditor::set_background(Ref<Texture2D> p_texture, Rect2 p_region, Vector2 p_offset, bool p_flip_h, bool p_flip_v, bool p_transpose, Color p_modulate) { - background_texture = p_texture; - background_region = p_region; - background_offset = p_offset; - background_h_flip = p_flip_h; - background_v_flip = p_flip_v; - background_transpose = p_transpose; - background_modulate = p_modulate; +void GenericTilePolygonEditor::set_background_tile(const TileSetAtlasSource *p_atlas_source, const Vector2 &p_atlas_coords, int p_alternative_id) { + ERR_FAIL_NULL(p_atlas_source); + background_atlas_source = p_atlas_source; + background_atlas_coords = p_atlas_coords; + background_alternative_id = p_alternative_id; base_control->queue_redraw(); } @@ -1457,7 +1494,7 @@ void TileDataOcclusionShapeEditor::_set_painted_value(TileSetAtlasSource *p_tile if (occluder_polygon.is_valid()) { polygon_editor->add_polygon(occluder_polygon->get_polygon()); } - polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), tile_data->get_texture_origin(), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); + polygon_editor->set_background_tile(p_tile_set_atlas_source, p_coords, p_alternative_tile); } void TileDataOcclusionShapeEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, const Variant &p_value) { @@ -1466,7 +1503,7 @@ void TileDataOcclusionShapeEditor::_set_value(TileSetAtlasSource *p_tile_set_atl Ref<OccluderPolygon2D> occluder_polygon = p_value; tile_data->set_occluder(occlusion_layer, occluder_polygon); - polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), tile_data->get_texture_origin(), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); + polygon_editor->set_background_tile(p_tile_set_atlas_source, p_coords, p_alternative_tile); } Variant TileDataOcclusionShapeEditor::_get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { @@ -1613,7 +1650,7 @@ void TileDataCollisionEditor::_set_painted_value(TileSetAtlasSource *p_tile_set_ E.value->update_property(); } - polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), tile_data->get_texture_origin(), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); + polygon_editor->set_background_tile(p_tile_set_atlas_source, p_coords, p_alternative_tile); } void TileDataCollisionEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, const Variant &p_value) { @@ -1632,7 +1669,7 @@ void TileDataCollisionEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_so tile_data->set_collision_polygon_one_way_margin(physics_layer, i, polygon_dict["one_way_margin"]); } - polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), tile_data->get_texture_origin(), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); + polygon_editor->set_background_tile(p_tile_set_atlas_source, p_coords, p_alternative_tile); } Variant TileDataCollisionEditor::_get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { @@ -2873,7 +2910,7 @@ void TileDataNavigationEditor::_set_painted_value(TileSetAtlasSource *p_tile_set polygon_editor->add_polygon(nav_polygon->get_outline(i)); } } - polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), tile_data->get_texture_origin(), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); + polygon_editor->set_background_tile(p_tile_set_atlas_source, p_coords, p_alternative_tile); } void TileDataNavigationEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, const Variant &p_value) { @@ -2882,7 +2919,7 @@ void TileDataNavigationEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_s Ref<NavigationPolygon> nav_polygon = p_value; tile_data->set_navigation_polygon(navigation_layer, nav_polygon); - polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), tile_data->get_texture_origin(), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); + polygon_editor->set_background_tile(p_tile_set_atlas_source, p_coords, p_alternative_tile); } Variant TileDataNavigationEditor::_get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { diff --git a/editor/plugins/tiles/tile_data_editors.h b/editor/plugins/tiles/tile_data_editors.h index f9d8f7d077..9b1eadf331 100644 --- a/editor/plugins/tiles/tile_data_editors.h +++ b/editor/plugins/tiles/tile_data_editors.h @@ -142,13 +142,9 @@ private: Vector2 panning; bool initializing = true; - Ref<Texture2D> background_texture; - Rect2 background_region; - Vector2 background_offset; - bool background_h_flip = false; - bool background_v_flip = false; - bool background_transpose = false; - Color background_modulate; + Ref<TileSetAtlasSource> background_atlas_source; + Vector2i background_atlas_coords; + int background_alternative_id; Color polygon_color = Color(1.0, 0.0, 0.0); @@ -183,7 +179,7 @@ public: void set_use_undo_redo(bool p_use_undo_redo); void set_tile_set(Ref<TileSet> p_tile_set); - void set_background(Ref<Texture2D> p_texture, Rect2 p_region = Rect2(), Vector2 p_offset = Vector2(), bool p_flip_h = false, bool p_flip_v = false, bool p_transpose = false, Color p_modulate = Color(1.0, 1.0, 1.0, 0.0)); + void set_background_tile(const TileSetAtlasSource *p_atlas_source, const Vector2 &p_atlas_coords, int p_alternative_id); int get_polygon_count(); int add_polygon(const Vector<Point2> &p_polygon, int p_index = -1); diff --git a/editor/plugins/tiles/tile_map_layer_editor.cpp b/editor/plugins/tiles/tile_map_layer_editor.cpp index 41b8b88f73..4a59530159 100644 --- a/editor/plugins/tiles/tile_map_layer_editor.cpp +++ b/editor/plugins/tiles/tile_map_layer_editor.cpp @@ -763,8 +763,13 @@ bool TileMapLayerEditorTilesPlugin::forward_canvas_gui_input(const Ref<InputEven } } else { - // Released - _stop_dragging(); + // Released. + if (drag_type == DRAG_TYPE_NONE) { + drag_erasing = false; + return false; + } else { + _stop_dragging(); + } drag_erasing = false; } @@ -3625,9 +3630,16 @@ TileMapLayer *TileMapLayerEditor::_get_edited_layer() const { void TileMapLayerEditor::_find_tile_map_layers_in_scene(Node *p_current, const Node *p_owner, Vector<TileMapLayer *> &r_list) const { ERR_FAIL_COND(!p_current || !p_owner); - if (p_current != p_owner && p_current->get_owner() != p_owner) { - return; + + if (p_current != p_owner) { + if (!p_current->get_owner()) { + return; + } + if (p_current->get_owner() != p_owner && !p_owner->is_editable_instance(p_current->get_owner())) { + return; + } } + TileMapLayer *layer = Object::cast_to<TileMapLayer>(p_current); if (layer) { r_list.append(layer); diff --git a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp index 94e68ccc7c..7e34a36a6e 100644 --- a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp +++ b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp @@ -651,7 +651,7 @@ void TileSetAtlasSourceEditor::_update_tile_data_editors() { tile_data_editors_tree->add_theme_constant_override("v_separation", 1); tile_data_editors_tree->add_theme_constant_override("h_separation", 3); - Color group_color = get_theme_color(SNAME("prop_category"), EditorStringName(Editor)); + Color group_color = get_theme_color(SNAME("separator_color"), EditorStringName(Editor)); // List of editors. // --- Rendering --- @@ -2451,6 +2451,8 @@ void TileSetAtlasSourceEditor::_notification(int p_what) { resize_handle = get_editor_theme_icon(SNAME("EditorHandle")); resize_handle_disabled = get_editor_theme_icon(SNAME("EditorHandleDisabled")); + + tile_data_editors_tree->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), "PopupPanel")); } break; case NOTIFICATION_INTERNAL_PROCESS: { @@ -2820,13 +2822,15 @@ void EditorPropertyTilePolygon::update_property() { ERR_FAIL_COND(atlas_tile_proxy_object->get_edited_tiles().is_empty()); Ref<TileSetAtlasSource> tile_set_atlas_source = atlas_tile_proxy_object->get_edited_tile_set_atlas_source(); - generic_tile_polygon_editor->set_tile_set(Ref<TileSet>(tile_set_atlas_source->get_tile_set())); + Ref<TileSet> tile_set(tile_set_atlas_source->get_tile_set()); + + // Update the polyugon editor tile_set. + generic_tile_polygon_editor->set_tile_set(tile_set); // Set the background Vector2i coords = atlas_tile_proxy_object->get_edited_tiles().front()->get().tile; int alternative = atlas_tile_proxy_object->get_edited_tiles().front()->get().alternative; - TileData *tile_data = tile_set_atlas_source->get_tile_data(coords, alternative); - generic_tile_polygon_editor->set_background(tile_set_atlas_source->get_texture(), tile_set_atlas_source->get_tile_texture_region(coords), tile_data->get_texture_origin(), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); + generic_tile_polygon_editor->set_background_tile(*tile_set_atlas_source, coords, alternative); // Reset the polygons. generic_tile_polygon_editor->clear_polygons(); diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index 3f6927c02a..2f36198b23 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -133,9 +133,9 @@ void VSRerouteNode::_notification(int p_what) { connect(SceneStringName(mouse_exited), callable_mp(this, &VSRerouteNode::_on_mouse_exited)); } break; case NOTIFICATION_DRAW: { - Vector2 offset = Vector2(0, -16); + Vector2 offset = Vector2(0, -16 * EDSCALE); Color drag_bg_color = get_theme_color(SNAME("drag_background"), SNAME("VSRerouteNode")); - draw_circle(get_size() * 0.5 + offset, 16, Color(drag_bg_color, selected ? 1 : icon_opacity)); + draw_circle(get_size() * 0.5 + offset, 16 * EDSCALE, Color(drag_bg_color, selected ? 1 : icon_opacity), true, -1, true); Ref<Texture2D> icon = get_editor_theme_icon(SNAME("ToolMove")); Point2 icon_offset = -icon->get_size() * 0.5 + get_size() * 0.5 + offset; @@ -154,6 +154,7 @@ VSRerouteNode::VSRerouteNode() { title_lbl->hide(); const Size2 size = Size2(32, 32) * EDSCALE; + print_line("VSRerouteNode size: " + size); Control *slot_area = memnew(Control); slot_area->set_custom_minimum_size(size); @@ -3649,12 +3650,15 @@ void VisualShaderEditor::_add_node(int p_idx, const Vector<Variant> &p_ops, cons if (output_port_type == VisualShaderNode::PORT_TYPE_SAMPLER) { if (is_texture2d) { + undo_redo->force_fixed_history(); // vsnode is freshly created and has no path, so history can't be correctly determined. undo_redo->add_do_method(vsnode.ptr(), "set_source", VisualShaderNodeTexture::SOURCE_PORT); } if (is_texture3d || is_texture2d_array) { + undo_redo->force_fixed_history(); undo_redo->add_do_method(vsnode.ptr(), "set_source", VisualShaderNodeSample3D::SOURCE_PORT); } if (is_cubemap) { + undo_redo->force_fixed_history(); undo_redo->add_do_method(vsnode.ptr(), "set_source", VisualShaderNodeCubemap::SOURCE_PORT); } } @@ -3754,16 +3758,19 @@ void VisualShaderEditor::_add_node(int p_idx, const Vector<Variant> &p_ops, cons //post-initialization if (is_texture2d || is_texture3d || is_curve || is_curve_xyz) { + undo_redo->force_fixed_history(); undo_redo->add_do_method(vsnode.ptr(), "set_texture", ResourceLoader::load(p_resource_path)); return; } if (is_cubemap) { + undo_redo->force_fixed_history(); undo_redo->add_do_method(vsnode.ptr(), "set_cube_map", ResourceLoader::load(p_resource_path)); return; } if (is_texture2d_array) { + undo_redo->force_fixed_history(); undo_redo->add_do_method(vsnode.ptr(), "set_texture_array", ResourceLoader::load(p_resource_path)); } } diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index c2d89b28f5..bdf4e41c5f 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -91,6 +91,10 @@ void ProjectSettingsEditor::update_plugins() { plugin_settings->update_plugins(); } +void ProjectSettingsEditor::init_autoloads() { + autoload_settings->init_autoloads(); +} + void ProjectSettingsEditor::_setting_edited(const String &p_name) { queue_save(); } @@ -231,28 +235,17 @@ void ProjectSettingsEditor::_select_type(Variant::Type p_type) { } void ProjectSettingsEditor::shortcut_input(const Ref<InputEvent> &p_event) { - ERR_FAIL_COND(p_event.is_null()); - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - const Ref<InputEventKey> k = p_event; if (k.is_valid() && k->is_pressed()) { bool handled = false; if (ED_IS_SHORTCUT("ui_undo", p_event)) { - String action = undo_redo->get_current_action_name(); - if (!action.is_empty()) { - EditorNode::get_log()->add_message(vformat(TTR("Undo: %s"), action), EditorLog::MSG_TYPE_EDITOR); - } - undo_redo->undo(); + EditorNode::get_singleton()->undo(); handled = true; } if (ED_IS_SHORTCUT("ui_redo", p_event)) { - undo_redo->redo(); - String action = undo_redo->get_current_action_name(); - if (!action.is_empty()) { - EditorNode::get_log()->add_message(vformat(TTR("Redo: %s"), action), EditorLog::MSG_TYPE_EDITOR); - } + EditorNode::get_singleton()->redo(); handled = true; } diff --git a/editor/project_settings_editor.h b/editor/project_settings_editor.h index fb294c6bbd..5890ed2c2d 100644 --- a/editor/project_settings_editor.h +++ b/editor/project_settings_editor.h @@ -123,6 +123,7 @@ public: void set_plugins_page(); void set_general_page(const String &p_category); void update_plugins(); + void init_autoloads(); EditorAutoloadSettings *get_autoload_settings() { return autoload_settings; } GroupSettingsEditor *get_group_settings() { return group_settings; } diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 7f60fbbdb9..94bd3e16d3 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -77,12 +77,12 @@ void SceneTreeDock::_quick_open() { void SceneTreeDock::_inspect_hovered_node() { select_node_hovered_at_end_of_drag = true; - if (tree_item_inspected != nullptr) { - tree_item_inspected->clear_custom_color(0); - } Tree *tree = scene_tree->get_scene_tree(); TreeItem *item = tree->get_item_with_metadata(node_hovered_now->get_path()); if (item) { + if (tree_item_inspected) { + tree_item_inspected->clear_custom_color(0); + } tree_item_inspected = item; tree_item_inspected->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor))); } @@ -133,8 +133,9 @@ void SceneTreeDock::input(const Ref<InputEvent> &p_event) { } if (mb->is_released()) { - if (tree_item_inspected != nullptr) { + if (tree_item_inspected) { tree_item_inspected->clear_custom_color(0); + tree_item_inspected = nullptr; } _reset_hovering_timer(); } diff --git a/editor/themes/editor_fonts.cpp b/editor/themes/editor_fonts.cpp index 71a050bc23..fc994a17d1 100644 --- a/editor/themes/editor_fonts.cpp +++ b/editor/themes/editor_fonts.cpp @@ -443,7 +443,7 @@ void editor_register_fonts(const Ref<Theme> &p_theme) { p_theme->set_font("rulers", EditorStringName(EditorFonts), default_fc); // Rotation widget font - p_theme->set_font_size("rotation_control_size", EditorStringName(EditorFonts), 14 * EDSCALE); + p_theme->set_font_size("rotation_control_size", EditorStringName(EditorFonts), 13 * EDSCALE); p_theme->set_font("rotation_control", EditorStringName(EditorFonts), default_fc); // Code font diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 9f0865d950..03752656c0 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1665,7 +1665,7 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the // GraphFrame's title Label. p_theme->set_type_variation("GraphFrameTitleLabel", "Label"); p_theme->set_stylebox(CoreStringName(normal), "GraphFrameTitleLabel", memnew(StyleBoxEmpty)); - p_theme->set_font_size(SceneStringName(font_size), "GraphFrameTitleLabel", 22); + p_theme->set_font_size(SceneStringName(font_size), "GraphFrameTitleLabel", 22 * EDSCALE); p_theme->set_color(SceneStringName(font_color), "GraphFrameTitleLabel", Color(1, 1, 1)); p_theme->set_color("font_shadow_color", "GraphFrameTitleLabel", Color(0, 0, 0, 0)); p_theme->set_color("font_outline_color", "GraphFrameTitleLabel", Color(1, 1, 1)); @@ -1680,7 +1680,7 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the { Ref<StyleBox> vs_reroute_panel_style = make_empty_stylebox(); Ref<StyleBox> vs_reroute_titlebar_style = vs_reroute_panel_style->duplicate(); - vs_reroute_titlebar_style->set_content_margin_all(16); + vs_reroute_titlebar_style->set_content_margin_all(16 * EDSCALE); p_theme->set_stylebox(SceneStringName(panel), "VSRerouteNode", vs_reroute_panel_style); p_theme->set_stylebox("panel_selected", "VSRerouteNode", vs_reroute_panel_style); p_theme->set_stylebox("titlebar", "VSRerouteNode", vs_reroute_titlebar_style); diff --git a/main/main.cpp b/main/main.cpp index 1cbd732747..060b3fe2f6 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -909,13 +909,11 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph // Benchmark tracking must be done after `OS::get_singleton()->initialize()` as on some // platforms, it's used to set up the time utilities. - OS::get_singleton()->benchmark_begin_measure("Startup", "Total"); - OS::get_singleton()->benchmark_begin_measure("Startup", "Setup"); + OS::get_singleton()->benchmark_begin_measure("Startup", "Main::Setup"); engine = memnew(Engine); MAIN_PRINT("Main: Initialize CORE"); - OS::get_singleton()->benchmark_begin_measure("Startup", "Core"); register_core_types(); register_core_driver_types(); @@ -2453,8 +2451,6 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph Thread::release_main_thread(); // If setup2() is called from another thread, that one will become main thread, so preventively release this one. set_current_thread_safe_for_nodes(false); - OS::get_singleton()->benchmark_end_measure("Startup", "Core"); - #if defined(STEAMAPI_ENABLED) if (editor || project_manager) { steam_tracker = memnew(SteamTracker); @@ -2465,7 +2461,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph return setup2(); } - OS::get_singleton()->benchmark_end_measure("Startup", "Setup"); + OS::get_singleton()->benchmark_end_measure("Startup", "Main::Setup"); return OK; error: @@ -2519,7 +2515,7 @@ error: } OS::get_singleton()->benchmark_end_measure("Startup", "Core"); - OS::get_singleton()->benchmark_end_measure("Startup", "Setup"); + OS::get_singleton()->benchmark_end_measure("Startup", "Main::Setup"); #if defined(STEAMAPI_ENABLED) if (steam_tracker) { @@ -2552,7 +2548,9 @@ Error _parse_resource_dummy(void *p_data, VariantParser::Stream *p_stream, Ref<R return OK; } -Error Main::setup2() { +Error Main::setup2(bool p_show_boot_logo) { + OS::get_singleton()->benchmark_begin_measure("Startup", "Main::Setup2"); + Thread::make_main_thread(); // Make whatever thread call this the main thread. set_current_thread_safe_for_nodes(true); @@ -2896,12 +2894,6 @@ Error Main::setup2() { MAIN_PRINT("Main: Setup Logo"); -#if !defined(TOOLS_ENABLED) && defined(WEB_ENABLED) - bool show_logo = false; -#else - bool show_logo = true; -#endif - if (init_windowed) { //do none.. } else if (init_maximized) { @@ -2913,67 +2905,11 @@ Error Main::setup2() { DisplayServer::get_singleton()->window_set_flag(DisplayServer::WINDOW_FLAG_ALWAYS_ON_TOP, true); } - MAIN_PRINT("Main: Load Boot Image"); - Color clear = GLOBAL_DEF_BASIC("rendering/environment/defaults/default_clear_color", Color(0.3, 0.3, 0.3)); RenderingServer::get_singleton()->set_default_clear_color(clear); - if (show_logo) { //boot logo! - const bool boot_logo_image = GLOBAL_DEF_BASIC("application/boot_splash/show_image", true); - const String boot_logo_path = String(GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/boot_splash/image", PROPERTY_HINT_FILE, "*.png"), String())).strip_edges(); - const bool boot_logo_scale = GLOBAL_DEF_BASIC("application/boot_splash/fullsize", true); - const bool boot_logo_filter = GLOBAL_DEF_BASIC("application/boot_splash/use_filter", true); - - Ref<Image> boot_logo; - - if (boot_logo_image) { - if (!boot_logo_path.is_empty()) { - boot_logo.instantiate(); - Error load_err = ImageLoader::load_image(boot_logo_path, boot_logo); - if (load_err) { - ERR_PRINT("Non-existing or invalid boot splash at '" + boot_logo_path + "'. Loading default splash."); - } - } - } else { - // Create a 1×1 transparent image. This will effectively hide the splash image. - boot_logo.instantiate(); - boot_logo->initialize_data(1, 1, false, Image::FORMAT_RGBA8); - boot_logo->set_pixel(0, 0, Color(0, 0, 0, 0)); - } - - Color boot_bg_color = GLOBAL_GET("application/boot_splash/bg_color"); - -#if defined(TOOLS_ENABLED) && !defined(NO_EDITOR_SPLASH) - boot_bg_color = - GLOBAL_DEF_BASIC("application/boot_splash/bg_color", - (editor || project_manager) ? boot_splash_editor_bg_color : boot_splash_bg_color); -#endif - if (boot_logo.is_valid()) { - RenderingServer::get_singleton()->set_boot_image(boot_logo, boot_bg_color, boot_logo_scale, - boot_logo_filter); - - } else { -#ifndef NO_DEFAULT_BOOT_LOGO - MAIN_PRINT("Main: Create bootsplash"); -#if defined(TOOLS_ENABLED) && !defined(NO_EDITOR_SPLASH) - Ref<Image> splash = (editor || project_manager) ? memnew(Image(boot_splash_editor_png)) : memnew(Image(boot_splash_png)); -#else - Ref<Image> splash = memnew(Image(boot_splash_png)); -#endif - - MAIN_PRINT("Main: ClearColor"); - RenderingServer::get_singleton()->set_default_clear_color(boot_bg_color); - MAIN_PRINT("Main: Image"); - RenderingServer::get_singleton()->set_boot_image(splash, boot_bg_color, false); -#endif - } - -#if defined(TOOLS_ENABLED) && defined(MACOS_ENABLED) - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_ICON) && OS::get_singleton()->get_bundle_icon_path().is_empty()) { - Ref<Image> icon = memnew(Image(app_icon_png)); - DisplayServer::get_singleton()->set_icon(icon); - } -#endif + if (p_show_boot_logo) { + setup_boot_logo(); } MAIN_PRINT("Main: Clear Color"); @@ -3003,6 +2939,8 @@ Error Main::setup2() { id->set_emulate_mouse_from_touch(bool(GLOBAL_DEF_BASIC("input_devices/pointing/emulate_mouse_from_touch", true))); } + GLOBAL_DEF("input_devices/buffering/android/use_accumulated_input", true); + GLOBAL_DEF("input_devices/buffering/android/use_input_buffering", true); GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_long_press_as_right_click", false); GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_pan_and_scale_gestures", false); GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "input_devices/pointing/android/rotary_input_scroll_axis", PROPERTY_HINT_ENUM, "Horizontal,Vertical"), 1); @@ -3211,11 +3149,76 @@ Error Main::setup2() { print_verbose("EDITOR API HASH: " + uitos(ClassDB::get_api_hash(ClassDB::API_EDITOR))); MAIN_PRINT("Main: Done"); - OS::get_singleton()->benchmark_end_measure("Startup", "Setup"); + OS::get_singleton()->benchmark_end_measure("Startup", "Main::Setup2"); return OK; } +void Main::setup_boot_logo() { + MAIN_PRINT("Main: Load Boot Image"); + +#if !defined(TOOLS_ENABLED) && defined(WEB_ENABLED) + bool show_logo = false; +#else + bool show_logo = true; +#endif + + if (show_logo) { //boot logo! + const bool boot_logo_image = GLOBAL_DEF_BASIC("application/boot_splash/show_image", true); + const String boot_logo_path = String(GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/boot_splash/image", PROPERTY_HINT_FILE, "*.png"), String())).strip_edges(); + const bool boot_logo_scale = GLOBAL_DEF_BASIC("application/boot_splash/fullsize", true); + const bool boot_logo_filter = GLOBAL_DEF_BASIC("application/boot_splash/use_filter", true); + + Ref<Image> boot_logo; + + if (boot_logo_image) { + if (!boot_logo_path.is_empty()) { + boot_logo.instantiate(); + Error load_err = ImageLoader::load_image(boot_logo_path, boot_logo); + if (load_err) { + ERR_PRINT("Non-existing or invalid boot splash at '" + boot_logo_path + "'. Loading default splash."); + } + } + } else { + // Create a 1×1 transparent image. This will effectively hide the splash image. + boot_logo.instantiate(); + boot_logo->initialize_data(1, 1, false, Image::FORMAT_RGBA8); + boot_logo->set_pixel(0, 0, Color(0, 0, 0, 0)); + } + + Color boot_bg_color = GLOBAL_GET("application/boot_splash/bg_color"); + +#if defined(TOOLS_ENABLED) && !defined(NO_EDITOR_SPLASH) + boot_bg_color = GLOBAL_DEF_BASIC("application/boot_splash/bg_color", (editor || project_manager) ? boot_splash_editor_bg_color : boot_splash_bg_color); +#endif + if (boot_logo.is_valid()) { + RenderingServer::get_singleton()->set_boot_image(boot_logo, boot_bg_color, boot_logo_scale, boot_logo_filter); + + } else { +#ifndef NO_DEFAULT_BOOT_LOGO + MAIN_PRINT("Main: Create bootsplash"); +#if defined(TOOLS_ENABLED) && !defined(NO_EDITOR_SPLASH) + Ref<Image> splash = (editor || project_manager) ? memnew(Image(boot_splash_editor_png)) : memnew(Image(boot_splash_png)); +#else + Ref<Image> splash = memnew(Image(boot_splash_png)); +#endif + + MAIN_PRINT("Main: ClearColor"); + RenderingServer::get_singleton()->set_default_clear_color(boot_bg_color); + MAIN_PRINT("Main: Image"); + RenderingServer::get_singleton()->set_boot_image(splash, boot_bg_color, false); +#endif + } + +#if defined(TOOLS_ENABLED) && defined(MACOS_ENABLED) + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_ICON) && OS::get_singleton()->get_bundle_icon_path().is_empty()) { + Ref<Image> icon = memnew(Image(app_icon_png)); + DisplayServer::get_singleton()->set_icon(icon); + } +#endif + } +} + String Main::get_rendering_driver_name() { return rendering_driver; } @@ -3227,6 +3230,8 @@ static MainTimerSync main_timer_sync; // and should move on to `OS::run`, and EXIT_FAILURE otherwise for // an early exit with that error code. int Main::start() { + OS::get_singleton()->benchmark_begin_measure("Startup", "Main::Start"); + ERR_FAIL_COND_V(!_start_success, false); bool has_icon = false; @@ -3950,7 +3955,7 @@ int Main::start() { } } - OS::get_singleton()->benchmark_end_measure("Startup", "Total"); + OS::get_singleton()->benchmark_end_measure("Startup", "Main::Start"); OS::get_singleton()->benchmark_dump(); return EXIT_SUCCESS; @@ -4038,6 +4043,7 @@ bool Main::iteration() { } Engine::get_singleton()->_in_physics = true; + Engine::get_singleton()->_physics_frames++; uint64_t physics_begin = OS::get_singleton()->get_ticks_usec(); @@ -4085,7 +4091,6 @@ bool Main::iteration() { physics_process_ticks = MAX(physics_process_ticks, OS::get_singleton()->get_ticks_usec() - physics_begin); // keep the largest one for reference physics_process_max = MAX(OS::get_singleton()->get_ticks_usec() - physics_begin, physics_process_max); - Engine::get_singleton()->_physics_frames++; Engine::get_singleton()->_in_physics = false; } @@ -4218,7 +4223,7 @@ void Main::force_redraw() { * The order matters as some of those steps are linked with each other. */ void Main::cleanup(bool p_force) { - OS::get_singleton()->benchmark_begin_measure("Shutdown", "Total"); + OS::get_singleton()->benchmark_begin_measure("Shutdown", "Main::Cleanup"); if (!p_force) { ERR_FAIL_COND(!_start_success); } @@ -4376,7 +4381,7 @@ void Main::cleanup(bool p_force) { unregister_core_types(); - OS::get_singleton()->benchmark_end_measure("Shutdown", "Total"); + OS::get_singleton()->benchmark_end_measure("Shutdown", "Main::Cleanup"); OS::get_singleton()->benchmark_dump(); OS::get_singleton()->finalize_core(); diff --git a/main/main.h b/main/main.h index ff0fba6b51..b1cfcd3c2d 100644 --- a/main/main.h +++ b/main/main.h @@ -72,8 +72,9 @@ public: static int test_entrypoint(int argc, char *argv[], bool &tests_need_run); static Error setup(const char *execpath, int argc, char *argv[], bool p_second_phase = true); - static Error setup2(); // The thread calling setup2() will effectively become the main thread. + static Error setup2(bool p_show_boot_logo = true); // The thread calling setup2() will effectively become the main thread. static String get_rendering_driver_name(); + static void setup_boot_logo(); #ifdef TESTS_ENABLED static Error test_setup(); static void test_cleanup(); diff --git a/misc/dist/html/editor.html b/misc/dist/html/editor.html index 5959b7b664..3a22055546 100644 --- a/misc/dist/html/editor.html +++ b/misc/dist/html/editor.html @@ -227,7 +227,6 @@ a:active { line-height: 1.3; visibility: visible; padding: 4px 6px; - visibility: visible; } </style> </head> diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected index 7b93df70fa..4b0d22a1aa 100644 --- a/misc/extension_api_validation/4.2-stable.expected +++ b/misc/extension_api_validation/4.2-stable.expected @@ -372,3 +372,10 @@ GH-91382 Validate extension JSON: Error: Field 'classes/AudioStreamPlaybackPolyphonic/methods/play_stream/arguments': size changed value in new API, from 4 to 6. Optional arguments added. Compatibility methods registered. + + +GH-93982 +-------- +Validate extension JSON: Error: Field 'classes/Sprite3D/properties/frame_coords': type changed value in new API, from "Vector2" to "Vector2i". + +The type was wrong to begin with and has been corrected. Vector2 and Vector2i are convertible, so it should be compatible. diff --git a/modules/enet/doc_classes/ENetConnection.xml b/modules/enet/doc_classes/ENetConnection.xml index 5795dd8976..ebd1577172 100644 --- a/modules/enet/doc_classes/ENetConnection.xml +++ b/modules/enet/doc_classes/ENetConnection.xml @@ -141,7 +141,7 @@ <return type="Array" /> <param index="0" name="timeout" type="int" default="0" /> <description> - Waits for events on the host specified and shuttles packets between the host and its peers. The returned [Array] will have 4 elements. An [enum EventType], the [ENetPacketPeer] which generated the event, the event associated data (if any), the event associated channel (if any). If the generated event is [constant EVENT_RECEIVE], the received packet will be queued to the associated [ENetPacketPeer]. + Waits for events on the specified host and shuttles packets between the host and its peers, with the given [param timeout] (in milliseconds). The returned [Array] will have 4 elements. An [enum EventType], the [ENetPacketPeer] which generated the event, the event associated data (if any), the event associated channel (if any). If the generated event is [constant EVENT_RECEIVE], the received packet will be queued to the associated [ENetPacketPeer]. Call this function regularly to handle connections, disconnections, and to receive new packets. </description> </method> diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 20d424894a..73b1e44db3 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -115,7 +115,7 @@ Variant GDScriptNativeClass::callp(const StringName &p_method, const Variant **p } GDScriptFunction *GDScript::_super_constructor(GDScript *p_script) { - if (p_script->initializer) { + if (likely(p_script->valid) && p_script->initializer) { return p_script->initializer; } else { GDScript *base_src = p_script->_base; @@ -136,7 +136,11 @@ void GDScript::_super_implicit_constructor(GDScript *p_script, GDScriptInstance } } ERR_FAIL_NULL(p_script->implicit_initializer); - p_script->implicit_initializer->call(p_instance, nullptr, 0, r_error); + if (likely(valid)) { + p_script->implicit_initializer->call(p_instance, nullptr, 0, r_error); + } else { + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; + } } GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error) { @@ -662,7 +666,7 @@ String GDScript::_get_debug_path() const { } Error GDScript::_static_init() { - if (static_initializer) { + if (likely(valid) && static_initializer) { Callable::CallError call_err; static_initializer->call(nullptr, nullptr, 0, call_err); if (call_err.error != Callable::CallError::CALL_OK) { @@ -679,8 +683,6 @@ Error GDScript::_static_init() { return err; } -#ifdef TOOLS_ENABLED - void GDScript::_static_default_init() { for (const KeyValue<StringName, MemberInfo> &E : static_variables_indices) { const GDScriptDataType &type = E.value.data_type; @@ -702,6 +704,8 @@ void GDScript::_static_default_init() { } } +#ifdef TOOLS_ENABLED + void GDScript::_save_old_static_data() { old_static_variables_indices = static_variables_indices; old_static_variables = static_variables; @@ -873,9 +877,6 @@ Error GDScript::reload(bool p_keep_state) { #ifdef TOOLS_ENABLED if (can_run && p_keep_state) { _restore_old_static_data(); - } else if (!can_run) { - // Initialize static variables with sane default values even if the constructor isn't called. - _static_default_init(); } #endif @@ -912,18 +913,15 @@ void GDScript::unload_static() const { } Variant GDScript::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { - if (unlikely(!valid)) { - r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; - return Variant(); - } - GDScript *top = this; while (top) { - HashMap<StringName, GDScriptFunction *>::Iterator E = top->member_functions.find(p_method); - if (E) { - ERR_FAIL_COND_V_MSG(!E->value->is_static(), Variant(), "Can't call non-static function '" + String(p_method) + "' in script."); + if (likely(top->valid)) { + HashMap<StringName, GDScriptFunction *>::Iterator E = top->member_functions.find(p_method); + if (E) { + ERR_FAIL_COND_V_MSG(!E->value->is_static(), Variant(), "Can't call non-static function '" + String(p_method) + "' in script."); - return E->value->call(nullptr, p_args, p_argcount, r_error); + return E->value->call(nullptr, p_args, p_argcount, r_error); + } } top = top->_base; } @@ -939,10 +937,6 @@ bool GDScript::_get(const StringName &p_name, Variant &r_ret) const { return true; } - if (unlikely(!valid)) { - return false; - } - const GDScript *top = this; while (top) { { @@ -956,7 +950,7 @@ bool GDScript::_get(const StringName &p_name, Variant &r_ret) const { { HashMap<StringName, MemberInfo>::ConstIterator E = top->static_variables_indices.find(p_name); if (E) { - if (E->value.getter) { + if (likely(top->valid) && E->value.getter) { Callable::CallError ce; r_ret = const_cast<GDScript *>(this)->callp(E->value.getter, nullptr, 0, ce); return true; @@ -966,7 +960,7 @@ bool GDScript::_get(const StringName &p_name, Variant &r_ret) const { } } - { + if (likely(top->valid)) { HashMap<StringName, GDScriptFunction *>::ConstIterator E = top->member_functions.find(p_name); if (E && E->value->is_static()) { if (top->rpc_config.has(p_name)) { @@ -999,10 +993,6 @@ bool GDScript::_set(const StringName &p_name, const Variant &p_value) { return true; } - if (unlikely(!valid)) { - return false; - } - GDScript *top = this; while (top) { HashMap<StringName, MemberInfo>::ConstIterator E = top->static_variables_indices.find(p_name); @@ -1017,7 +1007,7 @@ bool GDScript::_set(const StringName &p_name, const Variant &p_value) { return false; } } - if (member->setter) { + if (likely(top->valid) && member->setter) { const Variant *args = &value; Callable::CallError err; callp(member->setter, &args, 1, err); @@ -1037,10 +1027,6 @@ bool GDScript::_set(const StringName &p_name, const Variant &p_value) { void GDScript::_get_property_list(List<PropertyInfo> *p_properties) const { p_properties->push_back(PropertyInfo(Variant::STRING, "script/source", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); - if (unlikely(!valid)) { - return; - } - List<const GDScript *> classes; const GDScript *top = this; while (top) { @@ -1657,10 +1643,6 @@ GDScript::~GDScript() { ////////////////////////////// bool GDScriptInstance::set(const StringName &p_name, const Variant &p_value) { - if (unlikely(!script->valid)) { - return false; - } - { HashMap<StringName, GDScript::MemberInfo>::Iterator E = script->member_indices.find(p_name); if (E) { @@ -1674,7 +1656,7 @@ bool GDScriptInstance::set(const StringName &p_name, const Variant &p_value) { return false; } } - if (member->setter) { + if (likely(script->valid) && member->setter) { const Variant *args = &value; Callable::CallError err; callp(member->setter, &args, 1, err); @@ -1701,7 +1683,7 @@ bool GDScriptInstance::set(const StringName &p_name, const Variant &p_value) { return false; } } - if (member->setter) { + if (likely(sptr->valid) && member->setter) { const Variant *args = &value; Callable::CallError err; callp(member->setter, &args, 1, err); @@ -1713,7 +1695,7 @@ bool GDScriptInstance::set(const StringName &p_name, const Variant &p_value) { } } - { + if (likely(sptr->valid)) { HashMap<StringName, GDScriptFunction *>::Iterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._set); if (E) { Variant name = p_name; @@ -1734,14 +1716,10 @@ bool GDScriptInstance::set(const StringName &p_name, const Variant &p_value) { } bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { - if (unlikely(!script->valid)) { - return false; - } - { HashMap<StringName, GDScript::MemberInfo>::ConstIterator E = script->member_indices.find(p_name); if (E) { - if (E->value.getter) { + if (likely(script->valid) && E->value.getter) { Callable::CallError err; r_ret = const_cast<GDScriptInstance *>(this)->callp(E->value.getter, nullptr, 0, err); if (err.error == Callable::CallError::CALL_OK) { @@ -1766,7 +1744,7 @@ bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { { HashMap<StringName, GDScript::MemberInfo>::ConstIterator E = sptr->static_variables_indices.find(p_name); if (E) { - if (E->value.getter) { + if (likely(sptr->valid) && E->value.getter) { Callable::CallError ce; r_ret = const_cast<GDScript *>(sptr)->callp(E->value.getter, nullptr, 0, ce); return true; @@ -1784,7 +1762,7 @@ bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { } } - { + if (likely(sptr->valid)) { HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(p_name); if (E) { if (sptr->rpc_config.has(p_name)) { @@ -1804,7 +1782,7 @@ bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { } } - { + if (likely(sptr->valid)) { HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._get); if (E) { Variant name = p_name; @@ -1844,13 +1822,15 @@ void GDScriptInstance::validate_property(PropertyInfo &p_property) const { const GDScript *sptr = script.ptr(); while (sptr) { - HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._validate_property); - if (E) { - Callable::CallError err; - Variant ret = E->value->call(const_cast<GDScriptInstance *>(this), args, 1, err); - if (err.error == Callable::CallError::CALL_OK) { - p_property = PropertyInfo::from_dict(property); - return; + if (likely(sptr->valid)) { + HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._validate_property); + if (E) { + Callable::CallError err; + Variant ret = E->value->call(const_cast<GDScriptInstance *>(this), args, 1, err); + if (err.error == Callable::CallError::CALL_OK) { + p_property = PropertyInfo::from_dict(property); + return; + } } } sptr = sptr->_base; @@ -1858,49 +1838,47 @@ void GDScriptInstance::validate_property(PropertyInfo &p_property) const { } void GDScriptInstance::get_property_list(List<PropertyInfo> *p_properties) const { - if (unlikely(!script->valid)) { - return; - } - // exported members, not done yet! const GDScript *sptr = script.ptr(); List<PropertyInfo> props; while (sptr) { - HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._get_property_list); - if (E) { - Callable::CallError err; - Variant ret = const_cast<GDScriptFunction *>(E->value)->call(const_cast<GDScriptInstance *>(this), nullptr, 0, err); - if (err.error == Callable::CallError::CALL_OK) { - ERR_FAIL_COND_MSG(ret.get_type() != Variant::ARRAY, "Wrong type for _get_property_list, must be an array of dictionaries."); - - Array arr = ret; - for (int i = 0; i < arr.size(); i++) { - Dictionary d = arr[i]; - ERR_CONTINUE(!d.has("name")); - ERR_CONTINUE(!d.has("type")); - - PropertyInfo pinfo; - pinfo.name = d["name"]; - pinfo.type = Variant::Type(d["type"].operator int()); - if (d.has("hint")) { - pinfo.hint = PropertyHint(d["hint"].operator int()); - } - if (d.has("hint_string")) { - pinfo.hint_string = d["hint_string"]; - } - if (d.has("usage")) { - pinfo.usage = d["usage"]; - } - if (d.has("class_name")) { - pinfo.class_name = d["class_name"]; - } + if (likely(sptr->valid)) { + HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._get_property_list); + if (E) { + Callable::CallError err; + Variant ret = const_cast<GDScriptFunction *>(E->value)->call(const_cast<GDScriptInstance *>(this), nullptr, 0, err); + if (err.error == Callable::CallError::CALL_OK) { + ERR_FAIL_COND_MSG(ret.get_type() != Variant::ARRAY, "Wrong type for _get_property_list, must be an array of dictionaries."); + + Array arr = ret; + for (int i = 0; i < arr.size(); i++) { + Dictionary d = arr[i]; + ERR_CONTINUE(!d.has("name")); + ERR_CONTINUE(!d.has("type")); + + PropertyInfo pinfo; + pinfo.name = d["name"]; + pinfo.type = Variant::Type(d["type"].operator int()); + if (d.has("hint")) { + pinfo.hint = PropertyHint(d["hint"].operator int()); + } + if (d.has("hint_string")) { + pinfo.hint_string = d["hint_string"]; + } + if (d.has("usage")) { + pinfo.usage = d["usage"]; + } + if (d.has("class_name")) { + pinfo.class_name = d["class_name"]; + } - ERR_CONTINUE(pinfo.name.is_empty() && (pinfo.usage & PROPERTY_USAGE_STORAGE)); - ERR_CONTINUE(pinfo.type < 0 || pinfo.type >= Variant::VARIANT_MAX); + ERR_CONTINUE(pinfo.name.is_empty() && (pinfo.usage & PROPERTY_USAGE_STORAGE)); + ERR_CONTINUE(pinfo.type < 0 || pinfo.type >= Variant::VARIANT_MAX); - props.push_back(pinfo); + props.push_back(pinfo); + } } } } @@ -1940,21 +1918,19 @@ void GDScriptInstance::get_property_list(List<PropertyInfo> *p_properties) const } bool GDScriptInstance::property_can_revert(const StringName &p_name) const { - if (unlikely(!script->valid)) { - return false; - } - Variant name = p_name; const Variant *args[1] = { &name }; const GDScript *sptr = script.ptr(); while (sptr) { - HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._property_can_revert); - if (E) { - Callable::CallError err; - Variant ret = E->value->call(const_cast<GDScriptInstance *>(this), args, 1, err); - if (err.error == Callable::CallError::CALL_OK && ret.get_type() == Variant::BOOL && ret.operator bool()) { - return true; + if (likely(sptr->valid)) { + HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._property_can_revert); + if (E) { + Callable::CallError err; + Variant ret = E->value->call(const_cast<GDScriptInstance *>(this), args, 1, err); + if (err.error == Callable::CallError::CALL_OK && ret.get_type() == Variant::BOOL && ret.operator bool()) { + return true; + } } } sptr = sptr->_base; @@ -1964,22 +1940,20 @@ bool GDScriptInstance::property_can_revert(const StringName &p_name) const { } bool GDScriptInstance::property_get_revert(const StringName &p_name, Variant &r_ret) const { - if (unlikely(!script->valid)) { - return false; - } - Variant name = p_name; const Variant *args[1] = { &name }; const GDScript *sptr = script.ptr(); while (sptr) { - HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._property_get_revert); - if (E) { - Callable::CallError err; - Variant ret = E->value->call(const_cast<GDScriptInstance *>(this), args, 1, err); - if (err.error == Callable::CallError::CALL_OK && ret.get_type() != Variant::NIL) { - r_ret = ret; - return true; + if (likely(sptr->valid)) { + HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._property_get_revert); + if (E) { + Callable::CallError err; + Variant ret = E->value->call(const_cast<GDScriptInstance *>(this), args, 1, err); + if (err.error == Callable::CallError::CALL_OK && ret.get_type() != Variant::NIL) { + r_ret = ret; + return true; + } } } sptr = sptr->_base; @@ -2035,30 +2009,28 @@ void GDScriptInstance::_call_implicit_ready_recursively(GDScript *p_script) { if (p_script->_base) { _call_implicit_ready_recursively(p_script->_base); } - if (p_script->implicit_ready) { + if (likely(p_script->valid) && p_script->implicit_ready) { Callable::CallError err; p_script->implicit_ready->call(this, nullptr, 0, err); } } Variant GDScriptInstance::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { - if (unlikely(!script->valid)) { - r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; - return Variant(); - } - GDScript *sptr = script.ptr(); if (unlikely(p_method == SceneStringName(_ready))) { // Call implicit ready first, including for the super classes recursively. _call_implicit_ready_recursively(sptr); } while (sptr) { - HashMap<StringName, GDScriptFunction *>::Iterator E = sptr->member_functions.find(p_method); - if (E) { - return E->value->call(this, p_args, p_argcount, r_error); + if (likely(sptr->valid)) { + HashMap<StringName, GDScriptFunction *>::Iterator E = sptr->member_functions.find(p_method); + if (E) { + return E->value->call(this, p_args, p_argcount, r_error); + } } sptr = sptr->_base; } + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; return Variant(); } @@ -2083,12 +2055,14 @@ void GDScriptInstance::notification(int p_notification, bool p_reversed) { sptr = sptr->_base; } for (GDScript *sc : pl) { - HashMap<StringName, GDScriptFunction *>::Iterator E = sc->member_functions.find(GDScriptLanguage::get_singleton()->strings._notification); - if (E) { - Callable::CallError err; - E->value->call(this, args, 1, err); - if (err.error != Callable::CallError::CALL_OK) { - //print error about notification call + if (likely(sc->valid)) { + HashMap<StringName, GDScriptFunction *>::Iterator E = sc->member_functions.find(GDScriptLanguage::get_singleton()->strings._notification); + if (E) { + Callable::CallError err; + E->value->call(this, args, 1, err); + if (err.error != Callable::CallError::CALL_OK) { + //print error about notification call + } } } } @@ -2759,7 +2733,7 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b String source = f->get_as_utf8_string(); GDScriptParser parser; - err = parser.parse(source, p_path, false); + err = parser.parse(source, p_path, false, false); const GDScriptParser::ClassNode *c = parser.get_tree(); if (!c) { diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 728459de44..d097cb193b 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -169,9 +169,7 @@ private: GDScriptFunction *static_initializer = nullptr; Error _static_init(); -#ifdef TOOLS_ENABLED void _static_default_init(); // Initialize static variables with default values based on their types. -#endif int subclass_count = 0; RBSet<Object *> instances; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index c60672d3f4..a6b4bce000 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -2663,6 +2663,44 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig reduce_expression(p_assignment->assignee); +#ifdef DEBUG_ENABLED + { + bool is_subscript = false; + GDScriptParser::ExpressionNode *base = p_assignment->assignee; + while (base && base->type == GDScriptParser::Node::SUBSCRIPT) { + is_subscript = true; + base = static_cast<GDScriptParser::SubscriptNode *>(base)->base; + } + if (base && base->type == GDScriptParser::Node::IDENTIFIER) { + GDScriptParser::IdentifierNode *id = static_cast<GDScriptParser::IdentifierNode *>(base); + if (current_lambda && current_lambda->captures_indices.has(id->name)) { + bool need_warn = false; + if (is_subscript) { + const GDScriptParser::DataType &id_type = id->datatype; + if (id_type.is_hard_type()) { + switch (id_type.kind) { + case GDScriptParser::DataType::BUILTIN: + // TODO: Change `Variant::is_type_shared()` to include packed arrays? + need_warn = !Variant::is_type_shared(id_type.builtin_type) && id_type.builtin_type < Variant::PACKED_BYTE_ARRAY; + break; + case GDScriptParser::DataType::ENUM: + need_warn = true; + break; + default: + break; + } + } + } else { + need_warn = true; + } + if (need_warn) { + parser->push_warning(p_assignment, GDScriptWarning::CONFUSABLE_CAPTURE_REASSIGNMENT, id->name); + } + } + } + } +#endif + if (p_assignment->assigned_value == nullptr || p_assignment->assignee == nullptr) { return; } @@ -4299,7 +4337,8 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { // Must load GDScript separately to permit cyclic references // as ResourceLoader::load() detects and rejects those. - if (ResourceLoader::get_resource_type(p_preload->resolved_path) == "GDScript") { + const String &res_type = ResourceLoader::get_resource_type(p_preload->resolved_path); + if (res_type == "GDScript") { Error err = OK; Ref<GDScript> res = GDScriptCache::get_shallow_script(p_preload->resolved_path, err, parser->script_path); p_preload->resource = res; @@ -4307,7 +4346,11 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { push_error(vformat(R"(Could not preload resource script "%s".)", p_preload->resolved_path), p_preload->path); } } else { - p_preload->resource = ResourceLoader::load(p_preload->resolved_path); + Error err = OK; + p_preload->resource = ResourceLoader::load(p_preload->resolved_path, res_type, ResourceFormatLoader::CACHE_MODE_REUSE, &err); + if (err == ERR_BUSY) { + p_preload->resource = ResourceLoader::ensure_resource_ref_override_for_outer_load(p_preload->resolved_path, res_type); + } if (p_preload->resource.is_null()) { push_error(vformat(R"(Could not preload resource file "%s".)", p_preload->resolved_path), p_preload->path); } diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp index ac6f5f05c6..7c9fba799d 100644 --- a/modules/gdscript/gdscript_cache.cpp +++ b/modules/gdscript/gdscript_cache.cpp @@ -42,6 +42,10 @@ GDScriptParserRef::Status GDScriptParserRef::get_status() const { return status; } +String GDScriptParserRef::get_path() const { + return path; +} + uint32_t GDScriptParserRef::get_source_hash() const { return source_hash; } @@ -91,12 +95,8 @@ Error GDScriptParserRef::raise_status(Status p_new_status) { result = get_analyzer()->resolve_interface(); } break; case INTERFACE_SOLVED: { - status = BODY_SOLVED; - result = get_analyzer()->resolve_body(); - } break; - case BODY_SOLVED: { status = FULLY_SOLVED; - result = get_analyzer()->resolve_dependencies(); + result = get_analyzer()->resolve_body(); } break; case FULLY_SOLVED: { return result; @@ -135,9 +135,7 @@ void GDScriptParserRef::clear() { GDScriptParserRef::~GDScriptParserRef() { clear(); - - MutexLock lock(GDScriptCache::singleton->mutex); - GDScriptCache::singleton->parser_map.erase(path); + GDScriptCache::remove_parser(path); } GDScriptCache *GDScriptCache::singleton = nullptr; @@ -158,6 +156,11 @@ void GDScriptCache::move_script(const String &p_from, const String &p_to) { } singleton->parser_map.erase(p_from); + if (singleton->parser_inverse_dependencies.has(p_from) && !p_from.is_empty()) { + singleton->parser_inverse_dependencies[p_to] = singleton->parser_inverse_dependencies[p_from]; + } + singleton->parser_inverse_dependencies.erase(p_from); + if (singleton->shallow_gdscript_cache.has(p_from) && !p_from.is_empty()) { singleton->shallow_gdscript_cache[p_to] = singleton->shallow_gdscript_cache[p_from]; } @@ -181,13 +184,11 @@ void GDScriptCache::remove_script(const String &p_path) { } if (singleton->parser_map.has(p_path)) { - // Keep a local reference until it goes out of scope. - // Clearing it can trigger a reference to itself to go out of scope, destructing it before clear finishes. - Ref<GDScriptParserRef> parser_ref = singleton->parser_map[p_path]; - singleton->parser_map.erase(p_path); - parser_ref->clear(); + singleton->parser_map[p_path]->clear(); } + remove_parser(p_path); + singleton->dependencies.erase(p_path); singleton->shallow_gdscript_cache.erase(p_path); singleton->full_gdscript_cache.erase(p_path); @@ -198,6 +199,7 @@ Ref<GDScriptParserRef> GDScriptCache::get_parser(const String &p_path, GDScriptP Ref<GDScriptParserRef> ref; if (!p_owner.is_empty()) { singleton->dependencies[p_owner].insert(p_path); + singleton->parser_inverse_dependencies[p_path].insert(p_owner); } if (singleton->parser_map.has(p_path)) { ref = Ref<GDScriptParserRef>(singleton->parser_map[p_path]); @@ -229,6 +231,13 @@ void GDScriptCache::remove_parser(const String &p_path) { MutexLock lock(singleton->mutex); // Can't clear the parser because some other parser might be currently using it in the chain of calls. singleton->parser_map.erase(p_path); + + // Have to copy while iterating, because parser_inverse_dependencies is modified. + HashSet<String> ideps = singleton->parser_inverse_dependencies[p_path]; + singleton->parser_inverse_dependencies.erase(p_path); + for (String idep_path : ideps) { + remove_parser(idep_path); + } } String GDScriptCache::get_source_code(const String &p_path) { @@ -344,7 +353,11 @@ Ref<GDScript> GDScriptCache::get_full_script(const String &p_path, Error &r_erro } } + // Allowing lifting the lock might cause a script to be reloaded multiple times, + // which, as a last resort deadlock prevention strategy, is a good tradeoff. + uint32_t allowance_id = WorkerThreadPool::thread_enter_unlock_allowance_zone(&singleton->mutex); r_error = script->reload(true); + WorkerThreadPool::thread_exit_unlock_allowance_zone(allowance_id); if (r_error) { return script; } @@ -417,6 +430,8 @@ void GDScriptCache::clear() { } singleton->cleared = true; + singleton->parser_inverse_dependencies.clear(); + RBSet<Ref<GDScriptParserRef>> parser_map_refs; for (KeyValue<String, GDScriptParserRef *> &E : singleton->parser_map) { parser_map_refs.insert(E.value); diff --git a/modules/gdscript/gdscript_cache.h b/modules/gdscript/gdscript_cache.h index c738233beb..c927317e19 100644 --- a/modules/gdscript/gdscript_cache.h +++ b/modules/gdscript/gdscript_cache.h @@ -48,7 +48,6 @@ public: PARSED, INHERITANCE_SOLVED, INTERFACE_SOLVED, - BODY_SOLVED, FULLY_SOLVED, }; @@ -66,6 +65,7 @@ private: public: Status get_status() const; + String get_path() const; uint32_t get_source_hash() const; GDScriptParser *get_parser(); GDScriptAnalyzer *get_analyzer(); @@ -83,6 +83,7 @@ class GDScriptCache { HashMap<String, Ref<GDScript>> full_gdscript_cache; HashMap<String, Ref<GDScript>> static_gdscript_cache; HashMap<String, HashSet<String>> dependencies; + HashMap<String, HashSet<String>> parser_inverse_dependencies; friend class GDScript; friend class GDScriptParserRef; diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index eeffc13a10..5469dad3f7 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -3006,6 +3006,8 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser: has_static_data = has_static_data || inner_class->has_static_data; } + p_script->_static_default_init(); + p_script->valid = true; return OK; } @@ -3228,11 +3230,7 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri GDScriptCache::add_static_script(p_script); } - err = GDScriptCache::finish_compiling(main_script->path); - if (err) { - main_script->valid = false; - } - return err; + return GDScriptCache::finish_compiling(main_script->path); } String GDScriptCompiler::get_error() const { diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 8da829907e..b58b44973e 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -1959,11 +1959,14 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, break; } - if (base.value.in(index.value)) { - Variant value = base.value.get(index.value); - r_type = _type_from_variant(value, p_context); - found = true; - break; + { + bool valid; + Variant value = base.value.get(index.value, &valid); + if (valid) { + r_type = _type_from_variant(value, p_context); + found = true; + break; + } } // Look if it is a dictionary node. @@ -3657,11 +3660,21 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co return OK; } - StringName enum_name = ClassDB::get_integer_constant_enum(class_name, p_symbol, true); - if (enum_name != StringName()) { - r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM; + List<StringName> enums; + ClassDB::get_enum_list(class_name, &enums); + for (const StringName &E : enums) { + if (E == p_symbol) { + r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM; + r_result.class_name = base_type.native_type; + r_result.class_member = p_symbol; + return OK; + } + } + + if (!String(ClassDB::get_integer_constant_enum(class_name, p_symbol, true)).is_empty()) { + r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT; r_result.class_name = base_type.native_type; - r_result.class_member = enum_name; + r_result.class_member = p_symbol; return OK; } @@ -3735,6 +3748,15 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co return OK; } } break; + case GDScriptParser::DataType::ENUM: { + if (base_type.enum_values.has(p_symbol)) { + r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT; + r_result.class_name = String(base_type.native_type).get_slicec('.', 0); + r_result.class_member = p_symbol; + return OK; + } + base_type.kind = GDScriptParser::DataType::UNRESOLVED; + } break; default: { base_type.kind = GDScriptParser::DataType::UNRESOLVED; } break; diff --git a/modules/gdscript/gdscript_lambda_callable.cpp b/modules/gdscript/gdscript_lambda_callable.cpp index 626ef6ccb0..2162a727b3 100644 --- a/modules/gdscript/gdscript_lambda_callable.cpp +++ b/modules/gdscript/gdscript_lambda_callable.cpp @@ -84,7 +84,7 @@ int GDScriptLambdaCallable::get_argument_count(bool &r_is_valid) const { return 0; } r_is_valid = true; - return function->get_argument_count(); + return function->get_argument_count() - captures.size(); } void GDScriptLambdaCallable::call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const { @@ -198,13 +198,17 @@ ObjectID GDScriptLambdaSelfCallable::get_object() const { return object->get_instance_id(); } +StringName GDScriptLambdaSelfCallable::get_method() const { + return function->get_name(); +} + int GDScriptLambdaSelfCallable::get_argument_count(bool &r_is_valid) const { if (function == nullptr) { r_is_valid = false; return 0; } r_is_valid = true; - return function->get_argument_count(); + return function->get_argument_count() - captures.size(); } void GDScriptLambdaSelfCallable::call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const { diff --git a/modules/gdscript/gdscript_lambda_callable.h b/modules/gdscript/gdscript_lambda_callable.h index 45c0235913..2d27b8d679 100644 --- a/modules/gdscript/gdscript_lambda_callable.h +++ b/modules/gdscript/gdscript_lambda_callable.h @@ -87,6 +87,7 @@ public: CompareEqualFunc get_compare_equal_func() const override; CompareLessFunc get_compare_less_func() const override; ObjectID get_object() const override; + StringName get_method() const override; int get_argument_count(bool &r_is_valid) const override; void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override; diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index aaa09134f0..a1ea94667d 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -309,13 +309,14 @@ void GDScriptParser::set_last_completion_call_arg(int p_argument) { completion_call_stack.back()->get().argument = p_argument; } -Error GDScriptParser::parse(const String &p_source_code, const String &p_script_path, bool p_for_completion) { +Error GDScriptParser::parse(const String &p_source_code, const String &p_script_path, bool p_for_completion, bool p_parse_body) { clear(); String source = p_source_code; int cursor_line = -1; int cursor_column = -1; for_completion = p_for_completion; + parse_body = p_parse_body; int tab_size = 4; #ifdef TOOLS_ENABLED @@ -689,6 +690,12 @@ void GDScriptParser::parse_program() { } } + // When the only thing needed is the class name and the icon, we don't need to parse the hole file. + // It really speed up the call to GDScriptLanguage::get_global_class_name especially for large script. + if (!parse_body) { + return; + } + #undef PUSH_PENDING_ANNOTATIONS_TO_HEAD parse_class_body(true); diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 96358165c0..21942222cf 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1329,6 +1329,7 @@ private: bool _is_tool = false; String script_path; bool for_completion = false; + bool parse_body = true; bool panic_mode = false; bool can_break = false; bool can_continue = false; @@ -1560,7 +1561,7 @@ private: #endif // TOOLS_ENABLED public: - Error parse(const String &p_source_code, const String &p_script_path, bool p_for_completion); + Error parse(const String &p_source_code, const String &p_script_path, bool p_for_completion, bool p_parse_body = true); Error parse_binary(const Vector<uint8_t> &p_binary, const String &p_script_path); ClassNode *get_tree() const { return head; } bool is_tool() const { return _is_tool; } diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index 611a9ad2d9..e8fb1d94b3 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -145,6 +145,9 @@ String GDScriptWarning::get_message() const { case CONFUSABLE_LOCAL_USAGE: CHECK_SYMBOLS(1); return vformat(R"(The identifier "%s" will be shadowed below in the block.)", symbols[0]); + case CONFUSABLE_CAPTURE_REASSIGNMENT: + CHECK_SYMBOLS(1); + return vformat(R"(Reassigning lambda capture does not modify the outer local variable "%s".)", symbols[0]); case INFERENCE_ON_VARIANT: CHECK_SYMBOLS(1); return vformat("The %s type is being inferred from a Variant value, so it will be typed as Variant.", symbols[0]); @@ -231,6 +234,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "CONFUSABLE_IDENTIFIER", "CONFUSABLE_LOCAL_DECLARATION", "CONFUSABLE_LOCAL_USAGE", + "CONFUSABLE_CAPTURE_REASSIGNMENT", "INFERENCE_ON_VARIANT", "NATIVE_METHOD_OVERRIDE", "GET_NODE_DEFAULT_WITHOUT_ONREADY", diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index 3ad9488138..1c806bb4e2 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -85,6 +85,7 @@ public: CONFUSABLE_IDENTIFIER, // The identifier contains misleading characters that can be confused. E.g. "usеr" (has Cyrillic "е" instead of Latin "e"). CONFUSABLE_LOCAL_DECLARATION, // The parent block declares an identifier with the same name below. CONFUSABLE_LOCAL_USAGE, // The identifier will be shadowed below in the block. + CONFUSABLE_CAPTURE_REASSIGNMENT, // Reassigning lambda capture does not modify the outer local variable. INFERENCE_ON_VARIANT, // The declaration uses type inference but the value is typed as Variant. NATIVE_METHOD_OVERRIDE, // The script method overrides a native one, this may not work as intended. GET_NODE_DEFAULT_WITHOUT_ONREADY, // A class variable uses `get_node()` (or the `$` notation) as its default value, but does not use the @onready annotation. @@ -137,6 +138,7 @@ public: WARN, // CONFUSABLE_IDENTIFIER WARN, // CONFUSABLE_LOCAL_DECLARATION WARN, // CONFUSABLE_LOCAL_USAGE + WARN, // CONFUSABLE_CAPTURE_REASSIGNMENT ERROR, // INFERENCE_ON_VARIANT // Most likely done by accident, usually inference is trying for a particular type. ERROR, // NATIVE_METHOD_OVERRIDE // May not work as expected. ERROR, // GET_NODE_DEFAULT_WITHOUT_ONREADY // May not work as expected. diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/confusable_capture_reassignment.gd b/modules/gdscript/tests/scripts/analyzer/warnings/confusable_capture_reassignment.gd new file mode 100644 index 0000000000..9e1041db54 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/confusable_capture_reassignment.gd @@ -0,0 +1,23 @@ +var member := 1 + +func test(): + var number := 1 + var string := "1" + var vector := Vector2i(1, 0) + var array_assign := [1] + var array_index := [1] + var dictionary := { x = 0 } + + var lambda := func (): + member = 2 # Member variable, not captured. + number = 2 # Local variable, captured. + string += "2" # Test compound assignment operator. + vector.x = 2 # Test subscript assignment. + array_assign = [2] # Pass-by-reference type, reassignment. + array_index[0] = 2 # Pass-by-reference type, index access. + dictionary.x = 2 # Pass-by-reference type, attribute access. + + prints("lambda", member, number, string, vector, array_assign, array_index, dictionary) + + lambda.call() + prints("outer", member, number, string, vector, array_assign, array_index, dictionary) diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/confusable_capture_reassignment.out b/modules/gdscript/tests/scripts/analyzer/warnings/confusable_capture_reassignment.out new file mode 100644 index 0000000000..8d953818eb --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/confusable_capture_reassignment.out @@ -0,0 +1,19 @@ +GDTEST_OK +>> WARNING +>> Line: 13 +>> CONFUSABLE_CAPTURE_REASSIGNMENT +>> Reassigning lambda capture does not modify the outer local variable "number". +>> WARNING +>> Line: 14 +>> CONFUSABLE_CAPTURE_REASSIGNMENT +>> Reassigning lambda capture does not modify the outer local variable "string". +>> WARNING +>> Line: 15 +>> CONFUSABLE_CAPTURE_REASSIGNMENT +>> Reassigning lambda capture does not modify the outer local variable "vector". +>> WARNING +>> Line: 16 +>> CONFUSABLE_CAPTURE_REASSIGNMENT +>> Reassigning lambda capture does not modify the outer local variable "array_assign". +lambda 2 2 12 (2, 0) [2] [2] { "x": 2 } +outer 2 1 1 (1, 0) [1] [2] { "x": 2 } diff --git a/modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd b/modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd index 46b6856d22..c3a42288c7 100644 --- a/modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd +++ b/modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd @@ -9,6 +9,7 @@ func four_parameters(_a, callable : Callable, b=func(): print(10)): func test(): var v + @warning_ignore("confusable_capture_reassignment") v=func():v=1 if true: v=1 print(v) diff --git a/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.gd b/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.gd new file mode 100644 index 0000000000..67225cad6a --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.gd @@ -0,0 +1,18 @@ +# https://github.com/godotengine/godot/issues/93952 + +func foo(): + pass + +func test(): + var a: int + + var lambda_self := func (x: int) -> void: + foo() + print(a, x) + + print(lambda_self.get_argument_count()) # Should print 1. + + var lambda_non_self := func (x: int) -> void: + print(a, x) + + print(lambda_non_self.get_argument_count()) # Should print 1. diff --git a/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.out b/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.out new file mode 100644 index 0000000000..04b4638adf --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.out @@ -0,0 +1,3 @@ +GDTEST_OK +1 +1 diff --git a/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.gd b/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.gd new file mode 100644 index 0000000000..160e43a797 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.gd @@ -0,0 +1,21 @@ +# https://github.com/godotengine/godot/issues/94074 + +func foo(): + pass + +func test(): + var lambda_self := func test() -> void: + foo() + var anon_lambda_self := func() -> void: + foo() + + print(lambda_self.get_method()) # Should print "test". + print(anon_lambda_self.get_method()) # Should print "<anonymous lambda>". + + var lambda_non_self := func test() -> void: + pass + var anon_lambda_non_self := func() -> void: + pass + + print(lambda_non_self.get_method()) # Should print "test". + print(anon_lambda_non_self.get_method()) # Should print "<anonymous lambda>". diff --git a/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.out b/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.out new file mode 100644 index 0000000000..17ee47fca2 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.out @@ -0,0 +1,5 @@ +GDTEST_OK +test +<anonymous lambda> +test +<anonymous lambda> diff --git a/modules/gltf/doc_classes/GLTFAccessor.xml b/modules/gltf/doc_classes/GLTFAccessor.xml index 54762faed7..dd059e6b79 100644 --- a/modules/gltf/doc_classes/GLTFAccessor.xml +++ b/modules/gltf/doc_classes/GLTFAccessor.xml @@ -12,7 +12,7 @@ <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <members> - <member name="accessor_type" type="int" setter="set_accessor_type" getter="get_accessor_type" default="0"> + <member name="accessor_type" type="int" setter="set_accessor_type" getter="get_accessor_type" enum="GLTFAccessor.GLTFAccessorType" default="0"> The GLTF accessor type as an enum. Possible values are 0 for "SCALAR", 1 for "VEC2", 2 for "VEC3", 3 for "VEC4", 4 for "MAT2", 5 for "MAT3", and 6 for "MAT4". </member> <member name="buffer_view" type="int" setter="set_buffer_view" getter="get_buffer_view" default="-1"> @@ -54,8 +54,31 @@ <member name="sparse_values_byte_offset" type="int" setter="set_sparse_values_byte_offset" getter="get_sparse_values_byte_offset" default="0"> The offset relative to the start of the bufferView in bytes. </member> - <member name="type" type="int" setter="set_type" getter="get_type" default="0" deprecated="Use [member accessor_type] instead."> + <member name="type" type="int" setter="set_type" getter="get_type" deprecated="Use [member accessor_type] instead."> The GLTF accessor type as an enum. Use [member accessor_type] instead. </member> </members> + <constants> + <constant name="TYPE_SCALAR" value="0" enum="GLTFAccessorType"> + Accessor type "SCALAR". For the glTF object model, this can be used to map to a single float, int, or bool value, or a float array. + </constant> + <constant name="TYPE_VEC2" value="1" enum="GLTFAccessorType"> + Accessor type "VEC2". For the glTF object model, this maps to "float2", represented in the glTF JSON as an array of two floats. + </constant> + <constant name="TYPE_VEC3" value="2" enum="GLTFAccessorType"> + Accessor type "VEC3". For the glTF object model, this maps to "float3", represented in the glTF JSON as an array of three floats. + </constant> + <constant name="TYPE_VEC4" value="3" enum="GLTFAccessorType"> + Accessor type "VEC4". For the glTF object model, this maps to "float4", represented in the glTF JSON as an array of four floats. + </constant> + <constant name="TYPE_MAT2" value="4" enum="GLTFAccessorType"> + Accessor type "MAT2". For the glTF object model, this maps to "float2x2", represented in the glTF JSON as an array of four floats. + </constant> + <constant name="TYPE_MAT3" value="5" enum="GLTFAccessorType"> + Accessor type "MAT3". For the glTF object model, this maps to "float3x3", represented in the glTF JSON as an array of nine floats. + </constant> + <constant name="TYPE_MAT4" value="6" enum="GLTFAccessorType"> + Accessor type "MAT4". For the glTF object model, this maps to "float4x4", represented in the glTF JSON as an array of sixteen floats. + </constant> + </constants> </class> diff --git a/modules/gltf/editor/editor_import_blend_runner.cpp b/modules/gltf/editor/editor_import_blend_runner.cpp index 330310d92a..22c8adfe88 100644 --- a/modules/gltf/editor/editor_import_blend_runner.cpp +++ b/modules/gltf/editor/editor_import_blend_runner.cpp @@ -43,6 +43,7 @@ from xmlrpc.server import SimpleXMLRPCServer req = threading.Condition() res = threading.Condition() info = None +export_err = None def xmlrpc_server(): server = SimpleXMLRPCServer(('127.0.0.1', %d)) server.register_function(export_gltf) @@ -54,6 +55,10 @@ def export_gltf(opts): req.notify() with res: res.wait() + if export_err: + raise export_err + # Important to return a value to prevent the error 'cannot marshal None unless allow_none is enabled'. + return 'BLENDER_GODOT_EXPORT_SUCCESSFUL' if bpy.app.version < (3, 0, 0): print('Blender 3.0 or higher is required.', file=sys.stderr) threading.Thread(target=xmlrpc_server).start() @@ -64,12 +69,13 @@ while True: method, opts = info if method == 'export_gltf': try: + export_err = None bpy.ops.wm.open_mainfile(filepath=opts['path']) if opts['unpack_all']: bpy.ops.file.unpack_all(method='USE_LOCAL') bpy.ops.export_scene.gltf(**opts['gltf_options']) - except: - pass + except Exception as e: + export_err = e info = None with res: res.notify() @@ -184,7 +190,9 @@ Error EditorImportBlendRunner::do_import(const Dictionary &p_options) { EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0); rpc_port = 0; } - err = do_import_direct(p_options); + if (err != ERR_QUERY_FAILED) { + err = do_import_direct(p_options); + } } return err; } else { @@ -259,6 +267,7 @@ Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { // Wait for response. bool done = false; + PackedByteArray response; while (!done) { status = client->get_status(); switch (status) { @@ -268,7 +277,10 @@ Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { } case HTTPClient::STATUS_BODY: { client->poll(); - // Parse response here if needed. For now we can just ignore it. + response.append_array(client->read_response_body_chunk()); + break; + } + case HTTPClient::STATUS_CONNECTED: { done = true; break; } @@ -278,9 +290,56 @@ Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { } } + String response_text = "No response from Blender."; + if (response.size() > 0) { + response_text = String::utf8((const char *)response.ptr(), response.size()); + } + + if (client->get_response_code() != HTTPClient::RESPONSE_OK) { + ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Error received from Blender - status code: %s, error: %s", client->get_response_code(), response_text)); + } else if (response_text.find("BLENDER_GODOT_EXPORT_SUCCESSFUL") < 0) { + // Previous versions of Godot used a Python script where the RPC function did not return + // a value, causing the error 'cannot marshal None unless allow_none is enabled'. + // If an older version of Godot is running and has started Blender with this script, + // we will receive the error, but there's a good chance that the import was successful. + // We are discarding this error to maintain backward compatibility and prevent situations + // where the user needs to close the older version of Godot or kill Blender. + if (response_text.find("cannot marshal None unless allow_none is enabled") < 0) { + String error_message; + if (_extract_error_message_xml(response, error_message)) { + ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Blender exportation failed: %s", error_message)); + } else { + ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Blender exportation failed: %s", response_text)); + } + } + } + return OK; } +bool EditorImportBlendRunner::_extract_error_message_xml(const Vector<uint8_t> &p_response_data, String &r_error_message) { + // Based on RPC Xml spec from: https://xmlrpc.com/spec.md + Ref<XMLParser> parser = memnew(XMLParser); + Error err = parser->open_buffer(p_response_data); + if (err) { + return false; + } + + r_error_message = String(); + while (parser->read() == OK) { + if (parser->get_node_type() == XMLParser::NODE_TEXT) { + if (parser->get_node_data().size()) { + if (r_error_message.size()) { + r_error_message += " "; + } + r_error_message += parser->get_node_data().trim_suffix("\n"); + } + } + } + + return r_error_message.size(); +} + Error EditorImportBlendRunner::do_import_direct(const Dictionary &p_options) { // Export glTF directly. String python = vformat(PYTHON_SCRIPT_DIRECT, dict_to_python(p_options)); diff --git a/modules/gltf/editor/editor_import_blend_runner.h b/modules/gltf/editor/editor_import_blend_runner.h index 626f3c9eba..b3b49ebfb2 100644 --- a/modules/gltf/editor/editor_import_blend_runner.h +++ b/modules/gltf/editor/editor_import_blend_runner.h @@ -47,6 +47,7 @@ class EditorImportBlendRunner : public Node { void _resources_reimported(const PackedStringArray &p_files); void _kill_blender(); void _notification(int p_what); + bool _extract_error_message_xml(const Vector<uint8_t> &p_response_data, String &r_error_message); protected: int rpc_port = 0; diff --git a/modules/gltf/editor/editor_scene_importer_blend.cpp b/modules/gltf/editor/editor_scene_importer_blend.cpp index 79a2184745..b474128fd6 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.cpp +++ b/modules/gltf/editor/editor_scene_importer_blend.cpp @@ -132,12 +132,10 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ } #endif - source_global = source_global.c_escape(); - const String blend_basename = p_path.get_file().get_basename(); const String sink = ProjectSettings::get_singleton()->get_imported_files_path().path_join( vformat("%s-%s.gltf", blend_basename, p_path.md5_text())); - const String sink_global = ProjectSettings::get_singleton()->globalize_path(sink).c_escape(); + const String sink_global = ProjectSettings::get_singleton()->globalize_path(sink); // Handle configuration options. @@ -188,10 +186,18 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ } else { parameters_map["export_lights"] = false; } - if (p_options.has(SNAME("blender/meshes/colors")) && p_options[SNAME("blender/meshes/colors")]) { - parameters_map["export_colors"] = true; + if (blender_major_version > 4 || (blender_major_version == 4 && blender_minor_version >= 2)) { + if (p_options.has(SNAME("blender/meshes/colors")) && p_options[SNAME("blender/meshes/colors")]) { + parameters_map["export_vertex_color"] = "MATERIAL"; + } else { + parameters_map["export_vertex_color"] = "NONE"; + } } else { - parameters_map["export_colors"] = false; + if (p_options.has(SNAME("blender/meshes/colors")) && p_options[SNAME("blender/meshes/colors")]) { + parameters_map["export_colors"] = true; + } else { + parameters_map["export_colors"] = false; + } } if (p_options.has(SNAME("blender/nodes/visible"))) { int32_t visible = p_options["blender/nodes/visible"]; diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 32fe37d9af..c0232e6d0c 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -934,58 +934,58 @@ Error GLTFDocument::_encode_accessors(Ref<GLTFState> p_state) { return OK; } -String GLTFDocument::_get_accessor_type_name(const GLTFAccessorType p_accessor_type) { - if (p_accessor_type == GLTFAccessorType::TYPE_SCALAR) { +String GLTFDocument::_get_accessor_type_name(const GLTFAccessor::GLTFAccessorType p_accessor_type) { + if (p_accessor_type == GLTFAccessor::TYPE_SCALAR) { return "SCALAR"; } - if (p_accessor_type == GLTFAccessorType::TYPE_VEC2) { + if (p_accessor_type == GLTFAccessor::TYPE_VEC2) { return "VEC2"; } - if (p_accessor_type == GLTFAccessorType::TYPE_VEC3) { + if (p_accessor_type == GLTFAccessor::TYPE_VEC3) { return "VEC3"; } - if (p_accessor_type == GLTFAccessorType::TYPE_VEC4) { + if (p_accessor_type == GLTFAccessor::TYPE_VEC4) { return "VEC4"; } - if (p_accessor_type == GLTFAccessorType::TYPE_MAT2) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT2) { return "MAT2"; } - if (p_accessor_type == GLTFAccessorType::TYPE_MAT3) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT3) { return "MAT3"; } - if (p_accessor_type == GLTFAccessorType::TYPE_MAT4) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT4) { return "MAT4"; } ERR_FAIL_V("SCALAR"); } -GLTFAccessorType GLTFDocument::_get_accessor_type_from_str(const String &p_string) { +GLTFAccessor::GLTFAccessorType GLTFDocument::_get_accessor_type_from_str(const String &p_string) { if (p_string == "SCALAR") { - return GLTFAccessorType::TYPE_SCALAR; + return GLTFAccessor::TYPE_SCALAR; } if (p_string == "VEC2") { - return GLTFAccessorType::TYPE_VEC2; + return GLTFAccessor::TYPE_VEC2; } if (p_string == "VEC3") { - return GLTFAccessorType::TYPE_VEC3; + return GLTFAccessor::TYPE_VEC3; } if (p_string == "VEC4") { - return GLTFAccessorType::TYPE_VEC4; + return GLTFAccessor::TYPE_VEC4; } if (p_string == "MAT2") { - return GLTFAccessorType::TYPE_MAT2; + return GLTFAccessor::TYPE_MAT2; } if (p_string == "MAT3") { - return GLTFAccessorType::TYPE_MAT3; + return GLTFAccessor::TYPE_MAT3; } if (p_string == "MAT4") { - return GLTFAccessorType::TYPE_MAT4; + return GLTFAccessor::TYPE_MAT4; } - ERR_FAIL_V(GLTFAccessorType::TYPE_SCALAR); + ERR_FAIL_V(GLTFAccessor::TYPE_SCALAR); } Error GLTFDocument::_parse_accessors(Ref<GLTFState> p_state) { @@ -1088,7 +1088,7 @@ String GLTFDocument::_get_component_type_name(const uint32_t p_component) { return "<Error>"; } -Error GLTFDocument::_encode_buffer_view(Ref<GLTFState> p_state, const double *p_src, const int p_count, const GLTFAccessorType p_accessor_type, const int p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_vertex_indices) { +Error GLTFDocument::_encode_buffer_view(Ref<GLTFState> p_state, const double *p_src, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_vertex_indices) { const int component_count_for_type[7] = { 1, 2, 3, 4, 4, 9, 16 }; @@ -1103,18 +1103,18 @@ Error GLTFDocument::_encode_buffer_view(Ref<GLTFState> p_state, const double *p_ switch (p_component_type) { case COMPONENT_TYPE_BYTE: case COMPONENT_TYPE_UNSIGNED_BYTE: { - if (p_accessor_type == TYPE_MAT2) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT2) { skip_every = 2; skip_bytes = 2; } - if (p_accessor_type == TYPE_MAT3) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 3; skip_bytes = 1; } } break; case COMPONENT_TYPE_SHORT: case COMPONENT_TYPE_UNSIGNED_SHORT: { - if (p_accessor_type == TYPE_MAT3) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 6; skip_bytes = 4; } @@ -1296,7 +1296,7 @@ Error GLTFDocument::_encode_buffer_view(Ref<GLTFState> p_state, const double *p_ return OK; } -Error GLTFDocument::_decode_buffer_view(Ref<GLTFState> p_state, double *p_dst, const GLTFBufferViewIndex p_buffer_view, const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, const GLTFAccessorType p_accessor_type, const int p_component_count, const int p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex) { +Error GLTFDocument::_decode_buffer_view(Ref<GLTFState> p_state, double *p_dst, const GLTFBufferViewIndex p_buffer_view, const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_count, const int p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex) { const Ref<GLTFBufferView> bv = p_state->buffer_views[p_buffer_view]; int stride = p_element_size; @@ -1427,12 +1427,12 @@ Vector<double> GLTFDocument::_decode_accessor(Ref<GLTFState> p_state, const GLTF switch (a->component_type) { case COMPONENT_TYPE_BYTE: case COMPONENT_TYPE_UNSIGNED_BYTE: { - if (a->accessor_type == TYPE_MAT2) { + if (a->accessor_type == GLTFAccessor::TYPE_MAT2) { skip_every = 2; skip_bytes = 2; element_size = 8; //override for this case } - if (a->accessor_type == TYPE_MAT3) { + if (a->accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 3; skip_bytes = 1; element_size = 12; //override for this case @@ -1440,7 +1440,7 @@ Vector<double> GLTFDocument::_decode_accessor(Ref<GLTFState> p_state, const GLTF } break; case COMPONENT_TYPE_SHORT: case COMPONENT_TYPE_UNSIGNED_SHORT: { - if (a->accessor_type == TYPE_MAT3) { + if (a->accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 6; skip_bytes = 4; element_size = 16; //override for this case @@ -1474,7 +1474,7 @@ Vector<double> GLTFDocument::_decode_accessor(Ref<GLTFState> p_state, const GLTF indices.resize(a->sparse_count); const int indices_component_size = _get_component_type_size(a->sparse_indices_component_type); - Error err = _decode_buffer_view(p_state, indices.ptrw(), a->sparse_indices_buffer_view, 0, 0, indices_component_size, a->sparse_count, TYPE_SCALAR, 1, a->sparse_indices_component_type, indices_component_size, false, a->sparse_indices_byte_offset, false); + Error err = _decode_buffer_view(p_state, indices.ptrw(), a->sparse_indices_buffer_view, 0, 0, indices_component_size, a->sparse_count, GLTFAccessor::TYPE_SCALAR, 1, a->sparse_indices_component_type, indices_component_size, false, a->sparse_indices_byte_offset, false); if (err != OK) { return Vector<double>(); } @@ -1536,7 +1536,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_ints(Ref<GLTFState> p_state, p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_SCALAR; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_SCALAR; int component_type; if (max_index > 65535 || p_for_vertex) { component_type = GLTFDocument::COMPONENT_TYPE_INT; @@ -1650,7 +1650,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_vec2(Ref<GLTFState> p_state, p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC2; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC2; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1703,7 +1703,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_color(Ref<GLTFState> p_state p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1770,7 +1770,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_weights(Ref<GLTFState> p_sta p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1821,7 +1821,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_joints(Ref<GLTFState> p_stat p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; const int component_type = GLTFDocument::COMPONENT_TYPE_UNSIGNED_SHORT; accessor->max = type_max; @@ -1874,7 +1874,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_quaternions(Ref<GLTFState> p p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1949,7 +1949,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_floats(Ref<GLTFState> p_stat p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_SCALAR; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_SCALAR; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1999,7 +1999,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_vec3(Ref<GLTFState> p_state, p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC3; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC3; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -2075,7 +2075,7 @@ GLTFAccessorIndex GLTFDocument::_encode_sparse_accessor_as_vec3(Ref<GLTFState> p p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC3; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC3; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; sparse_accessor->normalized = false; @@ -2103,7 +2103,7 @@ GLTFAccessorIndex GLTFDocument::_encode_sparse_accessor_as_vec3(Ref<GLTFState> p } else { sparse_accessor->sparse_indices_component_type = GLTFDocument::COMPONENT_TYPE_UNSIGNED_SHORT; } - if (_encode_buffer_view(p_state, changed_indices.ptr(), changed_indices.size(), GLTFAccessorType::TYPE_SCALAR, sparse_accessor->sparse_indices_component_type, sparse_accessor->normalized, sparse_accessor->sparse_indices_byte_offset, false, buffer_view_i_indices) != OK) { + if (_encode_buffer_view(p_state, changed_indices.ptr(), changed_indices.size(), GLTFAccessor::TYPE_SCALAR, sparse_accessor->sparse_indices_component_type, sparse_accessor->normalized, sparse_accessor->sparse_indices_byte_offset, false, buffer_view_i_indices) != OK) { return -1; } // We use changed_indices.size() here, because we must pass the number of vec3 values rather than the number of components. @@ -2180,7 +2180,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_xform(Ref<GLTFState> p_state p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_MAT4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_MAT4; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -2234,9 +2234,9 @@ Vector<Color> GLTFDocument::_decode_accessor_as_color(Ref<GLTFState> p_state, co } const int accessor_type = p_state->accessors[p_accessor]->accessor_type; - ERR_FAIL_COND_V(!(accessor_type == TYPE_VEC3 || accessor_type == TYPE_VEC4), ret); + ERR_FAIL_COND_V(!(accessor_type == GLTFAccessor::TYPE_VEC3 || accessor_type == GLTFAccessor::TYPE_VEC4), ret); int vec_len = 3; - if (accessor_type == TYPE_VEC4) { + if (accessor_type == GLTFAccessor::TYPE_VEC4) { vec_len = 4; } @@ -5785,15 +5785,17 @@ struct SceneFormatImporterGLTFInterpolate { return 0.5f * ((2.0f * p1) + (-p0 + p2) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3); } - T bezier(T start, T control_1, T control_2, T end, float t) { - /* Formula from Wikipedia article on Bezier curves. */ - const real_t omt = (1.0 - t); - const real_t omt2 = omt * omt; - const real_t omt3 = omt2 * omt; + T hermite(T start, T tan_start, T end, T tan_end, float t) { + /* Formula from the glTF 2.0 specification. */ const real_t t2 = t * t; const real_t t3 = t2 * t; - return start * omt3 + control_1 * omt2 * t * 3.0 + control_2 * omt * t2 * 3.0 + end * t3; + const real_t h00 = 2.0 * t3 - 3.0 * t2 + 1.0; + const real_t h10 = t3 - 2.0 * t2 + t; + const real_t h01 = -2.0 * t3 + 3.0 * t2; + const real_t h11 = t3 - t2; + + return start * h00 + tan_start * h10 + end * h01 + tan_end * h11; } }; @@ -5814,7 +5816,7 @@ struct SceneFormatImporterGLTFInterpolate<Quaternion> { return p1.slerp(p2, c).normalized(); } - Quaternion bezier(const Quaternion start, const Quaternion control_1, const Quaternion control_2, const Quaternion end, const float t) { + Quaternion hermite(const Quaternion start, const Quaternion tan_start, const Quaternion end, const Quaternion tan_end, const float t) { ERR_FAIL_COND_V_MSG(!start.is_normalized(), Quaternion(), vformat("The start quaternion %s must be normalized.", start)); ERR_FAIL_COND_V_MSG(!end.is_normalized(), Quaternion(), vformat("The end quaternion %s must be normalized.", end)); @@ -5879,14 +5881,15 @@ T GLTFDocument::_interpolate_track(const Vector<real_t> &p_times, const Vector<T return p_values[(p_times.size() - 1) * 3 + 1]; } - const float c = (p_time - p_times[idx]) / (p_times[idx + 1] - p_times[idx]); + const float td = (p_times[idx + 1] - p_times[idx]); + const float c = (p_time - p_times[idx]) / td; const T &from = p_values[idx * 3 + 1]; - const T c1 = from + p_values[idx * 3 + 2]; + const T tan_from = td * p_values[idx * 3 + 2]; const T &to = p_values[idx * 3 + 4]; - const T c2 = to + p_values[idx * 3 + 3]; + const T tan_to = td * p_values[idx * 3 + 3]; - return interp.bezier(from, c1, c2, to, c); + return interp.hermite(from, tan_from, to, tan_to, c); } break; } diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index 4f92ceccca..d37544750d 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -111,7 +111,7 @@ private: int _get_component_type_size(const int p_component_type); Error _parse_scenes(Ref<GLTFState> p_state); Error _parse_nodes(Ref<GLTFState> p_state); - String _get_accessor_type_name(const GLTFAccessorType p_accessor_type); + String _get_accessor_type_name(const GLTFAccessor::GLTFAccessorType p_accessor_type); String _sanitize_animation_name(const String &p_name); String _gen_unique_animation_name(Ref<GLTFState> p_state, const String &p_name); String _sanitize_bone_name(const String &p_name); @@ -131,13 +131,13 @@ private: void _compute_node_heights(Ref<GLTFState> p_state); Error _parse_buffers(Ref<GLTFState> p_state, const String &p_base_path); Error _parse_buffer_views(Ref<GLTFState> p_state); - GLTFAccessorType _get_accessor_type_from_str(const String &p_string); + GLTFAccessor::GLTFAccessorType _get_accessor_type_from_str(const String &p_string); Error _parse_accessors(Ref<GLTFState> p_state); Error _decode_buffer_view(Ref<GLTFState> p_state, double *p_dst, const GLTFBufferViewIndex p_buffer_view, const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, - const GLTFAccessorType p_accessor_type, const int p_component_count, + const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_count, const int p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex); @@ -266,7 +266,7 @@ private: const Vector<Transform3D> p_attribs, const bool p_for_vertex); Error _encode_buffer_view(Ref<GLTFState> p_state, const double *p_src, - const int p_count, const GLTFAccessorType p_accessor_type, + const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_indices = false); diff --git a/modules/gltf/structures/gltf_accessor.cpp b/modules/gltf/structures/gltf_accessor.cpp index 602f0d9dc4..1ebc00a514 100644 --- a/modules/gltf/structures/gltf_accessor.cpp +++ b/modules/gltf/structures/gltf_accessor.cpp @@ -31,6 +31,14 @@ #include "gltf_accessor.h" void GLTFAccessor::_bind_methods() { + BIND_ENUM_CONSTANT(TYPE_SCALAR); + BIND_ENUM_CONSTANT(TYPE_VEC2); + BIND_ENUM_CONSTANT(TYPE_VEC3); + BIND_ENUM_CONSTANT(TYPE_VEC4); + BIND_ENUM_CONSTANT(TYPE_MAT2); + BIND_ENUM_CONSTANT(TYPE_MAT3); + BIND_ENUM_CONSTANT(TYPE_MAT4); + ClassDB::bind_method(D_METHOD("get_buffer_view"), &GLTFAccessor::get_buffer_view); ClassDB::bind_method(D_METHOD("set_buffer_view", "buffer_view"), &GLTFAccessor::set_buffer_view); ClassDB::bind_method(D_METHOD("get_byte_offset"), &GLTFAccessor::get_byte_offset); @@ -43,8 +51,8 @@ void GLTFAccessor::_bind_methods() { ClassDB::bind_method(D_METHOD("set_count", "count"), &GLTFAccessor::set_count); ClassDB::bind_method(D_METHOD("get_accessor_type"), &GLTFAccessor::get_accessor_type); ClassDB::bind_method(D_METHOD("set_accessor_type", "accessor_type"), &GLTFAccessor::set_accessor_type); - ClassDB::bind_method(D_METHOD("get_type"), &GLTFAccessor::get_accessor_type); - ClassDB::bind_method(D_METHOD("set_type", "type"), &GLTFAccessor::set_accessor_type); + ClassDB::bind_method(D_METHOD("get_type"), &GLTFAccessor::get_type); + ClassDB::bind_method(D_METHOD("set_type", "type"), &GLTFAccessor::set_type); ClassDB::bind_method(D_METHOD("get_min"), &GLTFAccessor::get_min); ClassDB::bind_method(D_METHOD("set_min", "min"), &GLTFAccessor::set_min); ClassDB::bind_method(D_METHOD("get_max"), &GLTFAccessor::get_max); @@ -67,8 +75,8 @@ void GLTFAccessor::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "component_type"), "set_component_type", "get_component_type"); // int ADD_PROPERTY(PropertyInfo(Variant::BOOL, "normalized"), "set_normalized", "get_normalized"); // bool ADD_PROPERTY(PropertyInfo(Variant::INT, "count"), "set_count", "get_count"); // int - ADD_PROPERTY(PropertyInfo(Variant::INT, "accessor_type"), "set_accessor_type", "get_accessor_type"); // GLTFAccessorType - ADD_PROPERTY(PropertyInfo(Variant::INT, "type"), "set_type", "get_type"); // Deprecated, GLTFAccessorType + ADD_PROPERTY(PropertyInfo(Variant::INT, "accessor_type"), "set_accessor_type", "get_accessor_type"); // GLTFAccessor::GLTFAccessorType + ADD_PROPERTY(PropertyInfo(Variant::INT, "type", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_type", "get_type"); // Deprecated, int for GLTFAccessor::GLTFAccessorType ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT64_ARRAY, "min"), "set_min", "get_min"); // Vector<real_t> ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT64_ARRAY, "max"), "set_max", "get_max"); // Vector<real_t> ADD_PROPERTY(PropertyInfo(Variant::INT, "sparse_count"), "set_sparse_count", "get_sparse_count"); // int @@ -119,11 +127,19 @@ void GLTFAccessor::set_count(int p_count) { count = p_count; } -int GLTFAccessor::get_accessor_type() { +GLTFAccessor::GLTFAccessorType GLTFAccessor::get_accessor_type() { + return accessor_type; +} + +void GLTFAccessor::set_accessor_type(GLTFAccessorType p_accessor_type) { + accessor_type = p_accessor_type; +} + +int GLTFAccessor::get_type() { return (int)accessor_type; } -void GLTFAccessor::set_accessor_type(int p_accessor_type) { +void GLTFAccessor::set_type(int p_accessor_type) { accessor_type = (GLTFAccessorType)p_accessor_type; // TODO: Register enum } diff --git a/modules/gltf/structures/gltf_accessor.h b/modules/gltf/structures/gltf_accessor.h index 51ca282630..1a3a2cb494 100644 --- a/modules/gltf/structures/gltf_accessor.h +++ b/modules/gltf/structures/gltf_accessor.h @@ -35,20 +35,21 @@ #include "core/io/resource.h" -enum GLTFAccessorType { - TYPE_SCALAR, - TYPE_VEC2, - TYPE_VEC3, - TYPE_VEC4, - TYPE_MAT2, - TYPE_MAT3, - TYPE_MAT4, -}; - struct GLTFAccessor : public Resource { GDCLASS(GLTFAccessor, Resource); friend class GLTFDocument; +public: + enum GLTFAccessorType { + TYPE_SCALAR, + TYPE_VEC2, + TYPE_VEC3, + TYPE_VEC4, + TYPE_MAT2, + TYPE_MAT3, + TYPE_MAT4, + }; + private: GLTFBufferViewIndex buffer_view = -1; int byte_offset = 0; @@ -84,8 +85,11 @@ public: int get_count(); void set_count(int p_count); - int get_accessor_type(); - void set_accessor_type(int p_accessor_type); + GLTFAccessorType get_accessor_type(); + void set_accessor_type(GLTFAccessorType p_accessor_type); + + int get_type(); + void set_type(int p_accessor_type); Vector<double> get_min(); void set_min(Vector<double> p_min); @@ -112,4 +116,6 @@ public: void set_sparse_values_byte_offset(int p_sparse_values_byte_offset); }; +VARIANT_ENUM_CAST(GLTFAccessor::GLTFAccessorType); + #endif // GLTF_ACCESSOR_H diff --git a/modules/gridmap/editor/grid_map_editor_plugin.cpp b/modules/gridmap/editor/grid_map_editor_plugin.cpp index b93b7c1a8c..f402e2a583 100644 --- a/modules/gridmap/editor/grid_map_editor_plugin.cpp +++ b/modules/gridmap/editor/grid_map_editor_plugin.cpp @@ -59,10 +59,18 @@ void GridMapEditor::_menu_option(int p_option) { switch (p_option) { case MENU_OPTION_PREV_LEVEL: { floor->set_value(floor->get_value() - 1); + if (selection.active && input_action == INPUT_SELECT) { + selection.current[edit_axis]--; + _validate_selection(); + } } break; case MENU_OPTION_NEXT_LEVEL: { floor->set_value(floor->get_value() + 1); + if (selection.active && input_action == INPUT_SELECT) { + selection.current[edit_axis]++; + _validate_selection(); + } } break; case MENU_OPTION_X_AXIS: @@ -754,19 +762,6 @@ EditorPlugin::AfterGUIInput GridMapEditor::forward_spatial_input_event(Camera3D } } } - - if (k->is_shift_pressed() && selection.active && input_action != INPUT_PASTE) { - if (k->get_keycode() == (Key)options->get_popup()->get_item_accelerator(options->get_popup()->get_item_index(MENU_OPTION_PREV_LEVEL))) { - selection.click[edit_axis]--; - _validate_selection(); - return EditorPlugin::AFTER_GUI_INPUT_STOP; - } - if (k->get_keycode() == (Key)options->get_popup()->get_item_accelerator(options->get_popup()->get_item_index(MENU_OPTION_NEXT_LEVEL))) { - selection.click[edit_axis]++; - _validate_selection(); - return EditorPlugin::AFTER_GUI_INPUT_STOP; - } - } } } diff --git a/modules/jpg/image_loader_jpegd.cpp b/modules/jpg/image_loader_jpegd.cpp index ada0cd01fa..53046de740 100644 --- a/modules/jpg/image_loader_jpegd.cpp +++ b/modules/jpg/image_loader_jpegd.cpp @@ -162,7 +162,7 @@ static Error _jpgd_save_to_output_stream(jpge::output_stream *p_output_stream, c ERR_FAIL_COND_V_MSG(error != OK, error, "Couldn't decompress image."); } if (image->get_format() != Image::FORMAT_RGB8) { - image = p_img->duplicate(); + image = image->duplicate(); image->convert(Image::FORMAT_RGB8); } diff --git a/modules/navigation/nav_map.cpp b/modules/navigation/nav_map.cpp index 8a7da64eb5..f917c988ea 100644 --- a/modules/navigation/nav_map.cpp +++ b/modules/navigation/nav_map.cpp @@ -644,6 +644,26 @@ Vector3 NavMap::get_closest_point_to_segment(const Vector3 &p_from, const Vector } } } + // Finally, check for a case when shortest distance is between some point located on a face's edge and some point located on a line segment. + if (!use_collision) { + for (size_t point_id = 0; point_id < p.points.size(); point_id += 1) { + Vector3 a, b; + + Geometry3D::get_closest_points_between_segments( + p_from, + p_to, + p.points[point_id].pos, + p.points[(point_id + 1) % p.points.size()].pos, + a, + b); + + const real_t d = a.distance_to(b); + if (d < closest_point_d) { + closest_point_d = d; + closest_point = b; + } + } + } } return closest_point; diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 541e369925..04edde8300 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -902,6 +902,47 @@ bool OpenXRAPI::setup_play_space() { new_reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL; will_emulate_local_floor = true; + + if (local_floor_emulation.local_space == XR_NULL_HANDLE) { + XrReferenceSpaceCreateInfo create_info = { + XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type + nullptr, // next + XR_REFERENCE_SPACE_TYPE_LOCAL, // referenceSpaceType + identityPose, // poseInReferenceSpace + }; + + XrResult result = xrCreateReferenceSpace(session, &create_info, &local_floor_emulation.local_space); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to create LOCAL space in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); + will_emulate_local_floor = false; + } + } + + if (local_floor_emulation.stage_space == XR_NULL_HANDLE) { + XrReferenceSpaceCreateInfo create_info = { + XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type + nullptr, // next + XR_REFERENCE_SPACE_TYPE_STAGE, // referenceSpaceType + identityPose, // poseInReferenceSpace + }; + + XrResult result = xrCreateReferenceSpace(session, &create_info, &local_floor_emulation.stage_space); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to create STAGE space in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); + will_emulate_local_floor = false; + } + } + + if (!will_emulate_local_floor) { + if (local_floor_emulation.local_space != XR_NULL_HANDLE) { + xrDestroySpace(local_floor_emulation.local_space); + local_floor_emulation.local_space = XR_NULL_HANDLE; + } + if (local_floor_emulation.stage_space != XR_NULL_HANDLE) { + xrDestroySpace(local_floor_emulation.stage_space); + local_floor_emulation.stage_space = XR_NULL_HANDLE; + } + } } else { // Fallback on LOCAL, which all OpenXR runtimes are required to support. print_verbose(String("OpenXR: ") + OpenXRUtil::get_reference_space_name(requested_reference_space) + String(" isn't supported, defaulting to LOCAL space.")); @@ -931,16 +972,11 @@ bool OpenXRAPI::setup_play_space() { play_space = new_play_space; reference_space = new_reference_space; - emulating_local_floor = will_emulate_local_floor; - if (emulating_local_floor) { - // We'll use the STAGE space to get the floor height, but we can't do that until - // after xrWaitFrame(), so just set this flag for now. - // Render state will be updated then. - should_reset_emulated_floor_height = true; - } else { - // Update render state so this play space is used rendering the upcoming frame. - set_render_play_space(play_space); - } + local_floor_emulation.enabled = will_emulate_local_floor; + local_floor_emulation.should_reset_floor_height = will_emulate_local_floor; + + // Update render state so this play space is used rendering the upcoming frame. + set_render_play_space(play_space); return true; } @@ -975,63 +1011,39 @@ bool OpenXRAPI::setup_view_space() { } bool OpenXRAPI::reset_emulated_floor_height() { - ERR_FAIL_COND_V(!emulating_local_floor, false); - - // This is based on the example code in the OpenXR spec which shows how to - // emulate LOCAL_FLOOR if it's not supported. - // See: https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_EXT_local_floor + ERR_FAIL_COND_V(!local_floor_emulation.enabled, false); + ERR_FAIL_COND_V(local_floor_emulation.local_space == XR_NULL_HANDLE, false); + ERR_FAIL_COND_V(local_floor_emulation.stage_space == XR_NULL_HANDLE, false); XrResult result; - XrPosef identityPose = { - { 0.0, 0.0, 0.0, 1.0 }, - { 0.0, 0.0, 0.0 } - }; - - XrSpace local_space = XR_NULL_HANDLE; - XrSpace stage_space = XR_NULL_HANDLE; - - XrReferenceSpaceCreateInfo create_info = { - XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type - nullptr, // next - XR_REFERENCE_SPACE_TYPE_LOCAL, // referenceSpaceType - identityPose, // poseInReferenceSpace - }; - - result = xrCreateReferenceSpace(session, &create_info, &local_space); - if (XR_FAILED(result)) { - print_line("OpenXR: Failed to create LOCAL space in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); - return false; - } - - create_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; - result = xrCreateReferenceSpace(session, &create_info, &stage_space); - if (XR_FAILED(result)) { - print_line("OpenXR: Failed to create STAGE space in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); - xrDestroySpace(local_space); - return false; - } - XrSpaceLocation stage_location = { XR_TYPE_SPACE_LOCATION, // type nullptr, // next 0, // locationFlags - identityPose, // pose + { { 0.0, 0.0, 0.0, 1.0 }, { 0.0, 0.0, 0.0 } }, // pose }; - result = xrLocateSpace(stage_space, local_space, get_predicted_display_time(), &stage_location); - - xrDestroySpace(local_space); - xrDestroySpace(stage_space); + result = xrLocateSpace(local_floor_emulation.stage_space, local_floor_emulation.local_space, get_predicted_display_time(), &stage_location); if (XR_FAILED(result)) { print_line("OpenXR: Failed to locate STAGE space in LOCAL space, in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); return false; } + XrPosef pose = { + { 0.0, 0.0, 0.0, 1.0 }, + { 0.0, stage_location.pose.position.y, 0.0 } + }; + + XrReferenceSpaceCreateInfo create_info = { + XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type + nullptr, // next + XR_REFERENCE_SPACE_TYPE_LOCAL, // referenceSpaceType + pose, // poseInReferenceSpace + }; + XrSpace new_play_space; - create_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; - create_info.poseInReferenceSpace.position.y = stage_location.pose.position.y; result = xrCreateReferenceSpace(session, &create_info, &new_play_space); if (XR_FAILED(result)) { print_line("OpenXR: Failed to recreate emulated LOCAL_FLOOR play space with latest floor estimate [", get_error_string(result), "]"); @@ -1275,6 +1287,16 @@ void OpenXRAPI::destroy_session() { xrDestroySpace(view_space); view_space = XR_NULL_HANDLE; } + if (local_floor_emulation.local_space != XR_NULL_HANDLE) { + xrDestroySpace(local_floor_emulation.local_space); + local_floor_emulation.local_space = XR_NULL_HANDLE; + } + if (local_floor_emulation.stage_space != XR_NULL_HANDLE) { + xrDestroySpace(local_floor_emulation.stage_space); + local_floor_emulation.stage_space = XR_NULL_HANDLE; + } + local_floor_emulation.enabled = false; + local_floor_emulation.should_reset_floor_height = false; if (supported_reference_spaces != nullptr) { // free previous results @@ -1953,8 +1975,8 @@ bool OpenXRAPI::poll_events() { XrEventDataReferenceSpaceChangePending *event = (XrEventDataReferenceSpaceChangePending *)&runtimeEvent; print_verbose(String("OpenXR EVENT: reference space type ") + OpenXRUtil::get_reference_space_name(event->referenceSpaceType) + " change pending!"); - if (emulating_local_floor) { - should_reset_emulated_floor_height = true; + if (local_floor_emulation.enabled) { + local_floor_emulation.should_reset_floor_height = true; } if (event->poseValid && xr_interface) { xr_interface->on_pose_recentered(); @@ -2097,16 +2119,18 @@ bool OpenXRAPI::process() { set_render_display_info(frame_state.predictedDisplayTime, frame_state.shouldRender); + // This is before setup_play_space() to ensure that it happens on the frame after + // the play space has been created. + if (unlikely(local_floor_emulation.should_reset_floor_height && !play_space_is_dirty)) { + reset_emulated_floor_height(); + local_floor_emulation.should_reset_floor_height = false; + } + if (unlikely(play_space_is_dirty)) { setup_play_space(); play_space_is_dirty = false; } - if (unlikely(should_reset_emulated_floor_height)) { - reset_emulated_floor_height(); - should_reset_emulated_floor_height = false; - } - for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_process(); } diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index f9d2e60148..748ef3af94 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -165,8 +165,16 @@ private: XrSpace view_space = XR_NULL_HANDLE; XRPose::TrackingConfidence head_pose_confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE; - bool emulating_local_floor = false; - bool should_reset_emulated_floor_height = false; + // When LOCAL_FLOOR isn't supported, we use an approach based on the example code in the + // OpenXR spec in order to emulate it. + // See: https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_EXT_local_floor + struct LocalFloorEmulation { + bool enabled = false; + XrSpace local_space = XR_NULL_HANDLE; + XrSpace stage_space = XR_NULL_HANDLE; + bool should_reset_floor_height = false; + } local_floor_emulation; + bool reset_emulated_floor_height(); bool load_layer_properties(); diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 0c87199635..499ddb703b 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -5248,7 +5248,7 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ if ((trim_pos >= 0 && sd->width > p_width) || enforce_ellipsis) { if (add_ellipsis && (ellipsis_pos > 0 || enforce_ellipsis)) { - // Insert an additional space when cutting word bound for esthetics. + // Insert an additional space when cutting word bound for aesthetics. if (cut_per_word && (ellipsis_pos > 0)) { Glyph gl; gl.count = 1; diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index 6cf6b236ed..b45c004011 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -4061,7 +4061,7 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ if ((trim_pos >= 0 && sd->width > p_width) || enforce_ellipsis) { if (add_ellipsis && (ellipsis_pos > 0 || enforce_ellipsis)) { - // Insert an additional space when cutting word bound for esthetics. + // Insert an additional space when cutting word bound for aesthetics. if (cut_per_word && (ellipsis_pos > 0)) { Glyph gl; gl.count = 1; diff --git a/modules/webxr/doc_classes/WebXRInterface.xml b/modules/webxr/doc_classes/WebXRInterface.xml index 9fd4511d2b..bd7192520a 100644 --- a/modules/webxr/doc_classes/WebXRInterface.xml +++ b/modules/webxr/doc_classes/WebXRInterface.xml @@ -54,9 +54,10 @@ # supported. webxr_interface.requested_reference_space_types = 'bounded-floor, local-floor, local' # In order to use 'local-floor' or 'bounded-floor' we must also - # mark the features as required or optional. + # mark the features as required or optional. By including 'hand-tracking' + # as an optional feature, it will be enabled if supported. webxr_interface.required_features = 'local-floor' - webxr_interface.optional_features = 'bounded-floor' + webxr_interface.optional_features = 'bounded-floor, hand-tracking' # This will return false if we're unable to even request the session, # however, it can still fail asynchronously later in the process, so we @@ -73,7 +74,10 @@ # This will be the reference space type you ultimately got, out of the # types that you requested above. This is useful if you want the game to # work a little differently in 'bounded-floor' versus 'local-floor'. - print ("Reference space type: " + webxr_interface.reference_space_type) + print("Reference space type: ", webxr_interface.reference_space_type) + # This will be the list of features that were successfully enabled + # (except on browsers that don't support this property). + print("Enabled features: ", webxr_interface.enabled_features) func _webxr_session_ended(): $Button.visible = true @@ -155,13 +159,14 @@ <members> <member name="enabled_features" type="String" setter="" getter="get_enabled_features"> A comma-separated list of features that were successfully enabled by [method XRInterface.initialize] when setting up the WebXR session. - This may include features requested by setting [member required_features] and [member optional_features]. + This may include features requested by setting [member required_features] and [member optional_features], and will only be available after [signal session_started] has been emitted. + [b]Note:[/b] This may not be support by all web browsers, in which case it will be an empty string. </member> <member name="optional_features" type="String" setter="set_optional_features" getter="get_optional_features"> A comma-seperated list of optional features used by [method XRInterface.initialize] when setting up the WebXR session. If a user's browser or device doesn't support one of the given features, initialization will continue, but you won't be able to use the requested feature. This doesn't have any effect on the interface when already initialized. - Possible values come from [url=https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpaceType]WebXR's XRReferenceSpaceType[/url]. If you want to use a particular reference space type, it must be listed in either [member required_features] or [member optional_features]. + Possible values come from [url=https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpaceType]WebXR's XRReferenceSpaceType[/url], or include other features like [code]"hand-tracking"[/code] to enable hand tracking. </member> <member name="reference_space_type" type="String" setter="" getter="get_reference_space_type"> The reference space type (from the list of requested types set in the [member requested_reference_space_types] property), that was ultimately used by [method XRInterface.initialize] when setting up the WebXR session. @@ -177,7 +182,7 @@ A comma-seperated list of required features used by [method XRInterface.initialize] when setting up the WebXR session. If a user's browser or device doesn't support one of the given features, initialization will fail and [signal session_failed] will be emitted. This doesn't have any effect on the interface when already initialized. - Possible values come from [url=https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpaceType]WebXR's XRReferenceSpaceType[/url]. If you want to use a particular reference space type, it must be listed in either [member required_features] or [member optional_features]. + Possible values come from [url=https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpaceType]WebXR's XRReferenceSpaceType[/url], or include other features like [code]"hand-tracking"[/code] to enable hand tracking. </member> <member name="session_mode" type="String" setter="set_session_mode" getter="get_session_mode"> The session mode used by [method XRInterface.initialize] when setting up the WebXR session. diff --git a/modules/webxr/godot_webxr.h b/modules/webxr/godot_webxr.h index caa7f217af..1b3e3b6a41 100644 --- a/modules/webxr/godot_webxr.h +++ b/modules/webxr/godot_webxr.h @@ -45,7 +45,7 @@ enum WebXRInputEvent { }; typedef void (*GodotWebXRSupportedCallback)(char *p_session_mode, int p_supported); -typedef void (*GodotWebXRStartedCallback)(char *p_reference_space_type, char *p_enabled_features); +typedef void (*GodotWebXRStartedCallback)(char *p_reference_space_type, char *p_enabled_features, char *p_environment_blend_mode); typedef void (*GodotWebXREndedCallback)(); typedef void (*GodotWebXRFailedCallback)(char *p_message); typedef void (*GodotWebXRInputEventCallback)(int p_event_type, int p_input_source_id); diff --git a/modules/webxr/native/library_godot_webxr.js b/modules/webxr/native/library_godot_webxr.js index 581da4d56f..155409f931 100644 --- a/modules/webxr/native/library_godot_webxr.js +++ b/modules/webxr/native/library_godot_webxr.js @@ -322,9 +322,12 @@ const GodotWebXR = { const reference_space_c_str = GodotRuntime.allocString(reference_space_type); const enabled_features = 'enabledFeatures' in session ? Array.from(session.enabledFeatures) : []; const enabled_features_c_str = GodotRuntime.allocString(enabled_features.join(',')); - onstarted(reference_space_c_str, enabled_features_c_str); + const environment_blend_mode = 'environmentBlendMode' in session ? session.environmentBlendMode : ''; + const environment_blend_mode_c_str = GodotRuntime.allocString(environment_blend_mode); + onstarted(reference_space_c_str, enabled_features_c_str, environment_blend_mode_c_str); GodotRuntime.free(reference_space_c_str); GodotRuntime.free(enabled_features_c_str); + GodotRuntime.free(environment_blend_mode_c_str); }, 0); } diff --git a/modules/webxr/native/webxr.externs.js b/modules/webxr/native/webxr.externs.js index 80a7f8d2de..18125d7869 100644 --- a/modules/webxr/native/webxr.externs.js +++ b/modules/webxr/native/webxr.externs.js @@ -83,6 +83,11 @@ XRSession.prototype.supportedFrameRates; XRSession.prototype.enabledFeatures; /** + * @type {string} + */ +XRSession.prototype.environmentBlendMode; + +/** * @type {?function (Event)} */ XRSession.prototype.onend; diff --git a/modules/webxr/webxr_interface_js.cpp b/modules/webxr/webxr_interface_js.cpp index 6c18b650ee..78a9db9b19 100644 --- a/modules/webxr/webxr_interface_js.cpp +++ b/modules/webxr/webxr_interface_js.cpp @@ -58,7 +58,7 @@ void _emwebxr_on_session_supported(char *p_session_mode, int p_supported) { interface->emit_signal(SNAME("session_supported"), session_mode, p_supported ? true : false); } -void _emwebxr_on_session_started(char *p_reference_space_type, char *p_enabled_features) { +void _emwebxr_on_session_started(char *p_reference_space_type, char *p_enabled_features, char *p_environment_blend_mode) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); @@ -68,6 +68,7 @@ void _emwebxr_on_session_started(char *p_reference_space_type, char *p_enabled_f String reference_space_type = String(p_reference_space_type); interface->_set_reference_space_type(reference_space_type); interface->_set_enabled_features(p_enabled_features); + interface->_set_environment_blend_mode(p_environment_blend_mode); interface->emit_signal(SNAME("session_started")); } @@ -230,6 +231,44 @@ Array WebXRInterfaceJS::get_available_display_refresh_rates() const { return ret; } +Array WebXRInterfaceJS::get_supported_environment_blend_modes() { + Array blend_modes; + // The blend mode can't be changed, so return the current blend mode as the only supported one. + blend_modes.push_back(environment_blend_mode); + return blend_modes; +} + +XRInterface::EnvironmentBlendMode WebXRInterfaceJS::get_environment_blend_mode() const { + return environment_blend_mode; +} + +bool WebXRInterfaceJS::set_environment_blend_mode(EnvironmentBlendMode p_new_environment_blend_mode) { + if (environment_blend_mode == p_new_environment_blend_mode) { + // Environment blend mode can't be changed, but we'll consider it a success to set it + // to what it already is. + return true; + } + return false; +} + +void WebXRInterfaceJS::_set_environment_blend_mode(String p_blend_mode_string) { + if (p_blend_mode_string == "opaque") { + environment_blend_mode = XRInterface::XR_ENV_BLEND_MODE_OPAQUE; + } else if (p_blend_mode_string == "additive") { + environment_blend_mode = XRInterface::XR_ENV_BLEND_MODE_ADDITIVE; + } else if (p_blend_mode_string == "alpha-blend") { + environment_blend_mode = XRInterface::XR_ENV_BLEND_MODE_ALPHA_BLEND; + } else { + // Not all browsers can give us this information, so as a fallback, + // we'll make some guesses about the blend mode. + if (session_mode == "immersive-ar") { + environment_blend_mode = XRInterface::XR_ENV_BLEND_MODE_ALPHA_BLEND; + } else { + environment_blend_mode = XRInterface::XR_ENV_BLEND_MODE_OPAQUE; + } + } +} + StringName WebXRInterfaceJS::get_name() const { return "WebXR"; }; @@ -336,6 +375,7 @@ void WebXRInterfaceJS::uninitialize() { texture_cache.clear(); reference_space_type.clear(); enabled_features.clear(); + environment_blend_mode = XRInterface::XR_ENV_BLEND_MODE_OPAQUE; initialized = false; }; }; diff --git a/modules/webxr/webxr_interface_js.h b/modules/webxr/webxr_interface_js.h index afce28d410..d02c8d2677 100644 --- a/modules/webxr/webxr_interface_js.h +++ b/modules/webxr/webxr_interface_js.h @@ -60,6 +60,8 @@ private: String reference_space_type; String enabled_features; + XRInterface::EnvironmentBlendMode environment_blend_mode = XRInterface::XR_ENV_BLEND_MODE_OPAQUE; + Size2 render_targetsize; RBMap<unsigned int, RID> texture_cache; struct Touch { @@ -113,6 +115,10 @@ public: virtual void set_display_refresh_rate(float p_refresh_rate) override; virtual Array get_available_display_refresh_rates() const override; + virtual Array get_supported_environment_blend_modes() override; + virtual XRInterface::EnvironmentBlendMode get_environment_blend_mode() const override; + virtual bool set_environment_blend_mode(EnvironmentBlendMode p_new_environment_blend_mode) override; + virtual StringName get_name() const override; virtual uint32_t get_capabilities() const override; @@ -136,8 +142,10 @@ public: void _on_input_event(int p_event_type, int p_input_source_id); + // Internal setters used by callbacks from Emscripten. inline void _set_reference_space_type(String p_reference_space_type) { reference_space_type = p_reference_space_type; } inline void _set_enabled_features(String p_enabled_features) { enabled_features = p_enabled_features; } + void _set_environment_blend_mode(String p_blend_mode_string); WebXRInterfaceJS(); ~WebXRInterfaceJS(); diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 288c37be29..06b304dcde 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -607,6 +607,7 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver)); memdelete(rendering_context); rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; return; } @@ -627,6 +628,7 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis ERR_PRINT(vformat("Failed to create %s window.", rendering_driver)); memdelete(rendering_context); rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; return; } @@ -635,7 +637,13 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis rendering_context->window_set_vsync_mode(MAIN_WINDOW_ID, p_vsync_mode); rendering_device = memnew(RenderingDevice); - rendering_device->initialize(rendering_context, MAIN_WINDOW_ID); + if (rendering_device->initialize(rendering_context, MAIN_WINDOW_ID) != OK) { + rendering_device = nullptr; + memdelete(rendering_context); + rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; + return; + } rendering_device->screen_create(MAIN_WINDOW_ID); RendererCompositorRD::make_current(); diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 4abc6548bf..0cc929d226 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -24,6 +24,10 @@ android:hasFragileUserData="false" android:requestLegacyExternalStorage="false" tools:ignore="GoogleAppIndexingWarning" > + <profileable + android:shell="true" + android:enabled="true" + tools:targetApi="29" /> <!-- Records the version of the Godot editor used for building --> <meta-data diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 01759a1b2f..eb9ad9de05 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -7,7 +7,7 @@ ext.versions = [ targetSdk : 34, buildTools : '34.0.0', kotlinVersion : '1.9.20', - fragmentVersion : '1.6.2', + fragmentVersion : '1.7.1', nexusPublishVersion: '1.3.0', javaVersion : JavaVersion.VERSION_17, // Also update 'platform/android/detect.py#get_ndk_version()' when this is updated. diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 55fe2a22fe..37f68d295a 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -9,7 +9,7 @@ dependencies { implementation "androidx.fragment:fragment:$versions.fragmentVersion" implementation project(":lib") - implementation "androidx.window:window:1.2.0" + implementation "androidx.window:window:1.3.0" implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" implementation "androidx.constraintlayout:constraintlayout:2.1.4" } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index 5515347bd6..dad397de61 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -117,6 +117,10 @@ open class GodotEditor : GodotActivity() { val longPressEnabled = enableLongPressGestures() val panScaleEnabled = enablePanAndScaleGestures() + val useInputBuffering = useInputBuffering() + val useAccumulatedInput = useAccumulatedInput() + GodotLib.updateInputDispatchSettings(useAccumulatedInput, useInputBuffering) + checkForProjectPermissionsToEnable() runOnUiThread { @@ -124,6 +128,7 @@ open class GodotEditor : GodotActivity() { godotFragment?.godot?.renderView?.inputHandler?.apply { enableLongPress(longPressEnabled) enablePanningAndScalingGestures(panScaleEnabled) + enableInputDispatchToRenderThread(!useInputBuffering && !useAccumulatedInput) } } } @@ -275,6 +280,13 @@ open class GodotEditor : GodotActivity() { java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures")) /** + * Use input buffering for the Godot Android editor. + */ + protected open fun useInputBuffering() = java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/editor/android/use_input_buffering")) + + protected open fun useAccumulatedInput() = java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/editor/android/use_accumulated_input")) + + /** * Whether we should launch the new godot instance in an adjacent window * @see https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT */ diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 8e4e089211..f50b5577c3 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -30,6 +30,8 @@ package org.godotengine.editor +import org.godotengine.godot.GodotLib + /** * Drives the 'run project' window of the Godot Editor. */ @@ -39,9 +41,13 @@ class GodotGame : GodotEditor() { override fun overrideOrientationRequest() = false - override fun enableLongPressGestures() = false + override fun enableLongPressGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) + + override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) + + override fun useInputBuffering() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_input_buffering")) - override fun enablePanAndScaleGestures() = false + override fun useAccumulatedInput() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_accumulated_input")) override fun checkForProjectPermissionsToEnable() { // Nothing to do.. by the time we get here, the project permissions will have already diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index 290be727ab..c188a97ca5 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -84,17 +84,20 @@ class Godot(private val context: Context) : SensorEventListener { private companion object { private val TAG = Godot::class.java.simpleName - } - private val windowManager: WindowManager by lazy { - requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager + // Supported build flavors + const val EDITOR_FLAVOR = "editor" + const val TEMPLATE_FLAVOR = "template" } + + private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() } - private val mSensorManager: SensorManager by lazy { - requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager - } private val mAccelerometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) } @@ -107,9 +110,6 @@ class Godot(private val context: Context) : SensorEventListener { private val mGyroscope: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) } - private val mClipboard: ClipboardManager by lazy { - requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - } private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int -> if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) { @@ -192,6 +192,8 @@ class Godot(private val context: Context) : SensorEventListener { return } + Log.v(TAG, "OnCreate: $primaryHost") + darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES beginBenchmarkMeasure("Startup", "Godot::onCreate") @@ -200,6 +202,8 @@ class Godot(private val context: Context) : SensorEventListener { val activity = requireActivity() val window = activity.window window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) + + Log.v(TAG, "Initializing Godot plugin registry") GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this)) if (io == null) { io = GodotIO(activity) @@ -323,13 +327,17 @@ class Godot(private val context: Context) : SensorEventListener { return false } - if (expansionPackPath.isNotEmpty()) { - commandLine.add("--main-pack") - commandLine.add(expansionPackPath) - } - val activity = requireActivity() - if (!nativeLayerInitializeCompleted) { - nativeLayerInitializeCompleted = GodotLib.initialize( + Log.v(TAG, "OnInitNativeLayer: $host") + + beginBenchmarkMeasure("Startup", "Godot::onInitNativeLayer") + try { + if (expansionPackPath.isNotEmpty()) { + commandLine.add("--main-pack") + commandLine.add(expansionPackPath) + } + val activity = requireActivity() + if (!nativeLayerInitializeCompleted) { + nativeLayerInitializeCompleted = GodotLib.initialize( activity, this, activity.assets, @@ -338,15 +346,20 @@ class Godot(private val context: Context) : SensorEventListener { directoryAccessHandler, fileAccessHandler, useApkExpansion, - ) - } + ) + Log.v(TAG, "Godot native layer initialization completed: $nativeLayerInitializeCompleted") + } - if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) { - nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts) - if (!nativeLayerSetupCompleted) { - Log.e(TAG, "Unable to setup the Godot engine! Aborting...") - alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit) + if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) { + nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts) + if (!nativeLayerSetupCompleted) { + throw IllegalStateException("Unable to setup the Godot engine! Aborting...") + } else { + Log.v(TAG, "Godot native layer setup completed") + } } + } finally { + endBenchmarkMeasure("Startup", "Godot::onInitNativeLayer") } return isNativeInitialized() } @@ -370,6 +383,9 @@ class Godot(private val context: Context) : SensorEventListener { throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view") } + Log.v(TAG, "OnInitRenderView: $host") + + beginBenchmarkMeasure("Startup", "Godot::onInitRenderView") try { val activity: Activity = host.activity containerLayout = providedContainerLayout @@ -392,8 +408,7 @@ class Godot(private val context: Context) : SensorEventListener { containerLayout?.addView(editText) renderView = if (usesVulkan()) { if (!meetsVulkanRequirements(activity.packageManager)) { - alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit) - return null + throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message)) } GodotVulkanRenderView(host, this) } else { @@ -482,11 +497,14 @@ class Godot(private val context: Context) : SensorEventListener { containerLayout?.removeAllViews() containerLayout = null } + + endBenchmarkMeasure("Startup", "Godot::onInitRenderView") } return containerLayout } fun onStart(host: GodotHost) { + Log.v(TAG, "OnStart: $host") if (host != primaryHost) { return } @@ -495,6 +513,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onResume(host: GodotHost) { + Log.v(TAG, "OnResume: $host") if (host != primaryHost) { return } @@ -527,6 +546,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onPause(host: GodotHost) { + Log.v(TAG, "OnPause: $host") if (host != primaryHost) { return } @@ -539,6 +559,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onStop(host: GodotHost) { + Log.v(TAG, "OnStop: $host") if (host != primaryHost) { return } @@ -547,6 +568,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onDestroy(primaryHost: GodotHost) { + Log.v(TAG, "OnDestroy: $primaryHost") if (this.primaryHost != primaryHost) { return } @@ -604,18 +626,29 @@ class Godot(private val context: Context) : SensorEventListener { * Invoked on the render thread when the Godot setup is complete. */ private fun onGodotSetupCompleted() { - Log.d(TAG, "OnGodotSetupCompleted") - - // These properties are defined after Godot setup completion, so we retrieve them here. - val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) - val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) - val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis")) - - runOnUiThread { - renderView?.inputHandler?.apply { - enableLongPress(longPressEnabled) - enablePanningAndScalingGestures(panScaleEnabled) - setRotaryInputAxis(rotaryInputAxis) + Log.v(TAG, "OnGodotSetupCompleted") + + if (!isEditorBuild()) { + // These properties are defined after Godot setup completion, so we retrieve them here. + val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) + val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) + val rotaryInputAxisValue = GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis") + + val useInputBuffering = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_input_buffering")) + val useAccumulatedInput = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_accumulated_input")) + GodotLib.updateInputDispatchSettings(useAccumulatedInput, useInputBuffering) + + runOnUiThread { + renderView?.inputHandler?.apply { + enableLongPress(longPressEnabled) + enablePanningAndScalingGestures(panScaleEnabled) + enableInputDispatchToRenderThread(!useInputBuffering && !useAccumulatedInput) + try { + setRotaryInputAxis(Integer.parseInt(rotaryInputAxisValue)) + } catch (e: NumberFormatException) { + Log.w(TAG, e) + } + } } } @@ -629,7 +662,7 @@ class Godot(private val context: Context) : SensorEventListener { * Invoked on the render thread when the Godot main loop has started. */ private fun onGodotMainLoopStarted() { - Log.d(TAG, "OnGodotMainLoopStarted") + Log.v(TAG, "OnGodotMainLoopStarted") for (plugin in pluginRegistry.allPlugins) { plugin.onGodotMainLoopStarted() @@ -646,12 +679,7 @@ class Godot(private val context: Context) : SensorEventListener { decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener) } - @Keep - private fun alert(message: String, title: String) { - alert(message, title, null) - } - - private fun alert( + fun alert( @StringRes messageResId: Int, @StringRes titleResId: Int, okCallback: Runnable? @@ -660,7 +688,9 @@ class Godot(private val context: Context) : SensorEventListener { alert(res.getString(messageResId), res.getString(titleResId), okCallback) } - private fun alert(message: String, title: String, okCallback: Runnable?) { + @JvmOverloads + @Keep + fun alert(message: String, title: String, okCallback: Runnable? = null) { val activity: Activity = getActivity() ?: return runOnUiThread { val builder = AlertDialog.Builder(activity) @@ -759,6 +789,11 @@ class Godot(private val context: Context) : SensorEventListener { return mClipboard.hasPrimaryClip() } + /** + * @return true if this is an editor build, false if this is a template build + */ + fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR + fun getClipboard(): String { val clipData = mClipboard.primaryClip ?: return "" val text = clipData.getItemAt(0).text ?: return "" @@ -770,7 +805,7 @@ class Godot(private val context: Context) : SensorEventListener { mClipboard.setPrimaryClip(clip) } - private fun forceQuit() { + fun forceQuit() { forceQuit(0) } @@ -881,7 +916,6 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun vibrate(durationMs: Int, amplitude: Int) { if (durationMs > 0 && requestPermission("VIBRATE")) { - val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (amplitude <= -1) { vibratorService.vibrate( diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java index a323045e1b..1612ddd0b3 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -42,6 +42,7 @@ import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Messenger; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -203,6 +204,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH if (godotContainerLayout == null) { throw new IllegalStateException("Unable to initialize engine render view"); } + } catch (IllegalStateException e) { + Log.e(TAG, "Engine initialization failed", e); + final String errorMessage = TextUtils.isEmpty(e.getMessage()) + ? getString(R.string.error_engine_setup_message) + : e.getMessage(); + godot.alert(errorMessage, getString(R.string.text_error_title), godot::forceQuit); } catch (IllegalArgumentException ignored) { final Activity activity = getActivity(); Intent notifierIntent = new Intent(activity, activity.getClass()); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index 4b51bd778d..219631284a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -121,7 +121,7 @@ public class GodotIO { activity.startActivity(intent); return 0; - } catch (ActivityNotFoundException e) { + } catch (Exception e) { Log.e(TAG, "Unable to open uri " + uriString, e); return 1; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index d0c3d4a687..37e889daf7 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -240,4 +240,11 @@ public class GodotLib { * @see GodotRenderer#onActivityPaused() */ public static native void onRendererPaused(); + + /** + * Invoked on the GL thread to update the input dispatch settings + * @param useAccumulatedInput True to use accumulated input, false otherwise + * @param useInputBuffering True to use input buffering, false otherwise + */ + public static native void updateInputDispatchSettings(boolean useAccumulatedInput, boolean useInputBuffering); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java index c316812404..c9421a3257 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -1704,15 +1704,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback mHasSurface = true; mFinishedCreatingEglSurface = false; sGLThreadManager.notifyAll(); - while (mWaitingForSurface - && !mFinishedCreatingEglSurface - && !mExited) { - try { - sGLThreadManager.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } } } @@ -1723,13 +1714,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } mHasSurface = false; sGLThreadManager.notifyAll(); - while((!mWaitingForSurface) && (!mExited)) { - try { - sGLThreadManager.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } } } @@ -1740,16 +1724,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } mRequestPaused = true; sGLThreadManager.notifyAll(); - while ((! mExited) && (! mPaused)) { - if (LOG_PAUSE_RESUME) { - Log.i("Main thread", "onPause waiting for mPaused."); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } @@ -1762,16 +1736,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback mRequestRender = true; mRenderComplete = false; sGLThreadManager.notifyAll(); - while ((! mExited) && mPaused && (!mRenderComplete)) { - if (LOG_PAUSE_RESUME) { - Log.i("Main thread", "onResume waiting for !mPaused."); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } @@ -1793,19 +1757,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } sGLThreadManager.notifyAll(); - - // Wait for thread to react to resize and render a frame - while (! mExited && !mPaused && !mRenderComplete - && ableToDraw()) { - if (LOG_SURFACE) { - Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt index 49b34a5229..4cd3bd8db9 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -44,7 +44,7 @@ import org.godotengine.godot.GodotLib * @See https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener * @See https://developer.android.com/reference/android/view/ScaleGestureDetector.OnScaleGestureListener */ -internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureListener { +internal class GodotGestureHandler(private val inputHandler: GodotInputHandler) : SimpleOnGestureListener(), OnScaleGestureListener { companion object { private val TAG = GodotGestureHandler::class.java.simpleName @@ -65,13 +65,13 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi private var lastDragY: Float = 0.0f override fun onDown(event: MotionEvent): Boolean { - GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap) + inputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap) nextDownIsDoubleTap = false return true } override fun onSingleTapUp(event: MotionEvent): Boolean { - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) return true } @@ -85,10 +85,10 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi } // Cancel the previous down event - GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL) + inputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL) // Turn a context click into a single tap right mouse button click. - GodotInputHandler.handleMouseEvent( + inputHandler.handleMouseEvent( event, MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_SECONDARY, @@ -104,7 +104,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (!hasCapture) { // Dispatch a mouse relative ACTION_UP event to signal the end of the capture - GodotInputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true) + inputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true) } pointerCaptureInProgress = hasCapture } @@ -131,9 +131,9 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (contextClickInProgress || GodotInputHandler.isMouseEvent(event)) { // This may be an ACTION_BUTTON_RELEASE event which we don't handle, // so we convert it to an ACTION_UP event. - GodotInputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP) + inputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP) } else { - GodotInputHandler.handleTouchEvent(event) + inputHandler.handleTouchEvent(event) } pointerCaptureInProgress = false dragInProgress = false @@ -148,7 +148,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi private fun onActionMove(event: MotionEvent): Boolean { if (contextClickInProgress) { - GodotInputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false) + inputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false) return true } else if (!scaleInProgress) { // The 'onScroll' event is triggered with a long delay. @@ -158,7 +158,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (lastDragX != event.getX(0) || lastDragY != event.getY(0)) { lastDragX = event.getX(0) lastDragY = event.getY(0) - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) return true } } @@ -168,9 +168,9 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi override fun onDoubleTapEvent(event: MotionEvent): Boolean { if (event.actionMasked == MotionEvent.ACTION_UP) { nextDownIsDoubleTap = false - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) } else if (event.actionMasked == MotionEvent.ACTION_MOVE && !panningAndScalingEnabled) { - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) } return true @@ -191,7 +191,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (dragInProgress || lastDragX != 0.0f || lastDragY != 0.0f) { if (originEvent != null) { // Cancel the drag - GodotInputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL) + inputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL) } dragInProgress = false lastDragX = 0.0f @@ -202,12 +202,12 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi val x = terminusEvent.x val y = terminusEvent.y if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) { - GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) + inputHandler.handlePanEvent(x, y, distanceX / 5f, distanceY / 5f) } else if (!scaleInProgress) { dragInProgress = true lastDragX = terminusEvent.getX(0) lastDragY = terminusEvent.getY(0) - GodotInputHandler.handleMotionEvent(terminusEvent) + inputHandler.handleMotionEvent(terminusEvent) } return true } @@ -218,11 +218,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi } if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) { - GodotLib.magnify( - detector.focusX, - detector.focusY, - detector.scaleFactor - ) + inputHandler.handleMagnifyEvent(detector.focusX, detector.focusY, detector.scaleFactor) } return true } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index 83e76e49c9..889618914d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -75,7 +75,9 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { */ private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN; - private static int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; + private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; + + private boolean dispatchInputToRenderThread = false; public GodotInputHandler(GodotRenderView godotView) { final Context context = godotView.getView().getContext(); @@ -83,7 +85,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); mInputManager.registerInputDeviceListener(this, null); - this.godotGestureHandler = new GodotGestureHandler(); + this.godotGestureHandler = new GodotGestureHandler(this); this.gestureDetector = new GestureDetector(context, godotGestureHandler); this.gestureDetector.setIsLongpressEnabled(false); this.scaleGestureDetector = new ScaleGestureDetector(context, godotGestureHandler); @@ -109,6 +111,22 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } /** + * Specifies whether input should be dispatch on the UI thread or on the Render thread. + * @param enable true to dispatch input on the Render thread, false to dispatch input on the UI thread + */ + public void enableInputDispatchToRenderThread(boolean enable) { + this.dispatchInputToRenderThread = enable; + } + + /** + * @return true if input must be dispatched from the render thread. If false, input is + * dispatched from the UI thread. + */ + private boolean shouldDispatchInputToRenderThread() { + return dispatchInputToRenderThread; + } + + /** * On Wear OS devices, sets which axis of the mouse wheel rotary input is mapped to. This is 1 (vertical axis) by default. */ public void setRotaryInputAxis(int axis) { @@ -151,14 +169,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (mJoystickIds.indexOfKey(deviceId) >= 0) { final int button = getGodotButton(keyCode); final int godotJoyId = mJoystickIds.get(deviceId); - GodotLib.joybutton(godotJoyId, button, false); + handleJoystickButtonEvent(godotJoyId, button, false); } } else { // getKeyCode(): The physical key that was pressed. final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0); + handleKeyEvent(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0); }; return true; @@ -187,13 +205,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (mJoystickIds.indexOfKey(deviceId) >= 0) { final int button = getGodotButton(keyCode); final int godotJoyId = mJoystickIds.get(deviceId); - GodotLib.joybutton(godotJoyId, button, true); + handleJoystickButtonEvent(godotJoyId, button, true); } } else { final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0); + handleKeyEvent(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0); } return true; @@ -248,7 +266,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (joystick.axesValues.indexOfKey(axis) < 0 || (float)joystick.axesValues.get(axis) != value) { // save value to prevent repeats joystick.axesValues.put(axis, value); - GodotLib.joyaxis(godotJoyId, i, value); + handleJoystickAxisEvent(godotJoyId, i, value); } } @@ -258,7 +276,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (joystick.hatX != hatX || joystick.hatY != hatY) { joystick.hatX = hatX; joystick.hatY = hatY; - GodotLib.joyhat(godotJoyId, hatX, hatY); + handleJoystickHatEvent(godotJoyId, hatX, hatY); } } return true; @@ -284,10 +302,12 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { int[] deviceIds = mInputManager.getInputDeviceIds(); for (int deviceId : deviceIds) { InputDevice device = mInputManager.getInputDevice(deviceId); - if (DEBUG) { - Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName())); + if (device != null) { + if (DEBUG) { + Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName())); + } + onInputDeviceAdded(deviceId); } - onInputDeviceAdded(deviceId); } } @@ -364,7 +384,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } mJoysticksDevices.put(deviceId, joystick); - GodotLib.joyconnectionchanged(id, true, joystick.name); + handleJoystickConnectionChangedEvent(id, true, joystick.name); } @Override @@ -378,7 +398,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final int godotJoyId = mJoystickIds.get(deviceId); mJoystickIds.delete(deviceId); mJoysticksDevices.delete(deviceId); - GodotLib.joyconnectionchanged(godotJoyId, false, ""); + handleJoystickConnectionChangedEvent(godotJoyId, false, ""); } @Override @@ -482,15 +502,15 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } } - static boolean handleMotionEvent(final MotionEvent event) { + boolean handleMotionEvent(final MotionEvent event) { return handleMotionEvent(event, event.getActionMasked()); } - static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) { + boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) { return handleMotionEvent(event, eventActionOverride, false); } - static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { if (isMouseEvent(event)) { return handleMouseEvent(event, eventActionOverride, doubleTap); } @@ -523,19 +543,19 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (float)Math.cos(orientation) * tiltMult; } - static boolean handleMouseEvent(final MotionEvent event) { + boolean handleMouseEvent(final MotionEvent event) { return handleMouseEvent(event, event.getActionMasked()); } - static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) { + boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) { return handleMouseEvent(event, eventActionOverride, false); } - static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { return handleMouseEvent(event, eventActionOverride, event.getButtonState(), doubleTap); } - static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) { + boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) { final float x = event.getX(); final float y = event.getY(); @@ -564,11 +584,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return handleMouseEvent(eventActionOverride, buttonMaskOverride, x, y, horizontalFactor, verticalFactor, doubleTap, sourceMouseRelative, pressure, getEventTiltX(event), getEventTiltY(event)); } - static boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) { + boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) { return handleMouseEvent(eventAction, 0, 0f, 0f, 0f, 0f, false, sourceMouseRelative, 1f, 0f, 0f); } - static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { + boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { // Fix the buttonsMask switch (eventAction) { case MotionEvent.ACTION_CANCEL: @@ -584,6 +604,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { break; } + final int updatedButtonsMask = buttonsMask; // We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically // follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate // stream of events to the engine. @@ -596,22 +617,26 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_SCROLL: { - GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY)); + } else { + GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); + } return true; } } return false; } - static boolean handleTouchEvent(final MotionEvent event) { + boolean handleTouchEvent(final MotionEvent event) { return handleTouchEvent(event, event.getActionMasked()); } - static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) { + boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) { return handleTouchEvent(event, eventActionOverride, false); } - static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { final int pointerCount = event.getPointerCount(); if (pointerCount == 0) { return true; @@ -636,10 +661,70 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_DOWN: { - GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap); + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap)); + } else { + GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap); + } return true; } } return false; } + + void handleMagnifyEvent(float x, float y, float factor) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.magnify(x, y, factor)); + } else { + GodotLib.magnify(x, y, factor); + } + } + + void handlePanEvent(float x, float y, float deltaX, float deltaY) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.pan(x, y, deltaX, deltaY)); + } else { + GodotLib.pan(x, y, deltaX, deltaY); + } + } + + private void handleJoystickButtonEvent(int device, int button, boolean pressed) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.joybutton(device, button, pressed)); + } else { + GodotLib.joybutton(device, button, pressed); + } + } + + private void handleJoystickAxisEvent(int device, int axis, float value) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.joyaxis(device, axis, value)); + } else { + GodotLib.joyaxis(device, axis, value); + } + } + + private void handleJoystickHatEvent(int device, int hatX, int hatY) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.joyhat(device, hatX, hatY)); + } else { + GodotLib.joyhat(device, hatX, hatY); + } + } + + private void handleJoystickConnectionChangedEvent(int device, boolean connected, String name) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.joyconnectionchanged(device, connected, name)); + } else { + GodotLib.joyconnectionchanged(device, connected, name); + } + } + + void handleKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo)); + } else { + GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo); + } + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index 06b565c30f..e545669970 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -93,8 +93,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) { for (int i = 0; i < count; ++i) { - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true, false); - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, false, false); if (mHasSelection) { mHasSelection = false; @@ -115,8 +115,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene // Return keys are handled through action events continue; } - GodotLib.key(0, character, 0, true, false); - GodotLib.key(0, character, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false); } } @@ -127,18 +127,16 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene if (characters != null) { for (int i = 0; i < characters.length(); i++) { final int character = characters.codePointAt(i); - GodotLib.key(0, character, 0, true, false); - GodotLib.key(0, character, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false); } } } if (pActionID == EditorInfo.IME_ACTION_DONE) { // Enter key has been pressed - mRenderView.queueOnRenderThread(() -> { - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true, false); - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false, false); - }); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, false, false); mRenderView.getView().requestFocus(); return true; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt index 69748c0a8d..d39f2309b8 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt @@ -81,7 +81,8 @@ fun beginBenchmarkMeasure(scope: String, label: String) { * * * Note: Only enabled on 'editorDev' build variant. */ -fun endBenchmarkMeasure(scope: String, label: String) { +@JvmOverloads +fun endBenchmarkMeasure(scope: String, label: String, dumpBenchmark: Boolean = false) { if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { return } @@ -93,6 +94,10 @@ fun endBenchmarkMeasure(scope: String, label: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Trace.endAsyncSection("[$scope] $label", 0) } + + if (dumpBenchmark) { + dumpBenchmark() + } } /** @@ -102,11 +107,11 @@ fun endBenchmarkMeasure(scope: String, label: String) { * * Note: Only enabled on 'editorDev' build variant. */ @JvmOverloads -fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) { +fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String? = benchmarkFile) { if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { return } - if (!useBenchmark) { + if (!useBenchmark || benchmarkTracker.isEmpty()) { return } diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 40068745d6..87d4281c5a 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -67,6 +67,13 @@ static AndroidInputHandler *input_handler = nullptr; static GodotJavaWrapper *godot_java = nullptr; static GodotIOJavaWrapper *godot_io_java = nullptr; +enum StartupStep { + STEP_TERMINATED = -1, + STEP_SETUP, + STEP_SHOW_LOGO, + STEP_STARTED +}; + static SafeNumeric<int> step; // Shared between UI and render threads static Size2 new_size; @@ -76,7 +83,7 @@ static Vector3 magnetometer; static Vector3 gyroscope; static void _terminate(JNIEnv *env, bool p_restart = false) { - step.set(-1); // Ensure no further steps are attempted and no further events are sent + step.set(STEP_TERMINATED); // Ensure no further steps are attempted and no further events are sent // lets cleanup // Unregister android plugins @@ -203,7 +210,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j os_android->set_display_size(Size2i(p_width, p_height)); // No need to reset the surface during startup - if (step.get() > 0) { + if (step.get() > STEP_SETUP) { if (p_surface) { ANativeWindow *native_window = ANativeWindow_fromSurface(env, p_surface); os_android->set_native_window(native_window); @@ -216,7 +223,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jobject p_surface) { if (os_android) { - if (step.get() == 0) { + if (step.get() == STEP_SETUP) { // During startup if (p_surface) { ANativeWindow *native_window = ANativeWindow_fromSurface(env, p_surface); @@ -230,7 +237,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *en } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz) { - if (step.get() == 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -244,20 +251,26 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *e } JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { - if (step.get() == -1) { + if (step.get() == STEP_TERMINATED) { return true; } - if (step.get() == 0) { + if (step.get() == STEP_SETUP) { // Since Godot is initialized on the UI thread, main_thread_id was set to that thread's id, // but for Godot purposes, the main thread is the one running the game loop - Main::setup2(); + Main::setup2(false); // The logo is shown in the next frame otherwise we run into rendering issues input_handler = new AndroidInputHandler(); step.increment(); return true; } - if (step.get() == 1) { + if (step.get() == STEP_SHOW_LOGO) { + Main::setup_boot_logo(); + step.increment(); + return true; + } + + if (step.get() == STEP_STARTED) { if (Main::start() != EXIT_SUCCESS) { return true; // should exit instead and print the error } @@ -283,7 +296,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative, jfloat p_pressure, jfloat p_tilt_x, jfloat p_tilt_y) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -292,7 +305,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JN // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray position, jboolean p_double_tap) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -313,7 +326,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JN // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } input_handler->process_magnify(Point2(p_x, p_y), p_factor); @@ -321,7 +334,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } input_handler->process_pan(Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y)); @@ -329,7 +342,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jcla // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -344,7 +357,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -359,7 +372,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -396,7 +409,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged( // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed, jboolean p_echo) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } input_handler->process_key_event(p_physical_keycode, p_unicode, p_key_label, p_pressed, p_echo); @@ -419,7 +432,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jclass clazz) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -427,7 +440,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -516,7 +529,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResu } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -528,7 +541,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -536,4 +549,11 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIE os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_PAUSED); } } + +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_updateInputDispatchSettings(JNIEnv *env, jclass clazz, jboolean p_use_accumulated_input, jboolean p_use_input_buffering) { + if (Input::get_singleton()) { + Input::get_singleton()->set_use_accumulated_input(p_use_accumulated_input); + Input::get_singleton()->set_use_input_buffering(p_use_input_buffering); + } +} } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index f32ffc291a..852c475e7e 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -69,6 +69,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResu JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_updateInputDispatchSettings(JNIEnv *env, jclass clazz, jboolean p_use_accumulated_input, jboolean p_use_input_buffering); } #endif // JAVA_GODOT_LIB_JNI_H diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index a454dd5ba0..802fbefc0d 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -91,6 +91,7 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver)); memdelete(rendering_context); rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; return; } @@ -107,7 +108,13 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode rendering_context->window_set_vsync_mode(MAIN_WINDOW_ID, p_vsync_mode); rendering_device = memnew(RenderingDevice); - rendering_device->initialize(rendering_context, MAIN_WINDOW_ID); + if (rendering_device->initialize(rendering_context, MAIN_WINDOW_ID) != OK) { + rendering_device = nullptr; + memdelete(rendering_context); + rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; + return; + } rendering_device->screen_create(MAIN_WINDOW_ID); RendererCompositorRD::make_current(); diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index 3fad8c2987..adc9beed66 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -1111,6 +1111,28 @@ Key DisplayServerWayland::keyboard_get_keycode_from_physical(Key p_keycode) cons return key; } +void DisplayServerWayland::try_suspend() { + // Due to various reasons, we manually handle display synchronization by + // waiting for a frame event (request to draw) or, if available, the actual + // window's suspend status. When a window is suspended, we can avoid drawing + // altogether, either because the compositor told us that we don't need to or + // because the pace of the frame events became unreliable. + if (emulate_vsync) { + bool frame = wayland_thread.wait_frame_suspend_ms(1000); + if (!frame) { + suspended = true; + } + } else { + if (wayland_thread.is_suspended()) { + suspended = true; + } + } + + if (suspended) { + DEBUG_LOG_WAYLAND("Window suspended."); + } +} + void DisplayServerWayland::process_events() { wayland_thread.mutex.lock(); @@ -1193,30 +1215,32 @@ void DisplayServerWayland::process_events() { wayland_thread.keyboard_echo_keys(); if (!suspended) { - if (emulate_vsync) { - // Due to various reasons, we manually handle display synchronization by - // waiting for a frame event (request to draw) or, if available, the actual - // window's suspend status. When a window is suspended, we can avoid drawing - // altogether, either because the compositor told us that we don't need to or - // because the pace of the frame events became unreliable. - bool frame = wayland_thread.wait_frame_suspend_ms(1000); - if (!frame) { - suspended = true; + // Due to the way legacy suspension works, we have to treat low processor + // usage mode very differently than the regular one. + if (OS::get_singleton()->is_in_low_processor_usage_mode()) { + // NOTE: We must avoid committing a surface if we expect a new frame, as we + // might otherwise commit some inconsistent data (e.g. buffer scale). Note + // that if a new frame is expected it's going to be committed by the renderer + // soon anyways. + if (!RenderingServer::get_singleton()->has_changed()) { + // We _can't_ commit in a different thread (such as in the frame callback + // itself) because we would risk to step on the renderer's feet, which would + // cause subtle but severe issues, such as crashes on setups with explicit + // sync. This isn't normally a problem, as the renderer commits at every + // frame (which is what we need for atomic surface updates anyways), but in + // low processor usage mode that expectation is broken. When it's on, our + // frame rate stops being constant. This also reflects in the frame + // information we use for legacy suspension. In order to avoid issues, let's + // manually commit all surfaces, so that we can get fresh frame data. + wayland_thread.commit_surfaces(); + try_suspend(); } } else { - if (wayland_thread.is_suspended()) { - suspended = true; - } - } - - if (suspended) { - DEBUG_LOG_WAYLAND("Window suspended."); - } - } else { - if (wayland_thread.get_reset_frame()) { - // At last, a sign of life! We're no longer suspended. - suspended = false; + try_suspend(); } + } else if (wayland_thread.get_reset_frame()) { + // At last, a sign of life! We're no longer suspended. + suspended = false; } #ifdef DBUS_ENABLED @@ -1445,7 +1469,14 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win #ifdef RD_ENABLED if (rendering_context) { rendering_device = memnew(RenderingDevice); - rendering_device->initialize(rendering_context, MAIN_WINDOW_ID); + if (rendering_device->initialize(rendering_context, MAIN_WINDOW_ID) != OK) { + memdelete(rendering_device); + rendering_device = nullptr; + memdelete(rendering_context); + rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; + return; + } rendering_device->screen_create(MAIN_WINDOW_ID); RendererCompositorRD::make_current(); diff --git a/platform/linuxbsd/wayland/display_server_wayland.h b/platform/linuxbsd/wayland/display_server_wayland.h index f02640a12e..e611533664 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.h +++ b/platform/linuxbsd/wayland/display_server_wayland.h @@ -154,6 +154,8 @@ class DisplayServerWayland : public DisplayServer { virtual void _show_window(); + void try_suspend(); + public: virtual bool has_feature(Feature p_feature) const override; diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index 63a8db07df..7bdc75db29 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -968,7 +968,6 @@ void WaylandThread::_frame_wl_callback_on_done(void *data, struct wl_callback *w ws->frame_callback = wl_surface_frame(ws->wl_surface), wl_callback_add_listener(ws->frame_callback, &frame_wl_callback_listener, ws); - wl_surface_commit(ws->wl_surface); if (ws->wl_surface && ws->buffer_scale_changed) { // NOTE: We're only now setting the buffer scale as the idea is to get this @@ -980,11 +979,6 @@ void WaylandThread::_frame_wl_callback_on_done(void *data, struct wl_callback *w // rendering if needed. wl_surface_set_buffer_scale(ws->wl_surface, window_state_get_preferred_buffer_scale(ws)); } - - // NOTE: Remember to set here also other buffer-dependent states (e.g. opaque - // region) if used, to be as close as possible to an atomic surface update. - // Ideally we'd only have one surface commit, but it's not really doable given - // the current state of things. } void WaylandThread::_wl_surface_on_leave(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { @@ -2055,9 +2049,14 @@ void WaylandThread::_wp_relative_pointer_on_relative_motion(void *data, struct z PointerData &pd = ss->pointer_data_buffer; + WindowState *ws = wl_surface_get_window_state(ss->pointed_surface); + ERR_FAIL_NULL(ws); + pd.relative_motion.x = wl_fixed_to_double(dx); pd.relative_motion.y = wl_fixed_to_double(dy); + pd.relative_motion *= window_state_get_scale_factor(ws); + pd.relative_motion_time = uptime_lo; } @@ -3241,10 +3240,6 @@ void WaylandThread::window_create(DisplayServer::WindowID p_window_id, int p_wid ws.frame_callback = wl_surface_frame(ws.wl_surface); wl_callback_add_listener(ws.frame_callback, &frame_wl_callback_listener, &ws); - // NOTE: This commit is only called once to start the whole frame callback - // "loop". - wl_surface_commit(ws.wl_surface); - if (registry.xdg_exporter) { ws.xdg_exported = zxdg_exporter_v1_export(registry.xdg_exporter, ws.wl_surface); zxdg_exported_v1_add_listener(ws.xdg_exported, &xdg_exported_listener, &ws); @@ -4120,6 +4115,10 @@ void WaylandThread::primary_set_text(const String &p_text) { wl_display_roundtrip(wl_display); } +void WaylandThread::commit_surfaces() { + wl_surface_commit(main_window.wl_surface); +} + void WaylandThread::set_frame() { frame = true; } diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h index 75d03181e2..775ca71346 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -992,6 +992,8 @@ public: void primary_set_text(const String &p_text); + void commit_surfaces(); + void set_frame(); bool get_reset_frame(); bool wait_frame_suspend_ms(int p_timeout); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index a605e664ce..edf3a40ccb 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -3040,7 +3040,7 @@ void DisplayServerX11::window_set_ime_active(const bool p_active, WindowID p_win XWindowAttributes xwa; XSync(x11_display, False); XGetWindowAttributes(x11_display, wd.x11_xim_window, &xwa); - if (xwa.map_state == IsViewable && _window_focus_check()) { + if (xwa.map_state == IsViewable) { _set_input_focus(wd.x11_xim_window, RevertToParent); } XSetICFocus(wd.xic); @@ -6262,7 +6262,14 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode #if defined(RD_ENABLED) if (rendering_context) { rendering_device = memnew(RenderingDevice); - rendering_device->initialize(rendering_context, MAIN_WINDOW_ID); + if (rendering_device->initialize(rendering_context, MAIN_WINDOW_ID) != OK) { + memdelete(rendering_device); + rendering_device = nullptr; + memdelete(rendering_context); + rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; + return; + } rendering_device->screen_create(MAIN_WINDOW_ID); RendererCompositorRD::make_current(); diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index a1a91345ac..da45391995 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -568,23 +568,7 @@ void DisplayServerMacOS::menu_callback(id p_sender) { } GodotMenuItem *value = [p_sender representedObject]; - if (value) { - if (value->max_states > 0) { - value->state++; - if (value->state >= value->max_states) { - value->state = 0; - } - } - - if (value->checkable_type == CHECKABLE_TYPE_CHECK_BOX) { - if ([p_sender state] == NSControlStateValueOff) { - [p_sender setState:NSControlStateValueOn]; - } else { - [p_sender setState:NSControlStateValueOff]; - } - } - if (value->callback.is_valid()) { MenuCall mc; mc.tag = value->meta; diff --git a/platform/macos/godot_main_macos.mm b/platform/macos/godot_main_macos.mm index 942c351ac0..eebaed0eaf 100644 --- a/platform/macos/godot_main_macos.mm +++ b/platform/macos/godot_main_macos.mm @@ -41,8 +41,8 @@ int main(int argc, char **argv) { #if defined(VULKAN_ENABLED) - // MoltenVK - enable full component swizzling support. - setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); + setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); // MoltenVK - enable full component swizzling support. + setenv("MVK_CONFIG_SWAPCHAIN_MIN_MAG_FILTER_USE_NEAREST", "0", 1); // MoltenVK - use linear surface scaling. TODO: remove when full DPI scaling is implemented. #endif #if defined(SANITIZERS_ENABLED) diff --git a/platform/macos/godot_menu_item.h b/platform/macos/godot_menu_item.h index b6e2d41c08..e1af317259 100644 --- a/platform/macos/godot_menu_item.h +++ b/platform/macos/godot_menu_item.h @@ -52,6 +52,7 @@ enum GlobalMenuCheckType { Callable hover_callback; Variant meta; GlobalMenuCheckType checkable_type; + bool checked; int max_states; int state; Ref<Image> img; diff --git a/platform/macos/godot_menu_item.mm b/platform/macos/godot_menu_item.mm index 30dac9be9b..479542113a 100644 --- a/platform/macos/godot_menu_item.mm +++ b/platform/macos/godot_menu_item.mm @@ -31,4 +31,18 @@ #include "godot_menu_item.h" @implementation GodotMenuItem + +- (id)init { + self = [super init]; + + self->callback = Callable(); + self->key_callback = Callable(); + self->checkable_type = GlobalMenuCheckType::CHECKABLE_TYPE_NONE; + self->checked = false; + self->max_states = 0; + self->state = 0; + + return self; +} + @end diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm index 1ae1137ca0..802d58dc26 100644 --- a/platform/macos/native_menu_macos.mm +++ b/platform/macos/native_menu_macos.mm @@ -373,12 +373,7 @@ int NativeMenuMacOS::add_submenu_item(const RID &p_rid, const String &p_label, c menu_item = [md->menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index]; GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = Callable(); - obj->key_callback = Callable(); obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; [menu_item setRepresentedObject:obj]; [md_sub->menu setTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()]]; @@ -417,9 +412,6 @@ int NativeMenuMacOS::add_item(const RID &p_rid, const String &p_label, const Cal obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -438,8 +430,6 @@ int NativeMenuMacOS::add_check_item(const RID &p_rid, const String &p_label, con obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -457,9 +447,6 @@ int NativeMenuMacOS::add_icon_item(const RID &p_rid, const Ref<Texture2D> &p_ico obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -489,8 +476,6 @@ int NativeMenuMacOS::add_icon_check_item(const RID &p_rid, const Ref<Texture2D> obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -520,8 +505,6 @@ int NativeMenuMacOS::add_radio_check_item(const RID &p_rid, const String &p_labe obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -540,8 +523,6 @@ int NativeMenuMacOS::add_icon_radio_check_item(const RID &p_rid, const Ref<Textu obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -570,7 +551,6 @@ int NativeMenuMacOS::add_multistate_item(const RID &p_rid, const String &p_label obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; obj->max_states = p_max_states; obj->state = p_default_state; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; @@ -640,7 +620,10 @@ bool NativeMenuMacOS::is_item_checked(const RID &p_rid, int p_idx) const { ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; if (menu_item) { - return ([menu_item state] == NSControlStateValueOn); + const GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->checked; + } } return false; } @@ -958,10 +941,14 @@ void NativeMenuMacOS::set_item_checked(const RID &p_rid, int p_idx, bool p_check ERR_FAIL_COND(p_idx >= item_start + item_count); NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; if (menu_item) { - if (p_checked) { - [menu_item setState:NSControlStateValueOn]; - } else { - [menu_item setState:NSControlStateValueOff]; + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + obj->checked = p_checked; + if (p_checked) { + [menu_item setState:NSControlStateValueOn]; + } else { + [menu_item setState:NSControlStateValueOff]; + } } } } diff --git a/platform/web/SCsub b/platform/web/SCsub index 3d36a888d6..e81f2ec516 100644 --- a/platform/web/SCsub +++ b/platform/web/SCsub @@ -71,8 +71,6 @@ if env["dlink_enabled"]: sys_env.Append(LINKFLAGS=["-s", "MAIN_MODULE=1"]) sys_env.Append(LINKFLAGS=["-s", "EXPORT_ALL=1"]) sys_env.Append(LINKFLAGS=["-s", "WARN_ON_UNDEFINED_SYMBOLS=0"]) - # Force exporting the standard library (printf, malloc, etc.) - sys_env["ENV"]["EMCC_FORCE_STDLIBS"] = "libc,libc++,libc++abi" sys_env["CCFLAGS"].remove("-fvisibility=hidden") sys_env["LINKFLAGS"].remove("-fvisibility=hidden") diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp index dd986e650c..b24c6cb1fd 100644 --- a/platform/web/audio_driver_web.cpp +++ b/platform/web/audio_driver_web.cpp @@ -33,6 +33,8 @@ #include "godot_audio.h" #include "core/config/project_settings.h" +#include "core/object/object.h" +#include "scene/main/node.h" #include "servers/audio/audio_stream.h" #include <emscripten.h> @@ -51,6 +53,33 @@ void AudioDriverWeb::_latency_update_callback(float p_latency) { AudioDriverWeb::audio_context.output_latency = p_latency; } +void AudioDriverWeb::_sample_playback_finished_callback(const char *p_playback_object_id) { + const ObjectID playback_id = ObjectID(String::to_int(p_playback_object_id)); + + Object *playback_object = ObjectDB::get_instance(playback_id); + if (playback_object == nullptr) { + return; + } + Ref<AudioSamplePlayback> playback = Object::cast_to<AudioSamplePlayback>(playback_object); + if (playback.is_null()) { + return; + } + + Object *player_object = ObjectDB::get_instance(playback->player_id); + if (player_object == nullptr) { + return; + } + Node *player = Object::cast_to<Node>(player_object); + if (player == nullptr) { + return; + } + + const StringName finished = SNAME("finished"); + if (player->has_signal(finished)) { + player->emit_signal(finished); + } +} + void AudioDriverWeb::_audio_driver_process(int p_from, int p_samples) { int32_t *stream_buffer = reinterpret_cast<int32_t *>(output_rb); const int max_samples = memarr_len(output_rb); @@ -132,6 +161,9 @@ Error AudioDriverWeb::init() { if (!input_rb) { return ERR_OUT_OF_MEMORY; } + + godot_audio_sample_set_finished_callback(&_sample_playback_finished_callback); + return OK; } diff --git a/platform/web/audio_driver_web.h b/platform/web/audio_driver_web.h index 298ad90fae..46c5ce4de1 100644 --- a/platform/web/audio_driver_web.h +++ b/platform/web/audio_driver_web.h @@ -58,6 +58,7 @@ private: WASM_EXPORT static void _state_change_callback(int p_state); WASM_EXPORT static void _latency_update_callback(float p_latency); + WASM_EXPORT static void _sample_playback_finished_callback(const char *p_playback_object_id); static AudioDriverWeb *singleton; diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index bd327f6f91..d83e465e8e 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -589,35 +589,50 @@ bool EditorExportPlatformWeb::poll_export() { } } - int prev = menu_options; - menu_options = preset.is_valid(); - HTTPServerState prev_server_state = server_state; - server_state = HTTP_SERVER_STATE_OFF; - if (server->is_listening()) { - if (preset.is_null() || menu_options == 0) { - server->stop(); - } else { - server_state = HTTP_SERVER_STATE_ON; - menu_options += 1; + RemoteDebugState prev_remote_debug_state = remote_debug_state; + remote_debug_state = REMOTE_DEBUG_STATE_UNAVAILABLE; + + if (preset.is_valid()) { + const bool debug = true; + // Throwaway variables to pass to `can_export`. + String err; + bool missing_templates; + + if (can_export(preset, err, missing_templates, debug)) { + if (server->is_listening()) { + remote_debug_state = REMOTE_DEBUG_STATE_SERVING; + } else { + remote_debug_state = REMOTE_DEBUG_STATE_AVAILABLE; + } } } - return server_state != prev_server_state || menu_options != prev; + if (remote_debug_state != REMOTE_DEBUG_STATE_SERVING && server->is_listening()) { + server->stop(); + } + + return remote_debug_state != prev_remote_debug_state; } Ref<ImageTexture> EditorExportPlatformWeb::get_option_icon(int p_index) const { Ref<ImageTexture> play_icon = EditorExportPlatform::get_option_icon(p_index); - switch (server_state) { - case HTTP_SERVER_STATE_OFF: { + switch (remote_debug_state) { + case REMOTE_DEBUG_STATE_UNAVAILABLE: { + return nullptr; + } break; + + case REMOTE_DEBUG_STATE_AVAILABLE: { switch (p_index) { case 0: case 1: return play_icon; + default: + ERR_FAIL_V(nullptr); } } break; - case HTTP_SERVER_STATE_ON: { + case REMOTE_DEBUG_STATE_SERVING: { switch (p_index) { case 0: return play_icon; @@ -625,18 +640,31 @@ Ref<ImageTexture> EditorExportPlatformWeb::get_option_icon(int p_index) const { return restart_icon; case 2: return stop_icon; + default: + ERR_FAIL_V(nullptr); } } break; } - ERR_FAIL_V_MSG(nullptr, vformat(R"(EditorExportPlatformWeb option icon index "%s" is invalid.)", p_index)); + return nullptr; } int EditorExportPlatformWeb::get_options_count() const { - if (server_state == HTTP_SERVER_STATE_ON) { - return 3; + switch (remote_debug_state) { + case REMOTE_DEBUG_STATE_UNAVAILABLE: { + return 0; + } break; + + case REMOTE_DEBUG_STATE_AVAILABLE: { + return 2; + } break; + + case REMOTE_DEBUG_STATE_SERVING: { + return 3; + } break; } - return 2; + + return 0; } String EditorExportPlatformWeb::get_option_label(int p_index) const { @@ -645,17 +673,22 @@ String EditorExportPlatformWeb::get_option_label(int p_index) const { String reexport_project = TTR("Re-export Project"); String stop_http_server = TTR("Stop HTTP Server"); - switch (server_state) { - case HTTP_SERVER_STATE_OFF: { + switch (remote_debug_state) { + case REMOTE_DEBUG_STATE_UNAVAILABLE: + return ""; + + case REMOTE_DEBUG_STATE_AVAILABLE: { switch (p_index) { case 0: return run_in_browser; case 1: return start_http_server; + default: + ERR_FAIL_V(""); } } break; - case HTTP_SERVER_STATE_ON: { + case REMOTE_DEBUG_STATE_SERVING: { switch (p_index) { case 0: return run_in_browser; @@ -663,11 +696,13 @@ String EditorExportPlatformWeb::get_option_label(int p_index) const { return reexport_project; case 2: return stop_http_server; + default: + ERR_FAIL_V(""); } } break; } - ERR_FAIL_V_MSG("", vformat(R"(EditorExportPlatformWeb option label index "%s" is invalid.)", p_index)); + return ""; } String EditorExportPlatformWeb::get_option_tooltip(int p_index) const { @@ -676,17 +711,22 @@ String EditorExportPlatformWeb::get_option_tooltip(int p_index) const { String reexport_project = TTR("Export project again to account for updates."); String stop_http_server = TTR("Stop the HTTP server."); - switch (server_state) { - case HTTP_SERVER_STATE_OFF: { + switch (remote_debug_state) { + case REMOTE_DEBUG_STATE_UNAVAILABLE: + return ""; + + case REMOTE_DEBUG_STATE_AVAILABLE: { switch (p_index) { case 0: return run_in_browser; case 1: return start_http_server; + default: + ERR_FAIL_V(""); } } break; - case HTTP_SERVER_STATE_ON: { + case REMOTE_DEBUG_STATE_SERVING: { switch (p_index) { case 0: return run_in_browser; @@ -694,11 +734,13 @@ String EditorExportPlatformWeb::get_option_tooltip(int p_index) const { return reexport_project; case 2: return stop_http_server; + default: + ERR_FAIL_V(""); } } break; } - ERR_FAIL_V_MSG("", vformat(R"(EditorExportPlatformWeb option tooltip index "%s" is invalid.)", p_index)); + return ""; } Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) { @@ -707,8 +749,12 @@ Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int const String bind_host = EDITOR_GET("export/web/http_host"); const bool use_tls = EDITOR_GET("export/web/use_tls"); - switch (server_state) { - case HTTP_SERVER_STATE_OFF: { + switch (remote_debug_state) { + case REMOTE_DEBUG_STATE_UNAVAILABLE: { + return FAILED; + } break; + + case REMOTE_DEBUG_STATE_AVAILABLE: { switch (p_option) { // Run in Browser. case 0: { @@ -731,10 +777,14 @@ Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int } return _start_server(bind_host, bind_port, use_tls); } break; + + default: { + ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option)); + } } } break; - case HTTP_SERVER_STATE_ON: { + case REMOTE_DEBUG_STATE_SERVING: { switch (p_option) { // Run in Browser. case 0: { @@ -754,11 +804,15 @@ Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int case 2: { return _stop_server(); } break; + + default: { + ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option)); + } } } break; } - ERR_FAIL_V_MSG(ERR_INVALID_PARAMETER, vformat(R"(Trying to run EditorExportPlatformWeb, but option "%s" isn't known.)", p_option)); + return FAILED; } Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) { diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h index d3d2083a23..2f67d8107f 100644 --- a/platform/web/export/export_plugin.h +++ b/platform/web/export/export_plugin.h @@ -46,19 +46,19 @@ class EditorExportPlatformWeb : public EditorExportPlatform { GDCLASS(EditorExportPlatformWeb, EditorExportPlatform); - enum HTTPServerState { - HTTP_SERVER_STATE_OFF, - HTTP_SERVER_STATE_ON, + enum RemoteDebugState { + REMOTE_DEBUG_STATE_UNAVAILABLE, + REMOTE_DEBUG_STATE_AVAILABLE, + REMOTE_DEBUG_STATE_SERVING, }; Ref<ImageTexture> logo; Ref<ImageTexture> run_icon; Ref<ImageTexture> stop_icon; Ref<ImageTexture> restart_icon; - HTTPServerState server_state = HTTP_SERVER_STATE_OFF; + RemoteDebugState remote_debug_state = REMOTE_DEBUG_STATE_UNAVAILABLE; Ref<EditorHTTPServer> server; - int menu_options = 0; String _get_template_name(bool p_extension, bool p_thread_support, bool p_debug) const { String name = "web"; diff --git a/platform/web/godot_audio.h b/platform/web/godot_audio.h index 8bebbcf7de..dd5bec00cf 100644 --- a/platform/web/godot_audio.h +++ b/platform/web/godot_audio.h @@ -57,6 +57,7 @@ extern void godot_audio_sample_set_pause(const char *p_playback_object_id, bool extern int godot_audio_sample_is_active(const char *p_playback_object_id); extern void godot_audio_sample_update_pitch_scale(const char *p_playback_object_id, float p_pitch_scale); extern void godot_audio_sample_set_volumes_linear(const char *p_playback_object_id, int *p_buses_buf, int p_buses_size, float *p_volumes_buf, int p_volumes_size); +extern void godot_audio_sample_set_finished_callback(void (*p_callback)(const char *)); extern void godot_audio_sample_bus_set_count(int p_count); extern void godot_audio_sample_bus_remove(int p_index); diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js index 4bca13d2d6..0b16b07261 100644 --- a/platform/web/js/libs/library_godot_audio.js +++ b/platform/web/js/libs/library_godot_audio.js @@ -142,7 +142,7 @@ class Sample { * @returns {void} */ clear() { - this.audioBuffer = null; + this.setAudioBuffer(null); GodotAudio.Sample.delete(this.id); } @@ -432,7 +432,7 @@ class SampleNode { /** @type {number} */ this._playbackRate = 44100; /** @type {LoopMode} */ - this.loopMode = 'disabled'; + this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled'; /** @type {number} */ this._pitchScale = 1; /** @type {number} */ @@ -445,7 +445,6 @@ class SampleNode { this._onended = null; this.setPlaybackRate(options.playbackRate ?? 44100); - this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled'; this._source.buffer = this.getSample().getAudioBuffer(); this._addEndedListener(); @@ -688,9 +687,15 @@ class SampleNode { } switch (self.getSample().loopMode) { - case 'disabled': + case 'disabled': { + const id = this.id; self.stop(); - break; + if (GodotAudio.sampleFinishedCallback != null) { + const idCharPtr = GodotRuntime.allocString(id); + GodotAudio.sampleFinishedCallback(idCharPtr); + GodotRuntime.free(idCharPtr); + } + } break; case 'forward': case 'backward': self.restart(); @@ -777,8 +782,7 @@ class Bus { */ static move(fromIndex, toIndex) { const movedBus = GodotAudio.Bus.getBus(fromIndex); - let buses = GodotAudio.buses; - buses = buses.filter((_, i) => i !== fromIndex); + const buses = GodotAudio.buses.filter((_, i) => i !== fromIndex); // Inserts at index. buses.splice(toIndex - 1, 0, movedBus); GodotAudio.buses = buses; @@ -1092,6 +1096,12 @@ const _GodotAudio = { busSolo: null, Bus, + /** + * Callback to signal that a sample has finished. + * @type {(playbackObjectIdPtr: number) => void | null} + */ + sampleFinishedCallback: null, + /** @type {AudioContext} */ ctx: null, input: null, @@ -1369,7 +1379,7 @@ const _GodotAudio = { */ set_sample_bus_volume_db: function (busIndex, volumeDb) { const bus = GodotAudio.Bus.getBus(busIndex); - bus.volumeDb = volumeDb; + bus.setVolumeDb(volumeDb); }, /** @@ -1766,6 +1776,17 @@ const _GodotAudio = { godot_audio_sample_bus_set_mute: function (bus, enable) { GodotAudio.set_sample_bus_mute(bus, Boolean(enable)); }, + + godot_audio_sample_set_finished_callback__proxy: 'sync', + godot_audio_sample_set_finished_callback__sig: 'vi', + /** + * Sets the finished callback + * @param {Number} callbackPtr Finished callback pointer + * @returns {void} + */ + godot_audio_sample_set_finished_callback: function (callbackPtr) { + GodotAudio.sampleFinishedCallback = GodotRuntime.get_func(callbackPtr); + }, }; autoAddDeps(_GodotAudio, '$GodotAudio'); diff --git a/platform/web/js/libs/library_godot_input.js b/platform/web/js/libs/library_godot_input.js index 7ea89d553f..6e3b97023d 100644 --- a/platform/web/js/libs/library_godot_input.js +++ b/platform/web/js/libs/library_godot_input.js @@ -112,6 +112,7 @@ const GodotIME = { ime.style.top = '0px'; ime.style.width = '100%'; ime.style.height = '40px'; + ime.style.pointerEvents = 'none'; ime.style.display = 'none'; ime.contentEditable = 'true'; diff --git a/platform/web/js/libs/library_godot_javascript_singleton.js b/platform/web/js/libs/library_godot_javascript_singleton.js index b17fde1544..6bb69bca95 100644 --- a/platform/web/js/libs/library_godot_javascript_singleton.js +++ b/platform/web/js/libs/library_godot_javascript_singleton.js @@ -81,11 +81,16 @@ const GodotJSWrapper = { case 0: return null; case 1: - return !!GodotRuntime.getHeapValue(val, 'i64'); - case 2: - return GodotRuntime.getHeapValue(val, 'i64'); + return Boolean(GodotRuntime.getHeapValue(val, 'i64')); + case 2: { + // `heap_value` may be a bigint. + const heap_value = GodotRuntime.getHeapValue(val, 'i64'); + return heap_value >= Number.MIN_SAFE_INTEGER && heap_value <= Number.MAX_SAFE_INTEGER + ? Number(heap_value) + : heap_value; + } case 3: - return GodotRuntime.getHeapValue(val, 'double'); + return Number(GodotRuntime.getHeapValue(val, 'double')); case 4: return GodotRuntime.parseString(GodotRuntime.getHeapValue(val, '*')); case 24: // OBJECT @@ -110,6 +115,9 @@ const GodotJSWrapper = { } GodotRuntime.setHeapValue(p_exchange, p_val, 'double'); return 3; // FLOAT + } else if (type === 'bigint') { + GodotRuntime.setHeapValue(p_exchange, p_val, 'i64'); + return 2; // INT } else if (type === 'string') { const c_str = GodotRuntime.allocString(p_val); GodotRuntime.setHeapValue(p_exchange, c_str, '*'); diff --git a/platform/web/web_main.cpp b/platform/web/web_main.cpp index 04513f6d57..d0c3bd7c0e 100644 --- a/platform/web/web_main.cpp +++ b/platform/web/web_main.cpp @@ -35,6 +35,8 @@ #include "core/config/engine.h" #include "core/io/resource_loader.h" #include "main/main.h" +#include "scene/main/scene_tree.h" +#include "scene/main/window.h" // SceneTree only forward declares it. #include <emscripten/emscripten.h> #include <stdlib.h> @@ -130,7 +132,7 @@ extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) { if (Engine::get_singleton()->is_project_manager_hint() && FileAccess::exists("/tmp/preload.zip")) { PackedStringArray ps; ps.push_back("/tmp/preload.zip"); - os->get_main_loop()->emit_signal(SNAME("files_dropped"), ps, -1); + SceneTree::get_singleton()->get_root()->emit_signal(SNAME("files_dropped"), ps); } #endif emscripten_set_main_loop(main_loop_callback, -1, false); diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 0ad84240e4..fee306a25c 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -142,8 +142,9 @@ def detect_build_env_arch(): if os.getenv("VCTOOLSINSTALLDIR"): host_path_index = os.getenv("PATH").upper().find(os.getenv("VCTOOLSINSTALLDIR").upper() + "BIN\\HOST") if host_path_index > -1: - first_path_arch = os.getenv("PATH").split(";")[0].rsplit("\\", 1)[-1].lower() - return msvc_target_aliases[first_path_arch] + first_path_arch = os.getenv("PATH")[host_path_index:].split(";")[0].rsplit("\\", 1)[-1].lower() + if first_path_arch in msvc_target_aliases.keys(): + return msvc_target_aliases[first_path_arch] msys_target_aliases = { "mingw32": "x86_32", @@ -499,6 +500,14 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["arch"] == "x86_64": env.AppendUnique(CPPDEFINES=["_WIN64"]) + # Sanitizers + prebuilt_lib_extra_suffix = "" + if env["use_asan"]: + env.extra_suffix += ".san" + prebuilt_lib_extra_suffix = ".san" + env.Append(CCFLAGS=["/fsanitize=address"]) + env.Append(LINKFLAGS=["/INFERASANLIBS"]) + ## Libs LIBS = [ @@ -566,7 +575,7 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): LIBS += ["WinPixEventRuntime"] env.Append(LIBPATH=[env["mesa_libs"] + "/bin"]) - LIBS += ["libNIR.windows." + env["arch"]] + LIBS += ["libNIR.windows." + env["arch"] + prebuilt_lib_extra_suffix] if env["opengl3"]: env.AppendUnique(CPPDEFINES=["GLES3_ENABLED"]) @@ -574,9 +583,9 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): env.AppendUnique(CPPDEFINES=["EGL_STATIC"]) env.Append(LIBPATH=[env["angle_libs"]]) LIBS += [ - "libANGLE.windows." + env["arch"], - "libEGL.windows." + env["arch"], - "libGLES.windows." + env["arch"], + "libANGLE.windows." + env["arch"] + prebuilt_lib_extra_suffix, + "libEGL.windows." + env["arch"] + prebuilt_lib_extra_suffix, + "libGLES.windows." + env["arch"] + prebuilt_lib_extra_suffix, ] LIBS += ["dxgi", "d3d9", "d3d11"] env.Prepend(CPPPATH=["#thirdparty/angle/include"]) @@ -612,12 +621,6 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): env.Prepend(CPPPATH=[p for p in str(os.getenv("INCLUDE")).split(";")]) env.Append(LIBPATH=[p for p in str(os.getenv("LIB")).split(";")]) - # Sanitizers - if env["use_asan"]: - env.extra_suffix += ".san" - env.Append(LINKFLAGS=["/INFERASANLIBS"]) - env.Append(CCFLAGS=["/fsanitize=address"]) - # Incremental linking fix env["BUILDERS"]["ProgramOriginal"] = env["BUILDERS"]["Program"] env["BUILDERS"]["Program"] = methods.precious_program diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 838beddac2..c1b3540f68 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -38,6 +38,7 @@ #include "core/version.h" #include "drivers/png/png_driver_common.h" #include "main/main.h" +#include "scene/resources/texture.h" #if defined(VULKAN_ENABLED) #include "rendering_context_driver_vulkan_windows.h" @@ -3807,9 +3808,9 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_ACTIVATE: { // Activation can happen just after the window has been created, even before the callbacks are set. // Therefore, it's safer to defer the delivery of the event. - if (!windows[window_id].activate_timer_id) { - windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); - } + // It's important to set an nIDEvent different from the SetTimer for move_timer_id because + // if the same nIDEvent is passed, the timer is replaced and the same timer_id is returned. + windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, DisplayServerWindows::TIMER_ID_WINDOW_ACTIVATION, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); windows[window_id].activate_state = GET_WM_ACTIVATE_STATE(wParam, lParam); return 0; } break; @@ -4727,7 +4728,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_ENTERSIZEMOVE: { Input::get_singleton()->release_pressed_events(); - windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); + windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, DisplayServerWindows::TIMER_ID_MOVE_REDRAW, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); } break; case WM_EXITSIZEMOVE: { KillTimer(windows[window_id].hWnd, windows[window_id].move_timer_id); @@ -5873,7 +5874,14 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win #if defined(RD_ENABLED) if (rendering_context) { rendering_device = memnew(RenderingDevice); - rendering_device->initialize(rendering_context, MAIN_WINDOW_ID); + if (rendering_device->initialize(rendering_context, MAIN_WINDOW_ID) != OK) { + memdelete(rendering_device); + rendering_device = nullptr; + memdelete(rendering_context); + rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; + return; + } rendering_device->screen_create(MAIN_WINDOW_ID); RendererCompositorRD::make_current(); diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 382f18c239..c2f4de7d81 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -332,6 +332,11 @@ class DisplayServerWindows : public DisplayServer { String tablet_driver; Vector<String> tablet_drivers; + enum TimerID { + TIMER_ID_MOVE_REDRAW = 1, + TIMER_ID_WINDOW_ACTIVATION = 2, + }; + enum { KEY_EVENT_BUFFER_SIZE = 512 }; diff --git a/platform/windows/native_menu_windows.cpp b/platform/windows/native_menu_windows.cpp index d9dc28e9d9..fde55918e4 100644 --- a/platform/windows/native_menu_windows.cpp +++ b/platform/windows/native_menu_windows.cpp @@ -81,22 +81,6 @@ void NativeMenuWindows::_menu_activate(HMENU p_menu, int p_index) const { if (GetMenuItemInfoW(md->menu, p_index, true, &item)) { MenuItemData *item_data = (MenuItemData *)item.dwItemData; if (item_data) { - if (item_data->max_states > 0) { - item_data->state++; - if (item_data->state >= item_data->max_states) { - item_data->state = 0; - } - } - - if (item_data->checkable_type == CHECKABLE_TYPE_CHECK_BOX) { - if ((item.fState & MFS_CHECKED) == MFS_CHECKED) { - item.fState &= ~MFS_CHECKED; - } else { - item.fState |= MFS_CHECKED; - } - SetMenuItemInfoW(md->menu, p_index, true, &item); - } - if (item_data->callback.is_valid()) { Variant ret; Callable::CallError ce; @@ -619,9 +603,12 @@ bool NativeMenuWindows::is_item_checked(const RID &p_rid, int p_idx) const { MENUITEMINFOW item; ZeroMemory(&item, sizeof(item)); item.cbSize = sizeof(item); - item.fMask = MIIM_STATE; + item.fMask = MIIM_STATE | MIIM_DATA; if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { - return (item.fState & MFS_CHECKED) == MFS_CHECKED; + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->checked; + } } return false; } @@ -861,12 +848,16 @@ void NativeMenuWindows::set_item_checked(const RID &p_rid, int p_idx, bool p_che MENUITEMINFOW item; ZeroMemory(&item, sizeof(item)); item.cbSize = sizeof(item); - item.fMask = MIIM_STATE; + item.fMask = MIIM_STATE | MIIM_DATA; if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { - if (p_checked) { - item.fState |= MFS_CHECKED; - } else { - item.fState &= ~MFS_CHECKED; + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + item_data->checked = p_checked; + if (p_checked) { + item.fState |= MFS_CHECKED; + } else { + item.fState &= ~MFS_CHECKED; + } } SetMenuItemInfoW(md->menu, p_idx, true, &item); } diff --git a/platform/windows/native_menu_windows.h b/platform/windows/native_menu_windows.h index 5c4aaa52c8..235a4b332a 100644 --- a/platform/windows/native_menu_windows.h +++ b/platform/windows/native_menu_windows.h @@ -51,6 +51,7 @@ class NativeMenuWindows : public NativeMenu { Callable callback; Variant meta; GlobalMenuCheckType checkable_type; + bool checked = false; int max_states = 0; int state = 0; Ref<Image> img; diff --git a/scene/2d/animated_sprite_2d.cpp b/scene/2d/animated_sprite_2d.cpp index 6d380aed3c..b3f735e044 100644 --- a/scene/2d/animated_sprite_2d.cpp +++ b/scene/2d/animated_sprite_2d.cpp @@ -267,7 +267,7 @@ void AnimatedSprite2D::_notification(int p_what) { } if (get_viewport() && get_viewport()->is_snap_2d_transforms_to_pixel_enabled()) { - ofs = ofs.round(); + ofs = (ofs + Point2(0.5, 0.5)).floor(); } Rect2 dst_rect(ofs, s); @@ -473,9 +473,10 @@ void AnimatedSprite2D::play(const StringName &p_name, float p_custom_scale, bool playing = true; custom_speed_scale = p_custom_scale; - int end_frame = MAX(0, frames->get_frame_count(animation) - 1); if (name != animation) { animation = name; + int end_frame = MAX(0, frames->get_frame_count(animation) - 1); + if (p_from_end) { set_frame_and_progress(end_frame, 1.0); } else { @@ -483,7 +484,9 @@ void AnimatedSprite2D::play(const StringName &p_name, float p_custom_scale, bool } emit_signal(SceneStringName(animation_changed)); } else { + int end_frame = MAX(0, frames->get_frame_count(animation) - 1); bool is_backward = signbit(speed_scale * custom_speed_scale); + if (p_from_end && is_backward && frame == 0 && frame_progress <= 0.0) { set_frame_and_progress(end_frame, 1.0); } else if (!p_from_end && !is_backward && frame == end_frame && frame_progress >= 1.0) { diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp index 514c5e7a8f..7020d162fe 100644 --- a/scene/2d/camera_2d.cpp +++ b/scene/2d/camera_2d.cpp @@ -302,6 +302,12 @@ void Camera2D::_notification(int p_what) { _interpolation_data.xform_prev = _interpolation_data.xform_curr; } break; + case NOTIFICATION_PAUSED: { + if (is_physics_interpolated_and_enabled()) { + _update_scroll(); + } + } break; + case NOTIFICATION_TRANSFORM_CHANGED: { if ((!position_smoothing_enabled && !is_physics_interpolated_and_enabled()) || _is_editing_in_editor()) { _update_scroll(); diff --git a/scene/2d/camera_2d.h b/scene/2d/camera_2d.h index 8754e35e88..be2da8b97a 100644 --- a/scene/2d/camera_2d.h +++ b/scene/2d/camera_2d.h @@ -108,7 +108,7 @@ protected: struct InterpolationData { Transform2D xform_curr; Transform2D xform_prev; - uint32_t last_update_physics_tick = 0; + uint32_t last_update_physics_tick = UINT32_MAX; // Ensure tick 0 is detected as a change. } _interpolation_data; void _ensure_update_interpolation_data(); diff --git a/scene/2d/navigation_agent_2d.cpp b/scene/2d/navigation_agent_2d.cpp index 9e3e6ea583..d0fae611d8 100644 --- a/scene/2d/navigation_agent_2d.cpp +++ b/scene/2d/navigation_agent_2d.cpp @@ -671,8 +671,6 @@ void NavigationAgent2D::_update_navigation() { return; } - update_frame_id = Engine::get_singleton()->get_physics_frames(); - Vector2 origin = agent_parent->get_global_position(); bool reload_path = false; @@ -767,7 +765,6 @@ void NavigationAgent2D::_request_repath() { target_reached = false; navigation_finished = false; last_waypoint_reached = false; - update_frame_id = 0; } bool NavigationAgent2D::_is_last_waypoint() const { diff --git a/scene/2d/navigation_agent_2d.h b/scene/2d/navigation_agent_2d.h index 0acfc82162..8741f578d0 100644 --- a/scene/2d/navigation_agent_2d.h +++ b/scene/2d/navigation_agent_2d.h @@ -91,8 +91,6 @@ class NavigationAgent2D : public Node { bool target_reached = false; bool navigation_finished = true; bool last_waypoint_reached = false; - // No initialized on purpose - uint32_t update_frame_id = 0; // Debug properties for exposed bindings bool debug_enabled = false; diff --git a/scene/2d/parallax_2d.cpp b/scene/2d/parallax_2d.cpp index aacab3213d..b3586a1da0 100644 --- a/scene/2d/parallax_2d.cpp +++ b/scene/2d/parallax_2d.cpp @@ -86,11 +86,10 @@ void Parallax2D::_update_scroll() { } Point2 scroll_ofs = screen_offset; - Size2 vps = get_viewport_rect().size; - if (Engine::get_singleton()->is_editor_hint()) { - vps = Size2(GLOBAL_GET("display/window/size/viewport_width"), GLOBAL_GET("display/window/size/viewport_height")); - } else { + if (!Engine::get_singleton()->is_editor_hint()) { + Size2 vps = get_viewport_rect().size; + if (limit_begin.x <= limit_end.x - vps.x) { scroll_ofs.x = CLAMP(scroll_ofs.x, limit_begin.x, limit_end.x - vps.x); } diff --git a/scene/2d/sprite_2d.cpp b/scene/2d/sprite_2d.cpp index efb5029ac4..2b5c40f212 100644 --- a/scene/2d/sprite_2d.cpp +++ b/scene/2d/sprite_2d.cpp @@ -98,7 +98,7 @@ void Sprite2D::_get_rects(Rect2 &r_src_rect, Rect2 &r_dst_rect, bool &r_filter_c } if (get_viewport() && get_viewport()->is_snap_2d_transforms_to_pixel_enabled()) { - dest_offset = dest_offset.round(); + dest_offset = (dest_offset + Point2(0.5, 0.5)).floor(); } r_dst_rect = Rect2(dest_offset, frame_size); @@ -400,7 +400,7 @@ Rect2 Sprite2D::get_rect() const { } if (get_viewport() && get_viewport()->is_snap_2d_transforms_to_pixel_enabled()) { - ofs = ofs.round(); + ofs = (ofs + Point2(0.5, 0.5)).floor(); } if (s == Size2(0, 0)) { diff --git a/scene/2d/tile_map_layer.cpp b/scene/2d/tile_map_layer.cpp index b6d0a8d73a..437790bb99 100644 --- a/scene/2d/tile_map_layer.cpp +++ b/scene/2d/tile_map_layer.cpp @@ -1887,6 +1887,12 @@ void TileMapLayer::_update_self_texture_repeat(RS::CanvasItemTextureRepeat p_tex emit_signal(CoreStringName(changed)); } +#ifdef TOOLS_ENABLED +bool TileMapLayer::_edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const { + return tile_set.is_valid() && get_cell_source_id(local_to_map(p_point)) != TileSet::INVALID_SOURCE; +} +#endif + void TileMapLayer::set_as_tile_map_internal_node(int p_index) { // Compatibility with TileMap. ERR_FAIL_NULL(get_parent()); diff --git a/scene/2d/tile_map_layer.h b/scene/2d/tile_map_layer.h index b0aaaafe5d..c71f13d7be 100644 --- a/scene/2d/tile_map_layer.h +++ b/scene/2d/tile_map_layer.h @@ -393,6 +393,10 @@ protected: virtual void _update_self_texture_repeat(RS::CanvasItemTextureRepeat p_texture_repeat) override; public: +#ifdef TOOLS_ENABLED + virtual bool _edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const override; +#endif + // TileMap node. void set_as_tile_map_internal_node(int p_index); int get_index_in_tile_map() const { diff --git a/scene/3d/bone_attachment_3d.cpp b/scene/3d/bone_attachment_3d.cpp index 6aade24e4e..4889512037 100644 --- a/scene/3d/bone_attachment_3d.cpp +++ b/scene/3d/bone_attachment_3d.cpp @@ -239,9 +239,20 @@ int BoneAttachment3D::get_bone_idx() const { } void BoneAttachment3D::set_override_pose(bool p_override) { + if (override_pose == p_override) { + return; + } + override_pose = p_override; set_notify_transform(override_pose); set_process_internal(override_pose); + if (!override_pose && bone_idx >= 0) { + Skeleton3D *sk = _get_skeleton3d(); + if (sk) { + sk->reset_bone_pose(bone_idx); + } + } + notify_property_list_changed(); } diff --git a/scene/3d/navigation_agent_3d.cpp b/scene/3d/navigation_agent_3d.cpp index dff413f5d2..5bbb724e2f 100644 --- a/scene/3d/navigation_agent_3d.cpp +++ b/scene/3d/navigation_agent_3d.cpp @@ -737,8 +737,6 @@ void NavigationAgent3D::_update_navigation() { return; } - update_frame_id = Engine::get_singleton()->get_physics_frames(); - Vector3 origin = agent_parent->get_global_position(); bool reload_path = false; @@ -835,7 +833,6 @@ void NavigationAgent3D::_request_repath() { target_reached = false; navigation_finished = false; last_waypoint_reached = false; - update_frame_id = 0; } bool NavigationAgent3D::_is_last_waypoint() const { diff --git a/scene/3d/navigation_agent_3d.h b/scene/3d/navigation_agent_3d.h index ade6afd445..d5721a56c8 100644 --- a/scene/3d/navigation_agent_3d.h +++ b/scene/3d/navigation_agent_3d.h @@ -98,8 +98,6 @@ class NavigationAgent3D : public Node { bool target_reached = false; bool navigation_finished = true; bool last_waypoint_reached = false; - // No initialized on purpose - uint32_t update_frame_id = 0; // Debug properties for exposed bindings bool debug_enabled = false; diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index ba3b32a031..8ac585719c 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -984,7 +984,7 @@ void Sprite3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "hframes", PROPERTY_HINT_RANGE, "1,16384,1"), "set_hframes", "get_hframes"); ADD_PROPERTY(PropertyInfo(Variant::INT, "vframes", PROPERTY_HINT_RANGE, "1,16384,1"), "set_vframes", "get_vframes"); ADD_PROPERTY(PropertyInfo(Variant::INT, "frame"), "set_frame", "get_frame"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "frame_coords", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_frame_coords", "get_frame_coords"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "frame_coords", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_frame_coords", "get_frame_coords"); ADD_GROUP("Region", "region_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "region_enabled"), "set_region_enabled", "is_region_enabled"); ADD_PROPERTY(PropertyInfo(Variant::RECT2, "region_rect", PROPERTY_HINT_NONE, "suffix:px"), "set_region_rect", "get_region_rect"); @@ -1334,9 +1334,10 @@ void AnimatedSprite3D::play(const StringName &p_name, float p_custom_scale, bool playing = true; custom_speed_scale = p_custom_scale; - int end_frame = MAX(0, frames->get_frame_count(animation) - 1); if (name != animation) { animation = name; + int end_frame = MAX(0, frames->get_frame_count(animation) - 1); + if (p_from_end) { set_frame_and_progress(end_frame, 1.0); } else { @@ -1344,7 +1345,9 @@ void AnimatedSprite3D::play(const StringName &p_name, float p_custom_scale, bool } emit_signal(SceneStringName(animation_changed)); } else { + int end_frame = MAX(0, frames->get_frame_count(animation) - 1); bool is_backward = signbit(speed_scale * custom_speed_scale); + if (p_from_end && is_backward && frame == 0 && frame_progress <= 0.0) { set_frame_and_progress(end_frame, 1.0); } else if (!p_from_end && !is_backward && frame == end_frame && frame_progress >= 1.0) { diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index c74348c2e7..e7626b3c2d 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -691,7 +691,9 @@ bool AnimationMixer::_update_caches() { track = track_value; - track_value->init_value = anim->track_get_key_value(i, 0); + bool is_value = track_src_type == Animation::TYPE_VALUE; + + track_value->init_value = is_value ? anim->track_get_key_value(i, 0) : (anim->track_get_key_value(i, 0).operator Array())[0]; track_value->init_value.zero(); track_value->is_init = false; @@ -703,7 +705,7 @@ bool AnimationMixer::_update_caches() { if (has_reset_anim) { int rt = reset_anim->find_track(path, track_src_type); if (rt >= 0) { - if (track_src_type == Animation::TYPE_VALUE) { + if (is_value) { if (reset_anim->track_get_key_count(rt) > 0) { track_value->init_value = reset_anim->track_get_key_value(rt, 0); } diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h index f270f32193..508b2c49fa 100644 --- a/scene/animation/animation_player.h +++ b/scene/animation/animation_player.h @@ -95,9 +95,9 @@ private: } bool operator<(const BlendKey &bk) const { if (from == bk.from) { - return to < bk.to; + return StringName::AlphCompare()(to, bk.to); } else { - return from < bk.from; + return StringName::AlphCompare()(from, bk.from); } } }; diff --git a/scene/audio/audio_stream_player_internal.cpp b/scene/audio/audio_stream_player_internal.cpp index b3a55ddee0..6653e01f25 100644 --- a/scene/audio/audio_stream_player_internal.cpp +++ b/scene/audio/audio_stream_player_internal.cpp @@ -152,6 +152,7 @@ Ref<AudioStreamPlayback> AudioStreamPlayerInternal::play_basic() { Ref<AudioSamplePlayback> sample_playback; sample_playback.instantiate(); sample_playback->stream = stream; + sample_playback->player_id = node->get_instance_id(); stream_playback->set_sample_playback(sample_playback); } } else if (!stream->is_meta_stream()) { diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp index 4e738216de..dd344121e1 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -530,7 +530,7 @@ Size2 Button::get_minimum_size_for_text_and_icon(const String &p_text, Ref<Textu } } - return _get_largest_stylebox_size() + minsize; + return (theme_cache.align_to_largest_stylebox ? _get_largest_stylebox_size() : _get_current_stylebox()->get_minimum_size()) + minsize; } void Button::_shape(Ref<TextParagraph> p_paragraph, String p_text) { diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 0bdb487300..c3c4b1d3fb 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -2331,18 +2331,19 @@ void CodeEdit::move_lines_up() { unfold_line(line); swap_lines(line - 1, line); } - } - - // Fix selection if it ended at column 0, since it wasn't moved. - for (int i = 0; i < get_caret_count(); i++) { - if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != 0) { - if (is_caret_after_selection_origin(i)) { - set_caret_line(get_caret_line(i) - 1, false, true, -1, i); - } else { - set_selection_origin_line(get_selection_origin_line(i) - 1, true, -1, i); + // Fix selection if the last one ends at column 0, since it wasn't moved. + for (int i = 0; i < get_caret_count(); i++) { + if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) == line_range.y + 1) { + if (is_caret_after_selection_origin(i)) { + set_caret_line(get_caret_line(i) - 1, false, true, -1, i); + } else { + set_selection_origin_line(get_selection_origin_line(i) - 1, true, -1, i); + } + break; } } } + adjust_viewport_to_caret(); end_multicaret_edit(); end_complex_operation(); @@ -2352,30 +2353,41 @@ void CodeEdit::move_lines_down() { begin_complex_operation(); begin_multicaret_edit(); - Vector<Point2i> line_ranges = get_line_ranges_from_carets(); - - // Fix selection if it ended at column 0, since it won't be moved. - for (int i = 0; i < get_caret_count(); i++) { - if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != get_line_count() - 1) { - if (is_caret_after_selection_origin(i)) { - set_caret_line(get_caret_line(i) + 1, false, true, -1, i); - } else { - set_selection_origin_line(get_selection_origin_line(i) + 1, true, -1, i); - } - } - } - // Move lines down by swapping each line with the one below it. + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + // Reverse in case line ranges are adjacent, if the first ends at column 0. + line_ranges.reverse(); for (Point2i line_range : line_ranges) { if (line_range.y == get_line_count() - 1) { continue; } + // Fix selection if the last one ends at column 0, since it won't be moved. + bool selection_to_line_at_end = false; + for (int i = 0; i < get_caret_count(); i++) { + if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) == line_range.y + 1) { + selection_to_line_at_end = get_selection_to_line(i) == get_line_count() - 1; + if (selection_to_line_at_end) { + break; + } + if (is_caret_after_selection_origin(i)) { + set_caret_line(get_caret_line(i) + 1, false, true, -1, i); + } else { + set_selection_origin_line(get_selection_origin_line(i) + 1, true, -1, i); + } + break; + } + } + if (selection_to_line_at_end) { + continue; + } + unfold_line(line_range.y + 1); for (int line = line_range.y; line >= line_range.x; line--) { unfold_line(line); swap_lines(line + 1, line); } } + adjust_viewport_to_caret(); end_multicaret_edit(); end_complex_operation(); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 0682c11a9b..1d53edbfa6 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -1732,11 +1732,15 @@ void Control::_size_changed() { new_size_cache.height = minimum_size.height; } - bool pos_changed = new_pos_cache != data.pos_cache; - bool size_changed = new_size_cache != data.size_cache; + bool pos_changed = !new_pos_cache.is_equal_approx(data.pos_cache); + bool size_changed = !new_size_cache.is_equal_approx(data.size_cache); - data.pos_cache = new_pos_cache; - data.size_cache = new_size_cache; + if (pos_changed) { + data.pos_cache = new_pos_cache; + } + if (size_changed) { + data.size_cache = new_size_cache; + } if (is_inside_tree()) { if (pos_changed || size_changed) { @@ -1751,12 +1755,10 @@ void Control::_size_changed() { } if (pos_changed && !size_changed) { - _update_canvas_item_transform(); //move because it won't be updated - } - } else { - if (pos_changed) { - _notify_transform(); + _update_canvas_item_transform(); } + } else if (pos_changed) { + _notify_transform(); } } diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index 33756dc1fd..55a2c607e3 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -546,6 +546,11 @@ void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) { GraphNode *graph_node = Object::cast_to<GraphNode>(p_node); ERR_FAIL_NULL(graph_node); + // Update all adjacent connections during the next redraw. + for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) { + conn->_cache.dirty = true; + } + minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); @@ -782,7 +787,9 @@ Rect2 GraphEdit::_compute_shrinked_frame_rect(const GraphFrame *p_frame) { return Rect2(p_frame->get_position_offset(), Size2()); } - min_point -= Size2(autoshrink_margin, autoshrink_margin); + const Size2 titlebar_size = p_frame->get_titlebar_size(); + + min_point -= Size2(autoshrink_margin, MAX(autoshrink_margin, titlebar_size.y)); max_point += Size2(autoshrink_margin, autoshrink_margin); return Rect2(min_point, max_point - min_point); diff --git a/scene/gui/graph_frame.cpp b/scene/gui/graph_frame.cpp index 8cd7dbbeb5..e85d007262 100644 --- a/scene/gui/graph_frame.cpp +++ b/scene/gui/graph_frame.cpp @@ -262,6 +262,10 @@ HBoxContainer *GraphFrame::get_titlebar_hbox() { return titlebar_hbox; } +Size2 GraphFrame::get_titlebar_size() const { + return titlebar_hbox->get_size() + theme_cache.titlebar->get_minimum_size(); +} + void GraphFrame::set_drag_margin(int p_margin) { drag_margin = p_margin; } diff --git a/scene/gui/graph_frame.h b/scene/gui/graph_frame.h index 21346586c8..2af09cf872 100644 --- a/scene/gui/graph_frame.h +++ b/scene/gui/graph_frame.h @@ -89,6 +89,7 @@ public: int get_autoshrink_margin() const; HBoxContainer *get_titlebar_hbox(); + Size2 get_titlebar_size() const; void set_drag_margin(int p_margin); int get_drag_margin() const; diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 4f07fdb87b..7f795ea710 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -2314,6 +2314,16 @@ bool PopupMenu::is_prefer_native_menu() const { return prefer_native; } +bool PopupMenu::is_native_menu() const { +#ifdef TOOLS_ENABLED + if (is_part_of_edited_scene()) { + return false; + } +#endif + + return global_menu.is_valid(); +} + bool PopupMenu::activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only) { ERR_FAIL_COND_V(p_event.is_null(), false); Key code = Key::NONE; @@ -2643,6 +2653,7 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("set_prefer_native_menu", "enabled"), &PopupMenu::set_prefer_native_menu); ClassDB::bind_method(D_METHOD("is_prefer_native_menu"), &PopupMenu::is_prefer_native_menu); + ClassDB::bind_method(D_METHOD("is_native_menu"), &PopupMenu::is_native_menu); ClassDB::bind_method(D_METHOD("add_item", "label", "id", "accel"), &PopupMenu::add_item, DEFVAL(-1), DEFVAL(0)); ClassDB::bind_method(D_METHOD("add_icon_item", "texture", "label", "id", "accel"), &PopupMenu::add_icon_item, DEFVAL(-1), DEFVAL(0)); diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index c6eef03aca..5313dae404 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -330,6 +330,8 @@ public: void set_prefer_native_menu(bool p_enabled); bool is_prefer_native_menu() const; + bool is_native_menu() const; + void scroll_to_item(int p_idx); bool activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only = false); diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 416a7fafb6..8ffa0f8c63 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -1249,7 +1249,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } if (is_inside_tree() && get_viewport()->is_snap_2d_transforms_to_pixel_enabled()) { - fx_offset = fx_offset.round(); + fx_offset = (fx_offset + Point2(0.5, 0.5)).floor(); } Vector2 char_off = char_xform.get_origin(); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 233c7200ff..9cc59f1def 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -472,7 +472,7 @@ String TextEdit::Text::get_custom_word_separators() const { } String TextEdit::Text::get_default_word_separators() const { - String concat_separators = "´`~$^=+|<>"; + String concat_separators = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~"; for (char32_t ch = 0x2000; ch <= 0x206F; ++ch) { // General punctuation block. concat_separators += ch; } @@ -1643,21 +1643,14 @@ void TextEdit::_notification(int p_what) { } break; case NOTIFICATION_DRAG_END: { - if (is_drag_successful()) { - if (selection_drag_attempt) { - // Dropped elsewhere. - if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { - delete_selection(); - } else if (deselect_on_focus_loss_enabled) { - deselect(); - } - } - } - if (drag_caret_index >= 0) { - if (drag_caret_index < carets.size()) { - remove_caret(drag_caret_index); + remove_drag_caret(); + if (selection_drag_attempt && is_drag_successful()) { + // Dropped elsewhere. + if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { + delete_selection(); + } else if (deselect_on_focus_loss_enabled) { + deselect(); } - drag_caret_index = -1; } selection_drag_attempt = false; drag_action = false; @@ -4606,6 +4599,15 @@ void TextEdit::remove_caret(int p_caret) { } } +void TextEdit::remove_drag_caret() { + if (drag_caret_index >= 0) { + if (drag_caret_index < carets.size()) { + remove_caret(drag_caret_index); + } + drag_caret_index = -1; + } +} + void TextEdit::remove_secondary_carets() { if (carets.size() == 1) { return; diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 6ed5cf4bdc..4d9d169c1c 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -881,6 +881,7 @@ public: int add_caret(int p_line, int p_column); void remove_caret(int p_caret); + void remove_drag_caret(); void remove_secondary_carets(); int get_caret_count() const; void add_caret_at_carets(bool p_below); diff --git a/scene/gui/texture_rect.cpp b/scene/gui/texture_rect.cpp index c52f463905..972a832985 100644 --- a/scene/gui/texture_rect.cpp +++ b/scene/gui/texture_rect.cpp @@ -30,7 +30,6 @@ #include "texture_rect.h" -#include "scene/resources/atlas_texture.h" #include "servers/rendering_server.h" void TextureRect::_notification(int p_what) { @@ -92,15 +91,6 @@ void TextureRect::_notification(int p_what) { } break; } - Ref<AtlasTexture> p_atlas = texture; - - if (p_atlas.is_valid() && !region.has_area()) { - Size2 scale_size(size.width / texture->get_width(), size.height / texture->get_height()); - - offset.width += hflip ? p_atlas->get_margin().get_position().width * scale_size.width * 2 : 0; - offset.height += vflip ? p_atlas->get_margin().get_position().height * scale_size.height * 2 : 0; - } - size.width *= hflip ? -1.0f : 1.0f; size.height *= vflip ? -1.0f : 1.0f; diff --git a/scene/main/node.cpp b/scene/main/node.cpp index ae8e2a493d..0396f3ab4a 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -177,6 +177,12 @@ void Node::_notification(int p_notification) { } } break; + case NOTIFICATION_PAUSED: { + if (is_physics_interpolated_and_enabled() && is_inside_tree()) { + reset_physics_interpolation(); + } + } break; + case NOTIFICATION_PATH_RENAMED: { if (data.path_cache) { memdelete(data.path_cache); @@ -2907,9 +2913,8 @@ void Node::_duplicate_properties(const Node *p_root, const Node *p_original, Nod arr[i] = p_copy->get_node_or_null(p_original->get_path_to(property_node)); } } - value = arr; - p_copy->set(name, value); } + p_copy->set(name, arr); } else { p_copy->set(name, value); } diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index 254bd38be7..c0ab636adc 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -3189,6 +3189,20 @@ StringName Animation::method_track_get_name(int p_track, int p_key_idx) const { return pm->methods[p_key_idx].method; } +Array Animation::make_default_bezier_key(float p_value) { + const double max_width = length / 2.0; + Array new_point; + new_point.resize(5); + + new_point[0] = p_value; + new_point[1] = MAX(-0.25, -max_width); + new_point[2] = 0; + new_point[3] = MIN(0.25, max_width); + new_point[4] = 0; + + return new_point; +} + int Animation::bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle) { ERR_FAIL_INDEX_V(p_track, tracks.size(), -1); Track *t = tracks[p_track]; diff --git a/scene/resources/animation.h b/scene/resources/animation.h index e9bfc298a5..cb12b12c0e 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -455,6 +455,7 @@ public: void track_set_interpolation_type(int p_track, InterpolationType p_interp); InterpolationType track_get_interpolation_type(int p_track) const; + Array make_default_bezier_key(float p_value); int bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle); void bezier_track_set_key_value(int p_track, int p_index, real_t p_value); void bezier_track_set_key_in_handle(int p_track, int p_index, const Vector2 &p_handle, real_t p_balanced_value_time_ratio = 1.0); diff --git a/scene/resources/atlas_texture.cpp b/scene/resources/atlas_texture.cpp index ef2f1eb135..28e4186048 100644 --- a/scene/resources/atlas_texture.cpp +++ b/scene/resources/atlas_texture.cpp @@ -164,20 +164,13 @@ void AtlasTexture::draw_rect(RID p_canvas_item, const Rect2 &p_rect, bool p_tile return; } - Rect2 rc = region; + Rect2 src_rect = Rect2(0, 0, get_width(), get_height()); - if (rc.size.width == 0) { - rc.size.width = atlas->get_width(); - } - - if (rc.size.height == 0) { - rc.size.height = atlas->get_height(); + Rect2 dr; + Rect2 src_c; + if (get_rect_region(p_rect, src_rect, dr, src_c)) { + atlas->draw_rect_region(p_canvas_item, dr, src_c, p_modulate, p_transpose, filter_clip); } - - Vector2 scale = p_rect.size / (region.size + margin.size); - Rect2 dr(p_rect.position + margin.position * scale, rc.size * scale); - - atlas->draw_rect_region(p_canvas_item, dr, rc, p_modulate, p_transpose, filter_clip); } void AtlasTexture::draw_rect_region(RID p_canvas_item, const Rect2 &p_rect, const Rect2 &p_src_rect, const Color &p_modulate, bool p_transpose, bool p_clip_uv) const { @@ -188,9 +181,9 @@ void AtlasTexture::draw_rect_region(RID p_canvas_item, const Rect2 &p_rect, cons Rect2 dr; Rect2 src_c; - get_rect_region(p_rect, p_src_rect, dr, src_c); - - atlas->draw_rect_region(p_canvas_item, dr, src_c, p_modulate, p_transpose, filter_clip); + if (get_rect_region(p_rect, p_src_rect, dr, src_c)) { + atlas->draw_rect_region(p_canvas_item, dr, src_c, p_modulate, p_transpose, filter_clip); + } } bool AtlasTexture::get_rect_region(const Rect2 &p_rect, const Rect2 &p_src_rect, Rect2 &r_rect, Rect2 &r_src_rect) const { diff --git a/scene/resources/font.cpp b/scene/resources/font.cpp index bc8e0b9015..37d9d57722 100644 --- a/scene/resources/font.cpp +++ b/scene/resources/font.cpp @@ -3213,7 +3213,6 @@ void SystemFont::_update_base_font() { } _invalidate_rids(); - notify_property_list_changed(); } void SystemFont::reset_state() { diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index b2567e431b..7d121c9d87 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -324,6 +324,21 @@ void ShaderMaterial::_get_property_list(List<PropertyInfo> *p_list) const { is_uniform_type_compatible = E->get().type == cached.get_type(); } +#ifndef DISABLE_DEPRECATED + // PackedFloat32Array -> PackedVector4Array conversion. + if (!is_uniform_type_compatible && E->get().type == Variant::PACKED_VECTOR4_ARRAY && cached.get_type() == Variant::PACKED_FLOAT32_ARRAY) { + PackedVector4Array varray; + PackedFloat32Array array = (PackedFloat32Array)cached; + + for (int i = 0; i + 3 < array.size(); i += 4) { + varray.push_back(Vector4(array[i], array[i + 1], array[i + 2], array[i + 3])); + } + + param_cache.insert(E->get().name, varray); + is_uniform_type_compatible = true; + } +#endif + if (is_uniform_type_compatible && E->get().type == Variant::OBJECT && cached.get_type() == Variant::OBJECT) { // Check if the Object class (hint string) changed, for example Texture2D sampler to Texture3D. // Allow inheritance, Texture2D type sampler should also accept CompressedTexture2D. diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index b1742bd5a3..900629f5f8 100644 --- a/scene/resources/packed_scene.cpp +++ b/scene/resources/packed_scene.cpp @@ -2124,6 +2124,56 @@ void PackedScene::recreate_state() { #endif } +#ifdef TOOLS_ENABLED +HashSet<StringName> PackedScene::get_scene_groups(const String &p_path) { + { + Ref<PackedScene> packed_scene = ResourceCache::get_ref(p_path); + if (packed_scene.is_valid()) { + return packed_scene->get_state()->get_all_groups(); + } + } + + if (p_path.get_extension() == "tscn") { + Ref<FileAccess> scene_file = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V(scene_file.is_null(), HashSet<StringName>()); + + HashSet<StringName> ret; + while (!scene_file->eof_reached()) { + const String line = scene_file->get_line(); + if (!line.begins_with("[node")) { + continue; + } + + int i = line.find("groups=["); + if (i == -1) { + continue; + } + + int j = line.find_char(']', i); + while (i < j) { + i = line.find_char('"', i); + if (i == -1) { + break; + } + + int k = line.find_char('"', i + 1); + if (k == -1) { + break; + } + + ret.insert(line.substr(i + 1, k - i - 1)); + i = k + 1; + } + } + return ret; + } else { + Ref<PackedScene> packed_scene = ResourceLoader::load(p_path); + ERR_FAIL_COND_V(packed_scene.is_null(), HashSet<StringName>()); + return packed_scene->get_state()->get_all_groups(); + } +} +#endif + Ref<SceneState> PackedScene::get_state() const { return state; } diff --git a/scene/resources/packed_scene.h b/scene/resources/packed_scene.h index c46a4dd5fe..e26b9f7b90 100644 --- a/scene/resources/packed_scene.h +++ b/scene/resources/packed_scene.h @@ -270,6 +270,7 @@ public: state->set_last_modified_time(p_time); } + static HashSet<StringName> get_scene_groups(const String &p_path); #endif Ref<SceneState> get_state() const; diff --git a/scene/resources/resource_format_text.cpp b/scene/resources/resource_format_text.cpp index 7c13e623c2..90102e44e4 100644 --- a/scene/resources/resource_format_text.cpp +++ b/scene/resources/resource_format_text.cpp @@ -191,8 +191,10 @@ Error ResourceLoaderText::_parse_ext_resource(VariantParser::Stream *p_stream, R } Ref<PackedScene> ResourceLoaderText::_parse_node_tag(VariantParser::ResourceParser &parser) { - Ref<PackedScene> packed_scene; - packed_scene.instantiate(); + Ref<PackedScene> packed_scene = ResourceLoader::get_resource_ref_override(local_path); + if (packed_scene.is_null()) { + packed_scene.instantiate(); + } while (true) { if (next_tag.name == "node") { @@ -664,39 +666,42 @@ Error ResourceLoaderText::load() { return error; } - Ref<Resource> cache = ResourceCache::get_ref(local_path); - if (cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE && cache.is_valid() && cache->get_class() == res_type) { - cache->reset_state(); - resource = cache; - } - MissingResource *missing_resource = nullptr; - if (!resource.is_valid()) { - Object *obj = ClassDB::instantiate(res_type); - if (!obj) { - if (ResourceLoader::is_creating_missing_resources_if_class_unavailable_enabled()) { - missing_resource = memnew(MissingResource); - missing_resource->set_original_class(res_type); - missing_resource->set_recording_properties(true); - obj = missing_resource; - } else { - error_text += "Can't create sub resource of type: " + res_type; + resource = ResourceLoader::get_resource_ref_override(local_path); + if (resource.is_null()) { + Ref<Resource> cache = ResourceCache::get_ref(local_path); + if (cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE && cache.is_valid() && cache->get_class() == res_type) { + cache->reset_state(); + resource = cache; + } + + if (!resource.is_valid()) { + Object *obj = ClassDB::instantiate(res_type); + if (!obj) { + if (ResourceLoader::is_creating_missing_resources_if_class_unavailable_enabled()) { + missing_resource = memnew(MissingResource); + missing_resource->set_original_class(res_type); + missing_resource->set_recording_properties(true); + obj = missing_resource; + } else { + error_text += "Can't create sub resource of type: " + res_type; + _printerr(); + error = ERR_FILE_CORRUPT; + return error; + } + } + + Resource *r = Object::cast_to<Resource>(obj); + if (!r) { + error_text += "Can't create sub resource of type, because not a resource: " + res_type; _printerr(); error = ERR_FILE_CORRUPT; return error; } - } - Resource *r = Object::cast_to<Resource>(obj); - if (!r) { - error_text += "Can't create sub resource of type, because not a resource: " + res_type; - _printerr(); - error = ERR_FILE_CORRUPT; - return error; + resource = Ref<Resource>(r); } - - resource = Ref<Resource>(r); } Dictionary missing_resource_properties; diff --git a/servers/audio/audio_stream.h b/servers/audio/audio_stream.h index 0ca4777d5c..9149109381 100644 --- a/servers/audio/audio_stream.h +++ b/servers/audio/audio_stream.h @@ -49,6 +49,7 @@ class AudioSamplePlayback : public RefCounted { public: Ref<AudioStream> stream; + ObjectID player_id; float offset = 0.0f; Vector<AudioFrame> volume_vector; StringName bus; diff --git a/servers/display_server_headless.h b/servers/display_server_headless.h index 7e4a517c1d..60422c16cc 100644 --- a/servers/display_server_headless.h +++ b/servers/display_server_headless.h @@ -51,7 +51,18 @@ private: return memnew(DisplayServerHeadless()); } + static void _dispatch_input_events(const Ref<InputEvent> &p_event) { + static_cast<DisplayServerHeadless *>(get_singleton())->_dispatch_input_event(p_event); + } + + void _dispatch_input_event(const Ref<InputEvent> &p_event) { + if (input_event_callback.is_valid()) { + input_event_callback.call(p_event); + } + } + NativeMenu *native_menu = nullptr; + Callable input_event_callback; public: bool has_feature(Feature p_feature) const override { return false; } @@ -86,7 +97,11 @@ public: void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override {} void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override {} - void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override {} + + void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override { + input_event_callback = p_callable; + } + void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override {} void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override {} @@ -137,7 +152,9 @@ public: int64_t window_get_native_handle(HandleType p_handle_type, WindowID p_window = MAIN_WINDOW_ID) const override { return 0; } - void process_events() override {} + void process_events() override { + Input::get_singleton()->flush_buffered_events(); + } void set_native_icon(const String &p_filename) override {} void set_icon(const Ref<Image> &p_icon) override {} @@ -179,7 +196,9 @@ public: DisplayServerHeadless() { native_menu = memnew(NativeMenu); + Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events); } + ~DisplayServerHeadless() { if (native_menu) { memdelete(native_menu); diff --git a/servers/rendering/renderer_canvas_cull.cpp b/servers/rendering/renderer_canvas_cull.cpp index e92050a323..c4286dcc0c 100644 --- a/servers/rendering/renderer_canvas_cull.cpp +++ b/servers/rendering/renderer_canvas_cull.cpp @@ -284,8 +284,8 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 } if (snapping_2d_transforms_to_pixel) { - final_xform.columns[2] = final_xform.columns[2].round(); - parent_xform.columns[2] = parent_xform.columns[2].round(); + final_xform.columns[2] = (final_xform.columns[2] + Point2(0.5, 0.5)).floor(); + parent_xform.columns[2] = (parent_xform.columns[2] + Point2(0.5, 0.5)).floor(); } final_xform = parent_xform * final_xform; diff --git a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp index 38bee1ecde..f97ed3d215 100644 --- a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp @@ -3587,22 +3587,31 @@ RID RenderForwardClustered::_render_buffers_get_velocity_texture(Ref<RenderScene } void RenderForwardClustered::environment_set_ssao_quality(RS::EnvironmentSSAOQuality p_quality, bool p_half_size, float p_adaptive_target, int p_blur_passes, float p_fadeout_from, float p_fadeout_to) { + ERR_FAIL_NULL(ss_effects); + ERR_FAIL_COND(p_quality < RS::EnvironmentSSAOQuality::ENV_SSAO_QUALITY_VERY_LOW || p_quality > RS::EnvironmentSSAOQuality::ENV_SSAO_QUALITY_ULTRA); ss_effects->ssao_set_quality(p_quality, p_half_size, p_adaptive_target, p_blur_passes, p_fadeout_from, p_fadeout_to); } void RenderForwardClustered::environment_set_ssil_quality(RS::EnvironmentSSILQuality p_quality, bool p_half_size, float p_adaptive_target, int p_blur_passes, float p_fadeout_from, float p_fadeout_to) { + ERR_FAIL_NULL(ss_effects); + ERR_FAIL_COND(p_quality < RS::EnvironmentSSILQuality::ENV_SSIL_QUALITY_VERY_LOW || p_quality > RS::EnvironmentSSILQuality::ENV_SSIL_QUALITY_ULTRA); ss_effects->ssil_set_quality(p_quality, p_half_size, p_adaptive_target, p_blur_passes, p_fadeout_from, p_fadeout_to); } void RenderForwardClustered::environment_set_ssr_roughness_quality(RS::EnvironmentSSRRoughnessQuality p_quality) { + ERR_FAIL_NULL(ss_effects); + ERR_FAIL_COND(p_quality < RS::EnvironmentSSRRoughnessQuality::ENV_SSR_ROUGHNESS_QUALITY_DISABLED || p_quality > RS::EnvironmentSSRRoughnessQuality::ENV_SSR_ROUGHNESS_QUALITY_HIGH); ss_effects->ssr_set_roughness_quality(p_quality); } void RenderForwardClustered::sub_surface_scattering_set_quality(RS::SubSurfaceScatteringQuality p_quality) { + ERR_FAIL_NULL(ss_effects); + ERR_FAIL_COND(p_quality < RS::SubSurfaceScatteringQuality::SUB_SURFACE_SCATTERING_QUALITY_DISABLED || p_quality > RS::SubSurfaceScatteringQuality::SUB_SURFACE_SCATTERING_QUALITY_HIGH); ss_effects->sss_set_quality(p_quality); } void RenderForwardClustered::sub_surface_scattering_set_scale(float p_scale, float p_depth_scale) { + ERR_FAIL_NULL(ss_effects); ss_effects->sss_set_scale(p_scale, p_depth_scale); } diff --git a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp index d1c8c71b7f..13b00be1c4 100644 --- a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp @@ -1165,8 +1165,8 @@ MaterialStorage::MaterialStorage() { global_shader_uniforms.buffer_values = memnew_arr(GlobalShaderUniforms::Value, global_shader_uniforms.buffer_size); memset(global_shader_uniforms.buffer_values, 0, sizeof(GlobalShaderUniforms::Value) * global_shader_uniforms.buffer_size); global_shader_uniforms.buffer_usage = memnew_arr(GlobalShaderUniforms::ValueUsage, global_shader_uniforms.buffer_size); - global_shader_uniforms.buffer_dirty_regions = memnew_arr(bool, global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE); - memset(global_shader_uniforms.buffer_dirty_regions, 0, sizeof(bool) * global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE); + global_shader_uniforms.buffer_dirty_regions = memnew_arr(bool, 1 + (global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE)); + memset(global_shader_uniforms.buffer_dirty_regions, 0, sizeof(bool) * (1 + (global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE))); global_shader_uniforms.buffer = RD::get_singleton()->storage_buffer_create(sizeof(GlobalShaderUniforms::Value) * global_shader_uniforms.buffer_size); } @@ -1769,7 +1769,7 @@ void MaterialStorage::global_shader_parameters_instance_update(RID p_instance, i void MaterialStorage::_update_global_shader_uniforms() { MaterialStorage *material_storage = MaterialStorage::get_singleton(); if (global_shader_uniforms.buffer_dirty_region_count > 0) { - uint32_t total_regions = global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE; + uint32_t total_regions = 1 + (global_shader_uniforms.buffer_size / GlobalShaderUniforms::BUFFER_DIRTY_REGION_SIZE); if (total_regions / global_shader_uniforms.buffer_dirty_region_count <= 4) { // 25% of regions dirty, just update all buffer RD::get_singleton()->buffer_update(global_shader_uniforms.buffer, 0, sizeof(GlobalShaderUniforms::Value) * global_shader_uniforms.buffer_size, global_shader_uniforms.buffer_values); diff --git a/servers/rendering/rendering_device.cpp b/servers/rendering/rendering_device.cpp index 38f1fe57bd..801ad1b825 100644 --- a/servers/rendering/rendering_device.cpp +++ b/servers/rendering/rendering_device.cpp @@ -5380,7 +5380,7 @@ Error RenderingDevice::initialize(RenderingContextDriver *p_context, DisplayServ frame = 0; frames.resize(frame_count); - max_timestamp_query_elements = 256; + max_timestamp_query_elements = GLOBAL_GET("debug/settings/profiler/max_timestamp_query_elements"); device = context->device_get(device_index); err = driver->initialize(device_index, frame_count); @@ -5507,7 +5507,7 @@ Error RenderingDevice::initialize(RenderingContextDriver *p_context, DisplayServ for (uint32_t i = 0; i < frames.size(); i++) { // Staging was never used, create a block. err = _insert_staging_block(); - ERR_CONTINUE(err != OK); + ERR_FAIL_COND_V(err, FAILED); } draw_list = nullptr; @@ -5631,7 +5631,7 @@ void RenderingDevice::_free_rids(T &p_owner, const char *p_type) { void RenderingDevice::capture_timestamp(const String &p_name) { ERR_FAIL_COND_MSG(draw_list != nullptr && draw_list->state.draw_count > 0, "Capturing timestamps during draw list creation is not allowed. Offending timestamp was: " + p_name); ERR_FAIL_COND_MSG(compute_list != nullptr && compute_list->state.dispatch_count > 0, "Capturing timestamps during compute list creation is not allowed. Offending timestamp was: " + p_name); - ERR_FAIL_COND(frames[frame].timestamp_count >= max_timestamp_query_elements); + ERR_FAIL_COND_MSG(frames[frame].timestamp_count >= max_timestamp_query_elements, vformat("Tried capturing more timestamps than the configured maximum (%d). You can increase this limit in the project settings under 'Debug/Settings' called 'Max Timestamp Query Elements'.", max_timestamp_query_elements)); draw_graph.add_capture_timestamp(frames[frame].timestamp_pool, frames[frame].timestamp_count); diff --git a/servers/rendering/rendering_device_binds.cpp b/servers/rendering/rendering_device_binds.cpp index 3678b70254..986f01a52c 100644 --- a/servers/rendering/rendering_device_binds.cpp +++ b/servers/rendering/rendering_device_binds.cpp @@ -157,9 +157,6 @@ Error RDShaderFile::parse_versions_from_text(const String &p_text, const String } } - Ref<RDShaderFile> shader_file; - shader_file.instantiate(); - if (base_error.is_empty()) { if (stage_found[RD::SHADER_STAGE_COMPUTE] && stages_found > 1) { ERR_FAIL_V_MSG(ERR_PARSE_ERROR, "When writing compute shaders, [compute] mustbe the only stage present."); diff --git a/servers/rendering/shader_language.cpp b/servers/rendering/shader_language.cpp index 1e9690a8ae..f5e0b811a2 100644 --- a/servers/rendering/shader_language.cpp +++ b/servers/rendering/shader_language.cpp @@ -1547,7 +1547,7 @@ bool ShaderLanguage::_validate_operator(OperatorNode *p_op, DataType *r_ret_type } DataType na = p_op->arguments[0]->get_datatype(); - valid = na > TYPE_BOOL && na < TYPE_MAT2; + valid = na > TYPE_BVEC4 && na < TYPE_MAT2; ret_type = na; } break; case OP_ADD: @@ -1567,7 +1567,7 @@ bool ShaderLanguage::_validate_operator(OperatorNode *p_op, DataType *r_ret_type } if (na == nb) { - valid = (na > TYPE_BOOL && na <= TYPE_MAT4); + valid = (na > TYPE_BVEC4 && na <= TYPE_MAT4); ret_type = na; } else if (na == TYPE_INT && nb == TYPE_IVEC2) { valid = true; @@ -1776,7 +1776,7 @@ bool ShaderLanguage::_validate_operator(OperatorNode *p_op, DataType *r_ret_type DataType nb = p_op->arguments[1]->get_datatype(); if (na == nb) { - valid = (na > TYPE_BOOL && na <= TYPE_MAT4); + valid = (na > TYPE_BVEC4 && na <= TYPE_MAT4); ret_type = na; } else if (na == TYPE_IVEC2 && nb == TYPE_INT) { valid = true; @@ -3982,12 +3982,9 @@ Variant ShaderLanguage::constant_value_to_variant(const Vector<ShaderLanguage::C } value = Variant(array); } else { - PackedFloat32Array array; + PackedVector4Array array; for (int i = 0; i < array_size; i += 4) { - array.push_back(p_value[i].real); - array.push_back(p_value[i + 1].real); - array.push_back(p_value[i + 2].real); - array.push_back(p_value[i + 3].real); + array.push_back(Vector4(p_value[i].real, p_value[i + 1].real, p_value[i + 2].real, p_value[i + 3].real)); } value = Variant(array); } @@ -4219,7 +4216,7 @@ PropertyInfo ShaderLanguage::uniform_to_property_info(const ShaderNode::Uniform if (p_uniform.hint == ShaderLanguage::ShaderNode::Uniform::HINT_SOURCE_COLOR) { pi.type = Variant::PACKED_COLOR_ARRAY; } else { - pi.type = Variant::PACKED_FLOAT32_ARRAY; + pi.type = Variant::PACKED_VECTOR4_ARRAY; } } else { if (p_uniform.hint == ShaderLanguage::ShaderNode::Uniform::HINT_SOURCE_COLOR) { @@ -10840,4 +10837,5 @@ ShaderLanguage::ShaderLanguage() { ShaderLanguage::~ShaderLanguage() { clear(); + global_func_set.clear(); } diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index dd3491f62c..70b585d683 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -347,6 +347,22 @@ void _get_tbn_from_axis_angle(const Vector3 &p_axis, float p_angle, Vector3 &r_n r_normal = tbn.rows[2]; } +AABB _compute_aabb_from_points(const Vector3 *p_data, int p_length) { + if (p_length == 0) { + return AABB(); + } + + Vector3 min = p_data[0]; + Vector3 max = p_data[0]; + + for (int i = 1; i < p_length; ++i) { + min = min.min(p_data[i]); + max = max.max(p_data[i]); + } + + return AABB(min, max - min); +} + Error RenderingServer::_surface_set_data(Array p_arrays, uint64_t p_format, uint32_t *p_offsets, uint32_t p_vertex_stride, uint32_t p_normal_stride, uint32_t p_attrib_stride, uint32_t p_skin_stride, Vector<uint8_t> &r_vertex_array, Vector<uint8_t> &r_attrib_array, Vector<uint8_t> &r_skin_array, int p_vertex_array_len, Vector<uint8_t> &r_index_array, int p_index_array_len, AABB &r_aabb, Vector<AABB> &r_bone_aabb, Vector4 &r_uv_scale) { uint8_t *vw = r_vertex_array.ptrw(); uint8_t *aw = r_attrib_array.ptrw(); @@ -440,18 +456,10 @@ Error RenderingServer::_surface_set_data(Array p_arrays, uint64_t p_format, uint const Vector3 *src = array.ptr(); - r_aabb = AABB(); + r_aabb = _compute_aabb_from_points(src, p_vertex_array_len); + r_aabb.size = r_aabb.size.max(SMALL_VEC3); if (p_format & ARRAY_FLAG_COMPRESS_ATTRIBUTES) { - // First we need to generate the AABB for the entire surface. - for (int i = 0; i < p_vertex_array_len; i++) { - if (i == 0) { - r_aabb = AABB(src[i], SMALL_VEC3); - } else { - r_aabb.expand_to(src[i]); - } - } - if (!(p_format & RS::ARRAY_FORMAT_NORMAL)) { // Early out if we are only setting vertex positions. for (int i = 0; i < p_vertex_array_len; i++) { @@ -592,12 +600,6 @@ Error RenderingServer::_surface_set_data(Array p_arrays, uint64_t p_format, uint float vector[3] = { (float)src[i].x, (float)src[i].y, (float)src[i].z }; memcpy(&vw[p_offsets[ai] + i * p_vertex_stride], vector, sizeof(float) * 3); - - if (i == 0) { - r_aabb = AABB(src[i], SMALL_VEC3); - } else { - r_aabb.expand_to(src[i]); - } } } } diff --git a/tests/core/math/test_vector2.h b/tests/core/math/test_vector2.h index fc3fd6a87d..7bd494ec80 100644 --- a/tests/core/math/test_vector2.h +++ b/tests/core/math/test_vector2.h @@ -353,7 +353,6 @@ TEST_CASE("[Vector2] Plane methods") { const Vector2 vector = Vector2(1.2, 3.4); const Vector2 vector_y = Vector2(0, 1); const Vector2 vector_normal = Vector2(0.95879811270838721622267, 0.2840883296913739899919); - const Vector2 vector_non_normal = Vector2(5.4, 1.6); const real_t p_d = 99.1; CHECK_MESSAGE( vector.bounce(vector_y) == Vector2(1.2, -3.4), @@ -383,6 +382,8 @@ TEST_CASE("[Vector2] Plane methods") { vector.slide(vector_normal).is_equal_approx(Vector2(-0.8292559899117276166456, 2.798738965952080706179)), "Vector2 slide with normal should return expected value."); // There's probably a better way to test these ones? +#ifdef MATH_CHECKS + const Vector2 vector_non_normal = Vector2(5.4, 1.6); ERR_PRINT_OFF; CHECK_MESSAGE( vector.bounce(vector_non_normal).is_equal_approx(Vector2()), @@ -394,6 +395,7 @@ TEST_CASE("[Vector2] Plane methods") { vector.slide(vector_non_normal).is_equal_approx(Vector2()), "Vector2 slide should return empty Vector2 with non-normalized input."); ERR_PRINT_ON; +#endif // MATH_CHECKS } TEST_CASE("[Vector2] Rounding methods") { diff --git a/tests/core/math/test_vector3.h b/tests/core/math/test_vector3.h index ca0aa02882..4cab753d6f 100644 --- a/tests/core/math/test_vector3.h +++ b/tests/core/math/test_vector3.h @@ -368,7 +368,6 @@ TEST_CASE("[Vector3] Plane methods") { const Vector3 vector = Vector3(1.2, 3.4, 5.6); const Vector3 vector_y = Vector3(0, 1, 0); const Vector3 vector_normal = Vector3(0.88763458893247992491, 0.26300284116517923701, 0.37806658417494515320); - const Vector3 vector_non_normal = Vector3(5.4, 1.6, 2.3); CHECK_MESSAGE( vector.bounce(vector_y) == Vector3(1.2, -3.4, 5.6), "Vector3 bounce on a plane with normal of the Y axis should."); @@ -394,6 +393,8 @@ TEST_CASE("[Vector3] Plane methods") { vector.slide(vector_normal).is_equal_approx(Vector3(-2.41848149148878681437, 2.32785733585517427722237, 4.0587949202918130235)), "Vector3 slide with normal should return expected value."); // There's probably a better way to test these ones? +#ifdef MATH_CHECKS + const Vector3 vector_non_normal = Vector3(5.4, 1.6, 2.3); ERR_PRINT_OFF; CHECK_MESSAGE( vector.bounce(vector_non_normal).is_equal_approx(Vector3()), @@ -405,6 +406,7 @@ TEST_CASE("[Vector3] Plane methods") { vector.slide(vector_non_normal).is_equal_approx(Vector3()), "Vector3 slide should return empty Vector3 with non-normalized input."); ERR_PRINT_ON; +#endif // MATH_CHECKS } TEST_CASE("[Vector3] Rounding methods") { diff --git a/tests/core/object/test_class_db.h b/tests/core/object/test_class_db.h index 381d759e5b..358bbc08a3 100644 --- a/tests/core/object/test_class_db.h +++ b/tests/core/object/test_class_db.h @@ -375,8 +375,10 @@ void validate_property(const Context &p_context, const ExposedClass &p_class, co } void validate_argument(const Context &p_context, const ExposedClass &p_class, const String &p_owner_name, const String &p_owner_type, const ArgumentData &p_arg) { +#ifdef DEBUG_METHODS_ENABLED TEST_COND((p_arg.name.is_empty() || p_arg.name.begins_with("_unnamed_arg")), vformat("Unnamed argument in position %d of %s '%s.%s'.", p_arg.position, p_owner_type, p_class.name, p_owner_name)); +#endif // DEBUG_METHODS_ENABLED const ExposedClass *arg_class = p_context.find_exposed_class(p_arg.type); if (arg_class) { diff --git a/tests/core/os/test_os.h b/tests/core/os/test_os.h index 6ee0ff82e7..1e2f5e222b 100644 --- a/tests/core/os/test_os.h +++ b/tests/core/os/test_os.h @@ -163,12 +163,14 @@ TEST_CASE("[OS] Processor count and memory information") { CHECK_MESSAGE( OS::get_singleton()->get_processor_count() >= 1, "The returned processor count should be greater than zero."); +#ifdef DEBUG_ENABLED CHECK_MESSAGE( OS::get_singleton()->get_static_memory_usage() >= 1, "The returned static memory usage should be greater than zero."); CHECK_MESSAGE( OS::get_singleton()->get_static_memory_peak_usage() >= 1, "The returned static memory peak usage should be greater than zero."); +#endif // DEBUG_ENABLED } TEST_CASE("[OS] Execute") { diff --git a/tests/core/variant/test_array.h b/tests/core/variant/test_array.h index c54854e4d7..787b8f39d9 100644 --- a/tests/core/variant/test_array.h +++ b/tests/core/variant/test_array.h @@ -597,6 +597,43 @@ TEST_CASE("[Array] Iteration and modification") { a4.clear(); } +TEST_CASE("[Array] Typed copying") { + TypedArray<int> a1; + a1.push_back(1); + + TypedArray<double> a2; + a2.push_back(1.0); + + Array a3 = a1; + TypedArray<int> a4 = a3; + + Array a5 = a2; + TypedArray<int> a6 = a5; + + a3[0] = 2; + a4[0] = 3; + + // Same typed TypedArray should be shared. + CHECK_EQ(a1[0], Variant(3)); + CHECK_EQ(a3[0], Variant(3)); + CHECK_EQ(a4[0], Variant(3)); + + a5[0] = 2.0; + a6[0] = 3.0; + + // Different typed TypedArray should not be shared. + CHECK_EQ(a2[0], Variant(2.0)); + CHECK_EQ(a5[0], Variant(2.0)); + CHECK_EQ(a6[0], Variant(3.0)); + + a1.clear(); + a2.clear(); + a3.clear(); + a4.clear(); + a5.clear(); + a6.clear(); +} + } // namespace TestArray #endif // TEST_ARRAY_H diff --git a/tests/display_server_mock.h b/tests/display_server_mock.h index e4946995a7..b44ff06b35 100644 --- a/tests/display_server_mock.h +++ b/tests/display_server_mock.h @@ -36,7 +36,7 @@ #include "servers/rendering/dummy/rasterizer_dummy.h" // Specialized DisplayServer for unittests based on DisplayServerHeadless, that -// additionally supports rudimentary InputEvent handling and mouse position. +// additionally supports things like mouse enter/exit events and clipboard. class DisplayServerMock : public DisplayServerHeadless { private: friend class DisplayServer; @@ -45,7 +45,6 @@ private: CursorShape cursor_shape = CursorShape::CURSOR_ARROW; bool window_over = false; Callable event_callback; - Callable input_event_callback; String clipboard_text; String primary_clipboard_text; @@ -62,16 +61,6 @@ private: return memnew(DisplayServerMock()); } - static void _dispatch_input_events(const Ref<InputEvent> &p_event) { - static_cast<DisplayServerMock *>(get_singleton())->_dispatch_input_event(p_event); - } - - void _dispatch_input_event(const Ref<InputEvent> &p_event) { - if (input_event_callback.is_valid()) { - input_event_callback.call(p_event); - } - } - void _set_mouse_position(const Point2i &p_position) { if (mouse_position == p_position) { return; @@ -153,18 +142,9 @@ public: event_callback = p_callable; } - virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override { - input_event_callback = p_callable; - } - static void register_mock_driver() { register_create_function("mock", create_func, get_rendering_drivers_func); } - - DisplayServerMock() { - Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events); - } - ~DisplayServerMock() {} }; #endif // DISPLAY_SERVER_MOCK_H diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h index 317dbe9ab9..a166002cdd 100644 --- a/tests/scene/test_code_edit.h +++ b/tests/scene/test_code_edit.h @@ -4889,6 +4889,17 @@ TEST_CASE("[SceneTree][CodeEdit] text manipulation") { CHECK(code_edit->get_caret_line() == 0); CHECK(code_edit->get_caret_column() == 1); + // Does nothing at the first line when selection ends at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(0, 0, 1, 0); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + // Works on empty line. code_edit->set_text("test\nlines\nto\n\nmove\naround"); code_edit->set_caret_line(3); @@ -4950,9 +4961,9 @@ TEST_CASE("[SceneTree][CodeEdit] text manipulation") { CHECK_FALSE(code_edit->has_selection(2)); CHECK(code_edit->get_caret_line(2) == 3); CHECK(code_edit->get_caret_column(2) == 4); + code_edit->remove_secondary_carets(); // Move multiple separate lines with multiple selections. - code_edit->remove_secondary_carets(); code_edit->set_text("test\nlines\nto\n\nmove\naround"); code_edit->select(2, 2, 1, 4); code_edit->add_caret(5, 0); @@ -4970,6 +4981,44 @@ TEST_CASE("[SceneTree][CodeEdit] text manipulation") { CHECK(code_edit->get_selection_origin_column(1) == 0); CHECK(code_edit->get_caret_line(1) == 4); CHECK(code_edit->get_caret_column(1) == 1); + code_edit->remove_secondary_carets(); + + // Move lines with adjacent selections that end at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(1, 2, 2, 0); + code_edit->add_caret(2, 2); + code_edit->select(2, 2, 3, 0, 1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "lines\nto\ntest\n\nmove\naround"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 0); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 1); + CHECK(code_edit->get_selection_origin_column(1) == 2); + CHECK(code_edit->get_caret_line(1) == 2); + CHECK(code_edit->get_caret_column(1) == 0); + code_edit->remove_secondary_carets(); + code_edit->deselect(); + + code_edit->set_line_folding_enabled(true); + + // Move line up into a folded region unfolds it. + code_edit->set_text("test\n\tline1 test\n\t\tline 2\ntest2"); + code_edit->set_caret_line(3); + code_edit->set_caret_column(0); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + code_edit->move_lines_up(); + CHECK(code_edit->get_caret_count() == 1); + CHECK_FALSE(code_edit->has_selection(0)); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 0); + CHECK(code_edit->get_text() == "test\n\tline1 test\ntest2\n\t\tline 2"); + CHECK_FALSE(code_edit->is_line_folded(0)); } SUBCASE("[SceneTree][CodeEdit] move lines down") { @@ -5004,6 +5053,17 @@ TEST_CASE("[SceneTree][CodeEdit] text manipulation") { CHECK(code_edit->get_caret_line() == 5); CHECK(code_edit->get_caret_column() == 1); + // Does nothing at the last line when selection ends at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(4, 0, 5, 0); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 4); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 5); + CHECK(code_edit->get_caret_column() == 0); + // Works on empty line. code_edit->set_text("test\nlines\nto\n\nmove\naround"); code_edit->set_caret_line(3); @@ -5085,6 +5145,64 @@ TEST_CASE("[SceneTree][CodeEdit] text manipulation") { CHECK(code_edit->get_selection_origin_column(1) == 0); CHECK(code_edit->get_caret_line(1) == 5); CHECK(code_edit->get_caret_column(1) == 2); + + // Move lines with adjacent selections that end at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(1, 2, 2, 0); + code_edit->add_caret(2, 2); + code_edit->select(2, 2, 3, 0, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\n\nlines\nto\nmove\naround"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 2); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 3); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 3); + CHECK(code_edit->get_selection_origin_column(1) == 2); + CHECK(code_edit->get_caret_line(1) == 4); + CHECK(code_edit->get_caret_column(1) == 0); + code_edit->remove_secondary_carets(); + + // Move lines with disconnected adjacent selections that end at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(0, 2, 1, 0); + code_edit->add_caret(2, 2); + code_edit->select(2, 0, 3, 0, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "lines\ntest\n\nto\nmove\naround"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 3); + CHECK(code_edit->get_selection_origin_column(1) == 0); + CHECK(code_edit->get_caret_line(1) == 4); + CHECK(code_edit->get_caret_column(1) == 0); + code_edit->remove_secondary_carets(); + code_edit->deselect(); + + code_edit->set_line_folding_enabled(true); + + // Move line down into a folded region unfolds it. + code_edit->set_text("test\ntest2\n\tline1 test\n\t\tline 2\ntest2"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(0); + code_edit->fold_line(1); + CHECK(code_edit->is_line_folded(1)); + code_edit->move_lines_down(); + CHECK(code_edit->get_caret_count() == 1); + CHECK_FALSE(code_edit->has_selection(0)); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + CHECK(code_edit->get_text() == "test2\ntest\n\tline1 test\n\t\tline 2\ntest2"); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); } SUBCASE("[SceneTree][CodeEdit] delete lines") { diff --git a/tests/scene/test_instance_placeholder.h b/tests/scene/test_instance_placeholder.h index d915c5d961..17f2151d54 100644 --- a/tests/scene/test_instance_placeholder.h +++ b/tests/scene/test_instance_placeholder.h @@ -333,6 +333,7 @@ TEST_CASE("[SceneTree][InstancePlaceholder] Instantiate from placeholder with ov } } +#ifdef TOOLS_ENABLED TEST_CASE("[SceneTree][InstancePlaceholder] Instance a PackedScene containing an InstancePlaceholder with no overrides") { GDREGISTER_CLASS(_TestInstancePlaceholderNode); @@ -526,6 +527,7 @@ TEST_CASE("[SceneTree][InstancePlaceholder] Instance a PackedScene containing an DirAccess::remove_file_or_error(internal_path); DirAccess::remove_file_or_error(main_path); } +#endif // TOOLS_ENABLED } //namespace TestInstancePlaceholder diff --git a/tests/scene/test_node.h b/tests/scene/test_node.h index 05764d8f29..e387c73f9f 100644 --- a/tests/scene/test_node.h +++ b/tests/scene/test_node.h @@ -529,6 +529,7 @@ TEST_CASE("[SceneTree][Node]Exported node checks") { memdelete(dup); } +#ifdef TOOLS_ENABLED SUBCASE("Saving instance with exported nodes should not store the unchanged property") { Ref<PackedScene> ps; ps.instantiate(); @@ -602,6 +603,7 @@ TEST_CASE("[SceneTree][Node]Exported node checks") { } CHECK_EQ(stored_properties, 2); } +#endif // TOOLS_ENABLED memdelete(node); } diff --git a/thirdparty/README.md b/thirdparty/README.md index 93469f69bb..4572687be2 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -882,7 +882,7 @@ instead of `miniz.h` as an external dependency. ## thorvg - Upstream: https://github.com/thorvg/thorvg -- Version: 0.13.7 (d2c0428a99f7305c086caffe0c730add601ebd6e, 2024) +- Version: 0.14.0 (ae4e9d003c93325f1eba64319fa9852a0d764b4c, 2024) - License: MIT Files extracted from upstream source: diff --git a/thirdparty/thorvg/AUTHORS b/thirdparty/thorvg/AUTHORS index da58481032..e00e91a696 100644 --- a/thirdparty/thorvg/AUTHORS +++ b/thirdparty/thorvg/AUTHORS @@ -1,10 +1,10 @@ -Hermet Park <hermet@lottiefiles.com>, <chuneon.park@samsung.com> +Hermet Park <hermet@lottiefiles.com> Prudhvi Raj Vasireddi <prudhvi.raj@samsung.com> Junsu Choi <jsuya.choi@samsung.com> Pranay Samanta <pranay.ks@samsung.com> Mateusz Palkowski <m.palkowski@samsung.com> Subhransu Mohanty <sub.mohanty@samsung.com> -Mira Grudzinska <veleveta@gmail.com>, <m.grudzinska@samsung.com> +Mira Grudzinska <mira@lottiefiles.com> Michal Szczecinski <m.szczecinsk@partner.samsung.com> Shinwoo Kim <cinoo.kim@samsung.com> Piotr Kalota <p.kalota@samsung.com> @@ -27,3 +27,7 @@ Jinny You <jinny@lottiefiles.com> Nattu Adnan <nattu@reallynattu.com> Gabor Kiss-Vamosi <kisvegabor@gmail.com> Lorcán Mc Donagh <lorcan@lmdsp.com> +Lucas Niu <hoiyu3twon9@gmail.com> +Francisco Ramírez <franchuti688@gmail.com> +Abdelrahman Ashraf <a.theashraf@gmail.com> +Neo Xu <neo.xu1990@gmail.com> diff --git a/thirdparty/thorvg/inc/config.h b/thirdparty/thorvg/inc/config.h index fc7f3ac36d..6218c18e68 100644 --- a/thirdparty/thorvg/inc/config.h +++ b/thirdparty/thorvg/inc/config.h @@ -15,5 +15,5 @@ // For internal debugging: //#define THORVG_LOG_ENABLED -#define THORVG_VERSION_STRING "0.13.8" +#define THORVG_VERSION_STRING "0.14.0" #endif diff --git a/thirdparty/thorvg/inc/thorvg.h b/thirdparty/thorvg/inc/thorvg.h index 8285aa1c4c..0b7e9771b9 100644 --- a/thirdparty/thorvg/inc/thorvg.h +++ b/thirdparty/thorvg/inc/thorvg.h @@ -72,6 +72,10 @@ class Animation; /** * @brief Enumeration specifying the result from the APIs. + * + * All ThorVG APIs could potentially return one of the values in the list. + * Please note that some APIs may additionally specify the reasons that trigger their return values. + * */ enum class Result { @@ -157,7 +161,7 @@ enum class CompositeMethod AlphaMask, ///< Alpha Masking using the compositing target's pixels as an alpha value. InvAlphaMask, ///< Alpha Masking using the complement to the compositing target's pixels as an alpha value. LumaMask, ///< Alpha Masking using the grayscale (0.2125R + 0.7154G + 0.0721*B) of the compositing target's pixels. @since 0.9 - InvLumaMask, ///< Alpha Masking using the grayscale (0.2125R + 0.7154G + 0.0721*B) of the complement to the compositing target's pixels. + InvLumaMask, ///< Alpha Masking using the grayscale (0.2125R + 0.7154G + 0.0721*B) of the complement to the compositing target's pixels. @since 0.11 AddMask, ///< Combines the target and source objects pixels using target alpha. (T * TA) + (S * (255 - TA)) (Experimental API) SubtractMask, ///< Subtracts the source color from the target color while considering their respective target alpha. (T * TA) - (S * (255 - TA)) (Experimental API) IntersectMask, ///< Computes the result by taking the minimum value between the target alpha and the source alpha and multiplies it with the target color. (T * min(TA, SA)) (Experimental API) @@ -277,8 +281,6 @@ public: * The rotational axis passes through the point on the object with zero coordinates. * * @param[in] degree The value of the angle in degrees. - * - * @retval Result::Success when succeed, Result::FailedAllocation otherwise. */ Result rotate(float degree) noexcept; @@ -286,8 +288,6 @@ public: * @brief Sets the scale value of the object. * * @param[in] factor The value of the scaling factor. The default value is 1. - * - * @retval Result::Success when succeed, Result::FailedAllocation otherwise. */ Result scale(float factor) noexcept; @@ -299,8 +299,6 @@ public: * * @param[in] x The value of the horizontal shift. * @param[in] y The value of the vertical shift. - * - * @retval Result::Success when succeed, Result::FailedAllocation otherwise. */ Result translate(float x, float y) noexcept; @@ -310,8 +308,6 @@ public: * The augmented matrix of the transformation is expected to be given. * * @param[in] m The 3x3 augmented matrix. - * - * @retval Result::Success when succeed, Result::FailedAllocation otherwise. */ Result transform(const Matrix& m) noexcept; @@ -332,8 +328,6 @@ public: * * @param[in] o The opacity value in the range [0 ~ 255], where 0 is completely transparent and 255 is opaque. * - * @retval Result::Success when succeed. - * * @note Setting the opacity with this API may require multiple render pass for composition. It is recommended to avoid changing the opacity if possible. * @note ClipPath won't use the opacity value. (see: enum class CompositeMethod::ClipPath) */ @@ -344,8 +338,6 @@ public: * * @param[in] target The paint of the target object. * @param[in] method The method used to composite the source object with the target. - * - * @retval Result::Success when succeed, Result::InvalidArguments otherwise. */ Result composite(std::unique_ptr<Paint> target, CompositeMethod method) noexcept; @@ -358,24 +350,11 @@ public: * * @param[in] method The blending method to be set. * - * @retval Result::Success when the blending method is successfully set. - * * @note Experimental API */ Result blend(BlendMethod method) const noexcept; /** - * @brief Gets the bounding box of the paint object before any transformation. - * - * @param[out] x The x coordinate of the upper left corner of the object. - * @param[out] y The y coordinate of the upper left corner of the object. - * @param[out] w The width of the object. - * @param[out] h The height of the object. - * - * @return Result::Success when succeed, Result::InsufficientCondition otherwise. - * - * @note The bounding box doesn't indicate the final rendered region. It's the smallest rectangle that encloses the object. - * @see Paint::bounds(float* x, float* y, float* w, float* h, bool transformed); * @deprecated Use bounds(float* x, float* y, float* w, float* h, bool transformed) instead */ TVG_DEPRECATED Result bounds(float* x, float* y, float* w, float* h) const noexcept; @@ -391,8 +370,6 @@ public: * @param[out] h The height of the object. * @param[in] transformed If @c true, the paint's transformations are taken into account, otherwise they aren't. * - * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. - * * @note The bounding box doesn't indicate the actual drawing region. It's the smallest rectangle that encloses the object. */ Result bounds(float* x, float* y, float* w, float* h, bool transformed) const noexcept; @@ -479,8 +456,6 @@ public: * * @param[in] colorStops An array of ColorStop data structure. * @param[in] cnt The count of the @p colorStops array equal to the colors number used in the gradient. - * - * @retval Result::Success when succeed. */ Result colorStops(const ColorStop* colorStops, uint32_t cnt) noexcept; @@ -488,8 +463,6 @@ public: * @brief Sets the FillSpread value, which specifies how to fill the area outside the gradient bounds. * * @param[in] s The FillSpread value. - * - * @retval Result::Success when succeed. */ Result spread(FillSpread s) noexcept; @@ -499,8 +472,6 @@ public: * The augmented matrix of the transformation is expected to be given. * * @param[in] m The 3x3 augmented matrix. - * - * @retval Result::Success when succeed, Result::FailedAllocation otherwise. */ Result transform(const Matrix& m) noexcept; @@ -567,16 +538,6 @@ public: Canvas(RenderMethod*); virtual ~Canvas(); - /** - * @brief Sets the size of the container, where all the paints pushed into the Canvas are stored. - * - * If the number of objects pushed into the Canvas is known in advance, calling the function - * prevents multiple memory reallocation, thus improving the performance. - * - * @param[in] n The number of objects for which the memory is to be reserved. - * - * @return Result::Success when succeed. - */ TVG_DEPRECATED Result reserve(uint32_t n) noexcept; /** @@ -599,9 +560,7 @@ public: * * @param[in] paint A Paint object to be drawn. * - * @retval Result::Success When succeed. * @retval Result::MemoryCorruption In case a @c nullptr is passed as the argument. - * @retval Result::InsufficientCondition An internal error. * * @note The rendering order of the paints is the same as the order as they were pushed into the canvas. Consider sorting the paints before pushing them if you intend to use layering. * @see Canvas::paints() @@ -618,7 +577,6 @@ public: * * @param[in] free If @c true, the memory occupied by paints is deallocated, otherwise it is not. * - * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. * * @see Canvas::push() * @see Canvas::paints() @@ -633,8 +591,6 @@ public: * * @param[in] paint A pointer to the Paint object or @c nullptr. * - * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. - * * @note The Update behavior can be asynchronous if the assigned thread number is greater than zero. */ virtual Result update(Paint* paint = nullptr) noexcept; @@ -642,8 +598,6 @@ public: /** * @brief Requests the canvas to draw the Paint objects. * - * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. - * * @note Drawing can be asynchronous if the assigned thread number is greater than zero. To guarantee the drawing is done, call sync() afterwards. * @see Canvas::sync() */ @@ -660,8 +614,6 @@ public: * @param[in] w The width of the rectangle. * @param[in] h The height of the rectangle. * - * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. - * * @see SwCanvas::target() * @see GlCanvas::target() * @see WgCanvas::target() @@ -679,7 +631,6 @@ public: * The Canvas rendering can be performed asynchronously. To make sure that rendering is finished, * the sync() must be called after the draw() regardless of threading. * - * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. * @see Canvas::draw() */ virtual Result sync() noexcept; @@ -713,8 +664,6 @@ public: * @param[in] x2 The horizontal coordinate of the second point used to determine the gradient bounds. * @param[in] y2 The vertical coordinate of the second point used to determine the gradient bounds. * - * @retval Result::Success when succeed. - * * @note In case the first and the second points are equal, an object filled with such a gradient fill is not rendered. */ Result linear(float x1, float y1, float x2, float y2) noexcept; @@ -730,8 +679,6 @@ public: * @param[out] y1 The vertical coordinate of the first point used to determine the gradient bounds. * @param[out] x2 The horizontal coordinate of the second point used to determine the gradient bounds. * @param[out] y2 The vertical coordinate of the second point used to determine the gradient bounds. - * - * @retval Result::Success when succeed. */ Result linear(float* x1, float* y1, float* x2, float* y2) const noexcept; @@ -775,7 +722,7 @@ public: * @param[in] cy The vertical coordinate of the center of the bounding circle. * @param[in] radius The radius of the bounding circle. * - * @retval Result::Success when succeed, Result::InvalidArguments in case the @p radius value is zero or less. + * @retval Result::InvalidArguments in case the @p radius value is zero or less. */ Result radial(float cx, float cy, float radius) noexcept; @@ -788,7 +735,6 @@ public: * @param[out] cy The vertical coordinate of the center of the bounding circle. * @param[out] radius The radius of the bounding circle. * - * @retval Result::Success when succeed. */ Result radial(float* cx, float* cy, float* radius) const noexcept; @@ -834,8 +780,6 @@ public: * * The transformation matrix, the color, the fill and the stroke properties are retained. * - * @retval Result::Success when succeed. - * * @note The memory, where the path data is stored, is not deallocated at this stage for caching effect. */ Result reset() noexcept; @@ -847,8 +791,6 @@ public: * * @param[in] x The horizontal coordinate of the initial point of the sub-path. * @param[in] y The vertical coordinate of the initial point of the sub-path. - * - * @retval Result::Success when succeed. */ Result moveTo(float x, float y) noexcept; @@ -860,8 +802,6 @@ public: * @param[in] x The horizontal coordinate of the end-point of the line. * @param[in] y The vertical coordinate of the end-point of the line. * - * @retval Result::Success when succeed. - * * @note In case this is the first command in the path, it corresponds to the moveTo() call. */ Result lineTo(float x, float y) noexcept; @@ -879,8 +819,6 @@ public: * @param[in] x The horizontal coordinate of the end-point of the curve. * @param[in] y The vertical coordinate of the end-point of the curve. * - * @retval Result::Success when succeed. - * * @note In case this is the first command in the path, no data from the path are rendered. */ Result cubicTo(float cx1, float cy1, float cx2, float cy2, float x, float y) noexcept; @@ -890,8 +828,6 @@ public: * * The value of the current point is set to the initial point of the closed sub-path. * - * @retval Result::Success when succeed. - * * @note In case the sub-path does not contain any points, this function has no effect. */ Result close() noexcept; @@ -916,8 +852,6 @@ public: * @param[in] rx The x-axis radius of the ellipse defining the rounded corners of the rectangle. * @param[in] ry The y-axis radius of the ellipse defining the rounded corners of the rectangle. * - * @retval Result::Success when succeed. - * * @note For @p rx and @p ry greater than or equal to the half of @p w and the half of @p h, respectively, the shape become an ellipse. */ Result appendRect(float x, float y, float w, float h, float rx = 0, float ry = 0) noexcept; @@ -936,7 +870,6 @@ public: * @param[in] rx The x-axis radius of the ellipse. * @param[in] ry The y-axis radius of the ellipse. * - * @retval Result::Success when succeed. */ Result appendCircle(float cx, float cy, float rx, float ry) noexcept; @@ -953,8 +886,6 @@ public: * @param[in] sweep The central angle of the arc given in degrees, measured counter-clockwise from @p startAngle. * @param[in] pie Specifies whether to draw radii from the arc's center to both of its end-point - drawn if @c true. * - * @retval Result::Success when succeed. - * * @note Setting @p sweep value greater than 360 degrees, is equivalent to calling appendCircle(cx, cy, radius, radius). */ Result appendArc(float cx, float cy, float radius, float startAngle, float sweep, bool pie) noexcept; @@ -971,8 +902,6 @@ public: * @param[in] pts The array of the two-dimensional points. * @param[in] ptsCnt The number of the points in the @p pts array. * - * @retval Result::Success when succeed, Result::InvalidArguments otherwise. - * * @note The interface is designed for optimal path setting if the caller has a completed path commands already. */ Result appendPath(const PathCommand* cmds, uint32_t cmdCnt, const Point* pts, uint32_t ptsCnt) noexcept; @@ -982,7 +911,6 @@ public: * * @param[in] width The width of the stroke. The default value is 0. * - * @retval Result::Success when succeed. */ Result stroke(float width) noexcept; @@ -994,7 +922,6 @@ public: * @param[in] b The blue color channel value in the range [0 ~ 255]. The default value is 0. * @param[in] a The alpha channel value in the range [0 ~ 255], where 0 is completely transparent and 255 is opaque. The default value is 0. * - * @retval Result::Success when succeed. */ Result stroke(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) noexcept; @@ -1003,8 +930,7 @@ public: * * @param[in] f The gradient fill. * - * @retval Result::Success When succeed. - * @retval Result::MemoryCorruption In case a @c nullptr is passed as the argument or an error with accessing it. + * @retval Result::MemoryCorruption In case a @c nullptr is passed as the argument. */ Result stroke(std::unique_ptr<Fill> f) noexcept; @@ -1014,8 +940,6 @@ public: * @param[in] dashPattern The array of consecutive pair values of the dash length and the gap length. * @param[in] cnt The length of the @p dashPattern array. * - * @retval Result::Success When succeed. - * @retval Result::FailedAllocation An internal error with a memory allocation for an object to be dashed. * @retval Result::InvalidArguments In case @p dashPattern is @c nullptr and @p cnt > 0, @p cnt is zero, any of the dash pattern values is zero or less. * * @note To reset the stroke dash pattern, pass @c nullptr to @p dashPattern and zero to @p cnt. @@ -1028,7 +952,6 @@ public: * * @param[in] cap The cap style value. The default value is @c StrokeCap::Square. * - * @retval Result::Success when succeed. */ Result stroke(StrokeCap cap) noexcept; @@ -1039,7 +962,6 @@ public: * * @param[in] join The join style value. The default value is @c StrokeJoin::Bevel. * - * @retval Result::Success when succeed. */ Result stroke(StrokeJoin join) noexcept; @@ -1048,7 +970,7 @@ public: * * @param[in] miterlimit The miterlimit imposes a limit on the extent of the stroke join, when the @c StrokeJoin::Miter join style is set. The default value is 4. * - * @retval Result::Success when succeed or Result::InvalidArgument for @p miterlimit values less than zero. + * @retval Result::InvalidArgument for @p miterlimit values less than zero. * * @since 0.11 */ @@ -1064,8 +986,6 @@ public: * @param[in] simultaneous Determines how to trim multiple paths within a single shape. If set to @c true (default), trimming is applied simultaneously to all paths; * Otherwise, all paths are treated as a single entity with a combined length equal to the sum of their individual lengths and are trimmed as such. * - * @retval Result::Success when succeed. - * * @note Experimental API */ Result strokeTrim(float begin, float end, bool simultaneous = true) noexcept; @@ -1080,8 +1000,6 @@ public: * @param[in] b The blue color channel value in the range [0 ~ 255]. The default value is 0. * @param[in] a The alpha channel value in the range [0 ~ 255], where 0 is completely transparent and 255 is opaque. The default value is 0. * - * @retval Result::Success when succeed. - * * @note Either a solid color or a gradient fill is applied, depending on what was set as last. * @note ClipPath won't use the fill values. (see: enum class CompositeMethod::ClipPath) */ @@ -1094,8 +1012,6 @@ public: * * @param[in] f The unique pointer to the gradient fill. * - * @retval Result::Success when succeed, Result::MemoryCorruption otherwise. - * * @note Either a solid color or a gradient fill is applied, depending on what was set as last. */ Result fill(std::unique_ptr<Fill> f) noexcept; @@ -1104,8 +1020,6 @@ public: * @brief Sets the fill rule for the Shape object. * * @param[in] r The fill rule value. The default value is @c FillRule::Winding. - * - * @retval Result::Success when succeed. */ Result fill(FillRule r) noexcept; @@ -1114,8 +1028,6 @@ public: * * @param[in] strokeFirst If @c true the stroke is rendered before the fill, otherwise the stroke is rendered as the second one (the default option). * - * @retval Result::Success when succeed. - * * @since 0.10 */ Result order(bool strokeFirst) noexcept; @@ -1179,7 +1091,6 @@ public: * @param[out] b The blue color channel value in the range [0 ~ 255]. * @param[out] a The alpha channel value in the range [0 ~ 255], where 0 is completely transparent and 255 is opaque. * - * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. */ Result strokeColor(uint8_t* r, uint8_t* g, uint8_t* b, uint8_t* a = nullptr) const noexcept; @@ -1228,7 +1139,7 @@ public: * @param[out] begin The starting point of the segment to display along the path. * @param[out] end Specifies the end of the segment to display along the path. * - * @retval @c true if trimming is applied simultaneously to all paths of the shape, @c false otherwise. + * @return @c true if trimming is applied simultaneously to all paths of the shape, @c false otherwise. * * @note Experimental API */ @@ -1277,10 +1188,8 @@ public: * * @param[in] path A path to the picture file. * - * @retval Result::Success When succeed. * @retval Result::InvalidArguments In case the @p path is invalid. * @retval Result::NonSupport When trying to load a file with an unknown extension. - * @retval Result::Unknown If an error occurs at a later stage. * * @note The Load behavior can be asynchronous if the assigned thread number is greater than zero. * @see Initializer::init() @@ -1288,41 +1197,26 @@ public: Result load(const std::string& path) noexcept; /** - * @brief Loads a picture data from a memory block of a given size. - * - * ThorVG efficiently caches the loaded data using the specified @p data address as a key - * when the @p copy has @c false. This means that loading the same data again will not result in duplicate operations - * for the sharable @p data. Instead, ThorVG will reuse the previously loaded picture data. - * - * @param[in] data A pointer to a memory location where the content of the picture file is stored. - * @param[in] size The size in bytes of the memory occupied by the @p data. - * @param[in] copy Decides whether the data should be copied into the engine local buffer. - * - * @retval Result::Success When succeed. - * @retval Result::InvalidArguments In case no data are provided or the @p size is zero or less. - * @retval Result::NonSupport When trying to load a file with an unknown extension. - * @retval Result::Unknown If an error occurs at a later stage. - * - * @warning: you have responsibility to release the @p data memory if the @p copy is true * @deprecated Use load(const char* data, uint32_t size, const std::string& mimeType, bool copy) instead. - * @see Result load(const char* data, uint32_t size, const std::string& mimeType, bool copy = false) noexcept */ TVG_DEPRECATED Result load(const char* data, uint32_t size, bool copy = false) noexcept; /** * @brief Loads a picture data from a memory block of a given size. * + * ThorVG efficiently caches the loaded data using the specified @p data address as a key + * when the @p copy has @c false. This means that loading the same data again will not result in duplicate operations + * for the sharable @p data. Instead, ThorVG will reuse the previously loaded picture data. + * * @param[in] data A pointer to a memory location where the content of the picture file is stored. * @param[in] size The size in bytes of the memory occupied by the @p data. * @param[in] mimeType Mimetype or extension of data such as "jpg", "jpeg", "lottie", "svg", "svg+xml", "png", etc. In case an empty string or an unknown type is provided, the loaders will be tried one by one. * @param[in] copy If @c true the data are copied into the engine local buffer, otherwise they are not. * - * @retval Result::Success When succeed. * @retval Result::InvalidArguments In case no data are provided or the @p size is zero or less. * @retval Result::NonSupport When trying to load a file with an unknown extension. - * @retval Result::Unknown If an error occurs at a later stage. * - * @warning: It's the user responsibility to release the @p data memory if the @p copy is @c true. + * @warning: It's the user responsibility to release the @p data memory. * * @note If you are unsure about the MIME type, you can provide an empty value like @c "", and thorvg will attempt to figure it out. * @since 0.5 @@ -1338,7 +1232,6 @@ public: * @param[in] w A new width of the image in pixels. * @param[in] h A new height of the image in pixels. * - * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. */ Result size(float w, float h) noexcept; @@ -1348,7 +1241,6 @@ public: * @param[out] w The width of the image in pixels. * @param[out] h The height of the image in pixels. * - * @retval Result::Success when succeed. */ Result size(float* w, float* h) const noexcept; @@ -1359,16 +1251,12 @@ public: * when the @p copy has @c false. This means that loading the same data again will not result in duplicate operations * for the sharable @p data. Instead, ThorVG will reuse the previously loaded picture data. * - * @param[in] paint A Tvg_Paint pointer to the picture object. * @param[in] data A pointer to a memory location where the content of the picture raw data is stored. * @param[in] w The width of the image @p data in pixels. * @param[in] h The height of the image @p data in pixels. * @param[in] premultiplied If @c true, the given image data is alpha-premultiplied. * @param[in] copy If @c true the data are copied into the engine local buffer, otherwise they are not. * - * @retval Result::Success When succeed, Result::InsufficientCondition otherwise. - * @retval Result::FailedAllocation An internal error possibly with memory allocation. - * * @since 0.9 */ Result load(uint32_t* data, uint32_t w, uint32_t h, bool copy) noexcept; @@ -1387,9 +1275,6 @@ public: * @param[in] triangles An array of Polygons(triangles) that make up the mesh, or null to remove the mesh. * @param[in] triangleCnt The number of Polygons(triangles) provided, or 0 to remove the mesh. * - * @retval Result::Success When succeed. - * @retval Result::Unknown If fails - * * @note The Polygons are copied internally, so modifying them after calling Mesh::mesh has no affect. * @warning Please do not use it, this API is not official one. It could be modified in the next version. * @@ -1456,24 +1341,12 @@ public: * * @param[in] paint A Paint object to be drawn. * - * @retval Result::Success when succeed, Result::MemoryCorruption otherwise. - * * @note The rendering order of the paints is the same as the order as they were pushed. Consider sorting the paints before pushing them if you intend to use layering. * @see Scene::paints() * @see Scene::clear() */ Result push(std::unique_ptr<Paint> paint) noexcept; - /** - * @brief Sets the size of the container, where all the paints pushed into the Scene are stored. - * - * If the number of objects pushed into the scene is known in advance, calling the function - * prevents multiple memory reallocation, thus improving the performance. - * - * @param[in] size The number of objects for which the memory is to be reserved. - * - * @return Result::Success when succeed, Result::FailedAllocation otherwise. - */ TVG_DEPRECATED Result reserve(uint32_t size) noexcept; /** @@ -1496,8 +1369,6 @@ public: * * @param[in] free If @c true, the memory occupied by paints is deallocated, otherwise it is not. * - * @retval Result::Success when succeed - * * @warning If you don't free the paints they become dangled. They are supposed to be reused, otherwise you are responsible for their lives. Thus please use the @p free argument only when you know how it works, otherwise it's not recommended. * * @since 0.2 @@ -1547,7 +1418,6 @@ public: * @param[in] style The style of the font. It can be used to set the font to 'italic'. * If not specified, the default style is used. Only 'italic' style is supported currently. * - * @retval Result::Success when the font properties are set successfully. * @retval Result::InsufficientCondition when the specified @p name cannot be found. * * @note Experimental API @@ -1562,8 +1432,6 @@ public: * * @param[in] text The multi-byte text encoded with utf8 string to be rendered. * - * @retval Result::Success when succeed. - * * @note Experimental API */ Result text(const char* text) noexcept; @@ -1575,7 +1443,6 @@ public: * @param[in] g The green color channel value in the range [0 ~ 255]. The default value is 0. * @param[in] b The blue color channel value in the range [0 ~ 255]. The default value is 0. * - * @retval Result::Success when succeed. * @retval Result::InsufficientCondition when the font has not been set up prior to this operation. * * @see Text::font() @@ -1591,7 +1458,6 @@ public: * * @param[in] f The unique pointer to the gradient fill. * - * @retval Result::Success when succeed, Result::MemoryCorruption otherwise. * @retval Result::InsufficientCondition when the font has not been set up prior to this operation. * * @note Either a solid color or a gradient fill is applied, depending on what was set as last. @@ -1602,7 +1468,7 @@ public: Result fill(std::unique_ptr<Fill> f) noexcept; /** - * @brief Loads a scalable font data(ttf) from a file. + * @brief Loads a scalable font data (ttf) from a file. * * ThorVG efficiently caches the loaded data using the specified @p path as a key. * This means that loading the same file again will not result in duplicate operations; @@ -1610,10 +1476,8 @@ public: * * @param[in] path The path to the font file. * - * @retval Result::Success When succeed. * @retval Result::InvalidArguments In case the @p path is invalid. * @retval Result::NonSupport When trying to load a file with an unknown extension. - * @retval Result::Unknown If an error occurs at a later stage. * * @note Experimental API * @@ -1622,13 +1486,39 @@ public: static Result load(const std::string& path) noexcept; /** + * @brief Loads a scalable font data (ttf) from a memory block of a given size. + * + * ThorVG efficiently caches the loaded font data using the specified @p name as a key. + * This means that loading the same fonts again will not result in duplicate operations. + * Instead, ThorVG will reuse the previously loaded font data. + * + * @param[in] name The name under which the font will be stored and accessible (e.x. in a @p font() API). + * @param[in] data A pointer to a memory location where the content of the font data is stored. + * @param[in] size The size in bytes of the memory occupied by the @p data. + * @param[in] mimeType Mimetype or extension of font data. In case an empty string is provided the loader will be determined automatically. + * @param[in] copy If @c true the data are copied into the engine local buffer, otherwise they are not (default). + * + * @retval Result::InvalidArguments If no name is provided or if @p size is zero while @p data points to a valid memory location. + * @retval Result::NonSupport When trying to load a file with an unsupported extension. + * @retval Result::InsufficientCondition If attempting to unload the font data that has not been previously loaded. + * + * @warning: It's the user responsibility to release the @p data memory. + * + * @note To unload the font data loaded using this API, pass the proper @p name and @c nullptr as @p data. + * @note If you are unsure about the MIME type, you can provide an empty value like @c "", and thorvg will attempt to figure it out. + * @note Experimental API + * + * @see Text::font(const char* name, float size, const char* style) + */ + static Result load(const char* name, const char* data, uint32_t size, const std::string& mimeType = "ttf", bool copy = false) noexcept; + + /** * @brief Unloads the specified scalable font data (TTF) that was previously loaded. * * This function is used to release resources associated with a font file that has been loaded into memory. * * @param[in] path The file path of the loaded font. * - * @retval Result::Success Successfully unloads the font data. * @retval Result::InsufficientCondition Fails if the loader is not initialized. * * @note If the font data is currently in use, it will not be immediately unloaded. @@ -1703,14 +1593,14 @@ public: * @param[in] h The height of the raster image. * @param[in] cs The value specifying the way the 32-bits colors should be read/written. * - * @retval Result::Success When succeed. - * @retval Result::MemoryCorruption When casting in the internal function implementation failed. * @retval Result::InvalidArguments In case no valid pointer is provided or the width, or the height or the stride is zero. + * @retval Result::InsufficientCondition if the canvas is performing rendering. Please ensure the canvas is synced. * @retval Result::NonSupport In case the software engine is not supported. * * @warning Do not access @p buffer during Canvas::push() - Canvas::sync(). It should not be accessed while the engine is writing on it. * * @see Canvas::viewport() + * @see Canvas::sync() */ Result target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h, Colorspace cs) noexcept; @@ -1726,7 +1616,6 @@ public: * * @param[in] policy The method specifying the Memory Pool behavior. The default value is @c MempoolPolicy::Default. * - * @retval Result::Success When succeed. * @retval Result::InsufficientCondition If the canvas contains some paints already. * @retval Result::NonSupport In case the software engine is not supported. * @@ -1756,7 +1645,7 @@ public: * * @warning Please do not use it. This class is not fully supported yet. * - * @note Experimental API + * @since 0.14 */ class TVG_API GlCanvas final : public Canvas { @@ -1773,10 +1662,11 @@ public: * @param[in] w The width (in pixels) of the raster image. * @param[in] h The height (in pixels) of the raster image. * - * @warning This API is experimental and not officially supported. It may be modified or removed in future versions. - * @warning Drawing on the main surface is currently not permitted. If the identifier (@p id) is set to @c 0, the operation will be aborted. + * @retval Result::InsufficientCondition if the canvas is performing rendering. Please ensure the canvas is synced. + * @retval Result::NonSupport In case the gl engine is not supported. * * @see Canvas::viewport() + * @see Canvas::sync() * * @note Currently, this only allows the GL_RGBA8 color space format. * @note Experimental API @@ -1788,7 +1678,7 @@ public: * * @return A new GlCanvas object. * - * @note Experimental API + * @since 0.14 */ static std::unique_ptr<GlCanvas> gen() noexcept; @@ -1811,14 +1701,22 @@ public: ~WgCanvas(); /** - * @brief Sets the target window for the rasterization. + * @brief Sets the drawing target for the rasterization. * - * @warning Please do not use it, this API is not official one. It could be modified in the next version. + * @param[in] instance WGPUInstance, context for all other wgpu objects. + * @param[in] surface WGPUSurface, handle to a presentable surface. + * @param[in] w The width of the surface. + * @param[in] h The height of the surface. + * + * @retval Result::InsufficientCondition if the canvas is performing rendering. Please ensure the canvas is synced. + * @retval Result::NonSupport In case the wg engine is not supported. * * @note Experimental API + * * @see Canvas::viewport() + * @see Canvas::sync() */ - Result target(void* window, uint32_t w, uint32_t h) noexcept; + Result target(void* instance, void* surface, uint32_t w, uint32_t h) noexcept; /** * @brief Creates a new WgCanvas object. @@ -1852,11 +1750,7 @@ public: * @param[in] engine The engine types to initialize. This is relative to the Canvas types, in which it will be used. For multiple backends bitwise operation is allowed. * @param[in] threads The number of additional threads. Zero indicates only the main thread is to be used. * - * @retval Result::Success When succeed. - * @retval Result::FailedAllocation An internal error possibly with memory allocation. - * @retval Result::InvalidArguments If unknown engine type chosen. * @retval Result::NonSupport In case the engine type is not supported on the system. - * @retval Result::Unknown Others. * * @note The Initializer keeps track of the number of times it was called. Threads count is fixed at the first init() call. * @see Initializer::term() @@ -1868,11 +1762,8 @@ public: * * @param[in] engine The engine types to terminate. This is relative to the Canvas types, in which it will be used. For multiple backends bitwise operation is allowed * - * @retval Result::Success When succeed. * @retval Result::InsufficientCondition In case there is nothing to be terminated. - * @retval Result::InvalidArguments If unknown engine type chosen. * @retval Result::NonSupport In case the engine type is not supported on the system. - * @retval Result::Unknown Others. * * @note Initializer does own reference counting for multiple calls. * @see Initializer::init() @@ -1903,7 +1794,6 @@ public: * * @param[in] no The index of the animation frame to be displayed. The index should be less than the totalFrame(). * - * @retval Result::Success Successfully set the frame. * @retval Result::InsufficientCondition if the given @p no is the same as the current frame value. * @retval Result::NonSupport The current Picture data does not support animations. * @@ -1975,9 +1865,7 @@ public: * @param[in] begin segment start. * @param[in] end segment end. * - * @retval Result::Success When succeed. * @retval Result::InsufficientCondition In case the animation is not loaded. - * @retval Result::InvalidArguments When the given parameter is invalid. * @retval Result::NonSupport When it's not animatable. * * @note Range from 0.0~1.0 @@ -1993,9 +1881,7 @@ public: * @param[out] begin segment start. * @param[out] end segment end. * - * @retval Result::Success When succeed. * @retval Result::InsufficientCondition In case the animation is not loaded. - * @retval Result::InvalidArguments When the given parameter is invalid. * @retval Result::NonSupport When it's not animatable. * * @note Experimental API @@ -2056,10 +1942,8 @@ public: * @param[in] path A path to the file, in which the paint data is to be saved. * @param[in] compress If @c true then compress data if possible. * - * @retval Result::Success When succeed. * @retval Result::InsufficientCondition If currently saving other resources. * @retval Result::NonSupport When trying to save a file with an unknown extension or in an unsupported format. - * @retval Result::MemoryCorruption An internal error. * @retval Result::Unknown In case an empty paint is to be saved. * * @note Saving can be asynchronous if the assigned thread number is greater than zero. To guarantee the saving is done, call sync() afterwards. @@ -2079,10 +1963,8 @@ public: * @param[in] quality The encoded quality level. @c 0 is the minimum, @c 100 is the maximum value(recommended). * @param[in] fps The desired frames per second (FPS). For example, to encode data at 60 FPS, pass 60. Pass 0 to keep the original frame data. * - * @retval Result::Success if the export succeeds. * @retval Result::InsufficientCondition if there are ongoing resource-saving operations. * @retval Result::NonSupport if an attempt is made to save the file with an unknown extension or in an unsupported format. - * @retval Result::MemoryCorruption in case of an internal error. * @retval Result::Unknown if attempting to save an empty paint. * * @note A higher frames per second (FPS) would result in a larger file size. It is recommended to use the default value. @@ -2101,9 +1983,6 @@ public: * Thus, if you wish to have a benefit of it, you must call sync() after the save() in the proper delayed time. * Otherwise, you can call sync() immediately. * - * @retval Result::Success when succeed. - * @retval Result::InsufficientCondition otherwise. - * * @note The asynchronous tasking is dependent on the Saver module implementation. * @see Saver::save() * diff --git a/thirdparty/thorvg/src/common/tvgMath.cpp b/thirdparty/thorvg/src/common/tvgMath.cpp index e6b5d47050..e99ec46681 100644 --- a/thirdparty/thorvg/src/common/tvgMath.cpp +++ b/thirdparty/thorvg/src/common/tvgMath.cpp @@ -111,3 +111,11 @@ void operator*=(Point& pt, const Matrix& m) pt.x = tx; pt.y = ty; } + + +Point operator*(const Point& pt, const Matrix& m) +{ + auto tx = pt.x * m.e11 + pt.y * m.e12 + m.e13; + auto ty = pt.x * m.e21 + pt.y * m.e22 + m.e23; + return {tx, ty}; +} diff --git a/thirdparty/thorvg/src/common/tvgMath.h b/thirdparty/thorvg/src/common/tvgMath.h index 0f877d919e..3555885c8e 100644 --- a/thirdparty/thorvg/src/common/tvgMath.h +++ b/thirdparty/thorvg/src/common/tvgMath.h @@ -152,9 +152,9 @@ static inline void operator*=(Matrix& lhs, const Matrix& rhs) } -static inline void mathLog(Matrix* m) +static inline void mathLog(const Matrix& m) { - TVGLOG("MATH", "Matrix: [%f %f %f] [%f %f %f] [%f %f %f]", m->e11, m->e12, m->e13, m->e21, m->e22, m->e23, m->e31, m->e32, m->e33); + TVGLOG("COMMON", "Matrix: [%f %f %f] [%f %f %f] [%f %f %f]", m.e11, m.e12, m.e13, m.e21, m.e22, m.e23, m.e31, m.e32, m.e33); } @@ -163,6 +163,7 @@ static inline void mathLog(Matrix* m) /************************************************************************/ void operator*=(Point& pt, const Matrix& m); +Point operator*(const Point& pt, const Matrix& m); static inline bool mathZero(const Point& p) @@ -231,6 +232,11 @@ static inline Point operator/(const Point& lhs, const float rhs) } +static inline void mathLog(const Point& pt) +{ + TVGLOG("COMMON", "Point: [%f %f]", pt.x, pt.y); +} + /************************************************************************/ /* Interpolation functions */ /************************************************************************/ diff --git a/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp b/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp index f29bc09b77..ae784ccfd7 100644 --- a/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp +++ b/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp @@ -647,9 +647,9 @@ static bool _hslToRgb(float hue, float saturation, float brightness, uint8_t* re } } - *red = static_cast<uint8_t>(roundf(_red * 255.0f)); - *green = static_cast<uint8_t>(roundf(_green * 255.0f)); - *blue = static_cast<uint8_t>(roundf(_blue * 255.0f)); + *red = static_cast<uint8_t>(ceil(_red * 255.0f)); + *green = static_cast<uint8_t>(ceil(_green * 255.0f)); + *blue = static_cast<uint8_t>(ceil(_blue * 255.0f)); return true; } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterAvx.h b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterAvx.h index cbaec28fa3..a072a88819 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterAvx.h +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterAvx.h @@ -101,47 +101,57 @@ static void avxRasterPixel32(uint32_t *dst, uint32_t val, uint32_t offset, int32 static bool avxRasterTranslucentRect(SwSurface* surface, const SwBBox& region, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { - if (surface->channelSize != sizeof(uint32_t)) { - TVGERR("SW_ENGINE", "Unsupported Channel Size = %d", surface->channelSize); - return false; - } - - auto color = surface->join(r, g, b, a); - auto buffer = surface->buf32 + (region.min.y * surface->stride) + region.min.x; auto h = static_cast<uint32_t>(region.max.y - region.min.y); auto w = static_cast<uint32_t>(region.max.x - region.min.x); - uint32_t ialpha = 255 - a; + //32bits channels + if (surface->channelSize == sizeof(uint32_t)) { + auto color = surface->join(r, g, b, a); + auto buffer = surface->buf32 + (region.min.y * surface->stride) + region.min.x; - auto avxColor = _mm_set1_epi32(color); - auto avxIalpha = _mm_set1_epi8(ialpha); + uint32_t ialpha = 255 - a; - for (uint32_t y = 0; y < h; ++y) { - auto dst = &buffer[y * surface->stride]; + auto avxColor = _mm_set1_epi32(color); + auto avxIalpha = _mm_set1_epi8(ialpha); - //1. fill the not aligned memory (for 128-bit registers a 16-bytes alignment is required) - auto notAligned = ((uintptr_t)dst & 0xf) / 4; - if (notAligned) { - notAligned = (N_32BITS_IN_128REG - notAligned > w ? w : N_32BITS_IN_128REG - notAligned); - for (uint32_t x = 0; x < notAligned; ++x, ++dst) { - *dst = color + ALPHA_BLEND(*dst, ialpha); + for (uint32_t y = 0; y < h; ++y) { + auto dst = &buffer[y * surface->stride]; + + //1. fill the not aligned memory (for 128-bit registers a 16-bytes alignment is required) + auto notAligned = ((uintptr_t)dst & 0xf) / 4; + if (notAligned) { + notAligned = (N_32BITS_IN_128REG - notAligned > w ? w : N_32BITS_IN_128REG - notAligned); + for (uint32_t x = 0; x < notAligned; ++x, ++dst) { + *dst = color + ALPHA_BLEND(*dst, ialpha); + } } - } - //2. fill the aligned memory - N_32BITS_IN_128REG pixels processed at once - uint32_t iterations = (w - notAligned) / N_32BITS_IN_128REG; - uint32_t avxFilled = iterations * N_32BITS_IN_128REG; - auto avxDst = (__m128i*)dst; - for (uint32_t x = 0; x < iterations; ++x, ++avxDst) { - *avxDst = _mm_add_epi32(avxColor, ALPHA_BLEND(*avxDst, avxIalpha)); - } + //2. fill the aligned memory - N_32BITS_IN_128REG pixels processed at once + uint32_t iterations = (w - notAligned) / N_32BITS_IN_128REG; + uint32_t avxFilled = iterations * N_32BITS_IN_128REG; + auto avxDst = (__m128i*)dst; + for (uint32_t x = 0; x < iterations; ++x, ++avxDst) { + *avxDst = _mm_add_epi32(avxColor, ALPHA_BLEND(*avxDst, avxIalpha)); + } - //3. fill the remaining pixels - int32_t leftovers = w - notAligned - avxFilled; - dst += avxFilled; - while (leftovers--) { - *dst = color + ALPHA_BLEND(*dst, ialpha); - dst++; + //3. fill the remaining pixels + int32_t leftovers = w - notAligned - avxFilled; + dst += avxFilled; + while (leftovers--) { + *dst = color + ALPHA_BLEND(*dst, ialpha); + dst++; + } + } + //8bit grayscale + } else if (surface->channelSize == sizeof(uint8_t)) { + TVGLOG("SW_ENGINE", "Require AVX Optimization, Channel Size = %d", surface->channelSize); + auto buffer = surface->buf8 + (region.min.y * surface->stride) + region.min.x; + auto ialpha = ~a; + for (uint32_t y = 0; y < h; ++y) { + auto dst = &buffer[y * surface->stride]; + for (uint32_t x = 0; x < w; ++x, ++dst) { + *dst = a + MULTIPLY(*dst, ialpha); + } } } return true; @@ -150,56 +160,68 @@ static bool avxRasterTranslucentRect(SwSurface* surface, const SwBBox& region, u static bool avxRasterTranslucentRle(SwSurface* surface, const SwRleData* rle, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { - if (surface->channelSize != sizeof(uint32_t)) { - TVGERR("SW_ENGINE", "Unsupported Channel Size = %d", surface->channelSize); - return false; - } - - auto color = surface->join(r, g, b, a); auto span = rle->spans; - uint32_t src; - for (uint32_t i = 0; i < rle->size; ++i) { - auto dst = &surface->buf32[span->y * surface->stride + span->x]; + //32bit channels + if (surface->channelSize == sizeof(uint32_t)) { + auto color = surface->join(r, g, b, a); + uint32_t src; - if (span->coverage < 255) src = ALPHA_BLEND(color, span->coverage); - else src = color; + for (uint32_t i = 0; i < rle->size; ++i) { + auto dst = &surface->buf32[span->y * surface->stride + span->x]; - auto ialpha = IA(src); + if (span->coverage < 255) src = ALPHA_BLEND(color, span->coverage); + else src = color; - //1. fill the not aligned memory (for 128-bit registers a 16-bytes alignment is required) - auto notAligned = ((uintptr_t)dst & 0xf) / 4; - if (notAligned) { - notAligned = (N_32BITS_IN_128REG - notAligned > span->len ? span->len : N_32BITS_IN_128REG - notAligned); - for (uint32_t x = 0; x < notAligned; ++x, ++dst) { - *dst = src + ALPHA_BLEND(*dst, ialpha); + auto ialpha = IA(src); + + //1. fill the not aligned memory (for 128-bit registers a 16-bytes alignment is required) + auto notAligned = ((uintptr_t)dst & 0xf) / 4; + if (notAligned) { + notAligned = (N_32BITS_IN_128REG - notAligned > span->len ? span->len : N_32BITS_IN_128REG - notAligned); + for (uint32_t x = 0; x < notAligned; ++x, ++dst) { + *dst = src + ALPHA_BLEND(*dst, ialpha); + } } - } - //2. fill the aligned memory using avx - N_32BITS_IN_128REG pixels processed at once - //In order to avoid unneccessary avx variables declarations a check is made whether there are any iterations at all - uint32_t iterations = (span->len - notAligned) / N_32BITS_IN_128REG; - uint32_t avxFilled = 0; - if (iterations > 0) { - auto avxSrc = _mm_set1_epi32(src); - auto avxIalpha = _mm_set1_epi8(ialpha); + //2. fill the aligned memory using avx - N_32BITS_IN_128REG pixels processed at once + //In order to avoid unneccessary avx variables declarations a check is made whether there are any iterations at all + uint32_t iterations = (span->len - notAligned) / N_32BITS_IN_128REG; + uint32_t avxFilled = 0; + if (iterations > 0) { + auto avxSrc = _mm_set1_epi32(src); + auto avxIalpha = _mm_set1_epi8(ialpha); + + avxFilled = iterations * N_32BITS_IN_128REG; + auto avxDst = (__m128i*)dst; + for (uint32_t x = 0; x < iterations; ++x, ++avxDst) { + *avxDst = _mm_add_epi32(avxSrc, ALPHA_BLEND(*avxDst, avxIalpha)); + } + } - avxFilled = iterations * N_32BITS_IN_128REG; - auto avxDst = (__m128i*)dst; - for (uint32_t x = 0; x < iterations; ++x, ++avxDst) { - *avxDst = _mm_add_epi32(avxSrc, ALPHA_BLEND(*avxDst, avxIalpha)); + //3. fill the remaining pixels + int32_t leftovers = span->len - notAligned - avxFilled; + dst += avxFilled; + while (leftovers--) { + *dst = src + ALPHA_BLEND(*dst, ialpha); + dst++; } - } - //3. fill the remaining pixels - int32_t leftovers = span->len - notAligned - avxFilled; - dst += avxFilled; - while (leftovers--) { - *dst = src + ALPHA_BLEND(*dst, ialpha); - dst++; + ++span; + } + //8bit grayscale + } else if (surface->channelSize == sizeof(uint8_t)) { + TVGLOG("SW_ENGINE", "Require AVX Optimization, Channel Size = %d", surface->channelSize); + uint8_t src; + for (uint32_t i = 0; i < rle->size; ++i, ++span) { + auto dst = &surface->buf8[span->y * surface->stride + span->x]; + if (span->coverage < 255) src = MULTIPLY(span->coverage, a); + else src = a; + auto ialpha = ~a; + for (uint32_t x = 0; x < span->len; ++x, ++dst) { + *dst = src + MULTIPLY(*dst, ialpha); + } } - - ++span; } return true; } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterNeon.h b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterNeon.h index 1ea6cd96cf..91cf7743c1 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterNeon.h +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterNeon.h @@ -91,44 +91,56 @@ static void neonRasterPixel32(uint32_t *dst, uint32_t val, uint32_t offset, int3 static bool neonRasterTranslucentRle(SwSurface* surface, const SwRleData* rle, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { - if (surface->channelSize != sizeof(uint32_t)) { - TVGERR("SW_ENGINE", "Unsupported Channel Size = %d", surface->channelSize); - return false; - } - - auto color = surface->join(r, g, b, a); auto span = rle->spans; - uint32_t src; - uint8x8_t *vDst = nullptr; - uint16_t align; - - for (uint32_t i = 0; i < rle->size; ++i) { - if (span->coverage < 255) src = ALPHA_BLEND(color, span->coverage); - else src = color; - - auto dst = &surface->buf32[span->y * surface->stride + span->x]; - auto ialpha = IA(src); - - if ((((uintptr_t) dst) & 0x7) != 0) { - //fill not aligned byte - *dst = src + ALPHA_BLEND(*dst, ialpha); - vDst = (uint8x8_t*)(dst + 1); - align = 1; - } else { - vDst = (uint8x8_t*) dst; - align = 0; - } - uint8x8_t vSrc = (uint8x8_t) vdup_n_u32(src); - uint8x8_t vIalpha = vdup_n_u8((uint8_t) ialpha); + //32bit channels + if (surface->channelSize == sizeof(uint32_t)) { + auto color = surface->join(r, g, b, a); + uint32_t src; + uint8x8_t *vDst = nullptr; + uint16_t align; + + for (uint32_t i = 0; i < rle->size; ++i) { + if (span->coverage < 255) src = ALPHA_BLEND(color, span->coverage); + else src = color; - for (uint32_t x = 0; x < (span->len - align) / 2; ++x) - vDst[x] = vadd_u8(vSrc, ALPHA_BLEND(vDst[x], vIalpha)); + auto dst = &surface->buf32[span->y * surface->stride + span->x]; + auto ialpha = IA(src); - auto leftovers = (span->len - align) % 2; - if (leftovers > 0) dst[span->len - 1] = src + ALPHA_BLEND(dst[span->len - 1], ialpha); + if ((((uintptr_t) dst) & 0x7) != 0) { + //fill not aligned byte + *dst = src + ALPHA_BLEND(*dst, ialpha); + vDst = (uint8x8_t*)(dst + 1); + align = 1; + } else { + vDst = (uint8x8_t*) dst; + align = 0; + } - ++span; + uint8x8_t vSrc = (uint8x8_t) vdup_n_u32(src); + uint8x8_t vIalpha = vdup_n_u8((uint8_t) ialpha); + + for (uint32_t x = 0; x < (span->len - align) / 2; ++x) + vDst[x] = vadd_u8(vSrc, ALPHA_BLEND(vDst[x], vIalpha)); + + auto leftovers = (span->len - align) % 2; + if (leftovers > 0) dst[span->len - 1] = src + ALPHA_BLEND(dst[span->len - 1], ialpha); + + ++span; + } + //8bit grayscale + } else if (surface->channelSize == sizeof(uint8_t)) { + TVGLOG("SW_ENGINE", "Require Neon Optimization, Channel Size = %d", surface->channelSize); + uint8_t src; + for (uint32_t i = 0; i < rle->size; ++i, ++span) { + auto dst = &surface->buf8[span->y * surface->stride + span->x]; + if (span->coverage < 255) src = MULTIPLY(span->coverage, a); + else src = a; + auto ialpha = ~a; + for (uint32_t x = 0; x < span->len; ++x, ++dst) { + *dst = src + MULTIPLY(*dst, ialpha); + } + } } return true; } @@ -136,41 +148,51 @@ static bool neonRasterTranslucentRle(SwSurface* surface, const SwRleData* rle, u static bool neonRasterTranslucentRect(SwSurface* surface, const SwBBox& region, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { - if (surface->channelSize != sizeof(uint32_t)) { - TVGERR("SW_ENGINE", "Unsupported Channel Size = %d", surface->channelSize); - return false; - } - - auto color = surface->join(r, g, b, a); - auto buffer = surface->buf32 + (region.min.y * surface->stride) + region.min.x; auto h = static_cast<uint32_t>(region.max.y - region.min.y); auto w = static_cast<uint32_t>(region.max.x - region.min.x); - auto ialpha = 255 - a; - auto vColor = vdup_n_u32(color); - auto vIalpha = vdup_n_u8((uint8_t) ialpha); + //32bits channels + if (surface->channelSize == sizeof(uint32_t)) { + auto color = surface->join(r, g, b, a); + auto buffer = surface->buf32 + (region.min.y * surface->stride) + region.min.x; + auto ialpha = 255 - a; - uint8x8_t* vDst = nullptr; - uint32_t align; + auto vColor = vdup_n_u32(color); + auto vIalpha = vdup_n_u8((uint8_t) ialpha); - for (uint32_t y = 0; y < h; ++y) { - auto dst = &buffer[y * surface->stride]; + uint8x8_t* vDst = nullptr; + uint32_t align; - if ((((uintptr_t) dst) & 0x7) != 0) { - //fill not aligned byte - *dst = color + ALPHA_BLEND(*dst, ialpha); - vDst = (uint8x8_t*) (dst + 1); - align = 1; - } else { - vDst = (uint8x8_t*) dst; - align = 0; - } + for (uint32_t y = 0; y < h; ++y) { + auto dst = &buffer[y * surface->stride]; - for (uint32_t x = 0; x < (w - align) / 2; ++x) - vDst[x] = vadd_u8((uint8x8_t)vColor, ALPHA_BLEND(vDst[x], vIalpha)); + if ((((uintptr_t) dst) & 0x7) != 0) { + //fill not aligned byte + *dst = color + ALPHA_BLEND(*dst, ialpha); + vDst = (uint8x8_t*) (dst + 1); + align = 1; + } else { + vDst = (uint8x8_t*) dst; + align = 0; + } - auto leftovers = (w - align) % 2; - if (leftovers > 0) dst[w - 1] = color + ALPHA_BLEND(dst[w - 1], ialpha); + for (uint32_t x = 0; x < (w - align) / 2; ++x) + vDst[x] = vadd_u8((uint8x8_t)vColor, ALPHA_BLEND(vDst[x], vIalpha)); + + auto leftovers = (w - align) % 2; + if (leftovers > 0) dst[w - 1] = color + ALPHA_BLEND(dst[w - 1], ialpha); + } + //8bit grayscale + } else if (surface->channelSize == sizeof(uint8_t)) { + TVGLOG("SW_ENGINE", "Require Neon Optimization, Channel Size = %d", surface->channelSize); + auto buffer = surface->buf8 + (region.min.y * surface->stride) + region.min.x; + auto ialpha = ~a; + for (uint32_t y = 0; y < h; ++y) { + auto dst = &buffer[y * surface->stride]; + for (uint32_t x = 0; x < w; ++x, ++dst) { + *dst = a + MULTIPLY(*dst, ialpha); + } + } } return true; } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h index bab534bba2..cfce7785c7 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h @@ -824,7 +824,7 @@ static AASpans* _AASpans(float ymin, float ymax, const SwImage* image, const SwB //Initialize X range auto height = yEnd - yStart; - aaSpans->lines = static_cast<AALine*>(calloc(height, sizeof(AALine))); + aaSpans->lines = static_cast<AALine*>(malloc(height * sizeof(AALine))); for (int32_t i = 0; i < height; i++) { aaSpans->lines[i].x[0] = INT32_MAX; @@ -878,7 +878,7 @@ static void _calcHorizCoverage(AALine *lines, int32_t eidx, int32_t y, int32_t x /* * This Anti-Aliasing mechanism is originated from Hermet Park's idea. * To understand this AA logic, you can refer this page: - * www.hermet.pe.kr/122 (hermetpark@gmail.com) + * https://uigraphics.tistory.com/1 */ static void _calcAAEdge(AASpans *aaSpans, int32_t eidx) { @@ -924,6 +924,9 @@ static void _calcAAEdge(AASpans *aaSpans, int32_t eidx) //Calculates AA Edges for (y++; y < yEnd; y++) { + + if (lines[y].x[0] == INT32_MAX) continue; + //Ready tx if (eidx == 0) { tx[0] = pEdge.x; diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp index 78108af095..0a3f5ef7e7 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp @@ -147,7 +147,7 @@ struct SwShapeTask : SwTask } } //Fill - if (flags & (RenderUpdateFlag::Gradient | RenderUpdateFlag::Transform | RenderUpdateFlag::Color)) { + if (flags & (RenderUpdateFlag::Path |RenderUpdateFlag::Gradient | RenderUpdateFlag::Transform | RenderUpdateFlag::Color)) { if (visibleFill || clipper) { if (!shapeGenRle(&shape, rshape, antialiasing(strokeWidth))) goto err; } @@ -160,7 +160,7 @@ struct SwShapeTask : SwTask } } //Stroke - if (flags & (RenderUpdateFlag::Stroke | RenderUpdateFlag::Transform)) { + if (flags & (RenderUpdateFlag::Path | RenderUpdateFlag::Stroke | RenderUpdateFlag::Transform)) { if (strokeWidth > 0.0f) { shapeResetStroke(&shape, rshape, transform); if (!shapeGenStrokeRle(&shape, rshape, transform, clipRegion, bbox, mpool, tid)) goto err; @@ -718,9 +718,6 @@ void* SwRenderer::prepareCommon(SwTask* task, const RenderTransform* transform, if (!surface) return task; if (flags == RenderUpdateFlag::None) return task; - //Finish previous task if it has duplicated request. - task->done(); - //TODO: Failed threading them. It would be better if it's possible. //See: https://github.com/thorvg/thorvg/issues/1409 //Guarantee composition targets get ready. @@ -769,8 +766,11 @@ RenderData SwRenderer::prepare(Surface* surface, const RenderMesh* mesh, RenderD //prepare task auto task = static_cast<SwImageTask*>(data); if (!task) task = new SwImageTask; + else task->done(); + task->source = surface; task->mesh = mesh; + return prepareCommon(task, transform, clips, opacity, flags); } @@ -780,6 +780,8 @@ RenderData SwRenderer::prepare(const Array<RenderData>& scene, RenderData data, //prepare task auto task = static_cast<SwSceneTask*>(data); if (!task) task = new SwSceneTask; + else task->done(); + task->scene = scene; //TODO: Failed threading them. It would be better if it's possible. @@ -788,6 +790,7 @@ RenderData SwRenderer::prepare(const Array<RenderData>& scene, RenderData data, for (auto task = scene.begin(); task < scene.end(); ++task) { static_cast<SwTask*>(*task)->done(); } + return prepareCommon(task, transform, clips, opacity, flags); } @@ -796,10 +799,10 @@ RenderData SwRenderer::prepare(const RenderShape& rshape, RenderData data, const { //prepare task auto task = static_cast<SwShapeTask*>(data); - if (!task) { - task = new SwShapeTask; - task->rshape = &rshape; - } + if (!task) task = new SwShapeTask; + else task->done(); + + task->rshape = &rshape; task->clipper = clipper; return prepareCommon(task, transform, clips, opacity, flags); diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp index 9ec4bd78a5..18f5f3eca8 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp @@ -238,7 +238,7 @@ static void _outside(SwStroke& stroke, int32_t side, SwFixed lineLength) } else { //this is a mitered (pointed) or beveled (truncated) corner auto rotate = SIDE_TO_ROTATE(side); - auto bevel = (stroke.join == StrokeJoin::Bevel) ? true : false; + auto bevel = stroke.join == StrokeJoin::Bevel; SwFixed phi = 0; SwFixed thcos = 0; @@ -816,7 +816,7 @@ void strokeReset(SwStroke* stroke, const RenderShape* rshape, const Matrix* tran stroke->width = HALF_STROKE(rshape->strokeWidth()); stroke->cap = rshape->strokeCap(); - stroke->miterlimit = static_cast<SwFixed>(rshape->strokeMiterlimit()) << 16; + stroke->miterlimit = static_cast<SwFixed>(rshape->strokeMiterlimit() * 65536.0f); //Save line join: it can be temporarily changed when stroking curves... stroke->joinSaved = stroke->join = rshape->strokeJoin(); diff --git a/thirdparty/thorvg/src/renderer/tvgCanvas.h b/thirdparty/thorvg/src/renderer/tvgCanvas.h index a4b866eacb..9d216e2f30 100644 --- a/thirdparty/thorvg/src/renderer/tvgCanvas.h +++ b/thirdparty/thorvg/src/renderer/tvgCanvas.h @@ -26,17 +26,15 @@ #include "tvgPaint.h" +enum Status : uint8_t {Synced = 0, Updating, Drawing, Damanged}; + struct Canvas::Impl { - enum Status : uint8_t {Synced = 0, Updating, Drawing}; - list<Paint*> paints; RenderMethod* renderer; RenderRegion vport = {0, 0, INT32_MAX, INT32_MAX}; Status status = Status::Synced; - bool refresh = false; //if all paints should be updated by force. - Impl(RenderMethod* pRenderer) : renderer(pRenderer) { renderer->ref(); @@ -87,18 +85,13 @@ struct Canvas::Impl return Result::Success; } - void needRefresh() - { - refresh = true; - } - Result update(Paint* paint, bool force) { if (paints.empty() || status == Status::Drawing) return Result::InsufficientCondition; Array<RenderData> clips; auto flag = RenderUpdateFlag::None; - if (refresh || force) flag = RenderUpdateFlag::All; + if (status == Status::Damanged || force) flag = RenderUpdateFlag::All; if (paint) { paint->pImpl->update(renderer, nullptr, clips, 255, flag); @@ -106,7 +99,6 @@ struct Canvas::Impl for (auto paint : paints) { paint->pImpl->update(renderer, nullptr, clips, 255, flag); } - refresh = false; } status = Status::Updating; return Result::Success; @@ -114,6 +106,7 @@ struct Canvas::Impl Result draw() { + if (status == Status::Damanged) update(nullptr, false); if (status == Status::Drawing || paints.empty() || !renderer->preRender()) return Result::InsufficientCondition; bool rendered = false; @@ -129,7 +122,7 @@ struct Canvas::Impl Result sync() { - if (status == Status::Synced) return Result::InsufficientCondition; + if (status == Status::Synced || status == Status::Damanged) return Result::InsufficientCondition; if (renderer->sync()) { status = Status::Synced; @@ -141,7 +134,8 @@ struct Canvas::Impl Result viewport(int32_t x, int32_t y, int32_t w, int32_t h) { - if (status != Status::Synced) return Result::InsufficientCondition; + if (status != Status::Damanged && status != Status::Synced) return Result::InsufficientCondition; + RenderRegion val = {x, y, w, h}; //intersect if the target buffer is already set. auto surface = renderer->mainSurface(); @@ -151,7 +145,7 @@ struct Canvas::Impl if (vport == val) return Result::Success; renderer->viewport(val); vport = val; - needRefresh(); + status = Status::Damanged; return Result::Success; } }; diff --git a/thirdparty/thorvg/src/renderer/tvgGlCanvas.cpp b/thirdparty/thorvg/src/renderer/tvgGlCanvas.cpp index 211dbb589c..82666b7ae3 100644 --- a/thirdparty/thorvg/src/renderer/tvgGlCanvas.cpp +++ b/thirdparty/thorvg/src/renderer/tvgGlCanvas.cpp @@ -45,15 +45,14 @@ struct GlCanvas::Impl /************************************************************************/ #ifdef THORVG_GL_RASTER_SUPPORT -GlCanvas::GlCanvas() : Canvas(GlRenderer::gen()), pImpl(new Impl) +GlCanvas::GlCanvas() : Canvas(GlRenderer::gen()), pImpl(nullptr) #else -GlCanvas::GlCanvas() : Canvas(nullptr), pImpl(new Impl) +GlCanvas::GlCanvas() : Canvas(nullptr), pImpl(nullptr) #endif { } - GlCanvas::~GlCanvas() { delete(pImpl); @@ -63,6 +62,10 @@ GlCanvas::~GlCanvas() Result GlCanvas::target(int32_t id, uint32_t w, uint32_t h) noexcept { #ifdef THORVG_GL_RASTER_SUPPORT + if (Canvas::pImpl->status != Status::Damanged && Canvas::pImpl->status != Status::Synced) { + return Result::InsufficientCondition; + } + //We know renderer type, avoid dynamic_cast for performance. auto renderer = static_cast<GlRenderer*>(Canvas::pImpl->renderer); if (!renderer) return Result::MemoryCorruption; @@ -72,7 +75,7 @@ Result GlCanvas::target(int32_t id, uint32_t w, uint32_t h) noexcept renderer->viewport(Canvas::pImpl->vport); //Paints must be updated again with this new target. - Canvas::pImpl->needRefresh(); + Canvas::pImpl->status = Status::Damanged; return Result::Success; #endif diff --git a/thirdparty/thorvg/src/renderer/tvgLoader.cpp b/thirdparty/thorvg/src/renderer/tvgLoader.cpp index 4b8d3256a9..6a81ddcdbb 100644 --- a/thirdparty/thorvg/src/renderer/tvgLoader.cpp +++ b/thirdparty/thorvg/src/renderer/tvgLoader.cpp @@ -433,3 +433,27 @@ LoadModule* LoaderMgr::loader(const uint32_t *data, uint32_t w, uint32_t h, bool delete(loader); return nullptr; } + + +//loads fonts from memory - loader is cached (regardless of copy value) in order to access it while setting font +LoadModule* LoaderMgr::loader(const char* name, const char* data, uint32_t size, TVG_UNUSED const string& mimeType, bool copy) +{ +#ifdef THORVG_TTF_LOADER_SUPPORT + //TODO: add check for mimetype ? + if (auto loader = _findFromCache(name)) return loader; + + //function is dedicated for ttf loader (the only supported font loader) + auto loader = new TtfLoader; + if (loader->open(data, size, copy)) { + loader->hashpath = strdup(name); + loader->pathcache = true; + ScopedLock lock(key); + _activeLoaders.back(loader); + return loader; + } + + TVGLOG("LOADER", "The font data \"%s\" could not be loaded.", name); + delete(loader); +#endif + return nullptr; +} diff --git a/thirdparty/thorvg/src/renderer/tvgLoader.h b/thirdparty/thorvg/src/renderer/tvgLoader.h index b15032df27..74c4f43964 100644 --- a/thirdparty/thorvg/src/renderer/tvgLoader.h +++ b/thirdparty/thorvg/src/renderer/tvgLoader.h @@ -32,6 +32,7 @@ struct LoaderMgr static LoadModule* loader(const string& path, bool* invalid); static LoadModule* loader(const char* data, uint32_t size, const string& mimeType, bool copy); static LoadModule* loader(const uint32_t* data, uint32_t w, uint32_t h, bool copy); + static LoadModule* loader(const char* name, const char* data, uint32_t size, const string& mimeType, bool copy); static LoadModule* loader(const char* key); static bool retrieve(const string& path); static bool retrieve(LoadModule* loader); diff --git a/thirdparty/thorvg/src/renderer/tvgShape.cpp b/thirdparty/thorvg/src/renderer/tvgShape.cpp index c010aa7bbf..3b9293a00e 100644 --- a/thirdparty/thorvg/src/renderer/tvgShape.cpp +++ b/thirdparty/thorvg/src/renderer/tvgShape.cpp @@ -88,6 +88,8 @@ Result Shape::appendPath(const PathCommand *cmds, uint32_t cmdCnt, const Point* pImpl->grow(cmdCnt, ptsCnt); pImpl->append(cmds, cmdCnt, pts, ptsCnt); + pImpl->flag |= RenderUpdateFlag::Path; + return Result::Success; } @@ -104,6 +106,8 @@ Result Shape::lineTo(float x, float y) noexcept { pImpl->lineTo(x, y); + pImpl->flag |= RenderUpdateFlag::Path; + return Result::Success; } @@ -112,6 +116,8 @@ Result Shape::cubicTo(float cx1, float cy1, float cx2, float cy2, float x, float { pImpl->cubicTo(cx1, cy1, cx2, cy2, x, y); + pImpl->flag |= RenderUpdateFlag::Path; + return Result::Success; } @@ -120,6 +126,8 @@ Result Shape::close() noexcept { pImpl->close(); + pImpl->flag |= RenderUpdateFlag::Path; + return Result::Success; } @@ -137,9 +145,12 @@ Result Shape::appendCircle(float cx, float cy, float rx, float ry) noexcept pImpl->cubicTo(cx + rxKappa, cy - ry, cx + rx, cy - ryKappa, cx + rx, cy); pImpl->close(); + pImpl->flag |= RenderUpdateFlag::Path; + return Result::Success; } + Result Shape::appendArc(float cx, float cy, float radius, float startAngle, float sweep, bool pie) noexcept { //just circle @@ -196,6 +207,8 @@ Result Shape::appendArc(float cx, float cy, float radius, float startAngle, floa if (pie) pImpl->close(); + pImpl->flag |= RenderUpdateFlag::Path; + return Result::Success; } @@ -234,6 +247,8 @@ Result Shape::appendRect(float x, float y, float w, float h, float rx, float ry) pImpl->close(); } + pImpl->flag |= RenderUpdateFlag::Path; + return Result::Success; } diff --git a/thirdparty/thorvg/src/renderer/tvgShape.h b/thirdparty/thorvg/src/renderer/tvgShape.h index 4e85db37d0..c45995a64d 100644 --- a/thirdparty/thorvg/src/renderer/tvgShape.h +++ b/thirdparty/thorvg/src/renderer/tvgShape.h @@ -167,24 +167,18 @@ struct Shape::Impl memcpy(rs.path.pts.end(), pts, sizeof(Point) * ptsCnt); rs.path.cmds.count += cmdCnt; rs.path.pts.count += ptsCnt; - - flag |= RenderUpdateFlag::Path; } void moveTo(float x, float y) { rs.path.cmds.push(PathCommand::MoveTo); rs.path.pts.push({x, y}); - - flag |= RenderUpdateFlag::Path; } void lineTo(float x, float y) { rs.path.cmds.push(PathCommand::LineTo); rs.path.pts.push({x, y}); - - flag |= RenderUpdateFlag::Path; } void cubicTo(float cx1, float cy1, float cx2, float cy2, float x, float y) @@ -193,8 +187,6 @@ struct Shape::Impl rs.path.pts.push({cx1, cy1}); rs.path.pts.push({cx2, cy2}); rs.path.pts.push({x, y}); - - flag |= RenderUpdateFlag::Path; } void close() @@ -203,8 +195,6 @@ struct Shape::Impl if (rs.path.cmds.count > 0 && rs.path.cmds.last() == PathCommand::Close) return; rs.path.cmds.push(PathCommand::Close); - - flag |= RenderUpdateFlag::Path; } void strokeWidth(float width) diff --git a/thirdparty/thorvg/src/renderer/tvgSwCanvas.cpp b/thirdparty/thorvg/src/renderer/tvgSwCanvas.cpp index 52d85d8320..d762492f22 100644 --- a/thirdparty/thorvg/src/renderer/tvgSwCanvas.cpp +++ b/thirdparty/thorvg/src/renderer/tvgSwCanvas.cpp @@ -46,9 +46,9 @@ struct SwCanvas::Impl /************************************************************************/ #ifdef THORVG_SW_RASTER_SUPPORT -SwCanvas::SwCanvas() : Canvas(SwRenderer::gen()), pImpl(new Impl) +SwCanvas::SwCanvas() : Canvas(SwRenderer::gen()), pImpl(nullptr) #else -SwCanvas::SwCanvas() : Canvas(nullptr), pImpl(new Impl) +SwCanvas::SwCanvas() : Canvas(nullptr), pImpl(nullptr) #endif { } @@ -82,6 +82,10 @@ Result SwCanvas::mempool(MempoolPolicy policy) noexcept Result SwCanvas::target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h, Colorspace cs) noexcept { #ifdef THORVG_SW_RASTER_SUPPORT + if (Canvas::pImpl->status != Status::Damanged && Canvas::pImpl->status != Status::Synced) { + return Result::InsufficientCondition; + } + //We know renderer type, avoid dynamic_cast for performance. auto renderer = static_cast<SwRenderer*>(Canvas::pImpl->renderer); if (!renderer) return Result::MemoryCorruption; @@ -90,12 +94,12 @@ Result SwCanvas::target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t Canvas::pImpl->vport = {0, 0, (int32_t)w, (int32_t)h}; renderer->viewport(Canvas::pImpl->vport); - //Paints must be updated again with this new target. - Canvas::pImpl->needRefresh(); - //FIXME: The value must be associated with an individual canvas instance. ImageLoader::cs = static_cast<ColorSpace>(cs); + //Paints must be updated again with this new target. + Canvas::pImpl->status = Status::Damanged; + return Result::Success; #endif return Result::NonSupport; diff --git a/thirdparty/thorvg/src/renderer/tvgText.cpp b/thirdparty/thorvg/src/renderer/tvgText.cpp index 1fe244c11d..4b5eb35ce5 100644 --- a/thirdparty/thorvg/src/renderer/tvgText.cpp +++ b/thirdparty/thorvg/src/renderer/tvgText.cpp @@ -71,6 +71,21 @@ Result Text::load(const std::string& path) noexcept } +Result Text::load(const char* name, const char* data, uint32_t size, const string& mimeType, bool copy) noexcept +{ + if (!name || (size == 0 && data)) return Result::InvalidArguments; + + //unload font + if (!data) { + if (LoaderMgr::retrieve(name)) return Result::Success; + return Result::InsufficientCondition; + } + + if (!LoaderMgr::loader(name, data, size, mimeType, copy)) return Result::NonSupport; + return Result::Success; +} + + Result Text::unload(const std::string& path) noexcept { if (LoaderMgr::retrieve(path)) return Result::Success; diff --git a/thirdparty/thorvg/src/renderer/tvgText.h b/thirdparty/thorvg/src/renderer/tvgText.h index f4fb12259a..c56ce8b878 100644 --- a/thirdparty/thorvg/src/renderer/tvgText.h +++ b/thirdparty/thorvg/src/renderer/tvgText.h @@ -100,7 +100,7 @@ struct Text::Impl bool render(RenderMethod* renderer) { if (paint) return PP(paint)->render(renderer); - return false; + return true; } bool load() diff --git a/thirdparty/thorvg/src/renderer/tvgWgCanvas.cpp b/thirdparty/thorvg/src/renderer/tvgWgCanvas.cpp index 7db77f6d0c..067e35b1f0 100644 --- a/thirdparty/thorvg/src/renderer/tvgWgCanvas.cpp +++ b/thirdparty/thorvg/src/renderer/tvgWgCanvas.cpp @@ -47,33 +47,39 @@ WgCanvas::WgCanvas() : Canvas(nullptr), pImpl(nullptr) { } + WgCanvas::~WgCanvas() { delete pImpl; } -Result WgCanvas::target(void* window, uint32_t w, uint32_t h) noexcept + +Result WgCanvas::target(void* instance, void* surface, uint32_t w, uint32_t h) noexcept { #ifdef THORVG_WG_RASTER_SUPPORT - if (!window) return Result::InvalidArguments; - if ((w == 0) || (h == 0)) return Result::InvalidArguments; + if (Canvas::pImpl->status != Status::Damanged && Canvas::pImpl->status != Status::Synced) { + return Result::InsufficientCondition; + } + + if (!instance || !surface || (w == 0) || (h == 0)) return Result::InvalidArguments; //We know renderer type, avoid dynamic_cast for performance. auto renderer = static_cast<WgRenderer*>(Canvas::pImpl->renderer); if (!renderer) return Result::MemoryCorruption; - if (!renderer->target(window, w, h)) return Result::Unknown; + if (!renderer->target((WGPUInstance)instance, (WGPUSurface)surface, w, h)) return Result::Unknown; Canvas::pImpl->vport = {0, 0, (int32_t)w, (int32_t)h}; renderer->viewport(Canvas::pImpl->vport); //Paints must be updated again with this new target. - Canvas::pImpl->needRefresh(); + Canvas::pImpl->status = Status::Damanged; return Result::Success; #endif return Result::NonSupport; } + unique_ptr<WgCanvas> WgCanvas::gen() noexcept { #ifdef THORVG_WG_RASTER_SUPPORT diff --git a/thirdparty/thorvg/update-thorvg.sh b/thirdparty/thorvg/update-thorvg.sh index 663d685d01..d08158bfe7 100755 --- a/thirdparty/thorvg/update-thorvg.sh +++ b/thirdparty/thorvg/update-thorvg.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -VERSION=0.13.8 +VERSION=0.14.0 cd thirdparty/thorvg/ || true rm -rf AUTHORS LICENSE inc/ src/ *.zip *.tar.gz tmp/ |