diff options
198 files changed, 3790 insertions, 2086 deletions
diff --git a/.github/workflows/android_builds.yml b/.github/workflows/android_builds.yml index 68e5575b3b..5cb66a40ab 100644 --- a/.github/workflows/android_builds.yml +++ b/.github/workflows/android_builds.yml @@ -19,6 +19,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Java 17 uses: actions/setup-java@v4 diff --git a/.github/workflows/godot_cpp_test.yml b/.github/workflows/godot_cpp_test.yml index f352afbc6b..7350920810 100644 --- a/.github/workflows/godot_cpp_test.yml +++ b/.github/workflows/godot_cpp_test.yml @@ -19,6 +19,8 @@ jobs: name: "Build and test Godot CPP" steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup python and scons uses: ./.github/actions/godot-deps diff --git a/.github/workflows/ios_builds.yml b/.github/workflows/ios_builds.yml index 6557120d87..e205d551ed 100644 --- a/.github/workflows/ios_builds.yml +++ b/.github/workflows/ios_builds.yml @@ -19,6 +19,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup Godot build cache uses: ./.github/actions/godot-cache diff --git a/.github/workflows/linux_builds.yml b/.github/workflows/linux_builds.yml index 4cc6d92fb8..0420a02b1d 100644 --- a/.github/workflows/linux_builds.yml +++ b/.github/workflows/linux_builds.yml @@ -86,6 +86,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive # Need newer mesa for lavapipe to work properly. - name: Linux dependencies for tests diff --git a/.github/workflows/macos_builds.yml b/.github/workflows/macos_builds.yml index c245077175..4967fe379f 100644 --- a/.github/workflows/macos_builds.yml +++ b/.github/workflows/macos_builds.yml @@ -34,6 +34,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup Godot build cache uses: ./.github/actions/godot-cache diff --git a/.github/workflows/web_builds.yml b/.github/workflows/web_builds.yml index a1b7f95cec..47f7e4d458 100644 --- a/.github/workflows/web_builds.yml +++ b/.github/workflows/web_builds.yml @@ -38,6 +38,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Emscripten latest uses: mymindstorm/setup-emsdk@v14 diff --git a/.github/workflows/windows_builds.yml b/.github/workflows/windows_builds.yml index 18ed92b57f..5443ba20ab 100644 --- a/.github/workflows/windows_builds.yml +++ b/.github/workflows/windows_builds.yml @@ -39,6 +39,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup Godot build cache uses: ./.github/actions/godot-cache diff --git a/core/input/input_event.cpp b/core/input/input_event.cpp index bd1fde5a85..bf1de8d3b2 100644 --- a/core/input/input_event.cpp +++ b/core/input/input_event.cpp @@ -132,6 +132,8 @@ void InputEvent::_bind_methods() { ClassDB::bind_method(D_METHOD("xformed_by", "xform", "local_ofs"), &InputEvent::xformed_by, DEFVAL(Vector2())); ADD_PROPERTY(PropertyInfo(Variant::INT, "device"), "set_device", "get_device"); + + BIND_CONSTANT(DEVICE_ID_EMULATION); } /////////////////////////////////// diff --git a/core/io/resource.cpp b/core/io/resource.cpp index 5edb045760..dc974a545a 100644 --- a/core/io/resource.cpp +++ b/core/io/resource.cpp @@ -214,6 +214,7 @@ Error Resource::copy_from(const Ref<Resource> &p_resource) { } return OK; } + void Resource::reload_from_file() { String path = get_path(); if (!path.is_resource_file()) { diff --git a/core/math/aabb.h b/core/math/aabb.h index 7927c431eb..48a883e64c 100644 --- a/core/math/aabb.h +++ b/core/math/aabb.h @@ -101,7 +101,7 @@ struct _NO_DISCARD_ AABB { _FORCE_INLINE_ void expand_to(const Vector3 &p_vector); /** expand to contain a point if necessary */ _FORCE_INLINE_ AABB abs() const { - return AABB(Vector3(position.x + MIN(size.x, (real_t)0), position.y + MIN(size.y, (real_t)0), position.z + MIN(size.z, (real_t)0)), size.abs()); + return AABB(position + size.min(Vector3()), size.abs()); } Variant intersects_segment_bind(const Vector3 &p_from, const Vector3 &p_to) const; diff --git a/core/math/delaunay_3d.h b/core/math/delaunay_3d.h index 7df8c37e3c..846acdecc3 100644 --- a/core/math/delaunay_3d.h +++ b/core/math/delaunay_3d.h @@ -281,9 +281,7 @@ public: } Vector3i grid_pos = Vector3i(points[i] * ACCEL_GRID_SIZE); - grid_pos.x = CLAMP(grid_pos.x, 0, ACCEL_GRID_SIZE - 1); - grid_pos.y = CLAMP(grid_pos.y, 0, ACCEL_GRID_SIZE - 1); - grid_pos.z = CLAMP(grid_pos.z, 0, ACCEL_GRID_SIZE - 1); + grid_pos = grid_pos.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1)); for (List<Simplex *>::Element *E = acceleration_grid[grid_pos.x][grid_pos.y][grid_pos.z].front(); E;) { List<Simplex *>::Element *N = E->next(); //may be deleted @@ -339,12 +337,8 @@ public: Vector3 extents = Vector3(radius2, radius2, radius2); Vector3i from = Vector3i((center - extents) * ACCEL_GRID_SIZE); Vector3i to = Vector3i((center + extents) * ACCEL_GRID_SIZE); - from.x = CLAMP(from.x, 0, ACCEL_GRID_SIZE - 1); - from.y = CLAMP(from.y, 0, ACCEL_GRID_SIZE - 1); - from.z = CLAMP(from.z, 0, ACCEL_GRID_SIZE - 1); - to.x = CLAMP(to.x, 0, ACCEL_GRID_SIZE - 1); - to.y = CLAMP(to.y, 0, ACCEL_GRID_SIZE - 1); - to.z = CLAMP(to.z, 0, ACCEL_GRID_SIZE - 1); + from = from.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1)); + to = to.clamp(Vector3i(), Vector3i(ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1, ACCEL_GRID_SIZE - 1)); for (int32_t x = from.x; x <= to.x; x++) { for (int32_t y = from.y; y <= to.y; y++) { diff --git a/core/math/dynamic_bvh.h b/core/math/dynamic_bvh.h index f586b845c3..26fc517f7f 100644 --- a/core/math/dynamic_bvh.h +++ b/core/math/dynamic_bvh.h @@ -376,13 +376,8 @@ void DynamicBVH::convex_query(const Plane *p_planes, int p_plane_count, const Ve volume.min = p_points[0]; volume.max = p_points[0]; } else { - volume.min.x = MIN(volume.min.x, p_points[i].x); - volume.min.y = MIN(volume.min.y, p_points[i].y); - volume.min.z = MIN(volume.min.z, p_points[i].z); - - volume.max.x = MAX(volume.max.x, p_points[i].x); - volume.max.y = MAX(volume.max.y, p_points[i].y); - volume.max.z = MAX(volume.max.z, p_points[i].z); + volume.min = volume.min.min(p_points[i]); + volume.max = volume.max.max(p_points[i]); } } diff --git a/core/math/geometry_2d.h b/core/math/geometry_2d.h index fbcaa018a8..1502b2807c 100644 --- a/core/math/geometry_2d.h +++ b/core/math/geometry_2d.h @@ -377,10 +377,8 @@ public: Vector2 further_away_opposite(1e20, 1e20); for (int i = 0; i < c; i++) { - further_away.x = MAX(p[i].x, further_away.x); - further_away.y = MAX(p[i].y, further_away.y); - further_away_opposite.x = MIN(p[i].x, further_away_opposite.x); - further_away_opposite.y = MIN(p[i].y, further_away_opposite.y); + further_away = further_away.max(p[i]); + further_away_opposite = further_away_opposite.min(p[i]); } // Make point outside that won't intersect with points in segment from p_point. diff --git a/core/math/rect2.h b/core/math/rect2.h index 0f874d4857..497ed8cf04 100644 --- a/core/math/rect2.h +++ b/core/math/rect2.h @@ -152,14 +152,12 @@ struct _NO_DISCARD_ Rect2 { return Rect2(); } - new_rect.position.x = MAX(p_rect.position.x, position.x); - new_rect.position.y = MAX(p_rect.position.y, position.y); + new_rect.position = p_rect.position.max(position); Point2 p_rect_end = p_rect.position + p_rect.size; Point2 end = position + size; - new_rect.size.x = MIN(p_rect_end.x, end.x) - new_rect.position.x; - new_rect.size.y = MIN(p_rect_end.y, end.y) - new_rect.position.y; + new_rect.size = p_rect_end.min(end) - new_rect.position; return new_rect; } @@ -172,11 +170,9 @@ struct _NO_DISCARD_ Rect2 { #endif Rect2 new_rect; - new_rect.position.x = MIN(p_rect.position.x, position.x); - new_rect.position.y = MIN(p_rect.position.y, position.y); + new_rect.position = p_rect.position.min(position); - new_rect.size.x = MAX(p_rect.position.x + p_rect.size.x, position.x + size.x); - new_rect.size.y = MAX(p_rect.position.y + p_rect.size.y, position.y + size.y); + new_rect.size = (p_rect.position + p_rect.size).max(position + size); new_rect.size = new_rect.size - new_rect.position; // Make relative again. @@ -282,7 +278,7 @@ struct _NO_DISCARD_ Rect2 { } _FORCE_INLINE_ Rect2 abs() const { - return Rect2(Point2(position.x + MIN(size.x, (real_t)0), position.y + MIN(size.y, (real_t)0)), size.abs()); + return Rect2(position + size.min(Point2()), size.abs()); } _FORCE_INLINE_ Rect2 round() const { diff --git a/core/math/rect2i.h b/core/math/rect2i.h index 205b2c7198..64806414c7 100644 --- a/core/math/rect2i.h +++ b/core/math/rect2i.h @@ -95,14 +95,12 @@ struct _NO_DISCARD_ Rect2i { return Rect2i(); } - new_rect.position.x = MAX(p_rect.position.x, position.x); - new_rect.position.y = MAX(p_rect.position.y, position.y); + new_rect.position = p_rect.position.max(position); Point2i p_rect_end = p_rect.position + p_rect.size; Point2i end = position + size; - new_rect.size.x = MIN(p_rect_end.x, end.x) - new_rect.position.x; - new_rect.size.y = MIN(p_rect_end.y, end.y) - new_rect.position.y; + new_rect.size = p_rect_end.min(end) - new_rect.position; return new_rect; } @@ -115,11 +113,9 @@ struct _NO_DISCARD_ Rect2i { #endif Rect2i new_rect; - new_rect.position.x = MIN(p_rect.position.x, position.x); - new_rect.position.y = MIN(p_rect.position.y, position.y); + new_rect.position = p_rect.position.min(position); - new_rect.size.x = MAX(p_rect.position.x + p_rect.size.x, position.x + size.x); - new_rect.size.y = MAX(p_rect.position.y + p_rect.size.y, position.y + size.y); + new_rect.size = (p_rect.position + p_rect.size).max(position + size); new_rect.size = new_rect.size - new_rect.position; // Make relative again. @@ -217,7 +213,7 @@ struct _NO_DISCARD_ Rect2i { } _FORCE_INLINE_ Rect2i abs() const { - return Rect2i(Point2i(position.x + MIN(size.x, 0), position.y + MIN(size.y, 0)), size.abs()); + return Rect2i(position + size.min(Point2i()), size.abs()); } _FORCE_INLINE_ void set_end(const Vector2i &p_end) { diff --git a/core/math/transform_interpolator.cpp b/core/math/transform_interpolator.cpp new file mode 100644 index 0000000000..7cfe880b5a --- /dev/null +++ b/core/math/transform_interpolator.cpp @@ -0,0 +1,76 @@ +/**************************************************************************/ +/* transform_interpolator.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "transform_interpolator.h" + +#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)); + 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)); +} diff --git a/core/math/transform_interpolator.h b/core/math/transform_interpolator.h new file mode 100644 index 0000000000..a9bce2bd7f --- /dev/null +++ b/core/math/transform_interpolator.h @@ -0,0 +1,46 @@ +/**************************************************************************/ +/* transform_interpolator.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TRANSFORM_INTERPOLATOR_H +#define TRANSFORM_INTERPOLATOR_H + +#include "core/math/math_defs.h" + +struct Transform2D; + +class TransformInterpolator { +private: + static bool _sign(real_t p_val) { return p_val >= 0; } + +public: + static void interpolate_transform_2d(const Transform2D &p_prev, const Transform2D &p_curr, Transform2D &r_result, real_t p_fraction); +}; + +#endif // TRANSFORM_INTERPOLATOR_H diff --git a/core/object/object.cpp b/core/object/object.cpp index e0a1ddcce0..8b6fd587e0 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -1100,11 +1100,6 @@ bool Object::_has_user_signal(const StringName &p_name) const { return signal_map[p_name].user.name.length() > 0; } -struct _ObjectSignalDisconnectData { - StringName signal; - Callable callable; -}; - Error Object::_emit_signal(const Variant **p_args, int p_argcount, Callable::CallError &r_error) { if (unlikely(p_argcount < 1)) { r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; @@ -1153,26 +1148,43 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int // which is needed in certain edge cases; e.g., https://github.com/godotengine/godot/issues/73889. Ref<RefCounted> rc = Ref<RefCounted>(Object::cast_to<RefCounted>(this)); - List<_ObjectSignalDisconnectData> disconnect_data; - // Ensure that disconnecting the signal or even deleting the object // will not affect the signal calling. - LocalVector<Connection> slot_conns; - slot_conns.resize(s->slot_map.size()); - { - uint32_t idx = 0; - for (const KeyValue<Callable, SignalData::Slot> &slot_kv : s->slot_map) { - slot_conns[idx++] = slot_kv.value.conn; + Callable *slot_callables = (Callable *)alloca(sizeof(Callable) * s->slot_map.size()); + uint32_t *slot_flags = (uint32_t *)alloca(sizeof(uint32_t) * s->slot_map.size()); + uint32_t slot_count = 0; + + for (const KeyValue<Callable, SignalData::Slot> &slot_kv : s->slot_map) { + memnew_placement(&slot_callables[slot_count], Callable(slot_kv.value.conn.callable)); + slot_flags[slot_count] = slot_kv.value.conn.flags; + ++slot_count; + } + + DEV_ASSERT(slot_count == s->slot_map.size()); + + // Disconnect all one-shot connections before emitting to prevent recursion. + for (uint32_t i = 0; i < slot_count; ++i) { + bool disconnect = slot_flags[i] & CONNECT_ONE_SHOT; +#ifdef TOOLS_ENABLED + if (disconnect && (slot_flags[i] & CONNECT_PERSIST) && Engine::get_singleton()->is_editor_hint()) { + // This signal was connected from the editor, and is being edited. Just don't disconnect for now. + disconnect = false; + } +#endif + if (disconnect) { + _disconnect(p_name, slot_callables[i]); } - DEV_ASSERT(idx == s->slot_map.size()); } OBJ_DEBUG_LOCK Error err = OK; - for (const Connection &c : slot_conns) { - if (!c.callable.is_valid()) { + for (uint32_t i = 0; i < slot_count; ++i) { + const Callable &callable = slot_callables[i]; + const uint32_t &flags = slot_flags[i]; + + if (!callable.is_valid()) { // Target might have been deleted during signal callback, this is expected and OK. continue; } @@ -1180,51 +1192,34 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int const Variant **args = p_args; int argc = p_argcount; - if (c.flags & CONNECT_DEFERRED) { - MessageQueue::get_singleton()->push_callablep(c.callable, args, argc, true); + if (flags & CONNECT_DEFERRED) { + MessageQueue::get_singleton()->push_callablep(callable, args, argc, true); } else { Callable::CallError ce; _emitting = true; Variant ret; - c.callable.callp(args, argc, ret, ce); + callable.callp(args, argc, ret, ce); _emitting = false; if (ce.error != Callable::CallError::CALL_OK) { #ifdef DEBUG_ENABLED - if (c.flags & CONNECT_PERSIST && Engine::get_singleton()->is_editor_hint() && (script.is_null() || !Ref<Script>(script)->is_tool())) { + if (flags & CONNECT_PERSIST && Engine::get_singleton()->is_editor_hint() && (script.is_null() || !Ref<Script>(script)->is_tool())) { continue; } #endif - Object *target = c.callable.get_object(); + Object *target = callable.get_object(); if (ce.error == Callable::CallError::CALL_ERROR_INVALID_METHOD && target && !ClassDB::class_exists(target->get_class_name())) { //most likely object is not initialized yet, do not throw error. } else { - ERR_PRINT("Error calling from signal '" + String(p_name) + "' to callable: " + Variant::get_callable_error_text(c.callable, args, argc, ce) + "."); + ERR_PRINT("Error calling from signal '" + String(p_name) + "' to callable: " + Variant::get_callable_error_text(callable, args, argc, ce) + "."); err = ERR_METHOD_NOT_FOUND; } } } - - bool disconnect = c.flags & CONNECT_ONE_SHOT; -#ifdef TOOLS_ENABLED - if (disconnect && (c.flags & CONNECT_PERSIST) && Engine::get_singleton()->is_editor_hint()) { - //this signal was connected from the editor, and is being edited. just don't disconnect for now - disconnect = false; - } -#endif - if (disconnect) { - _ObjectSignalDisconnectData dd; - dd.signal = p_name; - dd.callable = c.callable; - disconnect_data.push_back(dd); - } } - while (!disconnect_data.is_empty()) { - const _ObjectSignalDisconnectData &dd = disconnect_data.front()->get(); - - _disconnect(dd.signal, dd.callable); - disconnect_data.pop_front(); + for (uint32_t i = 0; i < slot_count; ++i) { + slot_callables[i].~Callable(); } return err; diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index 1196c2f787..73da0ba2af 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -34,6 +34,7 @@ #include "core/core_string_names.h" #include "core/debugger/engine_debugger.h" #include "core/debugger/script_debugger.h" +#include "core/io/resource_loader.h" #include <stdint.h> @@ -170,6 +171,24 @@ void Script::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "source_code", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_source_code", "get_source_code"); } +void Script::reload_from_file() { +#ifdef TOOLS_ENABLED + // Replicates how the ScriptEditor reloads script resources, which generally handles it. + // However, when scripts are to be reloaded but aren't open in the internal editor, we go through here instead. + const Ref<Script> rel = ResourceLoader::load(ResourceLoader::path_remap(get_path()), get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE); + if (rel.is_null()) { + return; + } + + set_source_code(rel->get_source_code()); + set_last_modified_time(rel->get_last_modified_time()); + + reload(); +#else + Resource::reload_from_file(); +#endif +} + void ScriptServer::set_scripting_enabled(bool p_enabled) { scripting_enabled = p_enabled; } diff --git a/core/object/script_language.h b/core/object/script_language.h index be50e58d79..c6c6f3de9f 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -125,6 +125,8 @@ protected: Dictionary _get_script_constant_map(); public: + virtual void reload_from_file() override; + virtual bool can_instantiate() const = 0; virtual Ref<Script> get_base_script() const = 0; //for script inheritance diff --git a/core/os/main_loop.h b/core/os/main_loop.h index b45eb38aeb..e48541d074 100644 --- a/core/os/main_loop.h +++ b/core/os/main_loop.h @@ -62,6 +62,7 @@ public: }; virtual void initialize(); + virtual void iteration_prepare() {} virtual bool physics_process(double p_time); virtual bool process(double p_time); virtual void finalize(); diff --git a/core/os/os.h b/core/os/os.h index e0dda0b155..4f0df1543f 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -56,7 +56,8 @@ class OS { bool _verbose_stdout = false; bool _debug_stdout = false; String _local_clipboard; - int _exit_code = EXIT_FAILURE; // unexpected exit is marked as failure + // Assume success by default, all failure cases need to set EXIT_FAILURE explicitly. + int _exit_code = EXIT_SUCCESS; bool _allow_hidpi = false; bool _allow_layered = false; bool _stdout_enabled = true; diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index a7e12138f2..dbc283946e 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -927,52 +927,49 @@ static _FORCE_INLINE_ signed char natural_cmp_common(const char32_t *&r_this_str return 0; } -signed char String::naturalcasecmp_to(const String &p_str) const { - const char32_t *this_str = get_data(); - const char32_t *that_str = p_str.get_data(); - - if (this_str && that_str) { - while (*this_str == '.' || *that_str == '.') { - if (*this_str++ != '.') { +static _FORCE_INLINE_ signed char naturalcasecmp_to_base(const char32_t *p_this_str, const char32_t *p_that_str) { + if (p_this_str && p_that_str) { + while (*p_this_str == '.' || *p_that_str == '.') { + if (*p_this_str++ != '.') { return 1; } - if (*that_str++ != '.') { + if (*p_that_str++ != '.') { return -1; } - if (!*that_str) { + if (!*p_that_str) { return 1; } - if (!*this_str) { + if (!*p_this_str) { return -1; } } - while (*this_str) { - if (!*that_str) { + while (*p_this_str) { + if (!*p_that_str) { return 1; - } else if (is_digit(*this_str)) { - if (!is_digit(*that_str)) { + } else if (is_digit(*p_this_str)) { + if (!is_digit(*p_that_str)) { return -1; } - signed char ret = natural_cmp_common(this_str, that_str); + signed char ret = natural_cmp_common(p_this_str, p_that_str); if (ret) { return ret; } - } else if (is_digit(*that_str)) { + } else if (is_digit(*p_that_str)) { return 1; } else { - if (*this_str < *that_str) { // If current character in this is less, we are less. + if (*p_this_str < *p_that_str) { // If current character in this is less, we are less. return -1; - } else if (*this_str > *that_str) { // If current character in this is greater, we are greater. + } else if (*p_this_str > *p_that_str) { // If current character in this is greater, we are greater. return 1; } - this_str++; - that_str++; + p_this_str++; + p_that_str++; } } - if (*that_str) { + if (*p_that_str) { return -1; } } @@ -980,52 +977,56 @@ signed char String::naturalcasecmp_to(const String &p_str) const { return 0; } -signed char String::naturalnocasecmp_to(const String &p_str) const { +signed char String::naturalcasecmp_to(const String &p_str) const { const char32_t *this_str = get_data(); const char32_t *that_str = p_str.get_data(); - if (this_str && that_str) { - while (*this_str == '.' || *that_str == '.') { - if (*this_str++ != '.') { + return naturalcasecmp_to_base(this_str, that_str); +} + +static _FORCE_INLINE_ signed char naturalnocasecmp_to_base(const char32_t *p_this_str, const char32_t *p_that_str) { + if (p_this_str && p_that_str) { + while (*p_this_str == '.' || *p_that_str == '.') { + if (*p_this_str++ != '.') { return 1; } - if (*that_str++ != '.') { + if (*p_that_str++ != '.') { return -1; } - if (!*that_str) { + if (!*p_that_str) { return 1; } - if (!*this_str) { + if (!*p_this_str) { return -1; } } - while (*this_str) { - if (!*that_str) { + while (*p_this_str) { + if (!*p_that_str) { return 1; - } else if (is_digit(*this_str)) { - if (!is_digit(*that_str)) { + } else if (is_digit(*p_this_str)) { + if (!is_digit(*p_that_str)) { return -1; } - signed char ret = natural_cmp_common(this_str, that_str); + signed char ret = natural_cmp_common(p_this_str, p_that_str); if (ret) { return ret; } - } else if (is_digit(*that_str)) { + } else if (is_digit(*p_that_str)) { return 1; } else { - if (_find_upper(*this_str) < _find_upper(*that_str)) { // If current character in this is less, we are less. + if (_find_upper(*p_this_str) < _find_upper(*p_that_str)) { // If current character in this is less, we are less. return -1; - } else if (_find_upper(*this_str) > _find_upper(*that_str)) { // If current character in this is greater, we are greater. + } else if (_find_upper(*p_this_str) > _find_upper(*p_that_str)) { // If current character in this is greater, we are greater. return 1; } - this_str++; - that_str++; + p_this_str++; + p_that_str++; } } - if (*that_str) { + if (*p_that_str) { return -1; } } @@ -1033,6 +1034,54 @@ signed char String::naturalnocasecmp_to(const String &p_str) const { return 0; } +signed char String::naturalnocasecmp_to(const String &p_str) const { + const char32_t *this_str = get_data(); + const char32_t *that_str = p_str.get_data(); + + return naturalnocasecmp_to_base(this_str, that_str); +} + +static _FORCE_INLINE_ signed char file_cmp_common(const char32_t *&r_this_str, const char32_t *&r_that_str) { + // Compare leading `_` sequences. + while (*r_this_str && *r_that_str) { + // Sort `_` lower than everything except `.` + if (*r_this_str != '_' && *r_that_str == '_') { + return *r_this_str == '.' ? -1 : 1; + } + if (*r_this_str == '_' && *r_that_str != '_') { + return *r_that_str == '.' ? 1 : -1; + } + r_this_str++; + r_that_str++; + } + + return 0; +} + +signed char String::filecasecmp_to(const String &p_str) const { + const char32_t *this_str = get_data(); + const char32_t *that_str = p_str.get_data(); + + signed char ret = file_cmp_common(this_str, that_str); + if (ret) { + return ret; + } + + return naturalcasecmp_to_base(this_str, that_str); +} + +signed char String::filenocasecmp_to(const String &p_str) const { + const char32_t *this_str = get_data(); + const char32_t *that_str = p_str.get_data(); + + signed char ret = file_cmp_common(this_str, that_str); + if (ret) { + return ret; + } + + return naturalnocasecmp_to_base(this_str, that_str); +} + const char32_t *String::get_data() const { static const char32_t zero = 0; return size() ? &operator[](0) : &zero; diff --git a/core/string/ustring.h b/core/string/ustring.h index 0fb72fccd2..fa904c8200 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -265,6 +265,9 @@ public: signed char nocasecmp_to(const String &p_str) const; signed char naturalcasecmp_to(const String &p_str) const; signed char naturalnocasecmp_to(const String &p_str) const; + // Special sorting for file names. Names starting with `_` are put before all others except those starting with `.`, otherwise natural comparison is used. + signed char filecasecmp_to(const String &p_str) const; + signed char filenocasecmp_to(const String &p_str) const; const char32_t *get_data() const; /* standard size stuff */ @@ -499,6 +502,12 @@ struct NaturalNoCaseComparator { } }; +struct FileNoCaseComparator { + bool operator()(const String &p_a, const String &p_b) const { + return p_a.filenocasecmp_to(p_b) < 0; + } +}; + template <typename L, typename R> _FORCE_INLINE_ bool is_str_less(const L *l_ptr, const R *r_ptr) { while (true) { diff --git a/core/templates/local_vector.h b/core/templates/local_vector.h index 6478297fd1..e0047e0782 100644 --- a/core/templates/local_vector.h +++ b/core/templates/local_vector.h @@ -104,6 +104,22 @@ public: return false; } + U erase_multiple_unordered(const T &p_val) { + U from = 0; + U occurrences = 0; + while (true) { + int64_t idx = find(p_val, from); + + if (idx == -1) { + break; + } + remove_at_unordered(idx); + from = idx; + occurrences++; + } + return occurrences; + } + void invert() { for (U i = 0; i < count / 2; i++) { SWAP(data[i], data[count - i - 1]); diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index 5f04c42536..ba7c44e405 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -1644,6 +1644,8 @@ static void _register_variant_builtin_methods() { bind_string_method(nocasecmp_to, sarray("to"), varray()); bind_string_method(naturalcasecmp_to, sarray("to"), varray()); bind_string_method(naturalnocasecmp_to, sarray("to"), varray()); + bind_string_method(filecasecmp_to, sarray("to"), varray()); + bind_string_method(filenocasecmp_to, sarray("to"), varray()); bind_string_method(length, sarray(), varray()); bind_string_method(substr, sarray("from", "len"), varray(-1)); bind_string_method(get_slice, sarray("delimiter", "slice"), varray()); diff --git a/doc/classes/AnimationNode.xml b/doc/classes/AnimationNode.xml index d7fb735b4d..960bbe68ad 100644 --- a/doc/classes/AnimationNode.xml +++ b/doc/classes/AnimationNode.xml @@ -6,6 +6,13 @@ <description> Base resource for [AnimationTree] nodes. In general, it's not used directly, but you can create custom ones with custom blending formulas. Inherit this when creating animation nodes mainly for use in [AnimationNodeBlendTree], otherwise [AnimationRootNode] should be used instead. + You can access the time information as read-only parameter which is processed and stored in the previous frame for all nodes except [AnimationNodeOutput]. + [b]Note:[/b] If more than two inputs exist in the [AnimationNode], which time information takes precedence depends on the type of [AnimationNode]. + [codeblock] + var current_length = $AnimationTree[parameters/AnimationNodeName/current_length] + var current_position = $AnimationTree[parameters/AnimationNodeName/current_position] + var current_delta = $AnimationTree[parameters/AnimationNodeName/current_delta] + [/codeblock] </description> <tutorials> <link title="Using AnimationTree">$DOCS_URL/tutorials/animation/animation_tree.html</link> @@ -56,7 +63,7 @@ When inheriting from [AnimationRootNode], implement this virtual method to return whether the [param parameter] is read-only. Parameters are custom local memory used for your animation nodes, given a resource can be reused in multiple trees. </description> </method> - <method name="_process" qualifiers="virtual const"> + <method name="_process" qualifiers="virtual const" deprecated="Currently this is mostly useless as there is a lack of many APIs to extend AnimationNode by GDScript. It is planned that a more flexible API using structures will be provided in the future."> <return type="float" /> <param index="0" name="time" type="float" /> <param index="1" name="seek" type="bool" /> @@ -65,7 +72,7 @@ <description> When inheriting from [AnimationRootNode], implement this virtual method to run some code when this animation node is processed. The [param time] parameter is a relative delta, unless [param seek] is [code]true[/code], in which case it is absolute. Here, call the [method blend_input], [method blend_node] or [method blend_animation] functions. You can also use [method get_parameter] and [method set_parameter] to modify local memory. - This function should return the time left for the current animation to finish (if unsure, pass the value from the main blend being called). + This function should return the delta. </description> </method> <method name="add_input"> diff --git a/doc/classes/AnimationNodeAnimation.xml b/doc/classes/AnimationNodeAnimation.xml index d965d31b03..5683371182 100644 --- a/doc/classes/AnimationNodeAnimation.xml +++ b/doc/classes/AnimationNodeAnimation.xml @@ -15,9 +15,27 @@ <member name="animation" type="StringName" setter="set_animation" getter="get_animation" default="&"""> Animation to use as an output. It is one of the animations provided by [member AnimationTree.anim_player]. </member> + <member name="loop_mode" type="int" setter="set_loop_mode" getter="get_loop_mode" enum="Animation.LoopMode"> + If [member use_custom_timeline] is [code]true[/code], override the loop settings of the original [Animation] resource with the value. + </member> <member name="play_mode" type="int" setter="set_play_mode" getter="get_play_mode" enum="AnimationNodeAnimation.PlayMode" default="0"> Determines the playback direction of the animation. </member> + <member name="start_offset" type="float" setter="set_start_offset" getter="get_start_offset"> + If [member use_custom_timeline] is [code]true[/code], offset the start position of the animation. + This is useful for adjusting which foot steps first in 3D walking animations. + </member> + <member name="stretch_time_scale" type="bool" setter="set_stretch_time_scale" getter="is_stretching_time_scale"> + If [code]true[/code], scales the time so that the length specified in [member timeline_length] is one cycle. + This is useful for matching the periods of walking and running animations. + If [code]false[/code], the original animation length is respected. If you set the loop to [member loop_mode], the animation will loop in [member timeline_length]. + </member> + <member name="timeline_length" type="float" setter="set_timeline_length" getter="get_timeline_length"> + If [member use_custom_timeline] is [code]true[/code], offset the start position of the animation. + </member> + <member name="use_custom_timeline" type="bool" setter="set_use_custom_timeline" getter="is_using_custom_timeline" default="false"> + If [code]true[/code], [AnimationNode] provides an animation based on the [Animation] resource with some parameters adjusted. + </member> </members> <constants> <constant name="PLAY_MODE_FORWARD" value="0" enum="PlayMode"> diff --git a/doc/classes/AnimationNodeOneShot.xml b/doc/classes/AnimationNodeOneShot.xml index ac7cf70133..6ff2d6f6db 100644 --- a/doc/classes/AnimationNodeOneShot.xml +++ b/doc/classes/AnimationNodeOneShot.xml @@ -66,17 +66,22 @@ <member name="autorestart_random_delay" type="float" setter="set_autorestart_random_delay" getter="get_autorestart_random_delay" default="0.0"> If [member autorestart] is [code]true[/code], a random additional delay (in seconds) between 0 and this value will be added to [member autorestart_delay]. </member> + <member name="break_loop_at_end" type="bool" setter="set_break_loop_at_end" getter="is_loop_broken_at_end" default="false"> + If [code]true[/code], breaks the loop at the end of the loop cycle for transition, even if the animation is looping. + </member> <member name="fadein_curve" type="Curve" setter="set_fadein_curve" getter="get_fadein_curve"> Determines how cross-fading between animations is eased. If empty, the transition will be linear. </member> <member name="fadein_time" type="float" setter="set_fadein_time" getter="get_fadein_time" default="0.0"> The fade-in duration. For example, setting this to [code]1.0[/code] for a 5 second length animation will produce a cross-fade that starts at 0 second and ends at 1 second during the animation. + [b]Note:[/b] [AnimationNodeOneShot] transitions the current state after the end of the fading. When [AnimationNodeOutput] is considered as the most upstream, so the [member fadein_time] is scaled depending on the downstream delta. For example, if this value is set to [code]1.0[/code] and a [AnimationNodeTimeScale] with a value of [code]2.0[/code] is chained downstream, the actual processing time will be 0.5 second. </member> <member name="fadeout_curve" type="Curve" setter="set_fadeout_curve" getter="get_fadeout_curve"> Determines how cross-fading between animations is eased. If empty, the transition will be linear. </member> <member name="fadeout_time" type="float" setter="set_fadeout_time" getter="get_fadeout_time" default="0.0"> The fade-out duration. For example, setting this to [code]1.0[/code] for a 5 second length animation will produce a cross-fade that starts at 4 second and ends at 5 second during the animation. + [b]Note:[/b] [AnimationNodeOneShot] transitions the current state after the end of the fading. When [AnimationNodeOutput] is considered as the most upstream, so the [member fadeout_time] is scaled depending on the downstream delta. For example, if this value is set to [code]1.0[/code] and an [AnimationNodeTimeScale] with a value of [code]2.0[/code] is chained downstream, the actual processing time will be 0.5 second. </member> <member name="mix_mode" type="int" setter="set_mix_mode" getter="get_mix_mode" enum="AnimationNodeOneShot.MixMode" default="0"> The blend type. diff --git a/doc/classes/AnimationNodeStateMachineTransition.xml b/doc/classes/AnimationNodeStateMachineTransition.xml index 7b7797f594..7bd0bd7e7e 100644 --- a/doc/classes/AnimationNodeStateMachineTransition.xml +++ b/doc/classes/AnimationNodeStateMachineTransition.xml @@ -28,6 +28,9 @@ <member name="advance_mode" type="int" setter="set_advance_mode" getter="get_advance_mode" enum="AnimationNodeStateMachineTransition.AdvanceMode" default="1"> Determines whether the transition should disabled, enabled when using [method AnimationNodeStateMachinePlayback.travel], or traversed automatically if the [member advance_condition] and [member advance_expression] checks are true (if assigned). </member> + <member name="break_loop_at_end" type="bool" setter="set_break_loop_at_end" getter="is_loop_broken_at_end" default="false"> + If [code]true[/code], breaks the loop at the end of the loop cycle for transition, even if the animation is looping. + </member> <member name="priority" type="int" setter="set_priority" getter="get_priority" default="1"> Lower priority transitions are preferred when travelling through the tree via [method AnimationNodeStateMachinePlayback.travel] or [member advance_mode] is set to [constant ADVANCE_MODE_AUTO]. </member> @@ -42,6 +45,7 @@ </member> <member name="xfade_time" type="float" setter="set_xfade_time" getter="get_xfade_time" default="0.0"> The time to cross-fade between this state and the next. + [b]Note:[/b] [AnimationNodeStateMachine] transitions the current state immediately after the start of the fading. The precise remaining time can only be inferred from the main animation. When [AnimationNodeOutput] is considered as the most upstream, so the [member xfade_time] is not scaled depending on the downstream delta. See also [member AnimationNodeOneShot.fadeout_time]. </member> </members> <signals> diff --git a/doc/classes/AnimationNodeTransition.xml b/doc/classes/AnimationNodeTransition.xml index 3e1a0a28b5..775a208735 100644 --- a/doc/classes/AnimationNodeTransition.xml +++ b/doc/classes/AnimationNodeTransition.xml @@ -42,6 +42,13 @@ <link title="Third Person Shooter Demo">https://godotengine.org/asset-library/asset/678</link> </tutorials> <methods> + <method name="is_input_loop_broken_at_end" qualifiers="const"> + <return type="bool" /> + <param index="0" name="input" type="int" /> + <description> + Returns whether the animation breaks the loop at the end of the loop cycle for transition. + </description> + </method> <method name="is_input_reset" qualifiers="const"> <return type="bool" /> <param index="0" name="input" type="int" /> @@ -64,6 +71,14 @@ Enables or disables auto-advance for the given [param input] index. If enabled, state changes to the next input after playing the animation once. If enabled for the last input state, it loops to the first. </description> </method> + <method name="set_input_break_loop_at_end"> + <return type="void" /> + <param index="0" name="input" type="int" /> + <param index="1" name="enable" type="bool" /> + <description> + If [code]true[/code], breaks the loop at the end of the loop cycle for transition, even if the animation is looping. + </description> + </method> <method name="set_input_reset"> <return type="void" /> <param index="0" name="input" type="int" /> @@ -85,6 +100,7 @@ </member> <member name="xfade_time" type="float" setter="set_xfade_time" getter="get_xfade_time" default="0.0"> Cross-fading time (in seconds) between each animation connected to the inputs. + [b]Note:[/b] [AnimationNodeTransition] transitions the current state immediately after the start of the fading. The precise remaining time can only be inferred from the main animation. When [AnimationNodeOutput] is considered as the most upstream, so the [member xfade_time] is not scaled depending on the downstream delta. See also [member AnimationNodeOneShot.fadeout_time]. </member> </members> </class> diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml index 43c3f5c1be..697afed636 100644 --- a/doc/classes/Control.xml +++ b/doc/classes/Control.xml @@ -1012,6 +1012,7 @@ Distance between the node's top edge and its parent control, based on [member anchor_top]. Offsets are often controlled by one or multiple parent [Container] nodes, so you should not modify them manually if your node is a direct child of a [Container]. Offsets update automatically when you move or resize the node. </member> + <member name="physics_interpolation_mode" type="int" setter="set_physics_interpolation_mode" getter="get_physics_interpolation_mode" overrides="Node" enum="Node.PhysicsInterpolationMode" default="2" /> <member name="pivot_offset" type="Vector2" setter="set_pivot_offset" getter="get_pivot_offset" default="Vector2(0, 0)"> By default, the node's pivot is its top-left corner. When you change its [member rotation] or [member scale], it will rotate or scale around this pivot. Set this property to [member size] / 2 to pivot around the Control's center. </member> diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index f446d5bb1f..48ed191db1 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -1031,6 +1031,12 @@ The number of pixels to scroll with every mouse wheel increment. Higher values make the script scroll by faster when using the mouse wheel. [b]Note:[/b] You can hold down [kbd]Alt[/kbd] while using the mouse wheel to temporarily scroll 5 times faster. </member> + <member name="text_editor/completion/add_node_path_literals" type="bool" setter="" getter=""> + If [code]true[/code], uses [NodePath] instead of [String] when appropriate for code autocompletion or for drag and dropping object properties into the script editor. + </member> + <member name="text_editor/completion/add_string_name_literals" type="bool" setter="" getter=""> + If [code]true[/code], uses [StringName] instead of [String] when appropriate for code autocompletion. + </member> <member name="text_editor/completion/add_type_hints" type="bool" setter="" getter=""> If [code]true[/code], adds [url=$DOCS_URL/tutorials/scripting/gdscript/static_typing.html]GDScript static typing[/url] hints such as [code]-> void[/code] and [code]: int[/code] when using code autocompletion or when creating onready variables by drag and dropping nodes into the script editor while pressing the [kbd]Ctrl[/kbd] key. If [code]true[/code], newly created scripts will also automatically have type hints added to their method parameters and return types. </member> diff --git a/doc/classes/InputEvent.xml b/doc/classes/InputEvent.xml index 391d060fc3..96a4612466 100644 --- a/doc/classes/InputEvent.xml +++ b/doc/classes/InputEvent.xml @@ -117,7 +117,12 @@ <members> <member name="device" type="int" setter="set_device" getter="get_device" default="0"> The event's device ID. - [b]Note:[/b] This device ID will always be [code]-1[/code] for emulated mouse input from a touchscreen. This can be used to distinguish emulated mouse input from physical mouse input. + [b]Note:[/b] [member device] can be negative for special use cases that don't refer to devices physically present on the system. See [constant DEVICE_ID_EMULATION]. </member> </members> + <constants> + <constant name="DEVICE_ID_EMULATION" value="-1"> + Device ID used for emulated mouse input from a touchscreen, or for emulated touch input from a mouse. This can be used to distinguish emulated mouse input from physical mouse input, or emulated touch input from physical touch input. + </constant> + </constants> </class> diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index b786b67933..c69e5edf0c 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -619,6 +619,21 @@ [method request_ready] resets it back to [code]false[/code]. </description> </method> + <method name="is_physics_interpolated" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if physics interpolation is enabled for this node (see [member physics_interpolation_mode]). + [b]Note:[/b] Interpolation will only be active if both the flag is set [b]and[/b] physics interpolation is enabled within the [SceneTree]. This can be tested using [method is_physics_interpolated_and_enabled]. + </description> + </method> + <method name="is_physics_interpolated_and_enabled" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if physics interpolation is enabled (see [member physics_interpolation_mode]) [b]and[/b] enabled in the [SceneTree]. + This is a convenience version of [method is_physics_interpolated] that also checks whether physics interpolation is enabled globally. + See [member SceneTree.physics_interpolation] and [member ProjectSettings.physics/common/physics_interpolation]. + </description> + </method> <method name="is_physics_processing" qualifiers="const"> <return type="bool" /> <description> @@ -793,6 +808,15 @@ [b]Note:[/b] This method only affects the current node. If the node's children also need to request ready, this method needs to be called for each one of them. When the node and its children enter the tree again, the order of [method _ready] callbacks will be the same as normal. </description> </method> + <method name="reset_physics_interpolation"> + <return type="void" /> + <description> + When physics interpolation is active, moving a node to a radically different transform (such as placement within a level) can result in a visible glitch as the object is rendered moving from the old to new position over the physics tick. + That glitch can be prevented by calling this method, which temporarily disables interpolation until the physics tick is complete. + The notification [constant NOTIFICATION_RESET_PHYSICS_INTERPOLATION] will be received by the node and all children recursively. + [b]Note:[/b] This function should be called [b]after[/b] moving the node, rather than before. + </description> + </method> <method name="rpc" qualifiers="vararg"> <return type="int" enum="Error" /> <param index="0" name="method" type="StringName" /> @@ -964,6 +988,10 @@ The owner of this node. The owner must be an ancestor of this node. When packing the owner node in a [PackedScene], all the nodes it owns are also saved with it. [b]Note:[/b] In the editor, nodes not owned by the scene root are usually not displayed in the Scene dock, and will [b]not[/b] be saved. To prevent this, remember to set the owner after calling [method add_child]. See also (see [member unique_name_in_owner]) </member> + <member name="physics_interpolation_mode" type="int" setter="set_physics_interpolation_mode" getter="get_physics_interpolation_mode" enum="Node.PhysicsInterpolationMode" default="0"> + Allows enabling or disabling physics interpolation per node, offering a finer grain of control than turning physics interpolation on and off globally. See [member ProjectSettings.physics/common/physics_interpolation] and [member SceneTree.physics_interpolation] for the global setting. + [b]Note:[/b] When teleporting a node to a distant position you should temporarily disable interpolation with [method Node.reset_physics_interpolation]. + </member> <member name="process_mode" type="int" setter="set_process_mode" getter="get_process_mode" enum="Node.ProcessMode" default="0"> The node's processing behavior (see [enum ProcessMode]). To check if the node can process in its current mode, use [method can_process]. </member> @@ -1122,6 +1150,9 @@ <constant name="NOTIFICATION_ENABLED" value="29"> Notification received when the node is enabled again after being disabled. See [constant PROCESS_MODE_DISABLED]. </constant> + <constant name="NOTIFICATION_RESET_PHYSICS_INTERPOLATION" value="2001"> + Notification received when [method reset_physics_interpolation] is called on the node or its ancestors. + </constant> <constant name="NOTIFICATION_EDITOR_PRE_SAVE" value="9001"> Notification received right before the scene with the node is saved in the editor. This notification is only sent in the Godot editor and will not occur in exported projects. </constant> @@ -1237,6 +1268,15 @@ <constant name="FLAG_PROCESS_THREAD_MESSAGES_ALL" value="3" enum="ProcessThreadMessages" is_bitfield="true"> Allows this node to process threaded messages created with [method call_deferred_thread_group] right before either [method _process] or [method _physics_process] are called. </constant> + <constant name="PHYSICS_INTERPOLATION_MODE_INHERIT" value="0" enum="PhysicsInterpolationMode"> + Inherits [member physics_interpolation_mode] from the node's parent. This is the default for any newly created node. + </constant> + <constant name="PHYSICS_INTERPOLATION_MODE_ON" value="1" enum="PhysicsInterpolationMode"> + Enables physics interpolation for this node and for children set to [constant PHYSICS_INTERPOLATION_MODE_INHERIT]. This is the default for the root node. + </constant> + <constant name="PHYSICS_INTERPOLATION_MODE_OFF" value="2" enum="PhysicsInterpolationMode"> + Disables physics interpolation for this node and for children set to [constant PHYSICS_INTERPOLATION_MODE_INHERIT]. + </constant> <constant name="DUPLICATE_SIGNALS" value="1" enum="DuplicateFlags"> Duplicate the node's signal connections. </constant> diff --git a/doc/classes/Parallax2D.xml b/doc/classes/Parallax2D.xml index 6db29b7a33..472aeb0bd3 100644 --- a/doc/classes/Parallax2D.xml +++ b/doc/classes/Parallax2D.xml @@ -25,6 +25,7 @@ <member name="limit_end" type="Vector2" setter="set_limit_end" getter="get_limit_end" default="Vector2(1e+07, 1e+07)"> Bottom-right limits for scrolling to end. If the camera is outside of this limit, the [Parallax2D] will stop scrolling. Must be higher than [member limit_begin] and the viewport size combined to work. </member> + <member name="physics_interpolation_mode" type="int" setter="set_physics_interpolation_mode" getter="get_physics_interpolation_mode" overrides="Node" enum="Node.PhysicsInterpolationMode" default="2" /> <member name="repeat_size" type="Vector2" setter="set_repeat_size" getter="get_repeat_size" default="Vector2(0, 0)"> Repeats the [Texture2D] of each of this node's children and offsets them by this value. When scrolling, the node's position loops, giving the illusion of an infinite scrolling background if the values are larger than the screen size. If an axis is set to [code]0[/code], the [Texture2D] will not be repeated. </member> @@ -40,7 +41,7 @@ </member> <member name="scroll_scale" type="Vector2" setter="set_scroll_scale" getter="get_scroll_scale" default="Vector2(1, 1)"> Multiplier to the final [Parallax2D]'s offset. Can be used to simulate distance from the camera. - For example, a value of [code]1[/code] scrolls at the same speed as the camera. A value greater than [code]1[/code] scrolls faster, making objects appear closer. Less than [code]1[/code] scrolls slower, making object appear closer and a value of [code]0[/code] stops the objects completely. + For example, a value of [code]1[/code] scrolls at the same speed as the camera. A value greater than [code]1[/code] scrolls faster, making objects appear closer. Less than [code]1[/code] scrolls slower, making objects appear further, and a value of [code]0[/code] stops the objects completely. </member> </members> </class> diff --git a/doc/classes/ParallaxLayer.xml b/doc/classes/ParallaxLayer.xml index fb92c9d85f..12482d6f66 100644 --- a/doc/classes/ParallaxLayer.xml +++ b/doc/classes/ParallaxLayer.xml @@ -23,5 +23,6 @@ <member name="motion_scale" type="Vector2" setter="set_motion_scale" getter="get_motion_scale" default="Vector2(1, 1)"> Multiplies the ParallaxLayer's motion. If an axis is set to [code]0[/code], it will not scroll. </member> + <member name="physics_interpolation_mode" type="int" setter="set_physics_interpolation_mode" getter="get_physics_interpolation_mode" overrides="Node" enum="Node.PhysicsInterpolationMode" default="2" /> </members> </class> diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 407041289c..98e8939c3f 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -2266,9 +2266,15 @@ Controls the maximum number of physics steps that can be simulated each rendered frame. The default value is tuned to avoid "spiral of death" situations where expensive physics simulations trigger more expensive simulations indefinitely. However, the game will appear to slow down if the rendering FPS is less than [code]1 / max_physics_steps_per_frame[/code] of [member physics/common/physics_ticks_per_second]. This occurs even if [code]delta[/code] is consistently used in physics calculations. To avoid this, increase [member physics/common/max_physics_steps_per_frame] if you have increased [member physics/common/physics_ticks_per_second] significantly above its default value. [b]Note:[/b] This property is only read when the project starts. To change the maximum number of simulated physics steps per frame at runtime, set [member Engine.max_physics_steps_per_frame] instead. </member> + <member name="physics/common/physics_interpolation" type="bool" setter="" getter="" default="false"> + If [code]true[/code], the renderer will interpolate the transforms of physics objects between the last two transforms, so that smooth motion is seen even when physics ticks do not coincide with rendered frames. See also [member Node.physics_interpolation_mode] and [method Node.reset_physics_interpolation]. + [b]Note:[/b] If [code]true[/code], the physics jitter fix should be disabled by setting [member physics/common/physics_jitter_fix] to [code]0.0[/code]. + [b]Note:[/b] This property is only read when the project starts. To toggle physics interpolation at runtime, set [member SceneTree.physics_interpolation] instead. + [b]Note:[/b] This feature is currently only implemented in the 2D renderer. + </member> <member name="physics/common/physics_jitter_fix" type="float" setter="" getter="" default="0.5"> Controls how much physics ticks are synchronized with real time. For 0 or less, the ticks are synchronized. Such values are recommended for network games, where clock synchronization matters. Higher values cause higher deviation of in-game clock and real clock, but allows smoothing out framerate jitters. The default value of 0.5 should be good enough for most; values above 2 could cause the game to react to dropped frames with a noticeable delay and are not recommended. - [b]Note:[/b] For best results, when using a custom physics interpolation solution, the physics jitter fix should be disabled by setting [member physics/common/physics_jitter_fix] to [code]0[/code]. + [b]Note:[/b] When using a physics interpolation solution (such as enabling [member physics/common/physics_interpolation] or using a custom solution), the physics jitter fix should be disabled by setting [member physics/common/physics_jitter_fix] to [code]0.0[/code]. [b]Note:[/b] This property is only read when the project starts. To change the physics jitter fix at runtime, set [member Engine.physics_jitter_fix] instead. </member> <member name="physics/common/physics_ticks_per_second" type="int" setter="" getter="" default="60"> diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index 324e6d50b6..5efda5f83f 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -424,6 +424,14 @@ [b]Note:[/b] The equivalent node is [CanvasItem]. </description> </method> + <method name="canvas_item_reset_physics_interpolation"> + <return type="void" /> + <param index="0" name="item" type="RID" /> + <description> + Prevents physics interpolation for the current physics tick. + This is useful when moving a canvas item to a new location, to give an instantaneous change rather than interpolation from the previous location. + </description> + </method> <method name="canvas_item_set_canvas_group_mode"> <return type="void" /> <param index="0" name="item" type="RID" /> @@ -504,6 +512,14 @@ Sets the index for the [CanvasItem]. </description> </method> + <method name="canvas_item_set_interpolated"> + <return type="void" /> + <param index="0" name="item" type="RID" /> + <param index="1" name="interpolated" type="bool" /> + <description> + If [param interpolated] is [code]true[/code], turns on physics interpolation for the canvas item. + </description> + </method> <method name="canvas_item_set_light_mask"> <return type="void" /> <param index="0" name="item" type="RID" /> @@ -612,6 +628,15 @@ Sets the [CanvasItem]'s Z index, i.e. its draw order (lower indexes are drawn first). </description> </method> + <method name="canvas_item_transform_physics_interpolation"> + <return type="void" /> + <param index="0" name="item" type="RID" /> + <param index="1" name="transform" type="Transform2D" /> + <description> + Transforms both the current and previous stored transform for a canvas item. + This allows transforming a canvas item without creating a "glitch" in the interpolation, which is particularly useful for large worlds utilising a shifting origin. + </description> + </method> <method name="canvas_light_attach_to_canvas"> <return type="void" /> <param index="0" name="light" type="RID" /> @@ -644,6 +669,14 @@ [b]Note:[/b] The equivalent node is [LightOccluder2D]. </description> </method> + <method name="canvas_light_occluder_reset_physics_interpolation"> + <return type="void" /> + <param index="0" name="occluder" type="RID" /> + <description> + Prevents physics interpolation for the current physics tick. + This is useful when moving an occluder to a new location, to give an instantaneous change rather than interpolation from the previous location. + </description> + </method> <method name="canvas_light_occluder_set_as_sdf_collision"> <return type="void" /> <param index="0" name="occluder" type="RID" /> @@ -659,6 +692,14 @@ Enables or disables light occluder. </description> </method> + <method name="canvas_light_occluder_set_interpolated"> + <return type="void" /> + <param index="0" name="occluder" type="RID" /> + <param index="1" name="interpolated" type="bool" /> + <description> + If [param interpolated] is [code]true[/code], turns on physics interpolation for the light occluder. + </description> + </method> <method name="canvas_light_occluder_set_light_mask"> <return type="void" /> <param index="0" name="occluder" type="RID" /> @@ -683,6 +724,23 @@ Sets a light occluder's [Transform2D]. </description> </method> + <method name="canvas_light_occluder_transform_physics_interpolation"> + <return type="void" /> + <param index="0" name="occluder" type="RID" /> + <param index="1" name="transform" type="Transform2D" /> + <description> + Transforms both the current and previous stored transform for a light occluder. + This allows transforming an occluder without creating a "glitch" in the interpolation, which is particularly useful for large worlds utilising a shifting origin. + </description> + </method> + <method name="canvas_light_reset_physics_interpolation"> + <return type="void" /> + <param index="0" name="light" type="RID" /> + <description> + Prevents physics interpolation for the current physics tick. + This is useful when moving a canvas item to a new location, to give an instantaneous change rather than interpolation from the previous location. + </description> + </method> <method name="canvas_light_set_blend_mode"> <return type="void" /> <param index="0" name="light" type="RID" /> @@ -723,6 +781,14 @@ Sets a canvas light's height. </description> </method> + <method name="canvas_light_set_interpolated"> + <return type="void" /> + <param index="0" name="light" type="RID" /> + <param index="1" name="interpolated" type="bool" /> + <description> + If [param interpolated] is [code]true[/code], turns on physics interpolation for the canvas light. + </description> + </method> <method name="canvas_light_set_item_cull_mask"> <return type="void" /> <param index="0" name="light" type="RID" /> @@ -829,6 +895,15 @@ Sets the Z range of objects that will be affected by this light. Equivalent to [member Light2D.range_z_min] and [member Light2D.range_z_max]. </description> </method> + <method name="canvas_light_transform_physics_interpolation"> + <return type="void" /> + <param index="0" name="light" type="RID" /> + <param index="1" name="transform" type="Transform2D" /> + <description> + Transforms both the current and previous stored transform for a canvas light. + This allows transforming a light without creating a "glitch" in the interpolation, which is is particularly useful for large worlds utilising a shifting origin. + </description> + </method> <method name="canvas_occluder_polygon_create"> <return type="RID" /> <description> diff --git a/doc/classes/SceneTree.xml b/doc/classes/SceneTree.xml index f1bb5a1cf6..bae5fe1205 100644 --- a/doc/classes/SceneTree.xml +++ b/doc/classes/SceneTree.xml @@ -264,6 +264,10 @@ - 2D and 3D physics will be stopped, as well as collision detection and related signals. - Depending on each node's [member Node.process_mode], their [method Node._process], [method Node._physics_process] and [method Node._input] callback methods may not called anymore. </member> + <member name="physics_interpolation" type="bool" setter="set_physics_interpolation_enabled" getter="is_physics_interpolation_enabled" default="false"> + If [code]true[/code], the renderer will interpolate the transforms of physics objects between the last two transforms, so that smooth motion is seen even when physics ticks do not coincide with rendered frames. + The default value of this property is controlled by [member ProjectSettings.physics/common/physics_interpolation]. + </member> <member name="quit_on_go_back" type="bool" setter="set_quit_on_go_back" getter="is_quit_on_go_back" default="true"> If [code]true[/code], the application quits automatically when navigating back (e.g. using the system "Back" button on Android). To handle 'Go Back' button when this option is disabled, use [constant DisplayServer.WINDOW_EVENT_GO_BACK_REQUEST]. diff --git a/doc/classes/String.xml b/doc/classes/String.xml index 17f953f48f..7592342602 100644 --- a/doc/classes/String.xml +++ b/doc/classes/String.xml @@ -112,7 +112,7 @@ <description> Performs a case-sensitive comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "Less than" and "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. With different string lengths, returns [code]1[/code] if this string is longer than the [param to] string, or [code]-1[/code] if shorter. Note that the length of empty strings is [i]always[/i] [code]0[/code]. - To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method nocasecmp_to], [method naturalcasecmp_to], and [method naturalnocasecmp_to]. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method nocasecmp_to], [method filecasecmp_to], and [method naturalcasecmp_to]. </description> </method> <method name="chr" qualifiers="static"> @@ -184,6 +184,22 @@ Returns a string with [param chars] characters erased starting from [param position]. If [param chars] goes beyond the string's length given the specified [param position], fewer characters will be erased from the returned string. Returns an empty string if either [param position] or [param chars] is negative. Returns the original string unmodified if [param chars] is [code]0[/code]. </description> </method> + <method name="filecasecmp_to" qualifiers="const"> + <return type="int" /> + <param index="0" name="to" type="String" /> + <description> + Like [method naturalcasecmp_to] but prioritises strings that begin with periods ([code].[/code]) and underscores ([code]_[/code]) before any other character. Useful when sorting folders or file names. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method filenocasecmp_to], [method naturalcasecmp_to], and [method casecmp_to]. + </description> + </method> + <method name="filenocasecmp_to" qualifiers="const"> + <return type="int" /> + <param index="0" name="to" type="String" /> + <description> + Like [method naturalnocasecmp_to] but prioritises strings that begin with periods ([code].[/code]) and underscores ([code]_[/code]) before any other character. Useful when sorting folders or file names. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method filecasecmp_to], [method naturalnocasecmp_to], and [method nocasecmp_to]. + </description> + </method> <method name="find" qualifiers="const"> <return type="int" /> <param index="0" name="what" type="String" /> @@ -586,7 +602,7 @@ Performs a [b]case-sensitive[/b], [i]natural order[/i] comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "Less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. When used for sorting, natural order comparison orders sequences of numbers by the combined value of each digit as is often expected, instead of the single digit's value. A sorted sequence of numbered strings will be [code]["1", "2", "3", ...][/code], not [code]["1", "10", "2", "3", ...][/code]. With different string lengths, returns [code]1[/code] if this string is longer than the [param to] string, or [code]-1[/code] if shorter. Note that the length of empty strings is [i]always[/i] [code]0[/code]. - To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method naturalnocasecmp_to], [method nocasecmp_to], and [method casecmp_to]. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method naturalnocasecmp_to], [method filecasecmp_to], and [method nocasecmp_to]. </description> </method> <method name="naturalnocasecmp_to" qualifiers="const"> @@ -596,7 +612,7 @@ Performs a [b]case-insensitive[/b], [i]natural order[/i] comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "Less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. Internally, lowercase characters are converted to uppercase for the comparison. When used for sorting, natural order comparison orders sequences of numbers by the combined value of each digit as is often expected, instead of the single digit's value. A sorted sequence of numbered strings will be [code]["1", "2", "3", ...][/code], not [code]["1", "10", "2", "3", ...][/code]. With different string lengths, returns [code]1[/code] if this string is longer than the [param to] string, or [code]-1[/code] if shorter. Note that the length of empty strings is [i]always[/i] [code]0[/code]. - To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method naturalcasecmp_to], [method nocasecmp_to], and [method casecmp_to]. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method naturalcasecmp_to], [method filenocasecmp_to], and [method casecmp_to]. </description> </method> <method name="nocasecmp_to" qualifiers="const"> @@ -605,7 +621,7 @@ <description> Performs a [b]case-insensitive[/b] comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "Less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. Internally, lowercase characters are converted to uppercase for the comparison. With different string lengths, returns [code]1[/code] if this string is longer than the [param to] string, or [code]-1[/code] if shorter. Note that the length of empty strings is [i]always[/i] [code]0[/code]. - To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method casecmp_to], [method naturalcasecmp_to], and [method naturalnocasecmp_to]. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method casecmp_to], [method filenocasecmp_to], and [method naturalnocasecmp_to]. </description> </method> <method name="num" qualifiers="static"> diff --git a/doc/classes/StringName.xml b/doc/classes/StringName.xml index 41763489f1..e837b65199 100644 --- a/doc/classes/StringName.xml +++ b/doc/classes/StringName.xml @@ -107,7 +107,7 @@ <description> Performs a case-sensitive comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "Less than" and "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. With different string lengths, returns [code]1[/code] if this string is longer than the [param to] string, or [code]-1[/code] if shorter. Note that the length of empty strings is [i]always[/i] [code]0[/code]. - To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method nocasecmp_to], [method naturalcasecmp_to], and [method naturalnocasecmp_to]. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method nocasecmp_to], [method filecasecmp_to], and [method naturalcasecmp_to]. </description> </method> <method name="contains" qualifiers="const"> @@ -168,6 +168,22 @@ Returns a string with [param chars] characters erased starting from [param position]. If [param chars] goes beyond the string's length given the specified [param position], fewer characters will be erased from the returned string. Returns an empty string if either [param position] or [param chars] is negative. Returns the original string unmodified if [param chars] is [code]0[/code]. </description> </method> + <method name="filecasecmp_to" qualifiers="const"> + <return type="int" /> + <param index="0" name="to" type="String" /> + <description> + Like [method naturalcasecmp_to] but prioritises strings that begin with periods ([code].[/code]) and underscores ([code]_[/code]) before any other character. Useful when sorting folders or file names. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method filenocasecmp_to], [method naturalcasecmp_to], and [method casecmp_to]. + </description> + </method> + <method name="filenocasecmp_to" qualifiers="const"> + <return type="int" /> + <param index="0" name="to" type="String" /> + <description> + Like [method naturalnocasecmp_to] but prioritises strings that begin with periods ([code].[/code]) and underscores ([code]_[/code]) before any other character. Useful when sorting folders or file names. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method filecasecmp_to], [method naturalnocasecmp_to], and [method nocasecmp_to]. + </description> + </method> <method name="find" qualifiers="const"> <return type="int" /> <param index="0" name="what" type="String" /> @@ -562,7 +578,7 @@ Performs a [b]case-sensitive[/b], [i]natural order[/i] comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "Less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. When used for sorting, natural order comparison orders sequences of numbers by the combined value of each digit as is often expected, instead of the single digit's value. A sorted sequence of numbered strings will be [code]["1", "2", "3", ...][/code], not [code]["1", "10", "2", "3", ...][/code]. With different string lengths, returns [code]1[/code] if this string is longer than the [param to] string, or [code]-1[/code] if shorter. Note that the length of empty strings is [i]always[/i] [code]0[/code]. - To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method naturalnocasecmp_to], [method nocasecmp_to], and [method casecmp_to]. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method naturalnocasecmp_to], [method filecasecmp_to], and [method nocasecmp_to]. </description> </method> <method name="naturalnocasecmp_to" qualifiers="const"> @@ -572,7 +588,7 @@ Performs a [b]case-insensitive[/b], [i]natural order[/i] comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "Less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. Internally, lowercase characters are converted to uppercase for the comparison. When used for sorting, natural order comparison orders sequences of numbers by the combined value of each digit as is often expected, instead of the single digit's value. A sorted sequence of numbered strings will be [code]["1", "2", "3", ...][/code], not [code]["1", "10", "2", "3", ...][/code]. With different string lengths, returns [code]1[/code] if this string is longer than the [param to] string, or [code]-1[/code] if shorter. Note that the length of empty strings is [i]always[/i] [code]0[/code]. - To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method naturalcasecmp_to], [method nocasecmp_to], and [method casecmp_to]. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method naturalcasecmp_to], [method filenocasecmp_to], and [method casecmp_to]. </description> </method> <method name="nocasecmp_to" qualifiers="const"> @@ -581,7 +597,7 @@ <description> Performs a [b]case-insensitive[/b] comparison to another string. Returns [code]-1[/code] if less than, [code]1[/code] if greater than, or [code]0[/code] if equal. "Less than" or "greater than" are determined by the [url=https://en.wikipedia.org/wiki/List_of_Unicode_characters]Unicode code points[/url] of each string, which roughly matches the alphabetical order. Internally, lowercase characters are converted to uppercase for the comparison. With different string lengths, returns [code]1[/code] if this string is longer than the [param to] string, or [code]-1[/code] if shorter. Note that the length of empty strings is [i]always[/i] [code]0[/code]. - To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method casecmp_to], [method naturalcasecmp_to], and [method naturalnocasecmp_to]. + To get a [bool] result from a string comparison, use the [code]==[/code] operator instead. See also [method casecmp_to], [method filenocasecmp_to], and [method naturalnocasecmp_to]. </description> </method> <method name="pad_decimals" qualifiers="const"> diff --git a/drivers/gles3/rasterizer_canvas_gles3.cpp b/drivers/gles3/rasterizer_canvas_gles3.cpp index 304dc9e328..9fa95a93f8 100644 --- a/drivers/gles3/rasterizer_canvas_gles3.cpp +++ b/drivers/gles3/rasterizer_canvas_gles3.cpp @@ -38,6 +38,7 @@ #include "core/config/project_settings.h" #include "core/math/geometry_2d.h" +#include "core/math/transform_interpolator.h" #include "servers/rendering/rendering_server_default.h" #include "storage/config.h" #include "storage/material_storage.h" @@ -226,7 +227,15 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_ ERR_CONTINUE(!clight); } - Vector2 canvas_light_pos = p_canvas_transform.xform(l->xform.get_origin()); //convert light position to canvas coordinates, as all computation is done in canvas coords to avoid precision loss + Transform2D final_xform; + if (!RSG::canvas->_interpolation_data.interpolation_enabled || !l->interpolated) { + final_xform = l->xform_curr; + } else { + real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); + TransformInterpolator::interpolate_transform_2d(l->xform_prev, l->xform_curr, final_xform, f); + } + // Convert light position to canvas coordinates, as all computation is done in canvas coordinates to avoid precision loss. + Vector2 canvas_light_pos = p_canvas_transform.xform(final_xform.get_origin()); state.light_uniforms[index].position[0] = canvas_light_pos.x; state.light_uniforms[index].position[1] = canvas_light_pos.y; @@ -820,7 +829,7 @@ void RasterizerCanvasGLES3::_record_item_commands(const Item *p_item, RID p_rend Transform2D base_transform = p_canvas_transform_inverse * p_item->final_transform; if (p_offset.x || p_offset.y) { - base_transform *= Transform2D(0, p_offset / p_item->xform.get_scale()); + base_transform *= Transform2D(0, p_offset / p_item->xform_curr.get_scale()); // TODO: Interpolate or explain why not needed. } Transform2D draw_transform; // Used by transform command diff --git a/drivers/gles3/storage/texture_storage.cpp b/drivers/gles3/storage/texture_storage.cpp index a032535827..c955b3f708 100644 --- a/drivers/gles3/storage/texture_storage.cpp +++ b/drivers/gles3/storage/texture_storage.cpp @@ -2812,8 +2812,7 @@ void TextureStorage::_render_target_allocate_sdf(RenderTarget *rt) { } rt->process_size = size * scale / 100; - rt->process_size.x = MAX(rt->process_size.x, 1); - rt->process_size.y = MAX(rt->process_size.y, 1); + rt->process_size = rt->process_size.max(Size2i(1, 1)); glGenTextures(2, rt->sdf_texture_process); glBindTexture(GL_TEXTURE_2D, rt->sdf_texture_process[0]); diff --git a/drivers/vulkan/rendering_device_driver_vulkan.cpp b/drivers/vulkan/rendering_device_driver_vulkan.cpp index 297407da41..1906d168fe 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_device_driver_vulkan.cpp @@ -760,8 +760,7 @@ Error RenderingDeviceDriverVulkan::_check_device_capabilities() { vrs_capabilities.max_texel_size.y = vrs_properties.maxFragmentShadingRateAttachmentTexelSize.height; // We'll attempt to default to a texel size of 16x16. - vrs_capabilities.texel_size.x = CLAMP(16, vrs_capabilities.min_texel_size.x, vrs_capabilities.max_texel_size.x); - vrs_capabilities.texel_size.y = CLAMP(16, vrs_capabilities.min_texel_size.y, vrs_capabilities.max_texel_size.y); + vrs_capabilities.texel_size = Vector2i(16, 16).clamp(vrs_capabilities.min_texel_size, vrs_capabilities.max_texel_size); print_verbose(String(" Attachment fragment shading rate") + String(", min texel size: (") + itos(vrs_capabilities.min_texel_size.x) + String(", ") + itos(vrs_capabilities.min_texel_size.y) + String(")") + String(", max texel size: (") + itos(vrs_capabilities.max_texel_size.x) + String(", ") + itos(vrs_capabilities.max_texel_size.y) + String(")")); } diff --git a/editor/animation_bezier_editor.cpp b/editor/animation_bezier_editor.cpp index fd06bf0533..a16446aea6 100644 --- a/editor/animation_bezier_editor.cpp +++ b/editor/animation_bezier_editor.cpp @@ -34,6 +34,7 @@ #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" +#include "editor/gui/editor_spin_slider.h" #include "editor/themes/editor_scale.h" #include "scene/gui/view_panner.h" #include "scene/resources/text_line.h" @@ -266,23 +267,11 @@ void AnimationBezierTrackEdit::_notification(int p_what) { RBMap<String, Vector<int>> track_indices; int track_count = animation->get_track_count(); for (int i = 0; i < track_count; ++i) { - if (animation->track_get_type(i) != Animation::TrackType::TYPE_BEZIER) { + if (!_is_track_displayed(i)) { continue; } String base_path = animation->track_get_path(i); - if (is_filtered) { - if (root && root->has_node(base_path)) { - Node *node = root->get_node(base_path); - if (!node) { - continue; // No node, no filter. - } - if (!EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) { - continue; // Skip track due to not selected. - } - } - } - int end = base_path.find(":"); if (end != -1) { base_path = base_path.substr(0, end + 1); @@ -520,28 +509,11 @@ void AnimationBezierTrackEdit::_notification(int p_what) { float scale = timeline->get_zoom_scale(); for (int i = 0; i < track_count; ++i) { - if (animation->track_get_type(i) != Animation::TrackType::TYPE_BEZIER || hidden_tracks.has(i)) { - continue; - } - - if (hidden_tracks.has(i) || locked_tracks.has(i)) { + if (!_is_track_curves_displayed(i) || locked_tracks.has(i)) { continue; } int key_count = animation->track_get_key_count(i); - String path = animation->track_get_path(i); - - if (is_filtered) { - if (root && root->has_node(path)) { - Node *node = root->get_node(path); - if (!node) { - continue; // No node, no filter. - } - if (!EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) { - continue; // Skip track due to not selected. - } - } - } for (int j = 0; j < key_count; ++j) { float offset = animation->track_get_key_time(i, j); @@ -648,6 +620,43 @@ void AnimationBezierTrackEdit::_notification(int p_what) { } } +// Check if a track is displayed in the bezier editor (track type = bezier and track not filtered). +bool AnimationBezierTrackEdit::_is_track_displayed(int p_track_index) { + if (animation->track_get_type(p_track_index) != Animation::TrackType::TYPE_BEZIER) { + return false; + } + + if (is_filtered) { + String path = animation->track_get_path(p_track_index); + if (root && root->has_node(path)) { + Node *node = root->get_node(path); + if (!node) { + return false; // No node, no filter. + } + if (!EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) { + return false; // Skip track due to not selected. + } + } + } + + return true; +} + +// Check if the curves for a track are displayed in the editor (not hidden). Includes the check on the track visibility. +bool AnimationBezierTrackEdit::_is_track_curves_displayed(int p_track_index) { + //Is the track is visible in the editor? + if (!_is_track_displayed(p_track_index)) { + return false; + } + + //And curves visible? + if (hidden_tracks.has(p_track_index)) { + return false; + } + + return true; +} + Ref<Animation> AnimationBezierTrackEdit::get_animation() const { return animation; } @@ -741,6 +750,60 @@ void AnimationBezierTrackEdit::set_filtered(bool p_filtered) { queue_redraw(); } +void AnimationBezierTrackEdit::auto_fit_vertically() { + int track_count = animation->get_track_count(); + real_t minimum_value = INFINITY; + real_t maximum_value = -INFINITY; + + int nb_track_visible = 0; + for (int i = 0; i < track_count; ++i) { + if (!_is_track_curves_displayed(i) || locked_tracks.has(i)) { + continue; + } + + int key_count = animation->track_get_key_count(i); + + for (int j = 0; j < key_count; ++j) { + real_t value = animation->bezier_track_get_key_value(i, j); + + minimum_value = MIN(value, minimum_value); + maximum_value = MAX(value, maximum_value); + + // We also want to includes the handles... + Vector2 in_vec = animation->bezier_track_get_key_in_handle(i, j); + Vector2 out_vec = animation->bezier_track_get_key_out_handle(i, j); + + minimum_value = MIN(value + in_vec.y, minimum_value); + maximum_value = MAX(value + in_vec.y, maximum_value); + minimum_value = MIN(value + out_vec.y, minimum_value); + maximum_value = MAX(value + out_vec.y, maximum_value); + } + + nb_track_visible++; + } + + if (nb_track_visible == 0) { + // No visible track... we will not adjust the vertical zoom + return; + } + + if (Math::is_finite(minimum_value) && Math::is_finite(maximum_value)) { + _zoom_vertically(minimum_value, maximum_value); + queue_redraw(); + } +} + +void AnimationBezierTrackEdit::_zoom_vertically(real_t p_minimum_value, real_t p_maximum_value) { + real_t target_height = p_maximum_value - p_minimum_value; + if (target_height <= CMP_EPSILON) { + timeline_v_scroll = p_maximum_value; + return; + } + + timeline_v_scroll = (p_maximum_value + p_minimum_value) / 2.0; + timeline_v_zoom = target_height / ((get_size().height - timeline->get_size().height) * 0.9); +} + void AnimationBezierTrackEdit::_zoom_changed() { queue_redraw(); play_position->queue_redraw(); @@ -838,7 +901,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { if (p_event->is_pressed()) { if (ED_IS_SHORTCUT("animation_editor/duplicate_selected_keys", p_event)) { if (!read_only) { - duplicate_selected_keys(-1.0); + duplicate_selected_keys(-1.0, false); } accept_event(); } @@ -856,7 +919,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { } if (ED_IS_SHORTCUT("animation_editor/paste_keys", p_event)) { if (!read_only) { - paste_keys(-1.0); + paste_keys(-1.0, false); } accept_event(); } @@ -931,10 +994,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { } if (Math::is_finite(minimum_value) && Math::is_finite(maximum_value)) { - timeline_v_scroll = (maximum_value + minimum_value) / 2.0; - if (maximum_value - minimum_value > CMP_EPSILON) { - timeline_v_zoom = (maximum_value - minimum_value) / ((get_size().height - timeline->get_size().height) * 0.9); - } + _zoom_vertically(minimum_value, maximum_value); } queue_redraw(); @@ -1179,6 +1239,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { moving_selection_attempt = true; moving_selection = false; + moving_selection_mouse_begin_x = mb->get_position().x; moving_selection_from_key = index; moving_selection_from_track = selected_track; moving_selection_offset = Vector2(); @@ -1260,7 +1321,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { if (moving_selection_attempt && mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { if (!read_only) { - if (moving_selection && (abs(moving_selection_offset.x) > 0 || abs(moving_selection_offset.y) > 0)) { + if (moving_selection && (abs(moving_selection_offset.x) > CMP_EPSILON || abs(moving_selection_offset.y) > CMP_EPSILON)) { //combit it EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); @@ -1274,7 +1335,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { } // 2- remove overlapped keys for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { - real_t newtime = editor->snap_time(animation->track_get_key_time(E->get().first, E->get().second) + moving_selection_offset.x); + real_t newtime = animation->track_get_key_time(E->get().first, E->get().second) + moving_selection_offset.x; int idx = animation->track_find_key(E->get().first, newtime, Animation::FIND_MODE_APPROX); if (idx == -1) { @@ -1298,7 +1359,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { // 3-move the keys (re insert them) for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { - real_t newpos = editor->snap_time(animation->track_get_key_time(E->get().first, E->get().second) + moving_selection_offset.x); + real_t newpos = animation->track_get_key_time(E->get().first, E->get().second) + moving_selection_offset.x; Array key = animation->track_get_key_value(E->get().first, E->get().second); real_t h = key[0]; h += moving_selection_offset.y; @@ -1316,7 +1377,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { // 4-(undo) remove inserted keys for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { - real_t newpos = editor->snap_time(animation->track_get_key_time(E->get().first, E->get().second) + moving_selection_offset.x); + real_t newpos = animation->track_get_key_time(E->get().first, E->get().second) + moving_selection_offset.x; undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->get().first, newpos); } @@ -1358,7 +1419,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { 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 = editor->snap_time(oldpos + moving_selection_offset.x); + 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); @@ -1366,14 +1427,15 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { undo_redo->commit_action(); - moving_selection = false; } 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); } + moving_selection = false; moving_selection_attempt = false; + moving_selection_mouse_begin_x = 0.0; queue_redraw(); } } @@ -1385,11 +1447,22 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { select_single_attempt = IntPair(-1, -1); } - float y = (get_size().height / 2.0 - mm->get_position().y) * timeline_v_zoom + timeline_v_scroll; - float x = editor->snap_time(((mm->get_position().x - limit) / timeline->get_zoom_scale()) + timeline->get_value()); - if (!read_only) { - moving_selection_offset = Vector2(x - animation->track_get_key_time(moving_selection_from_track, moving_selection_from_key), y - animation->bezier_track_get_key_value(moving_selection_from_track, moving_selection_from_key)); + float y = (get_size().height / 2.0 - mm->get_position().y) * timeline_v_zoom + timeline_v_scroll; + float moving_selection_begin_time = ((moving_selection_mouse_begin_x - limit) / timeline->get_zoom_scale()) + timeline->get_value(); + float new_time = ((mm->get_position().x - limit) / timeline->get_zoom_scale()) + timeline->get_value(); + float moving_selection_pivot = animation->track_get_key_time(moving_selection_from_track, moving_selection_from_key); + float time_delta = new_time - moving_selection_begin_time; + + float snapped_time = editor->snap_time(moving_selection_pivot + time_delta); + float time_offset = 0.0; + if (abs(moving_selection_offset.x) > CMP_EPSILON || (snapped_time > moving_selection_pivot && time_delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && time_delta < -CMP_EPSILON)) { + time_offset = snapped_time - moving_selection_pivot; + } + float moving_selection_begin_value = animation->bezier_track_get_key_value(moving_selection_from_track, moving_selection_from_key); + float y_offset = y - moving_selection_begin_value; + + moving_selection_offset = Vector2(time_offset, y_offset); } additional_moving_handle_lefts.clear(); @@ -1503,17 +1576,18 @@ bool AnimationBezierTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p moving_selection_attempt = true; moving_selection_from_key = pair.second; moving_selection_from_track = pair.first; + moving_selection_mouse_begin_x = p_pos.x; moving_selection_offset = Vector2(); moving_handle_track = pair.first; moving_handle_left = animation->bezier_track_get_key_in_handle(pair.first, pair.second); moving_handle_right = animation->bezier_track_get_key_out_handle(pair.first, pair.second); if (selection.has(pair)) { - select_single_attempt = pair; moving_selection = false; } else { moving_selection = true; } + select_single_attempt = pair; } set_animation_and_track(animation, pair.first, read_only); @@ -1583,25 +1657,28 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) { real_t time = ((menu_insert_key.x - limit) / timeline->get_zoom_scale()) + timeline->get_value(); - while (animation->track_find_key(selected_track, time, Animation::FIND_MODE_APPROX) != -1) { - time += 0.001; - } - switch (p_index) { case MENU_KEY_INSERT: { if (animation->get_track_count() > 0) { + if (editor->snap->is_pressed() && editor->step->get_value() != 0) { + time = editor->snap_time(time); + } + while (animation->track_find_key(selected_track, time, Animation::FIND_MODE_APPROX) != -1) { + 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); 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); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", selected_track, time); undo_redo->commit_action(); queue_redraw(); } } break; case MENU_KEY_DUPLICATE: { - duplicate_selected_keys(time); + duplicate_selected_keys(time, true); } break; case MENU_KEY_DELETE: { delete_selection(); @@ -1613,7 +1690,7 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) { copy_selected_keys(false); } break; case MENU_KEY_PASTE: { - paste_keys(time); + paste_keys(time, true); } break; case MENU_KEY_SET_HANDLE_FREE: { _change_selected_keys_handle_mode(Animation::HANDLE_MODE_FREE); @@ -1636,7 +1713,7 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) { } } -void AnimationBezierTrackEdit::duplicate_selected_keys(real_t p_ofs) { +void AnimationBezierTrackEdit::duplicate_selected_keys(real_t p_ofs, bool p_ofs_valid) { if (selection.size() == 0) { return; } @@ -1656,7 +1733,14 @@ void AnimationBezierTrackEdit::duplicate_selected_keys(real_t p_ofs) { for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { real_t t = animation->track_get_key_time(E->get().first, E->get().second); - real_t insert_pos = p_ofs >= 0 ? p_ofs : timeline->get_play_position(); + real_t insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position(); + + if (p_ofs_valid) { + if (editor->snap->is_pressed() && editor->step->get_value() != 0) { + insert_pos = editor->snap_time(insert_pos); + } + } + real_t dst_time = t + (insert_pos - top_time); int existing_idx = animation->track_find_key(E->get().first, dst_time, Animation::FIND_MODE_APPROX); @@ -1734,7 +1818,7 @@ void AnimationBezierTrackEdit::copy_selected_keys(bool p_cut) { } } -void AnimationBezierTrackEdit::paste_keys(real_t p_ofs) { +void AnimationBezierTrackEdit::paste_keys(real_t p_ofs, bool p_ofs_valid) { if (editor->is_key_clipboard_active() && animation.is_valid() && (selected_track >= 0 && selected_track < animation->get_track_count())) { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Animation Paste Keys")); @@ -1765,7 +1849,12 @@ void AnimationBezierTrackEdit::paste_keys(real_t p_ofs) { for (int i = 0; i < editor->key_clipboard.keys.size(); i++) { const AnimationTrackEditor::KeyClipboard::Key key = editor->key_clipboard.keys[i]; - float insert_pos = p_ofs >= 0 ? p_ofs : timeline->get_play_position(); + float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position(); + if (p_ofs_valid) { + if (editor->snap->is_pressed() && editor->step->get_value() != 0) { + insert_pos = editor->snap_time(insert_pos); + } + } float dst_time = key.time + insert_pos; int existing_idx = animation->track_find_key(selected_track, dst_time, Animation::FIND_MODE_APPROX); diff --git a/editor/animation_bezier_editor.h b/editor/animation_bezier_editor.h index ec2b52221e..ab667421f3 100644 --- a/editor/animation_bezier_editor.h +++ b/editor/animation_bezier_editor.h @@ -101,12 +101,15 @@ class AnimationBezierTrackEdit : public Control { void _menu_selected(int p_index); void _play_position_draw(); + bool _is_track_displayed(int p_track_index); + bool _is_track_curves_displayed(int p_track_index); Vector2 insert_at_pos; typedef Pair<int, int> IntPair; bool moving_selection_attempt = false; + float moving_selection_mouse_begin_x = 0.0; IntPair select_single_attempt; bool moving_selection = false; int moving_selection_from_key = 0; @@ -188,6 +191,7 @@ class AnimationBezierTrackEdit : public Control { void _draw_track(int p_track, const Color &p_color); float _bezier_h_to_pixel(float p_h); + void _zoom_vertically(real_t p_minimum_value, real_t p_maximum_value); protected: static void _bind_methods(); @@ -208,13 +212,14 @@ public: void set_editor(AnimationTrackEditor *p_editor); void set_root(Node *p_root); void set_filtered(bool p_filtered); + void auto_fit_vertically(); void set_play_position(real_t p_pos); void update_play_position(); - void duplicate_selected_keys(real_t p_ofs); + void duplicate_selected_keys(real_t p_ofs, bool p_ofs_valid); void copy_selected_keys(bool p_cut); - void paste_keys(real_t p_ofs); + 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); diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index 6f1439a91f..2746e9acb4 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -1305,7 +1305,11 @@ void AnimationTimelineEdit::_zoom_changed(double) { } float AnimationTimelineEdit::get_zoom_scale() const { - float zv = zoom->get_max() - zoom->get_value(); + return _get_zoom_scale(zoom->get_value()); +} + +float AnimationTimelineEdit::_get_zoom_scale(double p_zoom_value) const { + float zv = zoom->get_max() - p_zoom_value; if (zv < 1) { zv = 1.0 - zv; return Math::pow(1.0f + zv, 8.0f) * 100; @@ -1633,6 +1637,68 @@ void AnimationTimelineEdit::set_zoom(Range *p_zoom) { zoom->connect("value_changed", callable_mp(this, &AnimationTimelineEdit::_zoom_changed)); } +void AnimationTimelineEdit::auto_fit() { + if (!animation.is_valid()) { + return; + } + + float anim_end = animation->get_length(); + float anim_start = 0; + + // Search for keyframe outside animation boundaries to include keyframes before animation start and after animation length. + int track_count = animation->get_track_count(); + for (int track = 0; track < track_count; ++track) { + for (int i = 0; i < animation->track_get_key_count(track); i++) { + float key_time = animation->track_get_key_time(track, i); + if (key_time > anim_end) { + anim_end = key_time; + } + if (key_time < anim_start) { + anim_start = key_time; + } + } + } + + float anim_length = anim_end - anim_start; + int timeline_width_pixels = get_size().width - get_buttons_width() - get_name_limit(); + + // I want a little buffer at the end... (5% looks nice and we should keep some space for the bezier handles) + timeline_width_pixels *= 0.95; + + // The technique is to reuse the _get_zoom_scale function directly to be sure that the auto_fit is always calculated + // the same way as the zoom slider. It's a little bit more calculation then doing the inverse of get_zoom_scale but + // it's really easier to understand and should always be accurate. + float new_zoom = zoom->get_max(); + while (true) { + double test_zoom_scale = _get_zoom_scale(new_zoom); + + if (anim_length * test_zoom_scale <= timeline_width_pixels) { + // It fits... + break; + } + + new_zoom -= zoom->get_step(); + + if (new_zoom <= zoom->get_min()) { + new_zoom = zoom->get_min(); + break; + } + } + + // Horizontal scroll to get_min which should include keyframes that are before the animation start. + hscroll->set_value(hscroll->get_min()); + // Set the zoom value... the signal value_changed will be emitted and the timeline will be refreshed correctly! + zoom->set_value(new_zoom); + // The new zoom value must be applied correctly so the scrollbar are updated before we move the scrollbar to + // the beginning of the animation, hence the call deferred. + callable_mp(this, &AnimationTimelineEdit::_scroll_to_start).call_deferred(); +} + +void AnimationTimelineEdit::_scroll_to_start() { + // Horizontal scroll to get_min which should include keyframes that are before the animation start. + hscroll->set_value(hscroll->get_min()); +} + void AnimationTimelineEdit::set_track_edit(AnimationTrackEdit *p_track_edit) { track_edit = p_track_edit; } @@ -2001,13 +2067,13 @@ void AnimationTrackEdit::_notification(int p_what) { for (int i = 0; i < animation->track_get_key_count(track); i++) { float offset = animation->track_get_key_time(track, i) - timeline->get_value(); if (editor->is_key_selected(track, i) && editor->is_moving_selection()) { - offset = editor->snap_time(offset + editor->get_moving_selection_offset(), true); + offset = offset + editor->get_moving_selection_offset(); } offset = offset * scale + limit; if (i < animation->track_get_key_count(track) - 1) { float offset_n = animation->track_get_key_time(track, i + 1) - timeline->get_value(); if (editor->is_key_selected(track, i + 1) && editor->is_moving_selection()) { - offset_n = editor->snap_time(offset_n + editor->get_moving_selection_offset()); + offset_n = offset_n + editor->get_moving_selection_offset(); } offset_n = offset_n * scale + limit; @@ -2688,7 +2754,7 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) { if (p_event->is_pressed()) { if (ED_IS_SHORTCUT("animation_editor/duplicate_selected_keys", p_event)) { if (!read_only) { - emit_signal(SNAME("duplicate_request"), -1.0); + emit_signal(SNAME("duplicate_request"), -1.0, false); } accept_event(); } @@ -2707,7 +2773,7 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) { if (ED_IS_SHORTCUT("animation_editor/paste_keys", p_event)) { if (!read_only) { - emit_signal(SNAME("paste_request"), -1.0); + emit_signal(SNAME("paste_request"), -1.0, false); } accept_event(); } @@ -2908,8 +2974,10 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) { if (mb.is_valid() && moving_selection_attempt) { if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { moving_selection_attempt = false; - if (moving_selection) { - emit_signal(SNAME("move_selection_commit")); + if (moving_selection && moving_selection_effective) { + if (abs(editor->get_moving_selection_offset()) > CMP_EPSILON) { + emit_signal(SNAME("move_selection_commit")); + } } else if (select_single_attempt != -1) { emit_signal(SNAME("select_key"), select_single_attempt, true); } @@ -2982,8 +3050,18 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) { emit_signal(SNAME("move_selection_begin")); } - float new_ofs = (mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale(); - emit_signal(SNAME("move_selection"), new_ofs - moving_selection_from_ofs); + float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); + float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); + float delta = new_time - moving_begin_time; + float snapped_time = editor->snap_time(moving_selection_pivot + delta); + + float offset = 0.0; + if (abs(editor->get_moving_selection_offset()) > CMP_EPSILON || (snapped_time > moving_selection_pivot && delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && delta < -CMP_EPSILON)) { + offset = snapped_time - moving_selection_pivot; + moving_selection_effective = true; + } + + emit_signal(SNAME("move_selection"), offset); } } @@ -3026,12 +3104,16 @@ bool AnimationTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggre if (editor->is_key_selected(track, key_idx)) { if (p_deselectable) { emit_signal(SNAME("deselect_key"), key_idx); + moving_selection_pivot = 0.0f; + moving_selection_mouse_begin_x = 0.0f; } } else { emit_signal(SNAME("select_key"), key_idx, false); moving_selection_attempt = true; + moving_selection_effective = false; select_single_attempt = -1; - moving_selection_from_ofs = (p_pos.x - limit) / timeline->get_zoom_scale(); + moving_selection_pivot = animation->track_get_key_time(track, key_idx); + moving_selection_mouse_begin_x = p_pos.x; } } else { if (!editor->is_key_selected(track, key_idx)) { @@ -3042,12 +3124,15 @@ bool AnimationTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggre } moving_selection_attempt = true; - moving_selection_from_ofs = (p_pos.x - limit) / timeline->get_zoom_scale(); + moving_selection_effective = false; + moving_selection_pivot = animation->track_get_key_time(track, key_idx); + moving_selection_mouse_begin_x = p_pos.x; } if (read_only) { moving_selection_attempt = false; - moving_selection_from_ofs = 0.0f; + moving_selection_pivot = 0.0f; + moving_selection_mouse_begin_x = 0.0f; } return true; } @@ -3182,7 +3267,7 @@ void AnimationTrackEdit::_menu_selected(int p_index) { emit_signal(SNAME("insert_key"), insert_at_pos); } break; case MENU_KEY_DUPLICATE: { - emit_signal(SNAME("duplicate_request"), insert_at_pos); + emit_signal(SNAME("duplicate_request"), insert_at_pos, true); } break; case MENU_KEY_CUT: { emit_signal(SNAME("cut_request")); @@ -3191,7 +3276,7 @@ void AnimationTrackEdit::_menu_selected(int p_index) { emit_signal(SNAME("copy_request")); } break; case MENU_KEY_PASTE: { - emit_signal(SNAME("paste_request"), insert_at_pos); + emit_signal(SNAME("paste_request"), insert_at_pos, true); } break; case MENU_KEY_ADD_RESET: { emit_signal(SNAME("create_reset_request")); @@ -3265,11 +3350,11 @@ void AnimationTrackEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("move_selection_commit")); ADD_SIGNAL(MethodInfo("move_selection_cancel")); - ADD_SIGNAL(MethodInfo("duplicate_request", PropertyInfo(Variant::FLOAT, "offset"))); + ADD_SIGNAL(MethodInfo("duplicate_request", PropertyInfo(Variant::FLOAT, "offset"), PropertyInfo(Variant::BOOL, "is_offset_valid"))); ADD_SIGNAL(MethodInfo("create_reset_request")); ADD_SIGNAL(MethodInfo("copy_request")); ADD_SIGNAL(MethodInfo("cut_request")); - ADD_SIGNAL(MethodInfo("paste_request", PropertyInfo(Variant::FLOAT, "offset"))); + ADD_SIGNAL(MethodInfo("paste_request", PropertyInfo(Variant::FLOAT, "offset"), PropertyInfo(Variant::BOOL, "is_offset_valid"))); ADD_SIGNAL(MethodInfo("delete_request")); } @@ -3446,6 +3531,8 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re step->set_read_only(false); snap->set_disabled(false); snap_mode->set_disabled(false); + auto_fit->set_disabled(false); + auto_fit_bezier->set_disabled(false); imported_anim_warning->hide(); for (int i = 0; i < animation->get_track_count(); i++) { @@ -3466,6 +3553,8 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re snap->set_disabled(true); snap_mode->set_disabled(true); bezier_edit_icon->set_disabled(true); + auto_fit->set_disabled(true); + auto_fit_bezier->set_disabled(true); } } @@ -4763,6 +4852,8 @@ void AnimationTrackEditor::_notification(int p_what) { inactive_player_warning->set_icon(get_editor_theme_icon(SNAME("NodeWarning"))); main_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree"))); edit->get_popup()->set_item_icon(edit->get_popup()->get_item_index(EDIT_APPLY_RESET), get_editor_theme_icon(SNAME("Reload"))); + auto_fit->set_icon(get_editor_theme_icon(SNAME("AnimationAutoFit"))); + auto_fit_bezier->set_icon(get_editor_theme_icon(SNAME("AnimationAutoFitBezier"))); } break; case NOTIFICATION_READY: { @@ -5086,6 +5177,7 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { undo_redo->create_action(TTR("Add Position Key")); undo_redo->add_do_method(animation.ptr(), "position_track_insert_key", p_track, p_ofs, pos); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", p_track, p_ofs); undo_redo->commit_action(); @@ -5106,6 +5198,7 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { undo_redo->create_action(TTR("Add Rotation Key")); undo_redo->add_do_method(animation.ptr(), "rotation_track_insert_key", p_track, p_ofs, rot); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", p_track, p_ofs); undo_redo->commit_action(); @@ -5124,6 +5217,7 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { undo_redo->create_action(TTR("Add Scale Key")); undo_redo->add_do_method(animation.ptr(), "scale_track_insert_key", p_track, p_ofs, base->get_scale()); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", p_track, p_ofs); undo_redo->commit_action(); @@ -5169,6 +5263,7 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { undo_redo->create_action(TTR("Add Track Key")); undo_redo->add_do_method(animation.ptr(), "track_insert_key", p_track, p_ofs, arr); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", p_track, p_ofs); undo_redo->commit_action(); @@ -5181,6 +5276,7 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { undo_redo->create_action(TTR("Add Track Key")); undo_redo->add_do_method(animation.ptr(), "track_insert_key", p_track, p_ofs, ak); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", p_track, p_ofs); undo_redo->commit_action(); } break; @@ -5189,6 +5285,7 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { undo_redo->create_action(TTR("Add Track Key")); undo_redo->add_do_method(animation.ptr(), "track_insert_key", p_track, p_ofs, anim); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", p_track, p_ofs); undo_redo->commit_action(); } break; @@ -5229,6 +5326,7 @@ void AnimationTrackEditor::_add_method_key(const String &p_method) { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Add Method Track Key")); undo_redo->add_do_method(animation.ptr(), "track_insert_key", insert_key_from_track_call_track, insert_key_from_track_call_ofs, d); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", insert_key_from_track_call_track, insert_key_from_track_call_ofs); undo_redo->commit_action(); @@ -5427,7 +5525,7 @@ void AnimationTrackEditor::_move_selection_commit() { } // 2 - Remove overlapped keys. for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) { - float newtime = snap_time(E->get().pos + motion); + float newtime = E->get().pos + motion; int idx = animation->track_find_key(E->key().track, newtime, Animation::FIND_MODE_APPROX); if (idx == -1) { continue; @@ -5452,13 +5550,13 @@ void AnimationTrackEditor::_move_selection_commit() { // 3 - Move the keys (Reinsert them). for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) { - float newpos = snap_time(E->get().pos + motion); + float newpos = E->get().pos + motion; undo_redo->add_do_method(animation.ptr(), "track_insert_key", E->key().track, newpos, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key)); } // 4 - (Undo) Remove inserted keys. for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) { - float newpos = snap_time(E->get().pos + motion); + float newpos = E->get().pos + motion; undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newpos); } @@ -5478,7 +5576,7 @@ void AnimationTrackEditor::_move_selection_commit() { // 7 - Reselect. for (RBMap<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) { float oldpos = E->get().pos; - float newpos = snap_time(oldpos + motion); + float newpos = oldpos + motion; undo_redo->add_do_method(this, "_select_at_anim", animation, E->key().track, newpos); undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, oldpos); @@ -5617,6 +5715,8 @@ void AnimationTrackEditor::_cancel_bezier_edit() { bezier_edit->hide(); scroll->show(); bezier_edit_icon->set_pressed(false); + auto_fit->show(); + auto_fit_bezier->hide(); } void AnimationTrackEditor::_bezier_edit(int p_for_track) { @@ -5625,6 +5725,8 @@ void AnimationTrackEditor::_bezier_edit(int p_for_track) { bezier_edit->set_animation_and_track(animation, p_for_track, read_only); scroll->hide(); bezier_edit->show(); + auto_fit->hide(); + auto_fit_bezier->show(); // Search everything within the track and curve - edit it. } @@ -5635,7 +5737,7 @@ void AnimationTrackEditor::_bezier_track_set_key_handle_mode(Animation *p_anim, p_anim->bezier_track_set_key_handle_mode(p_track, p_index, p_mode, p_set_mode); } -void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, int p_track) { +void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, bool p_ofs_valid, int p_track) { if (selection.size() && animation.is_valid()) { int top_track = 0x7FFFFFFF; float top_time = 1e10; @@ -5688,7 +5790,13 @@ void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, int p_track) { const SelectedKey &sk = E->key(); float t = animation->track_get_key_time(sk.track, sk.key); - float insert_pos = p_ofs >= 0 ? p_ofs : timeline->get_play_position(); + float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position(); + + if (p_ofs_valid) { + if (snap->is_pressed() && step->get_value() != 0) { + insert_pos = snap_time(insert_pos); + } + } float dst_time = t + (insert_pos - top_time); int dst_track = sk.track + (start_track - top_track); @@ -5795,7 +5903,7 @@ void AnimationTrackEditor::_set_key_clipboard(int p_top_track, float p_top_time, } } -void AnimationTrackEditor::_anim_paste_keys(float p_ofs, int p_track) { +void AnimationTrackEditor::_anim_paste_keys(float p_ofs, bool p_ofs_valid, int p_track) { if (is_key_clipboard_active() && animation.is_valid()) { int start_track = p_track; if (p_track == -1) { // Pasting from shortcut or Edit menu. @@ -5830,7 +5938,13 @@ void AnimationTrackEditor::_anim_paste_keys(float p_ofs, int p_track) { for (int i = 0; i < key_clipboard.keys.size(); i++) { const KeyClipboard::Key key = key_clipboard.keys[i]; - float insert_pos = p_ofs >= 0 ? p_ofs : timeline->get_play_position(); + float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position(); + + if (p_ofs_valid) { + if (snap->is_pressed() && step->get_value() != 0) { + insert_pos = snap_time(insert_pos); + } + } float dst_time = key.time + insert_pos; int dst_track = key.track + start_track; @@ -6427,10 +6541,10 @@ void AnimationTrackEditor::_edit_menu_pressed(int p_option) { case EDIT_DUPLICATE_SELECTED_KEYS: { if (bezier_edit->is_visible()) { - bezier_edit->duplicate_selected_keys(-1.0); + bezier_edit->duplicate_selected_keys(-1.0, false); break; } - _anim_duplicate_keys(-1.0, -1.0); + _anim_duplicate_keys(-1.0, false, -1.0); } break; case EDIT_CUT_KEYS: { if (bezier_edit->is_visible()) { @@ -6447,7 +6561,7 @@ void AnimationTrackEditor::_edit_menu_pressed(int p_option) { _anim_copy_keys(false); } break; case EDIT_PASTE_KEYS: { - _anim_paste_keys(-1.0, -1.0); + _anim_paste_keys(-1.0, false, -1.0); } break; case EDIT_MOVE_FIRST_SELECTED_KEY_TO_CURSOR: { if (moving_selection || selection.is_empty()) { @@ -6865,6 +6979,18 @@ bool AnimationTrackEditor::is_grouping_tracks() { return !view_group->is_pressed(); } +void AnimationTrackEditor::_auto_fit() { + timeline->auto_fit(); +} + +void AnimationTrackEditor::_auto_fit_bezier() { + timeline->auto_fit(); + + if (bezier_edit->is_visible()) { + bezier_edit->auto_fit_vertically(); + } +} + void AnimationTrackEditor::_selection_changed() { if (selected_filter->is_pressed()) { _update_tracks(); // Needs updatin. @@ -7179,6 +7305,19 @@ AnimationTrackEditor::AnimationTrackEditor() { bottom_hb->add_child(zoom); timeline->set_zoom(zoom); + auto_fit = memnew(Button); + auto_fit->set_flat(true); + auto_fit->connect("pressed", callable_mp(this, &AnimationTrackEditor::_auto_fit)); + auto_fit->set_shortcut(ED_SHORTCUT("animation_editor/auto_fit", TTR("Fit to panel"), KeyModifierMask::ALT | Key::F)); + bottom_hb->add_child(auto_fit); + + auto_fit_bezier = memnew(Button); + auto_fit_bezier->set_flat(true); + auto_fit_bezier->set_visible(false); + auto_fit_bezier->connect("pressed", callable_mp(this, &AnimationTrackEditor::_auto_fit_bezier)); + auto_fit_bezier->set_shortcut(ED_SHORTCUT("animation_editor/auto_fit", TTR("Fit to panel"), KeyModifierMask::ALT | Key::F)); + bottom_hb->add_child(auto_fit_bezier); + edit = memnew(MenuButton); edit->set_shortcut_context(this); edit->set_text(TTR("Edit")); diff --git a/editor/animation_track_editor.h b/editor/animation_track_editor.h index d0da7b0062..f449b51b81 100644 --- a/editor/animation_track_editor.h +++ b/editor/animation_track_editor.h @@ -182,6 +182,9 @@ class AnimationTimelineEdit : public Range { virtual void gui_input(const Ref<InputEvent> &p_event) override; void _track_added(int p_track); + float _get_zoom_scale(double p_zoom_value) const; + void _scroll_to_start(); + protected: static void _bind_methods(); void _notification(int p_what); @@ -197,6 +200,7 @@ public: void set_track_edit(AnimationTrackEdit *p_track_edit); void set_zoom(Range *p_zoom); Range *get_zoom() const { return zoom; } + void auto_fit(); void set_play_position(float p_pos); float get_play_position() const; @@ -286,9 +290,11 @@ class AnimationTrackEdit : public Control { mutable int dropping_at = 0; float insert_at_pos = 0.0f; bool moving_selection_attempt = false; + bool moving_selection_effective = false; + float moving_selection_pivot = 0.0f; + float moving_selection_mouse_begin_x = 0.0f; int select_single_attempt = -1; bool moving_selection = false; - float moving_selection_from_ofs = 0.0f; bool in_group = false; AnimationTrackEditor *editor = nullptr; @@ -404,6 +410,8 @@ class AnimationTrackEditor : public VBoxContainer { Button *snap = nullptr; Button *bezier_edit_icon = nullptr; OptionButton *snap_mode = nullptr; + Button *auto_fit = nullptr; + Button *auto_fit_bezier = nullptr; Button *imported_anim_warning = nullptr; void _show_imported_anim_warning(); @@ -579,18 +587,21 @@ class AnimationTrackEditor : public VBoxContainer { void _cleanup_animation(Ref<Animation> p_animation); - void _anim_duplicate_keys(float p_ofs, int p_track); + void _anim_duplicate_keys(float p_ofs, bool p_ofs_valid, int p_track); void _anim_copy_keys(bool p_cut); bool _is_track_compatible(int p_target_track_idx, Variant::Type p_source_value_type, Animation::TrackType p_source_track_type); - void _anim_paste_keys(float p_ofs, int p_track); + void _anim_paste_keys(float p_ofs, bool p_ofs_valid, int p_track); void _view_group_toggle(); Button *view_group = nullptr; Button *selected_filter = nullptr; + void _auto_fit(); + void _auto_fit_bezier(); + void _selection_changed(); ConfirmationDialog *track_copy_dialog = nullptr; diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 6c16951186..0e369c91e1 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -941,6 +941,10 @@ void CodeTextEditor::_complete_request() { font_color = completion_string_color; } else if (e.insert_text.begins_with("##") || e.insert_text.begins_with("///")) { font_color = completion_doc_comment_color; + } else if (e.insert_text.begins_with("&")) { + font_color = completion_string_name_color; + } else if (e.insert_text.begins_with("^")) { + font_color = completion_node_path_color; } else if (e.insert_text.begins_with("#") || e.insert_text.begins_with("//")) { font_color = completion_comment_color; } @@ -997,6 +1001,8 @@ void CodeTextEditor::update_editor_settings() { // Theme: Highlighting completion_font_color = EDITOR_GET("text_editor/theme/highlighting/completion_font_color"); completion_string_color = EDITOR_GET("text_editor/theme/highlighting/string_color"); + completion_string_name_color = EDITOR_GET("text_editor/theme/highlighting/gdscript/string_name_color"); + completion_node_path_color = EDITOR_GET("text_editor/theme/highlighting/gdscript/node_path_color"); completion_comment_color = EDITOR_GET("text_editor/theme/highlighting/comment_color"); completion_doc_comment_color = EDITOR_GET("text_editor/theme/highlighting/doc_comment_color"); diff --git a/editor/code_editor.h b/editor/code_editor.h index c888619ac0..87031b672c 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -184,6 +184,8 @@ class CodeTextEditor : public VBoxContainer { Color completion_font_color; Color completion_string_color; + Color completion_string_name_color; + Color completion_node_path_color; Color completion_comment_color; Color completion_doc_comment_color; CodeTextEditorCodeCompleteFunc code_complete_func; diff --git a/editor/editor_atlas_packer.cpp b/editor/editor_atlas_packer.cpp index 5de4869c88..a0f5f1bf11 100644 --- a/editor/editor_atlas_packer.cpp +++ b/editor/editor_atlas_packer.cpp @@ -72,8 +72,7 @@ void EditorAtlasPacker::chart_pack(Vector<Chart> &charts, int &r_width, int &r_h Vector2 vtx = chart.vertices[chart.faces[j].vertex[k]]; vtx -= aabb.position; vtx /= divide_by; - vtx.x = MIN(vtx.x, w - 1); - vtx.y = MIN(vtx.y, h - 1); + vtx = vtx.min(Vector2(w - 1, h - 1)); v[k] = vtx; } diff --git a/editor/editor_data.cpp b/editor/editor_data.cpp index 47642c1592..4b68a21cb9 100644 --- a/editor/editor_data.cpp +++ b/editor/editor_data.cpp @@ -721,8 +721,12 @@ bool EditorData::check_and_update_scene(int p_idx) { } new_scene->set_scene_file_path(edited_scene[p_idx].root->get_scene_file_path()); - - memdelete(edited_scene[p_idx].root); + Node *old_root = edited_scene[p_idx].root; + for (int i = 0; i < old_root->get_child_count(); i++) { + memdelete(old_root->get_child(i)); + } + old_root->replace_by(new_scene); + memdelete(old_root); edited_scene.write[p_idx].root = new_scene; if (!new_scene->get_scene_file_path().is_empty()) { edited_scene.write[p_idx].path = new_scene->get_scene_file_path(); diff --git a/editor/editor_dock_manager.cpp b/editor/editor_dock_manager.cpp index 08719d6bf0..3fba07f686 100644 --- a/editor/editor_dock_manager.cpp +++ b/editor/editor_dock_manager.cpp @@ -49,8 +49,6 @@ EditorDockManager *EditorDockManager::singleton = nullptr; -static const char *META_TOGGLE_SHORTCUT = "_toggle_shortcut"; - void DockSplitContainer::_update_visibility() { if (is_updating) { return; @@ -106,359 +104,215 @@ void DockSplitContainer::remove_child_notify(Node *p_child) { _update_visibility(); } -void EditorDockManager::_dock_select_popup_theme_changed() { - if (dock_float) { - dock_float->set_icon(dock_select_popup->get_editor_theme_icon(SNAME("MakeFloating"))); - } - if (dock_select_popup->is_layout_rtl()) { - dock_tab_move_left->set_icon(dock_select_popup->get_editor_theme_icon(SNAME("Forward"))); - dock_tab_move_right->set_icon(dock_select_popup->get_editor_theme_icon(SNAME("Back"))); - } else { - dock_tab_move_left->set_icon(dock_select_popup->get_editor_theme_icon(SNAME("Back"))); - dock_tab_move_right->set_icon(dock_select_popup->get_editor_theme_icon(SNAME("Forward"))); - } - - dock_to_bottom->set_icon(dock_select_popup->get_editor_theme_icon(SNAME("ControlAlignBottomWide"))); +void EditorDockManager::_dock_split_dragged(int p_offset) { + EditorNode::get_singleton()->save_editor_layout_delayed(); } -void EditorDockManager::_dock_popup_exit() { - dock_select_rect_over_idx = -1; - dock_select->queue_redraw(); -} +void EditorDockManager::_dock_container_gui_input(const Ref<InputEvent> &p_input, TabContainer *p_dock_container) { + Ref<InputEventMouseButton> mb = p_input; -void EditorDockManager::_dock_pre_popup(int p_dock_slot) { - dock_popup_selected_idx = p_dock_slot; - dock_bottom_selected_idx = -1; + if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT && mb->is_pressed()) { + int tab_id = p_dock_container->get_tab_bar()->get_hovered_tab(); + if (tab_id < 0) { + return; + } - if (bool(dock_slot[p_dock_slot]->get_current_tab_control()->call("_can_dock_horizontal"))) { - dock_to_bottom->show(); - } else { - dock_to_bottom->hide(); + // Right click context menu. + dock_context_popup->set_dock(p_dock_container->get_tab_control(tab_id)); + dock_context_popup->set_position(p_dock_container->get_screen_position() + mb->get_position()); + dock_context_popup->popup(); } +} - if (dock_float) { - dock_float->show(); +void EditorDockManager::_bottom_dock_button_gui_input(const Ref<InputEvent> &p_input, Control *p_dock, Button *p_bottom_button) { + Ref<InputEventMouseButton> mb = p_input; + + if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT && mb->is_pressed()) { + // Right click context menu. + dock_context_popup->set_dock(p_dock); + dock_context_popup->set_position(p_bottom_button->get_screen_position() + mb->get_position()); + dock_context_popup->popup(); } - dock_tab_move_right->show(); - dock_tab_move_left->show(); } -void EditorDockManager::_dock_move_left() { - if (dock_popup_selected_idx < 0 || dock_popup_selected_idx >= DOCK_SLOT_MAX) { - return; - } - Control *current_ctl = dock_slot[dock_popup_selected_idx]->get_tab_control(dock_slot[dock_popup_selected_idx]->get_current_tab()); - Control *prev_ctl = dock_slot[dock_popup_selected_idx]->get_tab_control(dock_slot[dock_popup_selected_idx]->get_current_tab() - 1); - if (!current_ctl || !prev_ctl) { +void EditorDockManager::_dock_container_update_visibility(TabContainer *p_dock_container) { + if (!docks_visible) { return; } - dock_slot[dock_popup_selected_idx]->move_child(current_ctl, prev_ctl->get_index(false)); - dock_select->queue_redraw(); - _edit_current(); - emit_signal(SNAME("layout_changed")); + // Hide the dock container if there are no tabs. + p_dock_container->set_visible(p_dock_container->get_tab_count() > 0); } -void EditorDockManager::_dock_move_right() { - if (dock_popup_selected_idx < 0 || dock_popup_selected_idx >= DOCK_SLOT_MAX) { +void EditorDockManager::_update_layout() { + if (!dock_context_popup->is_inside_tree() || EditorNode::get_singleton()->is_exiting()) { return; } - Control *current_ctl = dock_slot[dock_popup_selected_idx]->get_tab_control(dock_slot[dock_popup_selected_idx]->get_current_tab()); - Control *next_ctl = dock_slot[dock_popup_selected_idx]->get_tab_control(dock_slot[dock_popup_selected_idx]->get_current_tab() + 1); - if (!current_ctl || !next_ctl) { - return; - } - dock_slot[dock_popup_selected_idx]->move_child(next_ctl, current_ctl->get_index(false)); - dock_select->queue_redraw(); - _edit_current(); - emit_signal(SNAME("layout_changed")); + EditorNode::get_singleton()->edit_current(); + dock_context_popup->docks_updated(); + EditorNode::get_singleton()->save_editor_layout_delayed(); } -void EditorDockManager::_dock_select_input(const Ref<InputEvent> &p_input) { - Ref<InputEventMouse> me = p_input; - - if (me.is_valid()) { - Vector2 point = me->get_position(); - - int nrect = -1; - for (int i = 0; i < DOCK_SLOT_MAX; i++) { - if (dock_select_rect[i].has_point(point)) { - nrect = i; - break; - } - } - - if (nrect != dock_select_rect_over_idx) { - dock_select->queue_redraw(); - dock_select_rect_over_idx = nrect; - } - - if (nrect == -1) { - return; - } - - Ref<InputEventMouseButton> mb = me; - - if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) { - if (dock_bottom_selected_idx != -1) { - EditorNode::get_bottom_panel()->remove_item(bottom_docks[dock_bottom_selected_idx]); - - bottom_docks[dock_bottom_selected_idx]->call("_set_dock_horizontal", false); - - dock_slot[nrect]->add_child(bottom_docks[dock_bottom_selected_idx]); - dock_slot[nrect]->show(); - bottom_docks.remove_at(dock_bottom_selected_idx); - dock_bottom_selected_idx = -1; - dock_popup_selected_idx = nrect; // Move to dock popup selected. - dock_select->queue_redraw(); +void EditorDockManager::_window_close_request(WindowWrapper *p_wrapper) { + // Give the dock back to the original owner. + Control *dock = _close_window(p_wrapper); + ERR_FAIL_COND(!all_docks.has(dock)); - update_dock_slots_visibility(true); - - _edit_current(); - emit_signal(SNAME("layout_changed")); - } - - if (dock_popup_selected_idx != nrect) { - dock_slot[nrect]->move_tab_from_tab_container(dock_slot[dock_popup_selected_idx], dock_slot[dock_popup_selected_idx]->get_current_tab(), dock_slot[nrect]->get_tab_count()); - - if (dock_slot[dock_popup_selected_idx]->get_tab_count() == 0) { - dock_slot[dock_popup_selected_idx]->hide(); - } else { - dock_slot[dock_popup_selected_idx]->set_current_tab(0); - } - - dock_popup_selected_idx = nrect; - dock_slot[nrect]->show(); - dock_select->queue_redraw(); - - update_dock_slots_visibility(true); - - _edit_current(); - emit_signal(SNAME("layout_changed")); - } - } - } + all_docks[dock].open = false; + open_dock(dock); + focus_dock(dock); } -void EditorDockManager::_dock_select_draw() { - Size2 s = dock_select->get_size(); - s.y /= 2.0; - s.x /= 6.0; +Control *EditorDockManager::_close_window(WindowWrapper *p_wrapper) { + p_wrapper->set_block_signals(true); + Control *dock = p_wrapper->release_wrapped_control(); + p_wrapper->set_block_signals(false); + ERR_FAIL_COND_V(!all_docks.has(dock), nullptr); - Color used = Color(0.6, 0.6, 0.6, 0.8); - Color used_selected = Color(0.8, 0.8, 0.8, 0.8); - Color tab_selected = dock_select->get_theme_color(SNAME("mono_color"), EditorStringName(Editor)); - Color unused = used; - unused.a = 0.4; - Color unusable = unused; - unusable.a = 0.1; + all_docks[dock].dock_window = nullptr; + dock_windows.erase(p_wrapper); + p_wrapper->queue_free(); + return dock; +} - Rect2 unr(s.x * 2, 0, s.x * 2, s.y * 2); - unr.position += Vector2(2, 5); - unr.size -= Vector2(4, 7); +void EditorDockManager::_open_dock_in_window(Control *p_dock, bool p_show_window) { + ERR_FAIL_NULL(p_dock); - dock_select->draw_rect(unr, unusable); + Size2 borders = Size2(4, 4) * EDSCALE; + // Remember size and position before removing it from the main window. + Size2 dock_size = p_dock->get_size() + borders * 2; + Point2 dock_screen_pos = p_dock->get_screen_position(); - dock_tab_move_left->set_disabled(true); - dock_tab_move_right->set_disabled(true); + WindowWrapper *wrapper = memnew(WindowWrapper); + wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), all_docks[p_dock].title)); + wrapper->set_margins_enabled(true); - if (dock_popup_selected_idx != -1 && dock_slot[dock_popup_selected_idx]->get_tab_count()) { - dock_tab_move_left->set_disabled(dock_slot[dock_popup_selected_idx]->get_current_tab() == 0); - dock_tab_move_right->set_disabled(dock_slot[dock_popup_selected_idx]->get_current_tab() >= dock_slot[dock_popup_selected_idx]->get_tab_count() - 1); - } + EditorNode::get_singleton()->get_gui_base()->add_child(wrapper); - for (int i = 0; i < DOCK_SLOT_MAX; i++) { - Vector2 ofs; - - switch (i) { - case DOCK_SLOT_LEFT_UL: { - } break; - case DOCK_SLOT_LEFT_BL: { - ofs.y += s.y; - } break; - case DOCK_SLOT_LEFT_UR: { - ofs.x += s.x; - } break; - case DOCK_SLOT_LEFT_BR: { - ofs += s; - } break; - case DOCK_SLOT_RIGHT_UL: { - ofs.x += s.x * 4; - } break; - case DOCK_SLOT_RIGHT_BL: { - ofs.x += s.x * 4; - ofs.y += s.y; - - } break; - case DOCK_SLOT_RIGHT_UR: { - ofs.x += s.x * 4; - ofs.x += s.x; - - } break; - case DOCK_SLOT_RIGHT_BR: { - ofs.x += s.x * 4; - ofs += s; - - } break; - } + _move_dock(p_dock, nullptr); + wrapper->set_wrapped_control(p_dock); - Rect2 r(ofs, s); - dock_select_rect[i] = r; - r.position += Vector2(2, 5); - r.size -= Vector2(4, 7); + all_docks[p_dock].dock_window = wrapper; + all_docks[p_dock].open = true; + p_dock->show(); - if (i == dock_select_rect_over_idx) { - dock_select->draw_rect(r, used_selected); - } else if (dock_slot[i]->get_tab_count() == 0) { - dock_select->draw_rect(r, unused); - } else { - dock_select->draw_rect(r, used); - } + wrapper->connect("window_close_requested", callable_mp(this, &EditorDockManager::_window_close_request).bind(wrapper)); + dock_windows.push_back(wrapper); - for (int j = 0; j < MIN(3, dock_slot[i]->get_tab_count()); j++) { - int xofs = (r.size.width / 3) * j; - Color c = used; - if (i == dock_popup_selected_idx && (dock_slot[i]->get_current_tab() > 3 || dock_slot[i]->get_current_tab() == j)) { - c = tab_selected; - } - dock_select->draw_rect(Rect2(2 + ofs.x + xofs, ofs.y, r.size.width / 3 - 1, 3), c); - } + if (p_show_window) { + wrapper->restore_window(Rect2i(dock_screen_pos, dock_size), EditorNode::get_singleton()->get_gui_base()->get_window()->get_current_screen()); + _update_layout(); + p_dock->get_window()->grab_focus(); } } -void EditorDockManager::_dock_split_dragged(int p_offset) { - EditorNode::get_singleton()->save_editor_layout_delayed(); -} - -void EditorDockManager::_dock_tab_changed(int p_tab) { - // Update visibility but don't set current tab. - update_dock_slots_visibility(true); -} +void EditorDockManager::_restore_dock_to_saved_window(Control *p_dock, const Dictionary &p_window_dump) { + if (!all_docks[p_dock].dock_window) { + _open_dock_in_window(p_dock, false); + } -void EditorDockManager::_edit_current() { - EditorNode::get_singleton()->edit_current(); + all_docks[p_dock].dock_window->restore_window_from_saved_position( + p_window_dump.get("window_rect", Rect2i()), + p_window_dump.get("window_screen", -1), + p_window_dump.get("window_screen_rect", Rect2i())); } -void EditorDockManager::_dock_floating_close_request(WindowWrapper *p_wrapper) { - int dock_slot_num = p_wrapper->get_meta("dock_slot"); - int dock_slot_index = p_wrapper->get_meta("dock_index"); - - // Give back the dock to the original owner. - Control *dock = p_wrapper->release_wrapped_control(); - - int target_index = MIN(dock_slot_index, dock_slot[dock_slot_num]->get_tab_count()); - dock_slot[dock_slot_num]->add_child(dock); - dock_slot[dock_slot_num]->move_child(dock, target_index); - dock_slot[dock_slot_num]->set_current_tab(target_index); +void EditorDockManager::_dock_move_to_bottom(Control *p_dock) { + _move_dock(p_dock, nullptr); - floating_docks.erase(p_wrapper); - p_wrapper->queue_free(); + all_docks[p_dock].at_bottom = true; + all_docks[p_dock].previous_at_bottom = false; - update_dock_slots_visibility(true); + p_dock->call("_set_dock_horizontal", true); - _edit_current(); + // 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("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); } -void EditorDockManager::_dock_make_selected_float() { - Control *dock = dock_slot[dock_popup_selected_idx]->get_current_tab_control(); - _dock_make_float(dock, dock_popup_selected_idx); +void EditorDockManager::_dock_remove_from_bottom(Control *p_dock) { + all_docks[p_dock].at_bottom = false; + all_docks[p_dock].previous_at_bottom = true; - dock_select_popup->hide(); - _edit_current(); + EditorNode::get_bottom_panel()->remove_item(p_dock); + p_dock->call("_set_dock_horizontal", false); } -void EditorDockManager::bottom_dock_show_placement_popup(const Rect2i &p_position, Control *p_dock) { - dock_bottom_selected_idx = bottom_docks.find(p_dock); - ERR_FAIL_COND(dock_bottom_selected_idx == -1); - dock_popup_selected_idx = -1; - dock_to_bottom->hide(); - - Vector2 popup_pos = p_position.position; - popup_pos.y += p_position.size.height; - - if (!EditorNode::get_singleton()->get_gui_base()->is_layout_rtl()) { - popup_pos.x -= dock_select_popup->get_size().width; - popup_pos.x += p_position.size.width; - } - dock_select_popup->set_position(popup_pos); - dock_select_popup->popup(); - if (dock_float) { - dock_float->hide(); - } - dock_tab_move_right->hide(); - dock_tab_move_left->hide(); +bool EditorDockManager::_is_dock_at_bottom(Control *p_dock) { + ERR_FAIL_COND_V(!all_docks.has(p_dock), false); + return all_docks[p_dock].at_bottom; } -void EditorDockManager::_dock_move_selected_to_bottom() { - Control *dock = dock_slot[dock_popup_selected_idx]->get_current_tab_control(); - dock_slot[dock_popup_selected_idx]->remove_child(dock); - - dock->call("_set_dock_horizontal", true); - - bottom_docks.push_back(dock); - - // Force docks moved to the bottom to appear first in the list, and give them their associated shortcut to toggle their bottom panel. - EditorNode::get_bottom_panel()->add_item(dock->get_name(), dock, dock->get_meta(META_TOGGLE_SHORTCUT), true); +void EditorDockManager::_move_dock_tab_index(Control *p_dock, int p_tab_index, bool p_set_current) { + TabContainer *dock_tab_container = Object::cast_to<TabContainer>(p_dock->get_parent()); + if (!dock_tab_container) { + return; + } - dock_select_popup->hide(); - update_dock_slots_visibility(true); - _edit_current(); - emit_signal(SNAME("layout_changed")); + dock_tab_container->set_block_signals(true); + int target_index = CLAMP(p_tab_index, 0, dock_tab_container->get_tab_count() - 1); + dock_tab_container->move_child(p_dock, dock_tab_container->get_tab_control(target_index)->get_index(false)); + all_docks[p_dock].previous_tab_index = target_index; - EditorNode::get_bottom_panel()->make_item_visible(dock); + if (p_set_current) { + dock_tab_container->set_current_tab(target_index); + } + dock_tab_container->set_block_signals(false); } -void EditorDockManager::_dock_make_float(Control *p_dock, int p_slot_index, bool p_show_window) { +void EditorDockManager::_move_dock(Control *p_dock, Control *p_target, int p_tab_index, bool p_set_current) { ERR_FAIL_NULL(p_dock); + ERR_FAIL_COND_MSG(!all_docks.has(p_dock), vformat("Cannot move unknown dock '%s'.", p_dock->get_name())); - Size2 borders = Size2(4, 4) * EDSCALE; - // Remember size and position before removing it from the main window. - Size2 dock_size = p_dock->get_size() + borders * 2; - Point2 dock_screen_pos = p_dock->get_screen_position(); - - int dock_index = p_dock->get_index() - 1; - dock_slot[p_slot_index]->remove_child(p_dock); - - WindowWrapper *wrapper = memnew(WindowWrapper); - wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), p_dock->get_name())); - wrapper->set_margins_enabled(true); - - EditorNode::get_singleton()->get_gui_base()->add_child(wrapper); - - wrapper->set_wrapped_control(p_dock); - wrapper->set_meta("dock_slot", p_slot_index); - wrapper->set_meta("dock_index", dock_index); - wrapper->set_meta("dock_name", p_dock->get_name().operator String()); - p_dock->show(); - - wrapper->connect("window_close_requested", callable_mp(this, &EditorDockManager::_dock_floating_close_request).bind(wrapper)); - - dock_select_popup->hide(); - - if (p_show_window) { - wrapper->restore_window(Rect2i(dock_screen_pos, dock_size), EditorNode::get_singleton()->get_gui_base()->get_window()->get_current_screen()); + Node *parent = p_dock->get_parent(); + if (parent == p_target) { + if (p_tab_index >= 0 && parent) { + // Only change the tab index. + _move_dock_tab_index(p_dock, p_tab_index, p_set_current); + } + return; } - update_dock_slots_visibility(true); - - floating_docks.push_back(wrapper); - - _edit_current(); -} - -void EditorDockManager::_restore_floating_dock(const Dictionary &p_dock_dump, Control *p_dock, int p_slot_index) { - WindowWrapper *wrapper = Object::cast_to<WindowWrapper>(p_dock); - if (!wrapper) { - _dock_make_float(p_dock, p_slot_index, false); - wrapper = floating_docks[floating_docks.size() - 1]; + // Remove dock from its existing parent. + if (parent) { + if (all_docks[p_dock].dock_window) { + _close_window(all_docks[p_dock].dock_window); + } else if (all_docks[p_dock].at_bottom) { + _dock_remove_from_bottom(p_dock); + } else { + all_docks[p_dock].previous_at_bottom = false; + TabContainer *parent_tabs = Object::cast_to<TabContainer>(parent); + if (parent_tabs) { + all_docks[p_dock].previous_tab_index = parent_tabs->get_tab_idx_from_control(p_dock); + } + parent->set_block_signals(true); + parent->remove_child(p_dock); + parent->set_block_signals(false); + if (parent_tabs) { + _dock_container_update_visibility(parent_tabs); + } + } } - wrapper->restore_window_from_saved_position( - p_dock_dump.get("window_rect", Rect2i()), - p_dock_dump.get("window_screen", -1), - p_dock_dump.get("window_screen_rect", Rect2i())); + // Add dock to its new parent, at the given tab index. + if (!p_target) { + return; + } + p_target->set_block_signals(true); + p_target->add_child(p_dock); + p_target->set_block_signals(false); + TabContainer *dock_tab_container = Object::cast_to<TabContainer>(p_target); + if (dock_tab_container) { + dock_tab_container->set_tab_title(dock_tab_container->get_tab_idx_from_control(p_dock), all_docks[p_dock].title); + if (p_tab_index >= 0) { + _move_dock_tab_index(p_dock, p_tab_index, p_set_current); + } + _dock_container_update_visibility(dock_tab_container); + } } void EditorDockManager::save_docks_to_config(Ref<ConfigFile> p_layout, const String &p_section) const { + // Save docks by dock slot. for (int i = 0; i < DOCK_SLOT_MAX; i++) { String names; for (int j = 0; j < dock_slot[i]->get_tab_count(); j++) { @@ -485,22 +339,23 @@ void EditorDockManager::save_docks_to_config(Ref<ConfigFile> p_layout, const Str } } + // Save docks in windows. Dictionary floating_docks_dump; - - for (WindowWrapper *wrapper : floating_docks) { + for (WindowWrapper *wrapper : dock_windows) { Control *dock = wrapper->get_wrapped_control(); - Dictionary dock_dump; - dock_dump["window_rect"] = wrapper->get_window_rect(); + Dictionary window_dump; + window_dump["window_rect"] = wrapper->get_window_rect(); int screen = wrapper->get_window_screen(); - dock_dump["window_screen"] = wrapper->get_window_screen(); - dock_dump["window_screen_rect"] = DisplayServer::get_singleton()->screen_get_usable_rect(screen); + window_dump["window_screen"] = wrapper->get_window_screen(); + window_dump["window_screen_rect"] = DisplayServer::get_singleton()->screen_get_usable_rect(screen); String name = dock->get_name(); - floating_docks_dump[name] = dock_dump; + floating_docks_dump[name] = window_dump; - int dock_slot_id = wrapper->get_meta("dock_slot"); + // Append to regular dock section so we know where to restore it to. + int dock_slot_id = all_docks[dock].dock_slot_index; String config_key = "dock_" + itos(dock_slot_id + 1); String names = p_layout->get_value(p_section, config_key, ""); @@ -511,17 +366,39 @@ void EditorDockManager::save_docks_to_config(Ref<ConfigFile> p_layout, const Str } p_layout->set_value(p_section, config_key, names); } - p_layout->set_value(p_section, "dock_floating", floating_docks_dump); + // Save closed and bottom docks. Array bottom_docks_dump; + Array closed_docks_dump; + for (const KeyValue<Control *, DockInfo> &d : all_docks) { + if (!d.value.at_bottom && d.value.open && (!d.value.previous_at_bottom || !d.value.dock_window)) { + continue; + } + // Use the name of the Control since it isn't translated. + String name = d.key->get_name(); + if (d.value.at_bottom || (d.value.previous_at_bottom && d.value.dock_window)) { + bottom_docks_dump.push_back(name); + } + if (!d.value.open) { + closed_docks_dump.push_back(name); + } - for (Control *bdock : bottom_docks) { - bottom_docks_dump.push_back(bdock->get_name()); - } + int dock_slot_id = all_docks[d.key].dock_slot_index; + String config_key = "dock_" + itos(dock_slot_id + 1); + String names = p_layout->get_value(p_section, config_key, ""); + if (names.is_empty()) { + names = name; + } else { + names += "," + name; + } + p_layout->set_value(p_section, config_key, names); + } p_layout->set_value(p_section, "dock_bottom", bottom_docks_dump); + p_layout->set_value(p_section, "dock_closed", closed_docks_dump); + // Save SplitContainer offsets. for (int i = 0; i < vsplits.size(); i++) { if (vsplits[i]->is_visible_in_tree()) { p_layout->set_value(p_section, "dock_split_" + itos(i + 1), vsplits[i]->get_split_offset()); @@ -537,9 +414,18 @@ void EditorDockManager::save_docks_to_config(Ref<ConfigFile> p_layout, const Str void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const String &p_section) { Dictionary floating_docks_dump = p_layout->get_value(p_section, "dock_floating", Dictionary()); + Array dock_bottom = p_layout->get_value(p_section, "dock_bottom", Array()); + Array closed_docks = p_layout->get_value(p_section, "dock_closed", Array()); bool restore_window_on_load = EDITOR_GET("interface/multi_window/restore_windows_on_load"); + // Store the docks by name for easy lookup. + HashMap<String, Control *> dock_map; + for (const KeyValue<Control *, DockInfo> &dock : all_docks) { + dock_map[dock.key->get_name()] = dock.key; + } + + // Load docks by slot. for (int i = 0; i < DOCK_SLOT_MAX; i++) { if (!p_layout->has_section_key(p_section, "dock_" + itos(i + 1))) { continue; @@ -550,135 +436,57 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S for (int j = names.size() - 1; j >= 0; j--) { String name = names[j]; - // FIXME: Find it, in a horribly inefficient way. - int atidx = -1; - int bottom_idx = -1; - Control *node = nullptr; - for (int k = 0; k < DOCK_SLOT_MAX; k++) { - if (!dock_slot[k]->has_node(name)) { - continue; - } - node = Object::cast_to<Control>(dock_slot[k]->get_node(name)); - if (!node) { - continue; - } - atidx = k; - break; - } - - if (atidx == -1) { - // Try floating docks. - for (WindowWrapper *wrapper : floating_docks) { - if (wrapper->get_meta("dock_name") == name) { - if (restore_window_on_load && floating_docks_dump.has(name)) { - _restore_floating_dock(floating_docks_dump[name], wrapper, i); - } else { - atidx = wrapper->get_meta("dock_slot"); - node = wrapper->get_wrapped_control(); - wrapper->set_window_enabled(false); - } - break; - } - } - } - - if (atidx == -1) { - // Try bottom docks. - for (Control *bdock : bottom_docks) { - if (bdock->get_name() == name) { - node = bdock; - bottom_idx = bottom_docks.find(node); - break; - } - } + if (!dock_map.has(name)) { + continue; } + Control *dock = dock_map[name]; - if (!node) { - // Well, it's not anywhere. + if (!all_docks[dock].enabled) { + // Don't open disabled docks. continue; } - - if (atidx == i) { - dock_slot[i]->move_child(node, 0); - } else if (atidx != -1) { - dock_slot[i]->set_block_signals(true); - dock_slot[atidx]->set_block_signals(true); - dock_slot[i]->move_tab_from_tab_container(dock_slot[atidx], dock_slot[atidx]->get_tab_idx_from_control(node), 0); - dock_slot[i]->set_block_signals(false); - dock_slot[atidx]->set_block_signals(false); - } else if (bottom_idx != -1) { - bottom_docks.erase(node); - EditorNode::get_bottom_panel()->remove_item(node); - dock_slot[i]->add_child(node); - node->call("_set_dock_horizontal", 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); + } else { + _move_dock(dock, dock_slot[i], 0); } - WindowWrapper *wrapper = Object::cast_to<WindowWrapper>(node); - if (restore_window_on_load && floating_docks_dump.has(name)) { - if (!dock_slot[i]->is_tab_hidden(dock_slot[i]->get_tab_idx_from_control(node))) { - _restore_floating_dock(floating_docks_dump[name], node, i); - } - } else if (wrapper) { - wrapper->set_window_enabled(false); + if (closed_docks.has(name)) { + _move_dock(dock, closed_dock_parent); + all_docks[dock].open = false; + dock->hide(); + } else { + // Make sure it is open. + all_docks[dock].open = true; + dock->show(); } + + all_docks[dock].dock_slot_index = i; + all_docks[dock].previous_tab_index = j; } + } - if (!p_layout->has_section_key(p_section, "dock_" + itos(i + 1) + "_selected_tab_idx")) { + // Set the selected tabs. + for (int i = 0; i < DOCK_SLOT_MAX; i++) { + if (dock_slot[i]->get_tab_count() == 0 || !p_layout->has_section_key(p_section, "dock_" + itos(i + 1) + "_selected_tab_idx")) { continue; } - int selected_tab_idx = p_layout->get_value(p_section, "dock_" + itos(i + 1) + "_selected_tab_idx"); if (selected_tab_idx >= 0 && selected_tab_idx < dock_slot[i]->get_tab_count()) { - callable_mp(dock_slot[i], &TabContainer::set_current_tab).call_deferred(selected_tab_idx); - } - } - - Array dock_bottom = p_layout->get_value(p_section, "dock_bottom", Array()); - for (int i = 0; i < dock_bottom.size(); i++) { - const String &name = dock_bottom[i]; - // FIXME: Find it, in a horribly inefficient way. - int atidx = -1; - Control *node = nullptr; - for (int k = 0; k < DOCK_SLOT_MAX; k++) { - if (!dock_slot[k]->has_node(name)) { - continue; - } - node = Object::cast_to<Control>(dock_slot[k]->get_node(name)); - if (!node) { - continue; - } - atidx = k; - break; - } - - if (atidx == -1) { - // Try floating docks. - for (WindowWrapper *wrapper : floating_docks) { - if (wrapper->get_meta("dock_name") == name) { - atidx = wrapper->get_meta("dock_slot"); - node = wrapper->get_wrapped_control(); - wrapper->set_window_enabled(false); - break; - } - } - } - - if (node) { - dock_slot[atidx]->remove_child(node); - - node->call("_set_dock_horizontal", true); - - bottom_docks.push_back(node); - // Force docks moved to the bottom to appear first in the list, and give them their associated shortcut to toggle their bottom panel. - EditorNode::get_bottom_panel()->add_item(node->get_name(), node, node->get_meta(META_TOGGLE_SHORTCUT), true); + dock_slot[i]->set_block_signals(true); + dock_slot[i]->set_current_tab(selected_tab_idx); + dock_slot[i]->set_block_signals(false); } } + // Load SplitContainer offsets. for (int i = 0; i < vsplits.size(); i++) { if (!p_layout->has_section_key(p_section, "dock_split_" + itos(i + 1))) { continue; } - int ofs = p_layout->get_value(p_section, "dock_split_" + itos(i + 1)); vsplits[i]->set_split_offset(ofs); } @@ -691,87 +499,155 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S hsplits[i]->set_split_offset(ofs); } - update_dock_slots_visibility(false); - FileSystemDock::get_singleton()->load_layout_from_config(p_layout, p_section); } -void EditorDockManager::update_dock_slots_visibility(bool p_keep_selected_tabs) { - if (!docks_visible) { - for (int i = 0; i < DOCK_SLOT_MAX; i++) { - dock_slot[i]->hide(); - } - } else { - for (int i = 0; i < DOCK_SLOT_MAX; i++) { - int first_tab_visible = -1; - for (int j = 0; j < dock_slot[i]->get_tab_count(); j++) { - if (!dock_slot[i]->is_tab_hidden(j)) { - first_tab_visible = j; - break; - } - } - if (first_tab_visible >= 0) { - dock_slot[i]->show(); - if (p_keep_selected_tabs) { - int current_tab = dock_slot[i]->get_current_tab(); - if (dock_slot[i]->is_tab_hidden(current_tab)) { - dock_slot[i]->set_block_signals(true); - dock_slot[i]->select_next_available(); - dock_slot[i]->set_block_signals(false); - } - } else { - dock_slot[i]->set_block_signals(true); - dock_slot[i]->set_current_tab(first_tab_visible); - dock_slot[i]->set_block_signals(false); - } - } else { - dock_slot[i]->hide(); - } - } +void EditorDockManager::bottom_dock_show_placement_popup(const Rect2i &p_position, Control *p_dock) { + ERR_FAIL_COND(!all_docks.has(p_dock)); + + dock_context_popup->set_dock(p_dock); + + Vector2 popup_pos = p_position.position; + popup_pos.y += p_position.size.height; + + if (!EditorNode::get_singleton()->get_gui_base()->is_layout_rtl()) { + popup_pos.x -= dock_context_popup->get_size().width; + popup_pos.x += p_position.size.width; } + dock_context_popup->set_position(popup_pos); + dock_context_popup->popup(); } -void EditorDockManager::close_all_floating_docks() { - for (WindowWrapper *wrapper : floating_docks) { - wrapper->set_window_enabled(false); +void EditorDockManager::set_dock_enabled(Control *p_dock, bool p_enabled) { + ERR_FAIL_NULL(p_dock); + ERR_FAIL_COND_MSG(!all_docks.has(p_dock), vformat("Cannot set enabled unknown dock '%s'.", p_dock->get_name())); + + if (all_docks[p_dock].enabled == p_enabled) { + return; + } + + all_docks[p_dock].enabled = p_enabled; + if (p_enabled) { + open_dock(p_dock, false); + } else { + close_dock(p_dock); } } -void EditorDockManager::add_control_to_dock(DockSlot p_slot, Control *p_control, const String &p_name, const Ref<Shortcut> &p_shortcut) { - ERR_FAIL_INDEX(p_slot, DOCK_SLOT_MAX); - p_control->set_meta(META_TOGGLE_SHORTCUT, p_shortcut); - dock_slot[p_slot]->add_child(p_control); - if (!p_name.is_empty()) { - dock_slot[p_slot]->set_tab_title(dock_slot[p_slot]->get_tab_idx_from_control(p_control), p_name); +void EditorDockManager::close_dock(Control *p_dock) { + ERR_FAIL_NULL(p_dock); + ERR_FAIL_COND_MSG(!all_docks.has(p_dock), vformat("Cannot close unknown dock '%s'.", p_dock->get_name())); + + if (!all_docks[p_dock].open) { + return; } + + _move_dock(p_dock, closed_dock_parent); + + all_docks[p_dock].open = false; + p_dock->hide(); + + _update_layout(); } -void EditorDockManager::remove_control_from_dock(Control *p_control) { - // If the dock is floating, close it first. - for (WindowWrapper *wrapper : floating_docks) { - if (p_control == wrapper->get_wrapped_control()) { - wrapper->set_window_enabled(false); - break; - } +void EditorDockManager::open_dock(Control *p_dock, bool p_set_current) { + ERR_FAIL_NULL(p_dock); + ERR_FAIL_COND_MSG(!all_docks.has(p_dock), vformat("Cannot open unknown dock '%s'.", p_dock->get_name())); + + if (all_docks[p_dock].open) { + return; } - Control *dock = nullptr; - for (int i = 0; i < DOCK_SLOT_MAX; i++) { - if (p_control->get_parent() == dock_slot[i]) { - dock = dock_slot[i]; - break; + all_docks[p_dock].open = true; + p_dock->show(); + + // Open dock to its previous location. + if (all_docks[p_dock].previous_at_bottom) { + _dock_move_to_bottom(p_dock); + } else { + TabContainer *slot = dock_slot[all_docks[p_dock].dock_slot_index]; + int tab_index = all_docks[p_dock].previous_tab_index; + if (tab_index < 0) { + tab_index = slot->get_tab_count(); } + _move_dock(p_dock, slot, tab_index, p_set_current); } - ERR_FAIL_NULL_MSG(dock, "Control is not in a dock."); + _update_layout(); +} - dock->remove_child(p_control); - update_dock_slots_visibility(); +TabContainer *EditorDockManager::get_dock_tab_container(Control *p_dock) const { + return Object::cast_to<TabContainer>(p_dock->get_parent()); +} + +void EditorDockManager::focus_dock(Control *p_dock) { + ERR_FAIL_NULL(p_dock); + ERR_FAIL_COND_MSG(!all_docks.has(p_dock), vformat("Cannot focus unknown dock '%s'.", p_dock->get_name())); + + if (!all_docks[p_dock].enabled) { + return; + } + + if (!all_docks[p_dock].open) { + open_dock(p_dock); + } + + if (all_docks[p_dock].dock_window) { + p_dock->get_window()->grab_focus(); + return; + } + + if (all_docks[p_dock].at_bottom) { + EditorNode::get_bottom_panel()->make_item_visible(p_dock); + return; + } + + if (!docks_visible) { + return; + } + + TabContainer *tab_container = get_dock_tab_container(p_dock); + if (!tab_container) { + return; + } + int tab_index = tab_container->get_tab_idx_from_control(p_dock); + tab_container->get_tab_bar()->grab_focus(); + tab_container->set_current_tab(tab_index); +} + +void EditorDockManager::add_control_to_dock(DockSlot p_slot, Control *p_dock, const String &p_title, const Ref<Shortcut> &p_shortcut) { + ERR_FAIL_NULL(p_dock); + ERR_FAIL_INDEX(p_slot, DOCK_SLOT_MAX); + ERR_FAIL_COND_MSG(all_docks.has(p_dock), vformat("Cannot add dock '%s', already added.", p_dock->get_name())); + + DockInfo dock_info; + dock_info.title = !p_title.is_empty() ? p_title : String(p_dock->get_name()); + dock_info.dock_slot_index = p_slot; + dock_info.shortcut = p_shortcut; + all_docks[p_dock] = dock_info; + + open_dock(p_dock, false); +} + +void EditorDockManager::remove_control_from_dock(Control *p_dock) { + ERR_FAIL_NULL(p_dock); + ERR_FAIL_COND_MSG(!all_docks.has(p_dock), vformat("Cannot remove unknown dock '%s'.", p_dock->get_name())); + + _move_dock(p_dock, nullptr); + + all_docks.erase(p_dock); + _update_layout(); } void EditorDockManager::set_docks_visible(bool p_show) { + if (docks_visible == p_show) { + return; + } docks_visible = p_show; - update_dock_slots_visibility(true); + for (int i = 0; i < DOCK_SLOT_MAX; i++) { + dock_slot[i]->set_visible(docks_visible && dock_slot[i]->get_tab_count() > 0); + } + _update_layout(); } bool EditorDockManager::are_docks_visible() const { @@ -796,80 +672,307 @@ void EditorDockManager::register_dock_slot(DockSlot p_dock_slot, TabContainer *p p_tab_container->set_custom_minimum_size(Size2(170, 0) * EDSCALE); p_tab_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); - p_tab_container->set_popup(dock_select_popup); - p_tab_container->connect("pre_popup_pressed", callable_mp(this, &EditorDockManager::_dock_pre_popup).bind(p_dock_slot)); + p_tab_container->set_popup(dock_context_popup); + p_tab_container->connect("pre_popup_pressed", callable_mp(dock_context_popup, &DockContextPopup::select_current_dock_in_dock_slot).bind(p_dock_slot)); p_tab_container->set_drag_to_rearrange_enabled(true); p_tab_container->set_tabs_rearrange_group(1); - p_tab_container->connect("tab_changed", callable_mp(this, &EditorDockManager::_dock_tab_changed)); + p_tab_container->connect("tab_changed", callable_mp(this, &EditorDockManager::_update_layout).unbind(1)); + p_tab_container->connect("active_tab_rearranged", callable_mp(this, &EditorDockManager::_update_layout).unbind(1)); + p_tab_container->connect("child_order_changed", callable_mp(this, &EditorDockManager::_dock_container_update_visibility).bind(p_tab_container)); p_tab_container->set_use_hidden_tabs_for_min_size(true); + p_tab_container->get_tab_bar()->connect("gui_input", callable_mp(this, &EditorDockManager::_dock_container_gui_input).bind(p_tab_container)); + p_tab_container->hide(); } int EditorDockManager::get_vsplit_count() const { return vsplits.size(); } -void EditorDockManager::_bind_methods() { - ADD_SIGNAL(MethodInfo("layout_changed")); -} - EditorDockManager::EditorDockManager() { singleton = this; - dock_select_popup = memnew(PopupPanel); - EditorNode::get_singleton()->get_gui_base()->add_child(dock_select_popup); - VBoxContainer *dock_vb = memnew(VBoxContainer); - dock_select_popup->add_child(dock_vb); - dock_select_popup->connect("theme_changed", callable_mp(this, &EditorDockManager::_dock_select_popup_theme_changed)); - - HBoxContainer *dock_hb = memnew(HBoxContainer); - dock_tab_move_left = memnew(Button); - dock_tab_move_left->set_flat(true); - dock_tab_move_left->set_focus_mode(Control::FOCUS_NONE); - dock_tab_move_left->connect("pressed", callable_mp(this, &EditorDockManager::_dock_move_left)); - dock_hb->add_child(dock_tab_move_left); - - Label *dock_label = memnew(Label); - dock_label->set_text(TTR("Dock Position")); - dock_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); - dock_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); - dock_hb->add_child(dock_label); - - dock_tab_move_right = memnew(Button); - dock_tab_move_right->set_flat(true); - dock_tab_move_right->set_focus_mode(Control::FOCUS_NONE); - dock_tab_move_right->connect("pressed", callable_mp(this, &EditorDockManager::_dock_move_right)); - - dock_hb->add_child(dock_tab_move_right); - dock_vb->add_child(dock_hb); + closed_dock_parent = EditorNode::get_singleton()->get_gui_base(); + + dock_context_popup = memnew(DockContextPopup); + EditorNode::get_singleton()->get_gui_base()->add_child(dock_context_popup); +} + +void DockContextPopup::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + if (make_float_button) { + make_float_button->set_icon(get_editor_theme_icon(SNAME("MakeFloating"))); + } + if (is_layout_rtl()) { + tab_move_left_button->set_icon(get_editor_theme_icon(SNAME("Forward"))); + tab_move_right_button->set_icon(get_editor_theme_icon(SNAME("Back"))); + tab_move_left_button->set_tooltip_text(TTR("Move this dock right one tab.")); + tab_move_right_button->set_tooltip_text(TTR("Move this dock left one tab.")); + } else { + tab_move_left_button->set_icon(get_editor_theme_icon(SNAME("Back"))); + tab_move_right_button->set_icon(get_editor_theme_icon(SNAME("Forward"))); + tab_move_left_button->set_tooltip_text(TTR("Move this dock left one tab.")); + tab_move_right_button->set_tooltip_text(TTR("Move this dock right one tab.")); + } + dock_to_bottom_button->set_icon(get_editor_theme_icon(SNAME("ControlAlignBottomWide"))); + } break; + } +} + +void DockContextPopup::_tab_move_left() { + TabContainer *tab_container = dock_manager->get_dock_tab_container(context_dock); + if (!tab_container) { + return; + } + int new_index = tab_container->get_tab_idx_from_control(context_dock) - 1; + dock_manager->_move_dock(context_dock, tab_container, new_index); + dock_manager->_update_layout(); + dock_select->queue_redraw(); +} + +void DockContextPopup::_tab_move_right() { + TabContainer *tab_container = dock_manager->get_dock_tab_container(context_dock); + if (!tab_container) { + return; + } + int new_index = tab_container->get_tab_idx_from_control(context_dock) + 1; + dock_manager->_move_dock(context_dock, tab_container, new_index); + dock_manager->_update_layout(); + dock_select->queue_redraw(); +} + +void DockContextPopup::_float_dock() { + hide(); + dock_manager->_open_dock_in_window(context_dock); +} + +void DockContextPopup::_move_dock_to_bottom() { + hide(); + dock_manager->_dock_move_to_bottom(context_dock); + dock_manager->_update_layout(); +} + +void DockContextPopup::_dock_select_input(const Ref<InputEvent> &p_input) { + Ref<InputEventMouse> me = p_input; + + if (me.is_valid()) { + Vector2 point = me->get_position(); + + int over_dock_slot = -1; + for (int i = 0; i < EditorDockManager::DOCK_SLOT_MAX; i++) { + if (dock_select_rects[i].has_point(point)) { + over_dock_slot = i; + break; + } + } + + if (over_dock_slot != dock_select_rect_over_idx) { + dock_select->queue_redraw(); + dock_select_rect_over_idx = over_dock_slot; + } + + if (over_dock_slot == -1) { + return; + } + + Ref<InputEventMouseButton> mb = me; + TabContainer *target_tab_container = dock_manager->dock_slot[over_dock_slot]; + + if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) { + if (dock_manager->get_dock_tab_container(context_dock) != target_tab_container) { + dock_manager->_move_dock(context_dock, target_tab_container, target_tab_container->get_tab_count()); + dock_manager->all_docks[context_dock].dock_slot_index = over_dock_slot; + dock_manager->_update_layout(); + hide(); + } + } + } +} + +void DockContextPopup::_dock_select_mouse_exited() { + dock_select_rect_over_idx = -1; + dock_select->queue_redraw(); +} + +void DockContextPopup::_dock_select_draw() { + Color used_dock_color = Color(0.6, 0.6, 0.6, 0.8); + Color hovered_dock_color = Color(0.8, 0.8, 0.8, 0.8); + Color tab_selected_color = dock_select->get_theme_color(SNAME("mono_color"), EditorStringName(Editor)); + Color tab_unselected_color = used_dock_color; + Color unused_dock_color = used_dock_color; + unused_dock_color.a = 0.4; + Color unusable_dock_color = unused_dock_color; + unusable_dock_color.a = 0.1; + + // Update sizes. + Size2 dock_size = dock_select->get_size(); + dock_size.x /= 6.0; + dock_size.y /= 2.0; + + Size2 center_panel_size = dock_size * 2.0; + Rect2 center_panel_rect(center_panel_size.x, 0, center_panel_size.x, center_panel_size.y); + + if (dock_select->is_layout_rtl()) { + dock_select_rects[EditorDockManager::DOCK_SLOT_RIGHT_UR] = Rect2(Point2(), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_RIGHT_BR] = Rect2(Point2(0, dock_size.y), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_RIGHT_UL] = Rect2(Point2(dock_size.x, 0), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_RIGHT_BL] = Rect2(dock_size, dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_LEFT_UR] = Rect2(Point2(dock_size.x * 4, 0), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_LEFT_BR] = Rect2(Point2(dock_size.x * 4, dock_size.y), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_LEFT_UL] = Rect2(Point2(dock_size.x * 5, 0), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_LEFT_BL] = Rect2(Point2(dock_size.x * 5, dock_size.y), dock_size); + } else { + dock_select_rects[EditorDockManager::DOCK_SLOT_LEFT_UL] = Rect2(Point2(), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_LEFT_BL] = Rect2(Point2(0, dock_size.y), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_LEFT_UR] = Rect2(Point2(dock_size.x, 0), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_LEFT_BR] = Rect2(dock_size, dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_RIGHT_UL] = Rect2(Point2(dock_size.x * 4, 0), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_RIGHT_BL] = Rect2(Point2(dock_size.x * 4, dock_size.y), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_RIGHT_UR] = Rect2(Point2(dock_size.x * 5, 0), dock_size); + dock_select_rects[EditorDockManager::DOCK_SLOT_RIGHT_BR] = Rect2(Point2(dock_size.x * 5, dock_size.y), dock_size); + } + + int max_tabs = 3; + int rtl_dir = dock_select->is_layout_rtl() ? -1 : 1; + real_t tab_height = 3.0 * EDSCALE; + real_t tab_spacing = 1.0 * EDSCALE; + real_t dock_spacing = 2.0 * EDSCALE; + real_t dock_top_spacing = tab_height + dock_spacing; + + TabContainer *context_tab_container = dock_manager->get_dock_tab_container(context_dock); + int context_tab_index = -1; + if (context_tab_container && context_tab_container->get_tab_count() > 0) { + context_tab_index = context_tab_container->get_tab_idx_from_control(context_dock); + } + + // Draw center panel. + Rect2 center_panel_draw_rect = center_panel_rect.grow_individual(-dock_spacing, -dock_top_spacing, -dock_spacing, -dock_spacing); + dock_select->draw_rect(center_panel_draw_rect, unusable_dock_color); + + // Draw all dock slots. + for (int i = 0; i < EditorDockManager::DOCK_SLOT_MAX; i++) { + Rect2 dock_slot_draw_rect = dock_select_rects[i].grow_individual(-dock_spacing, -dock_top_spacing, -dock_spacing, -dock_spacing); + real_t tab_width = Math::round(dock_slot_draw_rect.size.width / max_tabs); + Rect2 tab_draw_rect = Rect2(dock_slot_draw_rect.position.x, dock_select_rects[i].position.y, tab_width - tab_spacing, tab_height); + if (dock_select->is_layout_rtl()) { + tab_draw_rect.position.x += dock_slot_draw_rect.size.x - tab_draw_rect.size.x; + } + bool is_context_dock = context_tab_container == dock_manager->dock_slot[i]; + int tabs_to_draw = MIN(max_tabs, dock_manager->dock_slot[i]->get_tab_count()); + + if (i == dock_select_rect_over_idx) { + dock_select->draw_rect(dock_slot_draw_rect, hovered_dock_color); + } else if (tabs_to_draw == 0) { + dock_select->draw_rect(dock_slot_draw_rect, unused_dock_color); + } else { + dock_select->draw_rect(dock_slot_draw_rect, used_dock_color); + } + + // Draw tabs above each used dock slot. + for (int j = 0; j < tabs_to_draw; j++) { + Color tab_color = tab_unselected_color; + if (is_context_dock && context_tab_index == j) { + tab_color = tab_selected_color; + } + Rect2 tabj_draw_rect = tab_draw_rect; + tabj_draw_rect.position.x += tab_width * j * rtl_dir; + dock_select->draw_rect(tabj_draw_rect, tab_color); + } + } +} + +void DockContextPopup::_update_buttons() { + TabContainer *context_tab_container = dock_manager->get_dock_tab_container(context_dock); + bool dock_at_bottom = dock_manager->_is_dock_at_bottom(context_dock); + + // Update tab move buttons. + tab_move_left_button->set_disabled(true); + tab_move_right_button->set_disabled(true); + if (!dock_at_bottom && context_tab_container && context_tab_container->get_tab_count() > 0) { + int context_tab_index = context_tab_container->get_tab_idx_from_control(context_dock); + tab_move_left_button->set_disabled(context_tab_index == 0); + tab_move_right_button->set_disabled(context_tab_index >= context_tab_container->get_tab_count() - 1); + } + + dock_to_bottom_button->set_visible(!dock_at_bottom && bool(context_dock->call("_can_dock_horizontal"))); + reset_size(); +} + +void DockContextPopup::select_current_dock_in_dock_slot(int p_dock_slot) { + context_dock = dock_manager->dock_slot[p_dock_slot]->get_current_tab_control(); + _update_buttons(); +} + +void DockContextPopup::set_dock(Control *p_dock) { + context_dock = p_dock; + _update_buttons(); +} + +Control *DockContextPopup::get_dock() const { + return context_dock; +} + +void DockContextPopup::docks_updated() { + if (!is_visible()) { + return; + } + _update_buttons(); +} + +DockContextPopup::DockContextPopup() { + dock_manager = EditorDockManager::get_singleton(); + + dock_select_popup_vb = memnew(VBoxContainer); + add_child(dock_select_popup_vb); + + HBoxContainer *header_hb = memnew(HBoxContainer); + tab_move_left_button = memnew(Button); + tab_move_left_button->set_flat(true); + tab_move_left_button->set_focus_mode(Control::FOCUS_NONE); + tab_move_left_button->connect("pressed", callable_mp(this, &DockContextPopup::_tab_move_left)); + header_hb->add_child(tab_move_left_button); + + Label *position_label = memnew(Label); + position_label->set_text(TTR("Dock Position")); + position_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); + position_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + header_hb->add_child(position_label); + + tab_move_right_button = memnew(Button); + tab_move_right_button->set_flat(true); + tab_move_right_button->set_focus_mode(Control::FOCUS_NONE); + tab_move_right_button->connect("pressed", callable_mp(this, &DockContextPopup::_tab_move_right)); + + header_hb->add_child(tab_move_right_button); + dock_select_popup_vb->add_child(header_hb); dock_select = memnew(Control); dock_select->set_custom_minimum_size(Size2(128, 64) * EDSCALE); - dock_select->connect("gui_input", callable_mp(this, &EditorDockManager::_dock_select_input)); - dock_select->connect("draw", callable_mp(this, &EditorDockManager::_dock_select_draw)); - dock_select->connect("mouse_exited", callable_mp(this, &EditorDockManager::_dock_popup_exit)); + dock_select->connect("gui_input", callable_mp(this, &DockContextPopup::_dock_select_input)); + dock_select->connect("draw", callable_mp(this, &DockContextPopup::_dock_select_draw)); + dock_select->connect("mouse_exited", callable_mp(this, &DockContextPopup::_dock_select_mouse_exited)); dock_select->set_v_size_flags(Control::SIZE_EXPAND_FILL); - dock_vb->add_child(dock_select); + dock_select_popup_vb->add_child(dock_select); - dock_float = memnew(Button); - dock_float->set_text(TTR("Make Floating")); + make_float_button = memnew(Button); + make_float_button->set_text(TTR("Make Floating")); if (!EditorNode::get_singleton()->is_multi_window_enabled()) { - dock_float->set_disabled(true); - dock_float->set_tooltip_text(EditorNode::get_singleton()->get_multiwindow_support_tooltip_text()); + make_float_button->set_disabled(true); + make_float_button->set_tooltip_text(EditorNode::get_singleton()->get_multiwindow_support_tooltip_text()); } else { - dock_float->set_tooltip_text(TTR("Make this dock floating.")); - } - dock_float->set_focus_mode(Control::FOCUS_NONE); - dock_float->set_h_size_flags(Control::SIZE_EXPAND_FILL); - dock_float->connect("pressed", callable_mp(this, &EditorDockManager::_dock_make_selected_float)); - dock_vb->add_child(dock_float); - - dock_to_bottom = memnew(Button); - dock_to_bottom->set_text(TTR("Move to Bottom")); - dock_to_bottom->set_focus_mode(Control::FOCUS_NONE); - dock_to_bottom->set_h_size_flags(Control::SIZE_EXPAND_FILL); - dock_to_bottom->connect("pressed", callable_mp(this, &EditorDockManager::_dock_move_selected_to_bottom)); - dock_to_bottom->hide(); - dock_vb->add_child(dock_to_bottom); - - dock_select_popup->reset_size(); + make_float_button->set_tooltip_text(TTR("Make this dock floating.")); + } + make_float_button->set_focus_mode(Control::FOCUS_NONE); + make_float_button->set_h_size_flags(Control::SIZE_EXPAND_FILL); + make_float_button->connect("pressed", callable_mp(this, &DockContextPopup::_float_dock)); + dock_select_popup_vb->add_child(make_float_button); + + dock_to_bottom_button = memnew(Button); + dock_to_bottom_button->set_text(TTR("Move to Bottom")); + dock_to_bottom_button->set_tooltip_text(TTR("Move this dock to the bottom panel.")); + dock_to_bottom_button->set_focus_mode(Control::FOCUS_NONE); + dock_to_bottom_button->set_h_size_flags(Control::SIZE_EXPAND_FILL); + dock_to_bottom_button->connect("pressed", callable_mp(this, &DockContextPopup::_move_dock_to_bottom)); + dock_to_bottom_button->hide(); + dock_select_popup_vb->add_child(dock_to_bottom_button); } diff --git a/editor/editor_dock_manager.h b/editor/editor_dock_manager.h index 193ccd6541..370c149967 100644 --- a/editor/editor_dock_manager.h +++ b/editor/editor_dock_manager.h @@ -31,13 +31,15 @@ #ifndef EDITOR_DOCK_MANAGER_H #define EDITOR_DOCK_MANAGER_H +#include "scene/gui/popup.h" #include "scene/gui/split_container.h" class Button; class ConfigFile; class Control; -class PopupPanel; +class PopupMenu; class TabContainer; +class VBoxContainer; class WindowWrapper; class DockSplitContainer : public SplitContainer { @@ -53,6 +55,8 @@ protected: virtual void remove_child_notify(Node *p_child) override; }; +class DockContextPopup; + class EditorDockManager : public Object { GDCLASS(EditorDockManager, Object); @@ -70,49 +74,51 @@ public: }; private: + friend class DockContextPopup; + + struct DockInfo { + String title; + bool open = false; + bool enabled = true; + bool at_bottom = false; + int previous_tab_index = -1; + bool previous_at_bottom = false; + WindowWrapper *dock_window = nullptr; + int dock_slot_index = DOCK_SLOT_LEFT_UL; + Ref<Shortcut> shortcut; + }; + static EditorDockManager *singleton; // To access splits easily by index. Vector<DockSplitContainer *> vsplits; Vector<DockSplitContainer *> hsplits; - Vector<WindowWrapper *> floating_docks; - Vector<Control *> bottom_docks; + Vector<WindowWrapper *> dock_windows; TabContainer *dock_slot[DOCK_SLOT_MAX]; + HashMap<Control *, DockInfo> all_docks; bool docks_visible = true; - PopupPanel *dock_select_popup = nullptr; - Button *dock_float = nullptr; - Button *dock_to_bottom = nullptr; - Button *dock_tab_move_left = nullptr; - Button *dock_tab_move_right = nullptr; - Control *dock_select = nullptr; - Rect2 dock_select_rect[DOCK_SLOT_MAX]; - int dock_select_rect_over_idx = -1; - int dock_popup_selected_idx = -1; - int dock_bottom_selected_idx = -1; - - void _dock_select_popup_theme_changed(); - void _dock_popup_exit(); - void _dock_pre_popup(int p_dock_slot); - void _dock_move_left(); - void _dock_move_right(); - void _dock_select_input(const Ref<InputEvent> &p_input); - void _dock_select_draw(); - void _dock_split_dragged(int p_offset); + DockContextPopup *dock_context_popup = nullptr; + Control *closed_dock_parent = nullptr; - void _dock_tab_changed(int p_tab); - void _edit_current(); + void _dock_split_dragged(int p_offset); + void _dock_container_gui_input(const Ref<InputEvent> &p_input, TabContainer *p_dock_container); + void _bottom_dock_button_gui_input(const Ref<InputEvent> &p_input, Control *p_dock, Button *p_bottom_button); + void _dock_container_update_visibility(TabContainer *p_dock_container); + void _update_layout(); - void _dock_floating_close_request(WindowWrapper *p_wrapper); - void _dock_make_selected_float(); - void _dock_make_float(Control *p_control, int p_slot_index, bool p_show_window = true); - void _restore_floating_dock(const Dictionary &p_dock_dump, Control *p_wrapper, int p_slot_index); + void _window_close_request(WindowWrapper *p_wrapper); + Control *_close_window(WindowWrapper *p_wrapper); + void _open_dock_in_window(Control *p_dock, bool p_show_window = true); + void _restore_dock_to_saved_window(Control *p_dock, const Dictionary &p_window_dump); - void _dock_move_selected_to_bottom(); + void _dock_move_to_bottom(Control *p_dock); + void _dock_remove_from_bottom(Control *p_dock); + bool _is_dock_at_bottom(Control *p_dock); -protected: - static void _bind_methods(); + void _move_dock_tab_index(Control *p_dock, int p_tab_index, bool p_set_current); + void _move_dock(Control *p_dock, Control *p_target, int p_tab_index = -1, bool p_set_current = true); public: static EditorDockManager *get_singleton() { return singleton; } @@ -124,19 +130,64 @@ public: void save_docks_to_config(Ref<ConfigFile> p_layout, const String &p_section) const; void load_docks_from_config(Ref<ConfigFile> p_layout, const String &p_section); - void update_dock_slots_visibility(bool p_keep_selected_tabs = false); - void bottom_dock_show_placement_popup(const Rect2i &p_position, Control *p_dock); + void set_dock_enabled(Control *p_dock, bool p_enabled); + void close_dock(Control *p_dock); + void open_dock(Control *p_dock, bool p_set_current = true); + void focus_dock(Control *p_dock); + + TabContainer *get_dock_tab_container(Control *p_dock) const; - void close_all_floating_docks(); + void bottom_dock_show_placement_popup(const Rect2i &p_position, Control *p_dock); void set_docks_visible(bool p_show); bool are_docks_visible() const; - void add_control_to_dock(DockSlot p_slot, Control *p_control, const String &p_name = "", const Ref<Shortcut> &p_shortcut = nullptr); - void remove_control_from_dock(Control *p_control); + void add_control_to_dock(DockSlot p_slot, Control *p_dock, const String &p_title = "", const Ref<Shortcut> &p_shortcut = nullptr); + void remove_control_from_dock(Control *p_dock); EditorDockManager(); }; +class DockContextPopup : public PopupPanel { + GDCLASS(DockContextPopup, PopupPanel); + + VBoxContainer *dock_select_popup_vb = nullptr; + + Button *make_float_button = nullptr; + Button *tab_move_left_button = nullptr; + Button *tab_move_right_button = nullptr; + Button *dock_to_bottom_button = nullptr; + + Control *dock_select = nullptr; + Rect2 dock_select_rects[EditorDockManager::DOCK_SLOT_MAX]; + int dock_select_rect_over_idx = -1; + + Control *context_dock = nullptr; + + EditorDockManager *dock_manager = nullptr; + + void _tab_move_left(); + void _tab_move_right(); + void _float_dock(); + void _move_dock_to_bottom(); + + void _dock_select_input(const Ref<InputEvent> &p_input); + void _dock_select_mouse_exited(); + void _dock_select_draw(); + + void _update_buttons(); + +protected: + void _notification(int p_what); + +public: + void select_current_dock_in_dock_slot(int p_dock_slot); + void set_dock(Control *p_dock); + Control *get_dock() const; + void docks_updated(); + + DockContextPopup(); +}; + #endif // EDITOR_DOCK_MANAGER_H diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index ff42b82435..516b8f3d74 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -50,10 +50,6 @@ EditorFileSystem *EditorFileSystem::singleton = nullptr; //the name is the version, to keep compatibility with different versions of Godot #define CACHE_FILE_NAME "filesystem_cache8" -void EditorFileSystemDirectory::sort_files() { - files.sort_custom<FileInfoSort>(); -} - int EditorFileSystemDirectory::find_file_index(const String &p_file) const { for (int i = 0; i < files.size(); i++) { if (files[i]->file == p_file) { @@ -578,7 +574,7 @@ bool EditorFileSystem::_update_scan_actions() { case ItemAction::ACTION_DIR_ADD: { int idx = 0; for (int i = 0; i < ia.dir->subdirs.size(); i++) { - if (ia.new_dir->name.naturalnocasecmp_to(ia.dir->subdirs[i]->name) < 0) { + if (ia.new_dir->name.filenocasecmp_to(ia.dir->subdirs[i]->name) < 0) { break; } idx++; @@ -600,7 +596,7 @@ bool EditorFileSystem::_update_scan_actions() { case ItemAction::ACTION_FILE_ADD: { int idx = 0; for (int i = 0; i < ia.dir->files.size(); i++) { - if (ia.new_file->file.naturalnocasecmp_to(ia.dir->files[i]->file) < 0) { + if (ia.new_file->file.filenocasecmp_to(ia.dir->files[i]->file) < 0) { break; } idx++; @@ -810,8 +806,8 @@ void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAc da->list_dir_end(); - dirs.sort_custom<NaturalNoCaseComparator>(); - files.sort_custom<NaturalNoCaseComparator>(); + dirs.sort_custom<FileNoCaseComparator>(); + files.sort_custom<FileNoCaseComparator>(); int total = dirs.size() + files.size(); int idx = 0; @@ -832,7 +828,7 @@ void EditorFileSystem::_scan_new_dir(EditorFileSystemDirectory *p_dir, Ref<DirAc int idx2 = 0; for (int i = 0; i < p_dir->subdirs.size(); i++) { - if (efd->name.naturalnocasecmp_to(p_dir->subdirs[i]->name) < 0) { + if (efd->name.filenocasecmp_to(p_dir->subdirs[i]->name) < 0) { break; } idx2++; @@ -1409,7 +1405,7 @@ bool EditorFileSystem::_find_file(const String &p_file, EditorFileSystemDirector int idx2 = 0; for (int j = 0; j < fs->get_subdir_count(); j++) { - if (efsd->name.naturalnocasecmp_to(fs->get_subdir(j)->get_name()) < 0) { + if (efsd->name.filenocasecmp_to(fs->get_subdir(j)->get_name()) < 0) { break; } idx2++; @@ -1766,7 +1762,7 @@ void EditorFileSystem::update_file(const String &p_file) { String file_name = p_file.get_file(); for (int i = 0; i < fs->files.size(); i++) { - if (p_file.naturalnocasecmp_to(fs->files[i]->file) < 0) { + if (p_file.filenocasecmp_to(fs->files[i]->file) < 0) { break; } idx++; diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h index 818d725281..e45d6f8be3 100644 --- a/editor/editor_file_system.h +++ b/editor/editor_file_system.h @@ -68,14 +68,6 @@ class EditorFileSystemDirectory : public Object { String script_class_icon_path; }; - struct FileInfoSort { - bool operator()(const FileInfo *p_a, const FileInfo *p_b) const { - return p_a->file < p_b->file; - } - }; - - void sort_files(); - Vector<FileInfo *> files; static void _bind_methods(); diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index a14c6e8462..ff2b305a46 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -86,8 +86,7 @@ Size2 EditorProperty::get_minimum_size() const { } Size2 minsize = c->get_combined_minimum_size(); - ms.width = MAX(ms.width, minsize.width); - ms.height = MAX(ms.height, minsize.height); + ms = ms.max(minsize); } if (keying) { @@ -1463,8 +1462,7 @@ Size2 EditorInspectorSection::get_minimum_size() const { continue; } Size2 minsize = c->get_combined_minimum_size(); - ms.width = MAX(ms.width, minsize.width); - ms.height = MAX(ms.height, minsize.height); + ms = ms.max(minsize); } Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Tree")); diff --git a/editor/editor_log.cpp b/editor/editor_log.cpp index c0c26bbfeb..667405ff10 100644 --- a/editor/editor_log.cpp +++ b/editor/editor_log.cpp @@ -435,8 +435,7 @@ EditorLog::EditorLog() { clear_button = memnew(Button); clear_button->set_theme_type_variation("FlatButton"); clear_button->set_focus_mode(FOCUS_NONE); - clear_button->set_shortcut(ED_SHORTCUT("editor/clear_output", TTR("Clear Output"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::K)); - clear_button->set_shortcut_context(this); + clear_button->set_shortcut(ED_SHORTCUT("editor/clear_output", TTR("Clear Output"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::K)); clear_button->connect("pressed", callable_mp(this, &EditorLog::_clear_request)); hb_tools->add_child(clear_button); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index c2490a4365..edb6fee75c 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -3789,14 +3789,6 @@ void EditorNode::_set_current_scene_nocheck(int p_idx) { editor_folding.save_scene_folding(editor_data.get_edited_scene_root(p_idx), editor_data.get_scene_path(p_idx)); } - if (editor_data.check_and_update_scene(p_idx)) { - if (editor_data.get_scene_path(p_idx) != "") { - editor_folding.load_scene_folding(editor_data.get_edited_scene_root(p_idx), editor_data.get_scene_path(p_idx)); - } - - EditorUndoRedoManager::get_singleton()->clear_history(false, editor_data.get_scene_history_id(p_idx)); - } - changing_scene = true; editor_data.save_edited_scene_state(editor_selection, &editor_history, _get_main_scene_state()); @@ -3835,6 +3827,14 @@ void EditorNode::_set_current_scene_nocheck(int p_idx) { if (tabs_to_close.is_empty()) { callable_mp(this, &EditorNode::_set_main_scene_state).call_deferred(state, get_edited_scene()); // Do after everything else is done setting up. } + + if (editor_data.check_and_update_scene(p_idx)) { + if (!editor_data.get_scene_path(p_idx).is_empty()) { + editor_folding.load_scene_folding(editor_data.get_edited_scene_root(p_idx), editor_data.get_scene_path(p_idx)); + } + + EditorUndoRedoManager::get_singleton()->clear_history(false, editor_data.get_scene_history_id(p_idx)); + } } void EditorNode::setup_color_picker(ColorPicker *p_picker) { @@ -6062,19 +6062,13 @@ void EditorNode::_resource_loaded(Ref<Resource> p_resource, const String &p_path void EditorNode::_feature_profile_changed() { Ref<EditorFeatureProfile> profile = feature_profile_manager->get_current_profile(); - // FIXME: Close all floating docks to avoid crash. - editor_dock_manager->close_all_floating_docks(); - TabContainer *import_tabs = cast_to<TabContainer>(ImportDock::get_singleton()->get_parent()); - TabContainer *node_tabs = cast_to<TabContainer>(NodeDock::get_singleton()->get_parent()); - TabContainer *fs_tabs = cast_to<TabContainer>(FileSystemDock::get_singleton()->get_parent()); - TabContainer *history_tabs = cast_to<TabContainer>(history_dock->get_parent()); if (profile.is_valid()) { - node_tabs->set_tab_hidden(node_tabs->get_tab_idx_from_control(NodeDock::get_singleton()), profile->is_feature_disabled(EditorFeatureProfile::FEATURE_NODE_DOCK)); + editor_dock_manager->set_dock_enabled(NodeDock::get_singleton(), !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_NODE_DOCK)); // The Import dock is useless without the FileSystem dock. Ensure the configuration is valid. bool fs_dock_disabled = profile->is_feature_disabled(EditorFeatureProfile::FEATURE_FILESYSTEM_DOCK); - fs_tabs->set_tab_hidden(fs_tabs->get_tab_idx_from_control(FileSystemDock::get_singleton()), fs_dock_disabled); - import_tabs->set_tab_hidden(import_tabs->get_tab_idx_from_control(ImportDock::get_singleton()), fs_dock_disabled || profile->is_feature_disabled(EditorFeatureProfile::FEATURE_IMPORT_DOCK)); - history_tabs->set_tab_hidden(history_tabs->get_tab_idx_from_control(history_dock), profile->is_feature_disabled(EditorFeatureProfile::FEATURE_HISTORY_DOCK)); + editor_dock_manager->set_dock_enabled(FileSystemDock::get_singleton(), !fs_dock_disabled); + editor_dock_manager->set_dock_enabled(ImportDock::get_singleton(), !fs_dock_disabled && !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_IMPORT_DOCK)); + editor_dock_manager->set_dock_enabled(history_dock, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_HISTORY_DOCK)); main_editor_buttons[EDITOR_3D]->set_visible(!profile->is_feature_disabled(EditorFeatureProfile::FEATURE_3D)); main_editor_buttons[EDITOR_SCRIPT]->set_visible(!profile->is_feature_disabled(EditorFeatureProfile::FEATURE_SCRIPT)); @@ -6087,18 +6081,16 @@ void EditorNode::_feature_profile_changed() { editor_select(EDITOR_2D); } } else { - import_tabs->set_tab_hidden(import_tabs->get_tab_idx_from_control(ImportDock::get_singleton()), false); - node_tabs->set_tab_hidden(node_tabs->get_tab_idx_from_control(NodeDock::get_singleton()), false); - fs_tabs->set_tab_hidden(fs_tabs->get_tab_idx_from_control(FileSystemDock::get_singleton()), false); - history_tabs->set_tab_hidden(history_tabs->get_tab_idx_from_control(history_dock), false); + editor_dock_manager->set_dock_enabled(ImportDock::get_singleton(), true); + editor_dock_manager->set_dock_enabled(NodeDock::get_singleton(), true); + editor_dock_manager->set_dock_enabled(FileSystemDock::get_singleton(), true); + editor_dock_manager->set_dock_enabled(history_dock, true); main_editor_buttons[EDITOR_3D]->set_visible(true); main_editor_buttons[EDITOR_SCRIPT]->set_visible(true); if (AssetLibraryEditorPlugin::is_available()) { main_editor_buttons[EDITOR_ASSETLIB]->set_visible(true); } } - - editor_dock_manager->update_dock_slots_visibility(); } void EditorNode::_bind_methods() { @@ -6557,7 +6549,6 @@ EditorNode::EditorNode() { right_r_vsplit->add_child(dock_slot[EditorDockManager::DOCK_SLOT_RIGHT_BR]); editor_dock_manager = memnew(EditorDockManager); - editor_dock_manager->connect("layout_changed", callable_mp(this, &EditorNode::_save_editor_layout)); // Save the splits for easier access. editor_dock_manager->add_vsplit(left_l_vsplit); diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp index 8a15f4912a..a6c7d6b617 100644 --- a/editor/editor_properties_array_dict.cpp +++ b/editor/editor_properties_array_dict.cpp @@ -111,6 +111,7 @@ bool EditorPropertyDictionaryObject::_set(const StringName &p_name, const Varian } if (name.begins_with("indices")) { + dict = dict.duplicate(); int index = name.get_slicec('/', 1).to_int(); Variant key = dict.get_key_at_index(index); dict[key] = p_value; @@ -171,6 +172,31 @@ Variant EditorPropertyDictionaryObject::get_new_item_value() { return new_item_value; } +String EditorPropertyDictionaryObject::get_property_name_for_index(int p_index) { + switch (p_index) { + case NEW_KEY_INDEX: + return "new_item_key"; + case NEW_VALUE_INDEX: + return "new_item_value"; + default: + return "indices/" + itos(p_index); + } +} + +String EditorPropertyDictionaryObject::get_label_for_index(int p_index) { + switch (p_index) { + case NEW_KEY_INDEX: + return TTR("New Key:"); + break; + case NEW_VALUE_INDEX: + return TTR("New Value:"); + break; + default: + return dict.get_key_at_index(p_index).get_construct_string(); + break; + } +} + EditorPropertyDictionaryObject::EditorPropertyDictionaryObject() { } @@ -230,15 +256,14 @@ void EditorPropertyArray::_change_type_menu(int p_index) { Variant array = object->get_array().duplicate(); array.set(slots[changing_type_index].index, value); - emit_changed(get_edited_property(), array, "", true); - update_property(); + emit_changed(get_edited_property(), array); } void EditorPropertyArray::_object_id_selected(const StringName &p_property, ObjectID p_id) { emit_signal(SNAME("object_id_selected"), p_property, p_id); } -void EditorPropertyArray::create_new_property_slot() { +void EditorPropertyArray::_create_new_property_slot() { int idx = slots.size(); HBoxContainer *hbox = memnew(HBoxContainer); @@ -365,7 +390,7 @@ void EditorPropertyArray::update_property() { vbox->add_child(paginator); for (int i = 0; i < page_length; i++) { - create_new_property_slot(); + _create_new_property_slot(); } } @@ -434,8 +459,7 @@ void EditorPropertyArray::_remove_pressed(int p_slot_index) { Variant array = object->get_array().duplicate(); array.call("remove_at", slots[p_slot_index].index); - emit_changed(get_edited_property(), array, "", false); - update_property(); + emit_changed(get_edited_property(), array); } void EditorPropertyArray::_button_draw() { @@ -514,8 +538,7 @@ void EditorPropertyArray::drop_data_fw(const Point2 &p_point, const Variant &p_d } } - emit_changed(get_edited_property(), array, "", false); - update_property(); + emit_changed(get_edited_property(), array); } } @@ -607,8 +630,7 @@ void EditorPropertyArray::_length_changed(double p_page) { Variant array = object->get_array().duplicate(); array.call("resize", int(p_page)); - emit_changed(get_edited_property(), array, "", false); - update_property(); + emit_changed(get_edited_property(), array); } void EditorPropertyArray::_add_element() { @@ -700,8 +722,7 @@ void EditorPropertyArray::_reorder_button_up() { array.call("remove_at", reorder_slot.index); array.call("insert", reorder_to_index, value_to_move); - reorder_slot.index = reorder_slot.index % page_length + page_index * page_length; - emit_changed(get_edited_property(), array, "", false); + emit_changed(get_edited_property(), array); } Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); @@ -748,31 +769,19 @@ void EditorPropertyDictionary::_property_changed(const String &p_property, Varia p_value = Variant(); // `EditorResourcePicker` resets to `Ref<Resource>()`. See GH-82716. } - if (p_property == "new_item_key") { - object->set_new_item_key(p_value); - } else if (p_property == "new_item_value") { - object->set_new_item_value(p_value); - } else if (p_property.begins_with("indices")) { - int index = p_property.get_slice("/", 1).to_int(); - - Dictionary dict = object->get_dict().duplicate(); - Variant key = dict.get_key_at_index(index); - dict[key] = p_value; - - object->set_dict(dict); - emit_changed(get_edited_property(), dict, "", true); - } + object->set(p_property, p_value); + emit_changed(get_edited_property(), object->get_dict(), p_name, p_changing); } -void EditorPropertyDictionary::_change_type(Object *p_button, int p_index) { +void EditorPropertyDictionary::_change_type(Object *p_button, int p_slot_index) { Button *button = Object::cast_to<Button>(p_button); - + int index = slots[p_slot_index].index; Rect2 rect = button->get_screen_rect(); - change_type->set_item_disabled(change_type->get_item_index(Variant::VARIANT_MAX), p_index < 0); + change_type->set_item_disabled(change_type->get_item_index(Variant::VARIANT_MAX), index < 0); change_type->reset_size(); change_type->set_position(rect.get_end() - Vector2(change_type->get_contents_minimum_size().x, 0)); change_type->popup(); - changing_type_index = p_index; + changing_type_index = index; } void EditorPropertyDictionary::_add_key_value() { @@ -796,36 +805,59 @@ void EditorPropertyDictionary::_add_key_value() { VariantInternal::initialize(&new_value, type); object->set_new_item_value(new_value); - emit_changed(get_edited_property(), dict, "", false); - update_property(); + emit_changed(get_edited_property(), dict); } -void EditorPropertyDictionary::_change_type_menu(int p_index) { - if (changing_type_index < 0) { - Variant value; - VariantInternal::initialize(&value, Variant::Type(p_index)); - if (changing_type_index == -1) { - object->set_new_item_key(value); - } else { - object->set_new_item_value(value); - } - update_property(); - return; - } +void EditorPropertyDictionary::_create_new_property_slot(int p_idx) { + HBoxContainer *hbox = memnew(HBoxContainer); + EditorProperty *prop = memnew(EditorPropertyNil); + hbox->add_child(prop); - Dictionary dict = object->get_dict().duplicate(); - if (p_index < Variant::VARIANT_MAX) { - Variant value; - VariantInternal::initialize(&value, Variant::Type(p_index)); - Variant key = dict.get_key_at_index(changing_type_index); - dict[key] = value; + Button *edit_btn = memnew(Button); + edit_btn->set_icon(get_editor_theme_icon(SNAME("Edit"))); + edit_btn->set_disabled(is_read_only()); + edit_btn->connect("pressed", callable_mp(this, &EditorPropertyDictionary::_change_type).bind(edit_btn, slots.size())); + hbox->add_child(edit_btn); + if (add_panel) { + add_panel->get_child(0)->add_child(hbox); } else { - Variant key = dict.get_key_at_index(changing_type_index); - dict.erase(key); + property_vbox->add_child(hbox); } + Slot slot; + slot.prop = prop; + slot.object = object; + slot.container = hbox; + int index = p_idx + (p_idx >= 0 ? page_index * page_length : 0); + slot.set_index(index); + slots.push_back(slot); +} - emit_changed(get_edited_property(), dict, "", false); - update_property(); +void EditorPropertyDictionary::_change_type_menu(int p_index) { + Variant value; + switch (changing_type_index) { + case EditorPropertyDictionaryObject::NEW_KEY_INDEX: + case EditorPropertyDictionaryObject::NEW_VALUE_INDEX: + VariantInternal::initialize(&value, Variant::Type(p_index)); + if (changing_type_index == EditorPropertyDictionaryObject::NEW_KEY_INDEX) { + object->set_new_item_key(value); + } else { + object->set_new_item_value(value); + } + update_property(); + break; + + default: + Dictionary dict = object->get_dict().duplicate(); + Variant key = dict.get_key_at_index(changing_type_index); + if (p_index < Variant::VARIANT_MAX) { + VariantInternal::initialize(&value, Variant::Type(p_index)); + dict[key] = value; + } else { + dict.erase(key); + } + + emit_changed(get_edited_property(), dict); + } } void EditorPropertyDictionary::setup(PropertyHint p_hint) { @@ -877,331 +909,72 @@ void EditorPropertyDictionary::update_property() { paginator = memnew(EditorPaginator); paginator->connect("page_changed", callable_mp(this, &EditorPropertyDictionary::_page_changed)); vbox->add_child(paginator); - } else { - // Queue children for deletion, deleting immediately might cause errors. - for (int i = property_vbox->get_child_count() - 1; i >= 0; i--) { - property_vbox->get_child(i)->queue_free(); + + for (int i = 0; i < page_length; i++) { + _create_new_property_slot(slots.size()); } + + add_panel = memnew(PanelContainer); + property_vbox->add_child(add_panel); + add_panel->add_theme_style_override(SNAME("panel"), get_theme_stylebox(SNAME("DictionaryAddItem"), EditorStringName(EditorStyles))); + VBoxContainer *add_vbox = memnew(VBoxContainer); + add_panel->add_child(add_vbox); + + _create_new_property_slot(EditorPropertyDictionaryObject::NEW_KEY_INDEX); + _create_new_property_slot(EditorPropertyDictionaryObject::NEW_VALUE_INDEX); + + button_add_item = EditorInspector::create_inspector_action_button(TTR("Add Key/Value Pair")); + button_add_item->set_icon(get_theme_icon(SNAME("Add"), EditorStringName(EditorIcons))); + button_add_item->set_disabled(is_read_only()); + button_add_item->connect("pressed", callable_mp(this, &EditorPropertyDictionary::_add_key_value)); + add_vbox->add_child(button_add_item); } int size = dict.size(); int max_page = MAX(0, size - 1) / page_length; - page_index = MIN(page_index, max_page); + if (page_index > max_page) { + _page_changed(max_page); + } paginator->update(page_index, max_page); paginator->set_visible(max_page > 0); - int offset = page_index * page_length; - - int amount = MIN(size - offset, page_length); - int total_amount = page_index == max_page ? amount + 2 : amount; // For the "Add Key/Value Pair" box on last page. - - VBoxContainer *add_vbox = nullptr; - double default_float_step = EDITOR_GET("interface/inspector/default_float_step"); - - for (int i = 0; i < total_amount; i++) { - String prop_name; - Variant key; - Variant value; + add_panel->set_visible(page_index == max_page); - if (i < amount) { - prop_name = "indices/" + itos(i + offset); - key = dict.get_key_at_index(i + offset); - value = dict.get_value_at_index(i + offset); - } else if (i == amount) { - prop_name = "new_item_key"; - value = object->get_new_item_key(); - } else if (i == amount + 1) { - prop_name = "new_item_value"; - value = object->get_new_item_value(); + for (Slot &slot : slots) { + bool slot_visible = slot.index < size; + slot.container->set_visible(slot_visible); + // If not visible no need to update it. + if (!slot_visible) { + continue; } + Variant value = object->get(slot.prop_name); + Variant::Type value_type = value.get_type(); - EditorProperty *prop = nullptr; - - switch (value.get_type()) { - case Variant::NIL: { - prop = memnew(EditorPropertyNil); - - } break; - - // Atomic types. - case Variant::BOOL: { - prop = memnew(EditorPropertyCheck); - - } break; - case Variant::INT: { - EditorPropertyInteger *editor = memnew(EditorPropertyInteger); - editor->setup(-100000, 100000, 1, false, true, true); - prop = editor; - - } break; - case Variant::FLOAT: { - EditorPropertyFloat *editor = memnew(EditorPropertyFloat); - editor->setup(-100000, 100000, default_float_step, true, false, true, true); - prop = editor; - } break; - case Variant::STRING: { - if (i != amount && property_hint == PROPERTY_HINT_MULTILINE_TEXT) { - // If this is NOT the new key field and there's a multiline hint, - // show the field as multiline - prop = memnew(EditorPropertyMultilineText); - } else { - prop = memnew(EditorPropertyText); - } - - } break; - - // Math types. - case Variant::VECTOR2: { - EditorPropertyVector2 *editor = memnew(EditorPropertyVector2); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::VECTOR2I: { - EditorPropertyVector2i *editor = memnew(EditorPropertyVector2i); - editor->setup(-100000, 100000); - prop = editor; - - } break; - case Variant::RECT2: { - EditorPropertyRect2 *editor = memnew(EditorPropertyRect2); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::RECT2I: { - EditorPropertyRect2i *editor = memnew(EditorPropertyRect2i); - editor->setup(-100000, 100000); - prop = editor; - - } break; - case Variant::VECTOR3: { - EditorPropertyVector3 *editor = memnew(EditorPropertyVector3); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::VECTOR3I: { - EditorPropertyVector3i *editor = memnew(EditorPropertyVector3i); - editor->setup(-100000, 100000); - prop = editor; - - } break; - case Variant::VECTOR4: { - EditorPropertyVector4 *editor = memnew(EditorPropertyVector4); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::VECTOR4I: { - EditorPropertyVector4i *editor = memnew(EditorPropertyVector4i); - editor->setup(-100000, 100000); - prop = editor; - - } break; - case Variant::TRANSFORM2D: { - EditorPropertyTransform2D *editor = memnew(EditorPropertyTransform2D); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::PLANE: { - EditorPropertyPlane *editor = memnew(EditorPropertyPlane); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::QUATERNION: { - EditorPropertyQuaternion *editor = memnew(EditorPropertyQuaternion); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::AABB: { - EditorPropertyAABB *editor = memnew(EditorPropertyAABB); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::BASIS: { - EditorPropertyBasis *editor = memnew(EditorPropertyBasis); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::TRANSFORM3D: { - EditorPropertyTransform3D *editor = memnew(EditorPropertyTransform3D); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - case Variant::PROJECTION: { - EditorPropertyProjection *editor = memnew(EditorPropertyProjection); - editor->setup(-100000, 100000, default_float_step, true); - prop = editor; - - } break; - - // Miscellaneous types. - case Variant::COLOR: { - prop = memnew(EditorPropertyColor); - - } break; - case Variant::STRING_NAME: { - EditorPropertyText *ept = memnew(EditorPropertyText); - ept->set_string_name(true); - prop = ept; - - } break; - case Variant::NODE_PATH: { - prop = memnew(EditorPropertyNodePath); - - } break; - case Variant::RID: { - prop = memnew(EditorPropertyRID); - - } break; - case Variant::SIGNAL: { - prop = memnew(EditorPropertySignal); - - } break; - case Variant::CALLABLE: { - prop = memnew(EditorPropertyCallable); - - } break; - case Variant::OBJECT: { - if (Object::cast_to<EncodedObjectAsID>(value)) { - EditorPropertyObjectID *editor = memnew(EditorPropertyObjectID); - editor->setup("Object"); - prop = editor; - - } else { - EditorPropertyResource *editor = memnew(EditorPropertyResource); - editor->setup(object.ptr(), prop_name, "Resource"); - editor->set_use_folding(is_using_folding()); - prop = editor; - } - - } break; - case Variant::DICTIONARY: { - prop = memnew(EditorPropertyDictionary); - - } break; - case Variant::ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::ARRAY); - prop = editor; - } break; - - // Arrays. - case Variant::PACKED_BYTE_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_BYTE_ARRAY); - prop = editor; - } break; - case Variant::PACKED_INT32_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_INT32_ARRAY); - prop = editor; - } break; - case Variant::PACKED_FLOAT32_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_FLOAT32_ARRAY); - prop = editor; - } break; - case Variant::PACKED_INT64_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_INT64_ARRAY); - prop = editor; - } break; - case Variant::PACKED_FLOAT64_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_FLOAT64_ARRAY); - prop = editor; - } break; - case Variant::PACKED_STRING_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_STRING_ARRAY); - prop = editor; - } break; - case Variant::PACKED_VECTOR2_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_VECTOR2_ARRAY); - prop = editor; - } break; - case Variant::PACKED_VECTOR3_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_VECTOR3_ARRAY); - prop = editor; - } break; - case Variant::PACKED_COLOR_ARRAY: { - EditorPropertyArray *editor = memnew(EditorPropertyArray); - editor->setup(Variant::PACKED_COLOR_ARRAY); - prop = editor; - } break; - default: { + // Check if the editor property needs to be updated. + bool value_as_id = Object::cast_to<EncodedObjectAsID>(value); + if (value_type != slot.type || (value_type == Variant::OBJECT && value_as_id != slot.as_id)) { + slot.as_id = value_as_id; + slot.type = value_type; + EditorProperty *new_prop = nullptr; + if (value_type == Variant::OBJECT && value_as_id) { + EditorPropertyObjectID *editor = memnew(EditorPropertyObjectID); + editor->setup("Object"); + new_prop = editor; + } else { + new_prop = EditorInspector::instantiate_property_editor(nullptr, value_type, "", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE); } + new_prop->set_selectable(false); + new_prop->set_use_folding(is_using_folding()); + new_prop->connect(SNAME("property_changed"), callable_mp(this, &EditorPropertyDictionary::_property_changed)); + new_prop->connect(SNAME("object_id_selected"), callable_mp(this, &EditorPropertyDictionary::_object_id_selected)); + new_prop->set_h_size_flags(SIZE_EXPAND_FILL); + new_prop->set_read_only(is_read_only()); + slot.set_prop(new_prop); } - - ERR_FAIL_NULL(prop); - - prop->set_read_only(is_read_only()); - - if (i == amount) { - PanelContainer *pc = memnew(PanelContainer); - property_vbox->add_child(pc); - pc->add_theme_style_override(SNAME("panel"), get_theme_stylebox(SNAME("DictionaryAddItem"), EditorStringName(EditorStyles))); - - add_vbox = memnew(VBoxContainer); - pc->add_child(add_vbox); - } - prop->set_object_and_property(object.ptr(), prop_name); - int change_index = 0; - - if (i < amount) { - String cs = key.get_construct_string(); - prop->set_label(key.get_construct_string()); - prop->set_tooltip_text(cs); - change_index = i + offset; - } else if (i == amount) { - prop->set_label(TTR("New Key:")); - change_index = -1; - } else if (i == amount + 1) { - prop->set_label(TTR("New Value:")); - change_index = -2; - } - - prop->set_selectable(false); - prop->connect("property_changed", callable_mp(this, &EditorPropertyDictionary::_property_changed)); - prop->connect("object_id_selected", callable_mp(this, &EditorPropertyDictionary::_object_id_selected)); - - HBoxContainer *hbox = memnew(HBoxContainer); - if (add_vbox) { - add_vbox->add_child(hbox); - } else { - property_vbox->add_child(hbox); - } - hbox->add_child(prop); - prop->set_h_size_flags(SIZE_EXPAND_FILL); - Button *edit_btn = memnew(Button); - edit_btn->set_icon(get_editor_theme_icon(SNAME("Edit"))); - edit_btn->set_disabled(is_read_only()); - hbox->add_child(edit_btn); - edit_btn->connect("pressed", callable_mp(this, &EditorPropertyDictionary::_change_type).bind(edit_btn, change_index)); - - prop->update_property(); - - if (i == amount + 1) { - button_add_item = EditorInspector::create_inspector_action_button(TTR("Add Key/Value Pair")); - button_add_item->set_icon(get_editor_theme_icon(SNAME("Add"))); - button_add_item->set_disabled(is_read_only()); - button_add_item->connect("pressed", callable_mp(this, &EditorPropertyDictionary::_add_key_value)); - add_vbox->add_child(button_add_item); - } + slot.prop->update_property(); } - updating = false; } else { @@ -1210,6 +983,8 @@ void EditorPropertyDictionary::update_property() { memdelete(container); button_add_item = nullptr; container = nullptr; + add_panel = nullptr; + slots.clear(); } } } @@ -1254,10 +1029,17 @@ void EditorPropertyDictionary::_edit_pressed() { } void EditorPropertyDictionary::_page_changed(int p_page) { + page_index = p_page; + int i = p_page * page_length; + for (Slot &slot : slots) { + if (slot.index > -1) { + slot.set_index(i); + i++; + } + } if (updating) { return; } - page_index = p_page; update_property(); } diff --git a/editor/editor_properties_array_dict.h b/editor/editor_properties_array_dict.h index 0e81a0fae3..b1bf45f1b7 100644 --- a/editor/editor_properties_array_dict.h +++ b/editor/editor_properties_array_dict.h @@ -66,6 +66,11 @@ protected: bool _get(const StringName &p_name, Variant &r_ret) const; public: + enum { + NEW_KEY_INDEX = -2, + NEW_VALUE_INDEX, + }; + void set_dict(const Dictionary &p_dict); Dictionary get_dict(); @@ -75,6 +80,9 @@ public: void set_new_item_value(const Variant &p_new_item); Variant get_new_item_value(); + String get_label_for_index(int p_index); + String get_property_name_for_index(int p_index); + EditorPropertyDictionaryObject(); }; @@ -125,7 +133,7 @@ class EditorPropertyArray : public EditorProperty { void _reorder_button_gui_input(const Ref<InputEvent> &p_event); void _reorder_button_down(int p_index); void _reorder_button_up(); - void create_new_property_slot(); + void _create_new_property_slot(); protected: Ref<EditorPropertyArrayObject> object; @@ -160,6 +168,34 @@ public: class EditorPropertyDictionary : public EditorProperty { GDCLASS(EditorPropertyDictionary, EditorProperty); + struct Slot { + Ref<EditorPropertyDictionaryObject> object; + HBoxContainer *container = nullptr; + int index = -1; + Variant::Type type = Variant::VARIANT_MAX; + bool as_id = false; + EditorProperty *prop = nullptr; + String prop_name; + + void set_index(int p_idx) { + index = p_idx; + prop_name = object->get_property_name_for_index(p_idx); + update_prop_or_index(); + } + + void set_prop(EditorProperty *p_prop) { + prop->add_sibling(p_prop); + prop->queue_free(); + prop = p_prop; + update_prop_or_index(); + } + + void update_prop_or_index() { + prop->set_object_and_property(object.ptr(), prop_name); + prop->set_label(object->get_label_for_index(index)); + } + }; + PopupMenu *change_type = nullptr; bool updating = false; @@ -170,15 +206,18 @@ class EditorPropertyDictionary : public EditorProperty { Button *edit = nullptr; MarginContainer *container = nullptr; VBoxContainer *property_vbox = nullptr; + PanelContainer *add_panel = nullptr; EditorSpinSlider *size_sliderv = nullptr; Button *button_add_item = nullptr; EditorPaginator *paginator = nullptr; PropertyHint property_hint; + LocalVector<Slot> slots; + void _create_new_property_slot(int p_idx); void _page_changed(int p_page); void _edit_pressed(); void _property_changed(const String &p_property, Variant p_value, const String &p_name = "", bool p_changing = false); - void _change_type(Object *p_button, int p_index); + void _change_type(Object *p_button, int p_slot_index); void _change_type_menu(int p_index); void _add_key_value(); diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index a9225a3057..5450040bea 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -112,7 +112,7 @@ void EditorResourcePicker::_update_resource_preview(const String &p_path, const preview_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED); int thumbnail_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size"); thumbnail_size *= EDSCALE; - assign_button->set_custom_minimum_size(Size2(MAX(1, assign_button_min_size.x), MAX(thumbnail_size, assign_button_min_size.y))); + assign_button->set_custom_minimum_size(assign_button_min_size.max(Size2(1, thumbnail_size))); } preview_rect->set_texture(p_preview); diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 7436cd8629..ee33e171e3 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -653,6 +653,8 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { _initial_set("text_editor/completion/put_callhint_tooltip_below_current_line", true); _initial_set("text_editor/completion/complete_file_paths", true); _initial_set("text_editor/completion/add_type_hints", true); + _initial_set("text_editor/completion/add_string_name_literals", false); + _initial_set("text_editor/completion/add_node_path_literals", false); _initial_set("text_editor/completion/use_single_quotes", false); _initial_set("text_editor/completion/colorize_suggestions", true); diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index 0f83e109fa..309e091d02 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -776,12 +776,7 @@ void FileSystemDock::navigate_to_path(const String &p_path) { _navigate_to_path(p_path); // Ensure that the FileSystem dock is visible. - if (get_window() == get_tree()->get_root()) { - TabContainer *tab_container = (TabContainer *)get_parent_control(); - tab_container->set_current_tab(tab_container->get_tab_idx_from_control((Control *)this)); - } else { - get_window()->grab_focus(); - } + EditorDockManager::get_singleton()->focus_dock(this); } 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) { @@ -885,7 +880,7 @@ void FileSystemDock::_search(EditorFileSystemDirectory *p_path, List<FileInfo> * struct FileSystemDock::FileInfoTypeComparator { bool operator()(const FileInfo &p_a, const FileInfo &p_b) const { - return NaturalNoCaseComparator()(p_a.name.get_extension() + p_a.type + p_a.name.get_basename(), p_b.name.get_extension() + p_b.type + p_b.name.get_basename()); + return FileNoCaseComparator()(p_a.name.get_extension() + p_a.type + p_a.name.get_basename(), p_b.name.get_extension() + p_b.type + p_b.name.get_basename()); } }; @@ -2043,7 +2038,7 @@ Vector<String> FileSystemDock::_tree_get_selected(bool remove_self_inclusion, bo Vector<String> FileSystemDock::_remove_self_included_paths(Vector<String> selected_strings) { // Remove paths or files that are included into another. if (selected_strings.size() > 1) { - selected_strings.sort_custom<NaturalNoCaseComparator>(); + selected_strings.sort_custom<FileNoCaseComparator>(); String last_path = ""; for (int i = 0; i < selected_strings.size(); i++) { if (!last_path.is_empty() && selected_strings[i].begins_with(last_path)) { @@ -3905,6 +3900,7 @@ FileSystemDock::FileSystemDock() { add_child(split_box); tree = memnew(FileSystemTree); + tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); tree->set_hide_root(true); SET_DRAG_FORWARDING_GCD(tree, FileSystemDock); diff --git a/editor/filesystem_dock.h b/editor/filesystem_dock.h index 15a43dc6f2..b950075928 100644 --- a/editor/filesystem_dock.h +++ b/editor/filesystem_dock.h @@ -332,7 +332,7 @@ private: uint64_t modified_time = 0; bool operator<(const FileInfo &fi) const { - return NaturalNoCaseComparator()(name, fi.name); + return FileNoCaseComparator()(name, fi.name); } }; diff --git a/editor/gui/editor_file_dialog.cpp b/editor/gui/editor_file_dialog.cpp index fe86ac442b..df1f026f78 100644 --- a/editor/gui/editor_file_dialog.cpp +++ b/editor/gui/editor_file_dialog.cpp @@ -874,8 +874,8 @@ void EditorFileDialog::update_file_list() { item = dir_access->get_next(); } - dirs.sort_custom<NaturalNoCaseComparator>(); - files.sort_custom<NaturalNoCaseComparator>(); + dirs.sort_custom<FileNoCaseComparator>(); + files.sort_custom<FileNoCaseComparator>(); while (!dirs.is_empty()) { const String &dir_name = dirs.front()->get(); diff --git a/editor/gui/editor_spin_slider.cpp b/editor/gui/editor_spin_slider.cpp index fab5784f16..c0a704105c 100644 --- a/editor/gui/editor_spin_slider.cpp +++ b/editor/gui/editor_spin_slider.cpp @@ -238,28 +238,28 @@ void EditorSpinSlider::_grabber_gui_input(const Ref<InputEvent> &p_event) { void EditorSpinSlider::_value_input_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventKey> k = p_event; if (k.is_valid() && k->is_pressed() && !is_read_only()) { - double step = get_step(); - if (step < 1) { - double divisor = 1.0 / get_step(); - - if (trunc(divisor) == divisor) { - step = 1.0; - } - } - - if (k->is_command_or_control_pressed()) { - step *= 100.0; - } else if (k->is_shift_pressed()) { - step *= 10.0; - } else if (k->is_alt_pressed()) { - step *= 0.1; - } - Key code = k->get_keycode(); switch (code) { case Key::UP: case Key::DOWN: { + double step = get_step(); + if (step < 1) { + double divisor = 1.0 / step; + + if (trunc(divisor) == divisor) { + step = 1.0; + } + } + + if (k->is_command_or_control_pressed()) { + step *= 100.0; + } else if (k->is_shift_pressed()) { + step *= 10.0; + } else if (k->is_alt_pressed()) { + step *= 0.1; + } + _evaluate_input_text(); double last_value = get_value(); @@ -267,12 +267,6 @@ void EditorSpinSlider::_value_input_gui_input(const Ref<InputEvent> &p_event) { step *= -1; } set_value(last_value + step); - double new_value = get_value(); - - double clamp_value = CLAMP(new_value, get_min(), get_max()); - if (new_value != clamp_value) { - set_value(clamp_value); - } value_input_dirty = true; set_process_internal(true); diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index 248678662c..e87ddd6915 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -32,6 +32,7 @@ #include "core/config/project_settings.h" #include "core/object/script_language.h" +#include "editor/editor_dock_manager.h" #include "editor/editor_file_system.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" @@ -163,18 +164,15 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i set_selected(n); - TabContainer *tab_container = Object::cast_to<TabContainer>(NodeDock::get_singleton()->get_parent()); - NodeDock::get_singleton()->get_parent()->call("set_current_tab", tab_container->get_tab_idx_from_control(NodeDock::get_singleton())); + EditorDockManager::get_singleton()->focus_dock(NodeDock::get_singleton()); NodeDock::get_singleton()->show_connections(); - } else if (p_id == BUTTON_GROUPS) { editor_selection->clear(); editor_selection->add_node(n); set_selected(n); - TabContainer *tab_container = Object::cast_to<TabContainer>(NodeDock::get_singleton()->get_parent()); - NodeDock::get_singleton()->get_parent()->call("set_current_tab", tab_container->get_tab_idx_from_control(NodeDock::get_singleton())); + EditorDockManager::get_singleton()->focus_dock(NodeDock::get_singleton()); NodeDock::get_singleton()->show_groups(); } else if (p_id == BUTTON_UNIQUE) { undo_redo->create_action(TTR("Disable Scene Unique Name")); diff --git a/editor/icons/AnimationAutoFit.svg b/editor/icons/AnimationAutoFit.svg new file mode 100644 index 0000000000..fdde20d464 --- /dev/null +++ b/editor/icons/AnimationAutoFit.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m15.477 0.99862v14h-2.1017v-14zm-14.954 14.003v-14h2.1017v14zm11.946-6.823-3.5-3v6zm-8.9343 0.023985 3.5 3v-2h2.1464l0.00376-2h-2.1501v-2zm4.6005-7.0028c8.9077 15.09 8.9077 15.09 0 0zm-0.23085 14.003c-8.9077-15.09-8.9077-15.09 0 0z" fill="#e0e0e0" stroke-width="1.0251"/></svg> diff --git a/editor/icons/AnimationAutoFitBezier.svg b/editor/icons/AnimationAutoFitBezier.svg new file mode 100644 index 0000000000..1a79255c19 --- /dev/null +++ b/editor/icons/AnimationAutoFitBezier.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m12.469 8.1784-3.5-3v6zm-8.9343 0.023985 3.5 3v-2h2.1464l0.00376-2h-2.1501v-2zm9.4532 0.53338c-10.763 8.9077-10.763 8.9077 0 0zm0 7h-9.9859v-2h9.9859zm-9.9806-8.5564c10.763-8.9077 10.763-8.9077 0 0zm0-7h9.9859v2h-9.9859zm5.4684 2.8277c8.9077 10.763 8.9077 10.763 0 0zm7 0v9.9859h-2v-9.9859zm-7.8862 9.9859c-8.9077-10.763-8.9077-10.763 0 0zm-7 0v-9.9859h2v9.9859z" fill="#e0e0e0"/></svg> diff --git a/editor/plugins/animation_blend_tree_editor_plugin.cpp b/editor/plugins/animation_blend_tree_editor_plugin.cpp index 0412141775..bda2cb666b 100644 --- a/editor/plugins/animation_blend_tree_editor_plugin.cpp +++ b/editor/plugins/animation_blend_tree_editor_plugin.cpp @@ -256,10 +256,6 @@ void AnimationNodeBlendTreeEditor::update_graph() { options.push_back(F); } - if (tree->has_animation(anim->get_animation())) { - pb->set_max(tree->get_animation(anim->get_animation())->get_length()); - } - pb->set_show_percentage(false); pb->set_custom_minimum_size(Vector2(0, 14) * EDSCALE); animations[E] = pb; @@ -994,9 +990,10 @@ void AnimationNodeBlendTreeEditor::_notification(int p_what) { if (tree->has_animation(an->get_animation())) { Ref<Animation> anim = tree->get_animation(an->get_animation()); if (anim.is_valid()) { - E.value->set_max(anim->get_length()); //StringName path = AnimationTreeEditor::get_singleton()->get_base_path() + E.input_node; - StringName time_path = AnimationTreeEditor::get_singleton()->get_base_path() + String(E.key) + "/time"; + StringName length_path = AnimationTreeEditor::get_singleton()->get_base_path() + String(E.key) + "/current_length"; + StringName time_path = AnimationTreeEditor::get_singleton()->get_base_path() + String(E.key) + "/current_position"; + E.value->set_max(tree->get(length_path)); E.value->set_value(tree->get(time_path)); } } diff --git a/editor/plugins/animation_state_machine_editor.cpp b/editor/plugins/animation_state_machine_editor.cpp index dbfb143b22..db6e3dd69f 100644 --- a/editor/plugins/animation_state_machine_editor.cpp +++ b/editor/plugins/animation_state_machine_editor.cpp @@ -337,10 +337,7 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && tool_select->is_pressed()) { box_selecting = true; box_selecting_from = box_selecting_to = state_machine_draw->get_local_mouse_position(); - box_selecting_rect = Rect2(MIN(box_selecting_from.x, box_selecting_to.x), - MIN(box_selecting_from.y, box_selecting_to.y), - ABS(box_selecting_from.x - box_selecting_to.x), - ABS(box_selecting_from.y - box_selecting_to.y)); + box_selecting_rect = Rect2(box_selecting_from.min(box_selecting_to), (box_selecting_from - box_selecting_to).abs()); if (mb->is_command_or_control_pressed() || mb->is_shift_pressed()) { previous_selected = selected_nodes; @@ -423,10 +420,7 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv if (mm.is_valid() && box_selecting) { box_selecting_to = state_machine_draw->get_local_mouse_position(); - box_selecting_rect = Rect2(MIN(box_selecting_from.x, box_selecting_to.x), - MIN(box_selecting_from.y, box_selecting_to.y), - ABS(box_selecting_from.x - box_selecting_to.x), - ABS(box_selecting_from.y - box_selecting_to.y)); + box_selecting_rect = Rect2(box_selecting_from.min(box_selecting_to), (box_selecting_from - box_selecting_to).abs()); for (int i = 0; i < node_rects.size(); i++) { bool in_box = node_rects[i].node.intersects(box_selecting_rect); @@ -1247,14 +1241,14 @@ void AnimationNodeStateMachineEditor::_state_machine_pos_draw_all() { { float len = MAX(0.0001, current_length); float pos = CLAMP(current_play_pos, 0, len); - float c = current_length == HUGE_LENGTH ? 1 : (pos / len); + float c = pos / len; _state_machine_pos_draw_individual(playback->get_current_node(), c); } { float len = MAX(0.0001, fade_from_length); float pos = CLAMP(fade_from_current_play_pos, 0, len); - float c = fade_from_length == HUGE_LENGTH ? 1 : (pos / len); + float c = pos / len; _state_machine_pos_draw_individual(playback->get_fading_from_node(), c); } } diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 894eef74ec..41e5eee486 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -2643,6 +2643,7 @@ void CanvasItemEditor::_update_cursor() { void CanvasItemEditor::_update_lock_and_group_button() { bool all_locked = true; bool all_group = true; + bool has_canvas_item = false; List<Node *> selection = editor_selection->get_selected_node_list(); if (selection.is_empty()) { all_locked = false; @@ -2657,6 +2658,7 @@ void CanvasItemEditor::_update_lock_and_group_button() { if (all_group && !item->has_meta("_edit_group_")) { all_group = false; } + has_canvas_item = true; } if (!all_locked && !all_group) { break; @@ -2664,12 +2666,17 @@ void CanvasItemEditor::_update_lock_and_group_button() { } } + all_locked = all_locked && has_canvas_item; + all_group = all_group && has_canvas_item; + lock_button->set_visible(!all_locked); - lock_button->set_disabled(selection.is_empty()); + lock_button->set_disabled(!has_canvas_item); unlock_button->set_visible(all_locked); + unlock_button->set_disabled(!has_canvas_item); group_button->set_visible(!all_group); - group_button->set_disabled(selection.is_empty()); + group_button->set_disabled(!has_canvas_item); ungroup_button->set_visible(all_group); + ungroup_button->set_disabled(!has_canvas_item); } Control::CursorShape CanvasItemEditor::get_cursor_shape(const Point2 &p_pos) const { @@ -4011,6 +4018,9 @@ void CanvasItemEditor::_notification(int p_what) { AnimationPlayerEditor::get_singleton()->connect("animation_selected", callable_mp(this, &CanvasItemEditor::_keying_changed).unbind(1)); _keying_changed(); _update_editor_settings(); + + connect("item_lock_status_changed", callable_mp(this, &CanvasItemEditor::_update_lock_and_group_button)); + connect("item_group_status_changed", callable_mp(this, &CanvasItemEditor::_update_lock_and_group_button)); } break; case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { diff --git a/editor/plugins/editor_preview_plugins.cpp b/editor/plugins/editor_preview_plugins.cpp index e8804fdf12..70213b280c 100644 --- a/editor/plugins/editor_preview_plugins.cpp +++ b/editor/plugins/editor_preview_plugins.cpp @@ -130,7 +130,7 @@ Ref<Texture2D> EditorTexturePreviewPlugin::generate(const Ref<Resource> &p_from, if (new_size.y > p_size.y) { new_size = Vector2(new_size.x * p_size.y / new_size.y, p_size.y); } - Vector2i new_size_i(MAX(1, (int)new_size.x), MAX(1, (int)new_size.y)); + Vector2i new_size_i = Vector2i(new_size).max(Vector2i(1, 1)); img->resize(new_size_i.x, new_size_i.y, Image::INTERPOLATE_CUBIC); post_process_preview(img); diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index f38d42f681..7a4a7ddaec 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -7376,6 +7376,7 @@ void Node3DEditor::_selection_changed() { void Node3DEditor::_refresh_menu_icons() { bool all_locked = true; bool all_grouped = true; + bool has_node3d_item = false; List<Node *> &selection = editor_selection->get_selected_node_list(); @@ -7384,26 +7385,34 @@ void Node3DEditor::_refresh_menu_icons() { all_grouped = false; } else { for (Node *E : selection) { - if (Object::cast_to<Node3D>(E) && !Object::cast_to<Node3D>(E)->has_meta("_edit_lock_")) { - all_locked = false; - break; + Node3D *node = Object::cast_to<Node3D>(E); + if (node) { + if (all_locked && !node->has_meta("_edit_lock_")) { + all_locked = false; + } + if (all_grouped && !node->has_meta("_edit_group_")) { + all_grouped = false; + } + has_node3d_item = true; } - } - for (Node *E : selection) { - if (Object::cast_to<Node3D>(E) && !Object::cast_to<Node3D>(E)->has_meta("_edit_group_")) { - all_grouped = false; + if (!all_locked && !all_grouped) { break; } } } + all_locked = all_locked && has_node3d_item; + all_grouped = all_grouped && has_node3d_item; + tool_button[TOOL_LOCK_SELECTED]->set_visible(!all_locked); - tool_button[TOOL_LOCK_SELECTED]->set_disabled(selection.is_empty()); + tool_button[TOOL_LOCK_SELECTED]->set_disabled(!has_node3d_item); tool_button[TOOL_UNLOCK_SELECTED]->set_visible(all_locked); + tool_button[TOOL_UNLOCK_SELECTED]->set_disabled(!has_node3d_item); tool_button[TOOL_GROUP_SELECTED]->set_visible(!all_grouped); - tool_button[TOOL_GROUP_SELECTED]->set_disabled(selection.is_empty()); + tool_button[TOOL_GROUP_SELECTED]->set_disabled(!has_node3d_item); tool_button[TOOL_UNGROUP_SELECTED]->set_visible(all_grouped); + tool_button[TOOL_UNGROUP_SELECTED]->set_disabled(!has_node3d_item); } template <typename T> diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index edec4af094..43afc6f7d5 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -1892,7 +1892,7 @@ struct _ScriptEditorItemData { if (sort_key == id.sort_key) { return index < id.index; } else { - return sort_key.naturalnocasecmp_to(id.sort_key) < 0; + return sort_key.filenocasecmp_to(id.sort_key) < 0; } } else { return category < id.category; diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 13b1c4b6ac..750e4d263f 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -1946,9 +1946,12 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data 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 ? "^" : ""; // 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. - const String text_to_drop = _quote_drop_data(String(d["property"])); + text_to_drop += _quote_drop_data(String(d["property"])); te->set_caret_line(row); te->set_caret_column(col); diff --git a/editor/plugins/skeleton_3d_editor_plugin.cpp b/editor/plugins/skeleton_3d_editor_plugin.cpp index ffc59a3429..6a66a984c0 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.cpp +++ b/editor/plugins/skeleton_3d_editor_plugin.cpp @@ -505,10 +505,8 @@ void Skeleton3DEditor::_file_selected(const String &p_file) { position_max = Vector2(grest.origin.x, grest.origin.y); position_min = Vector2(grest.origin.x, grest.origin.y); } else { - position_max.x = MAX(grest.origin.x, position_max.x); - position_max.y = MAX(grest.origin.y, position_max.y); - position_min.x = MIN(grest.origin.x, position_min.x); - position_min.y = MIN(grest.origin.y, position_min.y); + position_max = position_max.max(Vector2(grest.origin.x, grest.origin.y)); + position_min = position_min.min(Vector2(grest.origin.x, grest.origin.y)); } } diff --git a/editor/plugins/tiles/tile_atlas_view.cpp b/editor/plugins/tiles/tile_atlas_view.cpp index 8c2cb68413..c3900b8235 100644 --- a/editor/plugins/tiles/tile_atlas_view.cpp +++ b/editor/plugins/tiles/tile_atlas_view.cpp @@ -501,8 +501,7 @@ Vector2i TileAtlasView::get_atlas_tile_coords_at_pos(const Vector2 p_pos, bool p // Clamp. if (p_clamp) { Vector2i size = tile_set_atlas_source->get_atlas_grid_size(); - ret.x = CLAMP(ret.x, 0, size.x - 1); - ret.y = CLAMP(ret.y, 0, size.y - 1); + ret = ret.clamp(Vector2i(), size - Vector2i(1, 1)); } return ret; diff --git a/editor/plugins/version_control_editor_plugin.cpp b/editor/plugins/version_control_editor_plugin.cpp index 9460f9fef2..1a602568fe 100644 --- a/editor/plugins/version_control_editor_plugin.cpp +++ b/editor/plugins/version_control_editor_plugin.cpp @@ -93,8 +93,7 @@ void VersionControlEditorPlugin::popup_vcs_set_up_dialog(const Control *p_gui_ba if (!available_plugins.is_empty()) { Size2 popup_size = Size2(400, 100); Size2 window_size = p_gui_base->get_viewport_rect().size; - popup_size.x = MIN(window_size.x * 0.5, popup_size.x); - popup_size.y = MIN(window_size.y * 0.5, popup_size.y); + popup_size = popup_size.min(window_size * 0.5); _populate_available_vcs_names(); diff --git a/main/main.cpp b/main/main.cpp index 9215f2e848..41f82177bc 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -767,6 +767,7 @@ Error Main::test_setup() { return OK; } + // The order is the same as in `Main::cleanup()`. void Main::test_cleanup() { ERR_FAIL_COND(!_start_success); @@ -978,8 +979,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph packed_data->add_pack_source(zip_packed_data); #endif - // Default exit code, can be modified for certain errors. - Error exit_code = ERR_INVALID_PARAMETER; + // Exit error code used in the `goto error` conditions. + // It's returned as the program exit code. ERR_HELP is special cased and handled as success (0). + Error exit_err = ERR_INVALID_PARAMETER; I = args.front(); while (I) { @@ -1029,12 +1031,12 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph } else if (I->get() == "-h" || I->get() == "--help" || I->get() == "/?") { // display help show_help = true; - exit_code = ERR_HELP; // Hack to force an early exit in `main()` with a success code. + exit_err = ERR_HELP; // Hack to force an early exit in `main()` with a success code. goto error; } else if (I->get() == "--version") { print_line(get_full_version_string()); - exit_code = ERR_HELP; // Hack to force an early exit in `main()` with a success code. + exit_err = ERR_HELP; // Hack to force an early exit in `main()` with a success code. goto error; } else if (I->get() == "-v" || I->get() == "--verbose") { // verbose output @@ -2461,7 +2463,7 @@ error: OS::get_singleton()->finalize_core(); locale = String(); - return exit_code; + return exit_err; } Error _parse_resource_dummy(void *p_data, VariantParser::Stream *p_stream, Ref<Resource> &r_res, int &line, String &r_err_str) { @@ -3143,7 +3145,10 @@ String Main::get_rendering_driver_name() { // everything the main loop needs to know about frame timings static MainTimerSync main_timer_sync; -bool Main::start() { +// Return value should be EXIT_SUCCESS if we start successfully +// and should move on to `OS::run`, and EXIT_FAILURE otherwise for +// an early exit with that error code. +int Main::start() { ERR_FAIL_COND_V(!_start_success, false); bool has_icon = false; @@ -3280,7 +3285,7 @@ bool Main::start() { { Ref<DirAccess> da = DirAccess::open(doc_tool_path); - ERR_FAIL_COND_V_MSG(da.is_null(), false, "Argument supplied to --doctool must be a valid directory path."); + ERR_FAIL_COND_V_MSG(da.is_null(), EXIT_FAILURE, "Argument supplied to --doctool must be a valid directory path."); } #ifndef MODULE_MONO_ENABLED @@ -3315,11 +3320,11 @@ bool Main::start() { // Create the module documentation directory if it doesn't exist Ref<DirAccess> da = DirAccess::create_for_path(path); err = da->make_dir_recursive(path); - ERR_FAIL_COND_V_MSG(err != OK, false, "Error: Can't create directory: " + path + ": " + itos(err)); + ERR_FAIL_COND_V_MSG(err != OK, EXIT_FAILURE, "Error: Can't create directory: " + path + ": " + itos(err)); print_line("Loading docs from: " + path); err = docsrc.load_classes(path); - ERR_FAIL_COND_V_MSG(err != OK, false, "Error loading docs from: " + path + ": " + itos(err)); + ERR_FAIL_COND_V_MSG(err != OK, EXIT_FAILURE, "Error loading docs from: " + path + ": " + itos(err)); } } @@ -3327,11 +3332,11 @@ bool Main::start() { // Create the main documentation directory if it doesn't exist Ref<DirAccess> da = DirAccess::create_for_path(index_path); err = da->make_dir_recursive(index_path); - ERR_FAIL_COND_V_MSG(err != OK, false, "Error: Can't create index directory: " + index_path + ": " + itos(err)); + ERR_FAIL_COND_V_MSG(err != OK, EXIT_FAILURE, "Error: Can't create index directory: " + index_path + ": " + itos(err)); print_line("Loading classes from: " + index_path); err = docsrc.load_classes(index_path); - ERR_FAIL_COND_V_MSG(err != OK, false, "Error loading classes from: " + index_path + ": " + itos(err)); + ERR_FAIL_COND_V_MSG(err != OK, EXIT_FAILURE, "Error loading classes from: " + index_path + ": " + itos(err)); checked_paths.insert(index_path); print_line("Merging docs..."); @@ -3340,20 +3345,19 @@ bool Main::start() { for (const String &E : checked_paths) { print_line("Erasing old docs at: " + E); err = DocTools::erase_classes(E); - ERR_FAIL_COND_V_MSG(err != OK, false, "Error erasing old docs at: " + E + ": " + itos(err)); + ERR_FAIL_COND_V_MSG(err != OK, EXIT_FAILURE, "Error erasing old docs at: " + E + ": " + itos(err)); } print_line("Generating new docs..."); err = doc.save_classes(index_path, doc_data_classes); - ERR_FAIL_COND_V_MSG(err != OK, false, "Error saving new docs:" + itos(err)); + ERR_FAIL_COND_V_MSG(err != OK, EXIT_FAILURE, "Error saving new docs:" + itos(err)); print_line("Deleting docs cache..."); if (FileAccess::exists(EditorHelp::get_cache_full_path())) { DirAccess::remove_file_or_error(EditorHelp::get_cache_full_path()); } - OS::get_singleton()->set_exit_code(EXIT_SUCCESS); - return false; + return EXIT_SUCCESS; } // GDExtension API and interface. @@ -3368,32 +3372,24 @@ bool Main::start() { } if (dump_gdextension_interface || dump_extension_api) { - OS::get_singleton()->set_exit_code(EXIT_SUCCESS); - return false; + return EXIT_SUCCESS; } if (validate_extension_api) { Engine::get_singleton()->set_editor_hint(true); // "extension_api.json" should always contains editor singletons. bool valid = GDExtensionAPIDump::validate_extension_json_file(validate_extension_api_file) == OK; - OS::get_singleton()->set_exit_code(valid ? EXIT_SUCCESS : EXIT_FAILURE); - return false; + return valid ? EXIT_SUCCESS : EXIT_FAILURE; } } #ifndef DISABLE_DEPRECATED if (converting_project) { int ret = ProjectConverter3To4(converter_max_kb_file, converter_max_line_length).convert(); - if (ret) { - OS::get_singleton()->set_exit_code(EXIT_SUCCESS); - } - return false; + return ret ? EXIT_SUCCESS : EXIT_FAILURE; } if (validating_converting_project) { bool ret = ProjectConverter3To4(converter_max_kb_file, converter_max_line_length).validate_conversion(); - if (ret) { - OS::get_singleton()->set_exit_code(EXIT_SUCCESS); - } - return false; + return ret ? EXIT_SUCCESS : EXIT_FAILURE; } #endif // DISABLE_DEPRECATED @@ -3410,7 +3406,7 @@ bool Main::start() { // this might end up triggered by valid usage, in which case we'll have to // fine-tune further. OS::get_singleton()->alert("Couldn't detect whether to run the editor, the project manager or a specific project. Aborting."); - ERR_FAIL_V_MSG(false, "Couldn't detect whether to run the editor, the project manager or a specific project. Aborting."); + ERR_FAIL_V_MSG(EXIT_FAILURE, "Couldn't detect whether to run the editor, the project manager or a specific project. Aborting."); } #endif @@ -3424,15 +3420,10 @@ bool Main::start() { if (!script.is_empty()) { Ref<Script> script_res = ResourceLoader::load(script); - ERR_FAIL_COND_V_MSG(script_res.is_null(), false, "Can't load script: " + script); + ERR_FAIL_COND_V_MSG(script_res.is_null(), EXIT_FAILURE, "Can't load script: " + script); if (check_only) { - if (!script_res->is_valid()) { - OS::get_singleton()->set_exit_code(EXIT_FAILURE); - } else { - OS::get_singleton()->set_exit_code(EXIT_SUCCESS); - } - return false; + return script_res->is_valid() ? EXIT_SUCCESS : EXIT_FAILURE; } if (script_res->can_instantiate()) { @@ -3444,13 +3435,13 @@ bool Main::start() { memdelete(obj); } OS::get_singleton()->alert(vformat("Can't load the script \"%s\" as it doesn't inherit from SceneTree or MainLoop.", script)); - ERR_FAIL_V_MSG(false, vformat("Can't load the script \"%s\" as it doesn't inherit from SceneTree or MainLoop.", script)); + ERR_FAIL_V_MSG(EXIT_FAILURE, vformat("Can't load the script \"%s\" as it doesn't inherit from SceneTree or MainLoop.", script)); } script_loop->set_script(script_res); main_loop = script_loop; } else { - return false; + return EXIT_FAILURE; } } else { // Not based on script path. if (!editor && !ClassDB::class_exists(main_loop_type) && ScriptServer::is_global_class(main_loop_type)) { @@ -3458,7 +3449,7 @@ bool Main::start() { Ref<Script> script_res = ResourceLoader::load(script_path); if (script_res.is_null()) { OS::get_singleton()->alert("Error: Could not load MainLoop script type: " + main_loop_type); - ERR_FAIL_V_MSG(false, vformat("Could not load global class %s.", main_loop_type)); + ERR_FAIL_V_MSG(EXIT_FAILURE, vformat("Could not load global class %s.", main_loop_type)); } StringName script_base = script_res->get_instance_base_type(); Object *obj = ClassDB::instantiate(script_base); @@ -3468,7 +3459,7 @@ bool Main::start() { memdelete(obj); } OS::get_singleton()->alert("Error: Invalid MainLoop script base type: " + script_base); - ERR_FAIL_V_MSG(false, vformat("The global class %s does not inherit from SceneTree or MainLoop.", main_loop_type)); + ERR_FAIL_V_MSG(EXIT_FAILURE, vformat("The global class %s does not inherit from SceneTree or MainLoop.", main_loop_type)); } script_loop->set_script(script_res); main_loop = script_loop; @@ -3482,15 +3473,15 @@ bool Main::start() { if (!main_loop) { if (!ClassDB::class_exists(main_loop_type)) { OS::get_singleton()->alert("Error: MainLoop type doesn't exist: " + main_loop_type); - return false; + return EXIT_FAILURE; } else { Object *ml = ClassDB::instantiate(main_loop_type); - ERR_FAIL_NULL_V_MSG(ml, false, "Can't instance MainLoop type."); + ERR_FAIL_NULL_V_MSG(ml, EXIT_FAILURE, "Can't instance MainLoop type."); main_loop = Object::cast_to<MainLoop>(ml); if (!main_loop) { memdelete(ml); - ERR_FAIL_V_MSG(false, "Invalid MainLoop type."); + ERR_FAIL_V_MSG(EXIT_FAILURE, "Invalid MainLoop type."); } } } @@ -3614,7 +3605,7 @@ bool Main::start() { Error err; Vector<String> paths = get_files_with_extension(gdscript_docs_path, "gd"); - ERR_FAIL_COND_V_MSG(paths.is_empty(), false, "Couldn't find any GDScript files under the given directory: " + gdscript_docs_path); + ERR_FAIL_COND_V_MSG(paths.is_empty(), EXIT_FAILURE, "Couldn't find any GDScript files under the given directory: " + gdscript_docs_path); for (const String &path : paths) { Ref<GDScript> gdscript = ResourceLoader::load(path); @@ -3629,14 +3620,13 @@ bool Main::start() { Ref<DirAccess> da = DirAccess::create_for_path(doc_tool_path); err = da->make_dir_recursive(doc_tool_path); - ERR_FAIL_COND_V_MSG(err != OK, false, "Error: Can't create GDScript docs directory: " + doc_tool_path + ": " + itos(err)); + ERR_FAIL_COND_V_MSG(err != OK, EXIT_FAILURE, "Error: Can't create GDScript docs directory: " + doc_tool_path + ": " + itos(err)); HashMap<String, String> doc_data_classes; err = docs.save_classes(doc_tool_path, doc_data_classes, false); - ERR_FAIL_COND_V_MSG(err != OK, false, "Error saving GDScript docs:" + itos(err)); + ERR_FAIL_COND_V_MSG(err != OK, EXIT_FAILURE, "Error saving GDScript docs:" + itos(err)); - OS::get_singleton()->set_exit_code(EXIT_SUCCESS); - return false; + return EXIT_SUCCESS; } #endif // MODULE_GDSCRIPT_ENABLED @@ -3751,7 +3741,7 @@ bool Main::start() { if (sep == -1) { Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_FAIL_COND_V(da.is_null(), false); + ERR_FAIL_COND_V(da.is_null(), EXIT_FAILURE); local_game_path = da->get_current_dir().path_join(local_game_path); } else { @@ -3801,7 +3791,7 @@ bool Main::start() { scene = scenedata->instantiate(); } - ERR_FAIL_NULL_V_MSG(scene, false, "Failed loading scene: " + local_game_path + "."); + ERR_FAIL_NULL_V_MSG(scene, EXIT_FAILURE, "Failed loading scene: " + local_game_path + "."); sml->add_current_scene(scene); #ifdef MACOS_ENABLED @@ -3874,7 +3864,7 @@ bool Main::start() { OS::get_singleton()->benchmark_end_measure("Startup", "Total"); OS::get_singleton()->benchmark_dump(); - return true; + return EXIT_SUCCESS; } /* Main iteration @@ -3904,10 +3894,10 @@ static uint64_t physics_process_max = 0; static uint64_t process_max = 0; static uint64_t navigation_process_max = 0; +// Return false means iterating further, returning true means `OS::run` +// will terminate the program. In case of failure, the OS exit code needs +// to be set explicitly here (defaults to EXIT_SUCCESS). bool Main::iteration() { - //for now do not error on this - //ERR_FAIL_COND_V(iterating, false); - iterating++; const uint64_t ticks = OS::get_singleton()->get_ticks_usec(); @@ -3967,6 +3957,11 @@ bool Main::iteration() { PhysicsServer3D::get_singleton()->flush_queries(); #endif // _3D_DISABLED + // Prepare the fixed timestep interpolated nodes BEFORE they are updated + // by the physics server, otherwise the current and previous transforms + // may be the same, and no interpolation takes place. + OS::get_singleton()->get_main_loop()->iteration_prepare(); + PhysicsServer2D::get_singleton()->sync(); PhysicsServer2D::get_singleton()->flush_queries(); diff --git a/main/main.h b/main/main.h index 755c7d841a..ff0fba6b51 100644 --- a/main/main.h +++ b/main/main.h @@ -78,7 +78,7 @@ public: static Error test_setup(); static void test_cleanup(); #endif - static bool start(); + static int start(); static bool iteration(); static void force_redraw(); diff --git a/main/main_timer_sync.cpp b/main/main_timer_sync.cpp index 569930d427..d358d9fa93 100644 --- a/main/main_timer_sync.cpp +++ b/main/main_timer_sync.cpp @@ -299,6 +299,17 @@ int64_t MainTimerSync::DeltaSmoother::smooth_delta(int64_t p_delta) { // before advance_core considers changing the physics_steps return from // the typical values as defined by typical_physics_steps double MainTimerSync::get_physics_jitter_fix() { + // Turn off jitter fix when using fixed timestep interpolation. + // Note this shouldn't be on UNTIL 3d interpolation is implemented, + // otherwise we will get people making 3d games with the physics_interpolation + // set to on getting jitter fix disabled unexpectedly. +#if 0 + if (Engine::get_singleton()->is_physics_interpolation_enabled()) { + // Would be better to write a simple bypass for jitter fix but this will do to get started. + return 0.0; + } +#endif + return Engine::get_singleton()->get_physics_jitter_fix(); } diff --git a/modules/fbx/fbx_document.cpp b/modules/fbx/fbx_document.cpp index 367117edcb..1dbe4c535a 100644 --- a/modules/fbx/fbx_document.cpp +++ b/modules/fbx/fbx_document.cpp @@ -2347,7 +2347,7 @@ PackedByteArray FBXDocument::generate_buffer(Ref<GLTFState> p_state) { return PackedByteArray(); } -Error write_to_filesystem(Ref<GLTFState> p_state, const String &p_path) { +Error FBXDocument::write_to_filesystem(Ref<GLTFState> p_state, const String &p_path) { return ERR_UNAVAILABLE; } diff --git a/modules/fbx/fbx_document.h b/modules/fbx/fbx_document.h index ba48eb11ae..c9256df444 100644 --- a/modules/fbx/fbx_document.h +++ b/modules/fbx/fbx_document.h @@ -53,14 +53,13 @@ public: static String _gen_unique_name(HashSet<String> &unique_names, const String &p_name); public: - Error append_from_file(String p_path, Ref<GLTFState> p_state, uint32_t p_flags = 0, String p_base_path = String()); - Error append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags = 0); - Error append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags = 0); + Error append_from_file(String p_path, Ref<GLTFState> p_state, uint32_t p_flags = 0, String p_base_path = String()) override; + Error append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags = 0) override; + Error append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags = 0) override; -public: - Node *generate_scene(Ref<GLTFState> p_state, float p_bake_fps = 30.0f, bool p_trimming = false, bool p_remove_immutable_tracks = true); - PackedByteArray generate_buffer(Ref<GLTFState> p_state); - Error write_to_filesystem(Ref<GLTFState> p_state, const String &p_path); + Node *generate_scene(Ref<GLTFState> p_state, float p_bake_fps = 30.0f, bool p_trimming = false, bool p_remove_immutable_tracks = true) override; + PackedByteArray generate_buffer(Ref<GLTFState> p_state) override; + Error write_to_filesystem(Ref<GLTFState> p_state, const String &p_path) override; protected: static void _bind_methods(); diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 12ff22c878..854c944e14 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -2665,6 +2665,8 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c GDScriptParser::DataType base_type = p_base.type; const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\""; + const bool use_string_names = EDITOR_GET("text_editor/completion/add_string_name_literals"); + const bool use_node_paths = EDITOR_GET("text_editor/completion/add_node_path_literals"); while (base_type.is_set() && !base_type.is_variant()) { switch (base_type.kind) { @@ -2698,8 +2700,14 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c List<String> options; obj->get_argument_options(p_method, p_argidx, &options); for (String &opt : options) { + // Handle user preference. if (opt.is_quoted()) { - opt = opt.unquote().quote(quote_style); // Handle user preference. + opt = opt.unquote().quote(quote_style); + if (use_string_names && info.arguments[p_argidx].type == Variant::STRING_NAME) { + opt = opt.indent("&"); + } else if (use_node_paths && info.arguments[p_argidx].type == Variant::NODE_PATH) { + opt = opt.indent("^"); + } } ScriptLanguage::CodeCompletionOption option(opt, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION); r_result.insert(option.display, option); diff --git a/modules/gdscript/tests/scripts/runtime/features/emit_after_await.gd b/modules/gdscript/tests/scripts/runtime/features/emit_after_await.gd new file mode 100644 index 0000000000..21fd526acc --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/emit_after_await.gd @@ -0,0 +1,12 @@ +# https://github.com/godotengine/godot/issues/89439 +extends Node + +signal my_signal + +func async_func(): + await my_signal + my_signal.emit() + +func test(): + async_func() + my_signal.emit() diff --git a/modules/gdscript/tests/scripts/runtime/features/emit_after_await.out b/modules/gdscript/tests/scripts/runtime/features/emit_after_await.out new file mode 100644 index 0000000000..d73c5eb7cd --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/emit_after_await.out @@ -0,0 +1 @@ +GDTEST_OK diff --git a/modules/gdscript/tests/scripts/runtime/features/emit_one_shot_is_non_recursive.gd b/modules/gdscript/tests/scripts/runtime/features/emit_one_shot_is_non_recursive.gd new file mode 100644 index 0000000000..5c328dcfcd --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/emit_one_shot_is_non_recursive.gd @@ -0,0 +1,22 @@ +# https://github.com/godotengine/godot/issues/89439 + +signal my_signal + +func foo(): + print("Foo") + my_signal.emit() + +func bar(): + print("Bar") + +func baz(): + print("Baz") + +func test(): + @warning_ignore("return_value_discarded") + my_signal.connect(foo, CONNECT_ONE_SHOT) + @warning_ignore("return_value_discarded") + my_signal.connect(bar, CONNECT_ONE_SHOT) + @warning_ignore("return_value_discarded") + my_signal.connect(baz) + my_signal.emit() diff --git a/modules/gdscript/tests/scripts/runtime/features/emit_one_shot_is_non_recursive.out b/modules/gdscript/tests/scripts/runtime/features/emit_one_shot_is_non_recursive.out new file mode 100644 index 0000000000..3399e08878 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/emit_one_shot_is_non_recursive.out @@ -0,0 +1,5 @@ +GDTEST_OK +Foo +Baz +Bar +Baz diff --git a/modules/gltf/extensions/physics/gltf_physics_shape.cpp b/modules/gltf/extensions/physics/gltf_physics_shape.cpp index 35c99adbe5..6c9ed82a69 100644 --- a/modules/gltf/extensions/physics/gltf_physics_shape.cpp +++ b/modules/gltf/extensions/physics/gltf_physics_shape.cpp @@ -129,6 +129,34 @@ void GLTFPhysicsShape::set_importer_mesh(Ref<ImporterMesh> p_importer_mesh) { importer_mesh = p_importer_mesh; } +Ref<ImporterMesh> _convert_hull_points_to_mesh(const Vector<Vector3> &p_hull_points) { + Ref<ImporterMesh> importer_mesh; + ERR_FAIL_COND_V_MSG(p_hull_points.size() < 3, importer_mesh, "GLTFPhysicsShape: Convex hull has fewer points (" + itos(p_hull_points.size()) + ") than the minimum of 3. At least 3 points are required in order to save to GLTF, since it uses a mesh to represent convex hulls."); + if (p_hull_points.size() > 255) { + WARN_PRINT("GLTFPhysicsShape: Convex hull has more points (" + itos(p_hull_points.size()) + ") than the recommended maximum of 255. This may not load correctly in other engines."); + } + // Convert the convex hull points into an array of faces. + Geometry3D::MeshData md; + Error err = ConvexHullComputer::convex_hull(p_hull_points, md); + ERR_FAIL_COND_V_MSG(err != OK, importer_mesh, "GLTFPhysicsShape: Failed to compute convex hull."); + Vector<Vector3> face_vertices; + for (uint32_t i = 0; i < md.faces.size(); i++) { + uint32_t index_count = md.faces[i].indices.size(); + for (uint32_t j = 1; j < index_count - 1; j++) { + face_vertices.append(p_hull_points[md.faces[i].indices[0]]); + face_vertices.append(p_hull_points[md.faces[i].indices[j]]); + face_vertices.append(p_hull_points[md.faces[i].indices[j + 1]]); + } + } + // Create an ImporterMesh from the faces. + importer_mesh.instantiate(); + Array surface_array; + surface_array.resize(Mesh::ArrayType::ARRAY_MAX); + surface_array[Mesh::ArrayType::ARRAY_VERTEX] = face_vertices; + importer_mesh->add_surface(Mesh::PRIMITIVE_TRIANGLES, surface_array); + return importer_mesh; +} + Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_godot_shape_node) { Ref<GLTFPhysicsShape> gltf_shape; gltf_shape.instantiate(); @@ -163,30 +191,8 @@ Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_godo gltf_shape->shape_type = "convex"; Ref<ConvexPolygonShape3D> convex = shape_resource; Vector<Vector3> hull_points = convex->get_points(); - ERR_FAIL_COND_V_MSG(hull_points.size() < 3, gltf_shape, "GLTFPhysicsShape: Convex hull has fewer points (" + itos(hull_points.size()) + ") than the minimum of 3. At least 3 points are required in order to save to GLTF, since it uses a mesh to represent convex hulls."); - if (hull_points.size() > 255) { - WARN_PRINT("GLTFPhysicsShape: Convex hull has more points (" + itos(hull_points.size()) + ") than the recommended maximum of 255. This may not load correctly in other engines."); - } - // Convert the convex hull points into an array of faces. - Geometry3D::MeshData md; - Error err = ConvexHullComputer::convex_hull(hull_points, md); - ERR_FAIL_COND_V_MSG(err != OK, gltf_shape, "GLTFPhysicsShape: Failed to compute convex hull."); - Vector<Vector3> face_vertices; - for (uint32_t i = 0; i < md.faces.size(); i++) { - uint32_t index_count = md.faces[i].indices.size(); - for (uint32_t j = 1; j < index_count - 1; j++) { - face_vertices.append(hull_points[md.faces[i].indices[0]]); - face_vertices.append(hull_points[md.faces[i].indices[j]]); - face_vertices.append(hull_points[md.faces[i].indices[j + 1]]); - } - } - // Create an ImporterMesh from the faces. - Ref<ImporterMesh> importer_mesh; - importer_mesh.instantiate(); - Array surface_array; - surface_array.resize(Mesh::ArrayType::ARRAY_MAX); - surface_array[Mesh::ArrayType::ARRAY_VERTEX] = face_vertices; - importer_mesh->add_surface(Mesh::PRIMITIVE_TRIANGLES, surface_array); + Ref<ImporterMesh> importer_mesh = _convert_hull_points_to_mesh(hull_points); + ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), gltf_shape, "GLTFPhysicsShape: Failed to convert convex hull points to a mesh."); gltf_shape->set_importer_mesh(importer_mesh); } else if (cast_to<const ConcavePolygonShape3D>(shape_resource.ptr())) { gltf_shape->shape_type = "trimesh"; diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index 1001b482cd..1901e89d51 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -311,13 +311,13 @@ private: static float get_max_component(const Color &p_color); public: - Error append_from_file(String p_path, Ref<GLTFState> p_state, uint32_t p_flags = 0, String p_base_path = String()); - Error append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags = 0); - Error append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags = 0); + virtual Error append_from_file(String p_path, Ref<GLTFState> p_state, uint32_t p_flags = 0, String p_base_path = String()); + virtual Error append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags = 0); + virtual Error append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags = 0); - Node *generate_scene(Ref<GLTFState> p_state, float p_bake_fps = 30.0f, bool p_trimming = false, bool p_remove_immutable_tracks = true); - PackedByteArray generate_buffer(Ref<GLTFState> p_state); - Error write_to_filesystem(Ref<GLTFState> p_state, const String &p_path); + virtual Node *generate_scene(Ref<GLTFState> p_state, float p_bake_fps = 30.0f, bool p_trimming = false, bool p_remove_immutable_tracks = true); + virtual PackedByteArray generate_buffer(Ref<GLTFState> p_state); + virtual Error write_to_filesystem(Ref<GLTFState> p_state, const String &p_path); public: Error _parse_gltf_state(Ref<GLTFState> p_state, const String &p_search_path); diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp index 5d22cb1301..b6f5d6ce57 100644 --- a/modules/lightmapper_rd/lightmapper_rd.cpp +++ b/modules/lightmapper_rd/lightmapper_rd.cpp @@ -233,8 +233,7 @@ Lightmapper::BakeError LightmapperRD::_blit_meshes_into_atlas(int p_max_texture_ MeshInstance &mi = mesh_instances.write[m_i]; Size2i s = Size2i(mi.data.albedo_on_uv2->get_width(), mi.data.albedo_on_uv2->get_height()); sizes.push_back(s); - atlas_size.width = MAX(atlas_size.width, s.width + 2); - atlas_size.height = MAX(atlas_size.height, s.height + 2); + atlas_size = atlas_size.max(s + Size2i(2, 2)); } int max = nearest_power_of_2_templated(atlas_size.width); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs index 63af6ee6e8..feaa1d07da 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs @@ -733,10 +733,7 @@ namespace Godot /// Converts this <see cref="Aabb"/> to a string. /// </summary> /// <returns>A string representation of this AABB.</returns> - public override readonly string ToString() - { - return $"{_position}, {_size}"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Aabb"/> to a string with the given <paramref name="format"/>. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs index 589d6596f0..8d24582ba1 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs @@ -1134,10 +1134,7 @@ namespace Godot /// Converts this <see cref="Basis"/> to a string. /// </summary> /// <returns>A string representation of this basis.</returns> - public override readonly string ToString() - { - return $"[X: {X}, Y: {Y}, Z: {Z}]"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Basis"/> to a string with the given <paramref name="format"/>. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs index a1f1ade9b8..d3b9f2d2ae 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs @@ -1329,10 +1329,7 @@ namespace Godot /// Converts this <see cref="Color"/> to a string. /// </summary> /// <returns>A string representation of this color.</returns> - public override readonly string ToString() - { - return $"({R}, {G}, {B}, {A})"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Color"/> to a string with the given <paramref name="format"/>. @@ -1340,9 +1337,7 @@ namespace Godot /// <returns>A string representation of this color.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"({R.ToString(format)}, {G.ToString(format)}, {B.ToString(format)}, {A.ToString(format)})"; -#pragma warning restore CA1305 + return $"({R.ToString(format, CultureInfo.InvariantCulture)}, {G.ToString(format, CultureInfo.InvariantCulture)}, {B.ToString(format, CultureInfo.InvariantCulture)}, {A.ToString(format, CultureInfo.InvariantCulture)})"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs index c5998eca5c..1f50a38e78 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -424,10 +425,7 @@ namespace Godot /// Converts this <see cref="Plane"/> to a string. /// </summary> /// <returns>A string representation of this plane.</returns> - public override readonly string ToString() - { - return $"{_normal}, {_d}"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Plane"/> to a string with the given <paramref name="format"/>. @@ -435,9 +433,7 @@ namespace Godot /// <returns>A string representation of this plane.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"{_normal.ToString(format)}, {_d.ToString(format)}"; -#pragma warning restore CA1305 + return $"{_normal.ToString(format)}, {_d.ToString(format, CultureInfo.InvariantCulture)}"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Projection.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Projection.cs index c0889fb0e8..fed0eb5e78 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Projection.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Projection.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -1012,10 +1013,7 @@ namespace Godot /// Converts this <see cref="Projection"/> to a string. /// </summary> /// <returns>A string representation of this projection.</returns> - public override readonly string ToString() - { - return $"{X.X}, {X.Y}, {X.Z}, {X.W}\n{Y.X}, {Y.Y}, {Y.Z}, {Y.W}\n{Z.X}, {Z.Y}, {Z.Z}, {Z.W}\n{W.X}, {W.Y}, {W.Z}, {W.W}\n"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Projection"/> to a string with the given <paramref name="format"/>. @@ -1023,12 +1021,10 @@ namespace Godot /// <returns>A string representation of this projection.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"{X.X.ToString(format)}, {X.Y.ToString(format)}, {X.Z.ToString(format)}, {X.W.ToString(format)}\n" + - $"{Y.X.ToString(format)}, {Y.Y.ToString(format)}, {Y.Z.ToString(format)}, {Y.W.ToString(format)}\n" + - $"{Z.X.ToString(format)}, {Z.Y.ToString(format)}, {Z.Z.ToString(format)}, {Z.W.ToString(format)}\n" + - $"{W.X.ToString(format)}, {W.Y.ToString(format)}, {W.Z.ToString(format)}, {W.W.ToString(format)}\n"; -#pragma warning restore CA1305 + return $"{X.X.ToString(format, CultureInfo.InvariantCulture)}, {X.Y.ToString(format, CultureInfo.InvariantCulture)}, {X.Z.ToString(format, CultureInfo.InvariantCulture)}, {X.W.ToString(format, CultureInfo.InvariantCulture)}\n" + + $"{Y.X.ToString(format, CultureInfo.InvariantCulture)}, {Y.Y.ToString(format, CultureInfo.InvariantCulture)}, {Y.Z.ToString(format, CultureInfo.InvariantCulture)}, {Y.W.ToString(format, CultureInfo.InvariantCulture)}\n" + + $"{Z.X.ToString(format, CultureInfo.InvariantCulture)}, {Z.Y.ToString(format, CultureInfo.InvariantCulture)}, {Z.Z.ToString(format, CultureInfo.InvariantCulture)}, {Z.W.ToString(format, CultureInfo.InvariantCulture)}\n" + + $"{W.X.ToString(format, CultureInfo.InvariantCulture)}, {W.Y.ToString(format, CultureInfo.InvariantCulture)}, {W.Z.ToString(format, CultureInfo.InvariantCulture)}, {W.W.ToString(format, CultureInfo.InvariantCulture)}\n"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Quaternion.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Quaternion.cs index 6a8cb1ba04..09511ebdca 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Quaternion.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Quaternion.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -811,10 +812,7 @@ namespace Godot /// Converts this <see cref="Quaternion"/> to a string. /// </summary> /// <returns>A string representation of this quaternion.</returns> - public override readonly string ToString() - { - return $"({X}, {Y}, {Z}, {W})"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Quaternion"/> to a string with the given <paramref name="format"/>. @@ -822,9 +820,7 @@ namespace Godot /// <returns>A string representation of this quaternion.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)}, {W.ToString(format)})"; -#pragma warning restore CA1305 + return $"({X.ToString(format, CultureInfo.InvariantCulture)}, {Y.ToString(format, CultureInfo.InvariantCulture)}, {Z.ToString(format, CultureInfo.InvariantCulture)}, {W.ToString(format, CultureInfo.InvariantCulture)})"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs index cf4ac45a9f..9d9065911e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs @@ -469,10 +469,7 @@ namespace Godot /// Converts this <see cref="Rect2"/> to a string. /// </summary> /// <returns>A string representation of this rect.</returns> - public override readonly string ToString() - { - return $"{_position}, {_size}"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Rect2"/> to a string with the given <paramref name="format"/>. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs index 58560df0c5..65704b3da7 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs @@ -429,10 +429,7 @@ namespace Godot /// Converts this <see cref="Rect2I"/> to a string. /// </summary> /// <returns>A string representation of this rect.</returns> - public override readonly string ToString() - { - return $"{_position}, {_size}"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Rect2I"/> to a string with the given <paramref name="format"/>. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs index fccae94eac..4f0015c7ae 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs @@ -1,8 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; +using System.Globalization; using System.Runtime.InteropServices; -using Godot.NativeInterop; #nullable enable @@ -102,6 +101,6 @@ namespace Godot /// Converts this <see cref="Rid"/> to a string. /// </summary> /// <returns>A string representation of this Rid.</returns> - public override string ToString() => $"RID({Id})"; + public override readonly string ToString() => $"RID({Id.ToString(null, CultureInfo.InvariantCulture)})"; } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs index 3443277fee..a5fa89d3bf 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs @@ -650,10 +650,7 @@ namespace Godot /// Converts this <see cref="Transform2D"/> to a string. /// </summary> /// <returns>A string representation of this transform.</returns> - public override readonly string ToString() - { - return $"[X: {X}, Y: {Y}, O: {Origin}]"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Transform2D"/> to a string with the given <paramref name="format"/>. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform3D.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform3D.cs index f80c0bd8dd..0f534d477f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform3D.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform3D.cs @@ -674,10 +674,7 @@ namespace Godot /// Converts this <see cref="Transform3D"/> to a string. /// </summary> /// <returns>A string representation of this transform.</returns> - public override readonly string ToString() - { - return $"[X: {Basis.X}, Y: {Basis.Y}, Z: {Basis.Z}, O: {Origin}]"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Transform3D"/> to a string with the given <paramref name="format"/>. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs index 8f1bc109c0..856fd54352 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -1016,10 +1017,7 @@ namespace Godot /// Converts this <see cref="Vector2"/> to a string. /// </summary> /// <returns>A string representation of this vector.</returns> - public override readonly string ToString() - { - return $"({X}, {Y})"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Vector2"/> to a string with the given <paramref name="format"/>. @@ -1027,9 +1025,7 @@ namespace Godot /// <returns>A string representation of this vector.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"({X.ToString(format)}, {Y.ToString(format)})"; -#pragma warning restore CA1305 + return $"({X.ToString(format, CultureInfo.InvariantCulture)}, {Y.ToString(format, CultureInfo.InvariantCulture)})"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs index 1a386d9da1..511cc7971c 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -589,10 +590,7 @@ namespace Godot /// Converts this <see cref="Vector2I"/> to a string. /// </summary> /// <returns>A string representation of this vector.</returns> - public override readonly string ToString() - { - return $"({X}, {Y})"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Vector2I"/> to a string with the given <paramref name="format"/>. @@ -600,9 +598,7 @@ namespace Godot /// <returns>A string representation of this vector.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"({X.ToString(format)}, {Y.ToString(format)})"; -#pragma warning restore CA1305 + return $"({X.ToString(format, CultureInfo.InvariantCulture)}, {Y.ToString(format, CultureInfo.InvariantCulture)})"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs index 74c1616e3d..6300705107 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -1118,10 +1119,7 @@ namespace Godot /// Converts this <see cref="Vector3"/> to a string. /// </summary> /// <returns>A string representation of this vector.</returns> - public override readonly string ToString() - { - return $"({X}, {Y}, {Z})"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Vector3"/> to a string with the given <paramref name="format"/>. @@ -1129,9 +1127,7 @@ namespace Godot /// <returns>A string representation of this vector.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)})"; -#pragma warning restore CA1305 + return $"({X.ToString(format, CultureInfo.InvariantCulture)}, {Y.ToString(format, CultureInfo.InvariantCulture)}, {Z.ToString(format, CultureInfo.InvariantCulture)})"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs index d0ad1922f6..aea46efc5b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -644,10 +645,7 @@ namespace Godot /// Converts this <see cref="Vector3I"/> to a string. /// </summary> /// <returns>A string representation of this vector.</returns> - public override readonly string ToString() - { - return $"({X}, {Y}, {Z})"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Vector3I"/> to a string with the given <paramref name="format"/>. @@ -655,9 +653,7 @@ namespace Godot /// <returns>A string representation of this vector.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)})"; -#pragma warning restore CA1305 + return $"({X.ToString(format, CultureInfo.InvariantCulture)}, {Y.ToString(format, CultureInfo.InvariantCulture)}, {Z.ToString(format, CultureInfo.InvariantCulture)})"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs index 115e65bbdb..7c4832943c 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -894,10 +895,7 @@ namespace Godot /// Converts this <see cref="Vector4"/> to a string. /// </summary> /// <returns>A string representation of this vector.</returns> - public override string ToString() - { - return $"({X}, {Y}, {Z}, {W})"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Vector4"/> to a string with the given <paramref name="format"/>. @@ -905,9 +903,7 @@ namespace Godot /// <returns>A string representation of this vector.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)}, {W.ToString(format)})"; -#pragma warning restore CA1305 + return $"({X.ToString(format, CultureInfo.InvariantCulture)}, {Y.ToString(format, CultureInfo.InvariantCulture)}, {Z.ToString(format, CultureInfo.InvariantCulture)}, {W.ToString(format, CultureInfo.InvariantCulture)})"; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs index 527c8e5022..27aa86b7e4 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; #nullable enable @@ -665,10 +666,7 @@ namespace Godot /// Converts this <see cref="Vector4I"/> to a string. /// </summary> /// <returns>A string representation of this vector.</returns> - public override readonly string ToString() - { - return $"({X}, {Y}, {Z}, {W})"; - } + public override readonly string ToString() => ToString(null); /// <summary> /// Converts this <see cref="Vector4I"/> to a string with the given <paramref name="format"/>. @@ -676,9 +674,7 @@ namespace Godot /// <returns>A string representation of this vector.</returns> public readonly string ToString(string? format) { -#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" - return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)}, {W.ToString(format)})"; -#pragma warning restore CA1305 + return $"({X.ToString(format, CultureInfo.InvariantCulture)}, {Y.ToString(format, CultureInfo.InvariantCulture)}, {Z.ToString(format, CultureInfo.InvariantCulture)}, {W.ToString(format, CultureInfo.InvariantCulture)})"; } } } diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 894c13cc0b..64ef1397ba 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -3096,7 +3096,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return err; } if (user_data.libs.size() > 0) { - Ref<FileAccess> fa = FileAccess::open(GDEXTENSION_LIBS_PATH, FileAccess::WRITE); + Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE); fa->store_string(JSON::stringify(user_data.libs, "\t")); } } else { diff --git a/platform/ios/app_delegate.mm b/platform/ios/app_delegate.mm index 5a0c57be93..37d2696434 100644 --- a/platform/ios/app_delegate.mm +++ b/platform/ios/app_delegate.mm @@ -116,6 +116,8 @@ static ViewController *mainViewController = nil; } else if (sessionCategorySetting == SESSION_CATEGORY_PLAY_AND_RECORD) { category = AVAudioSessionCategoryPlayAndRecord; options |= AVAudioSessionCategoryOptionDefaultToSpeaker; + options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP; + options |= AVAudioSessionCategoryOptionAllowAirPlay; } else if (sessionCategorySetting == SESSION_CATEGORY_PLAYBACK) { category = AVAudioSessionCategoryPlayback; } else if (sessionCategorySetting == SESSION_CATEGORY_RECORD) { diff --git a/platform/ios/godot_ios.mm b/platform/ios/godot_ios.mm index 5e66c8b47b..9d35d43344 100644 --- a/platform/ios/godot_ios.mm +++ b/platform/ios/godot_ios.mm @@ -102,15 +102,16 @@ int ios_main(int argc, char **argv) { Error err = Main::setup(fargv[0], argc - 1, &fargv[1], false); - if (err == ERR_HELP) { // Returned by --help and --version, so success. - return 0; - } else if (err != OK) { - return 255; + if (err != OK) { + if (err == ERR_HELP) { // Returned by --help and --version, so success. + return EXIT_SUCCESS; + } + return EXIT_FAILURE; } os->initialize_modules(); - return 0; + return os->get_exit_code(); } void ios_finish() { diff --git a/platform/linuxbsd/godot_linuxbsd.cpp b/platform/linuxbsd/godot_linuxbsd.cpp index a2b6fbeb25..b0880c86b8 100644 --- a/platform/linuxbsd/godot_linuxbsd.cpp +++ b/platform/linuxbsd/godot_linuxbsd.cpp @@ -72,18 +72,19 @@ int main(int argc, char *argv[]) { char *ret = getcwd(cwd, PATH_MAX); Error err = Main::setup(argv[0], argc - 1, &argv[1]); + if (err != OK) { free(cwd); - if (err == ERR_HELP) { // Returned by --help and --version, so success. - return 0; + return EXIT_SUCCESS; } - return 255; + return EXIT_FAILURE; } - if (Main::start()) { - os.set_exit_code(EXIT_SUCCESS); - os.run(); // it is actually the OS that decides how to run + if (Main::start() == EXIT_SUCCESS) { + os.run(); + } else { + os.set_exit_code(EXIT_FAILURE); } Main::cleanup(); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 34aa15abbb..226e123648 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -1995,8 +1995,7 @@ void DisplayServerX11::window_set_current_screen(int p_screen, WindowID p_window Size2i wsize = window_get_size(p_window); wpos += srect.position; if (srect != Rect2i()) { - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - wsize.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - wsize.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - wsize / 3); } window_set_position(wpos, p_window); } @@ -2224,8 +2223,7 @@ void DisplayServerX11::window_set_size(const Size2i p_size, WindowID p_window) { ERR_FAIL_COND(!windows.has(p_window)); Size2i size = p_size; - size.x = MAX(1, size.x); - size.y = MAX(1, size.y); + size = size.max(Size2i(1, 1)); WindowData &wd = windows[p_window]; @@ -5451,8 +5449,7 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, V } else { Rect2i srect = screen_get_usable_rect(rq_screen); Point2i wpos = p_rect.position; - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - p_rect.size.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - p_rect.size.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - p_rect.size / 3); win_rect.position = wpos; } diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index c52ed00b3d..99e9d20f2b 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -83,8 +83,7 @@ DisplayServerMacOS::WindowID DisplayServerMacOS::_create_window(WindowMode p_mod Rect2i srect = screen_get_usable_rect(rq_screen); Point2i wpos = p_rect.position; if (srect != Rect2i()) { - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - p_rect.size.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - p_rect.size.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - p_rect.size / 3); } // macOS native y-coordinate relative to _get_screens_origin() is negative, // Godot passes a positive value. @@ -1877,8 +1876,7 @@ void DisplayServerMacOS::window_set_current_screen(int p_screen, WindowID p_wind Size2i wsize = window_get_size(p_window); wpos += srect.position; - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - wsize.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - wsize.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - wsize / 3); window_set_position(wpos, p_window); if (was_fullscreen) { @@ -2309,8 +2307,7 @@ void DisplayServerMacOS::window_set_window_buttons_offset(const Vector2i &p_offs WindowData &wd = windows[p_window]; float scale = screen_get_max_scale(); wd.wb_offset = p_offset / scale; - wd.wb_offset.x = MAX(wd.wb_offset.x, 12); - wd.wb_offset.y = MAX(wd.wb_offset.y, 12); + wd.wb_offset = wd.wb_offset.max(Vector2i(12, 12)); if (wd.window_button_view) { [wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)]; } diff --git a/platform/macos/godot_main_macos.mm b/platform/macos/godot_main_macos.mm index 3959fb686c..279d805462 100644 --- a/platform/macos/godot_main_macos.mm +++ b/platform/macos/godot_main_macos.mm @@ -69,18 +69,21 @@ int main(int argc, char **argv) { err = Main::setup(argv[0], argc - first_arg, &argv[first_arg]); } - if (err == ERR_HELP) { // Returned by --help and --version, so success. - return 0; - } else if (err != OK) { - return 255; + if (err != OK) { + if (err == ERR_HELP) { // Returned by --help and --version, so success. + return EXIT_SUCCESS; + } + return EXIT_FAILURE; } - bool ok; + int ret; @autoreleasepool { - ok = Main::start(); + ret = Main::start(); } - if (ok) { - os.run(); // It is actually the OS that decides how to run. + if (ret) { + os.run(); + } else { + os.set_exit_code(EXIT_FAILURE); } @autoreleasepool { diff --git a/platform/web/detect.py b/platform/web/detect.py index e692c79a20..2d2cc288a1 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -86,7 +86,7 @@ def configure(env: "SConsEnvironment"): supported_arches = ["wasm32"] if env["arch"] not in supported_arches: print( - 'Unsupported CPU architecture "%s" for iOS. Supported architectures are: %s.' + 'Unsupported CPU architecture "%s" for Web. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) sys.exit() diff --git a/platform/web/web_main.cpp b/platform/web/web_main.cpp index ad2a801881..04513f6d57 100644 --- a/platform/web/web_main.cpp +++ b/platform/web/web_main.cpp @@ -109,24 +109,22 @@ extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) { // Proper shutdown in case of setup failure. if (err != OK) { - int exit_code = (int)err; - if (err == ERR_HELP) { - exit_code = 0; // Called with --help. - } - os->set_exit_code(exit_code); // Will only exit after sync. emscripten_set_main_loop(exit_callback, -1, false); godot_js_os_finish_async(cleanup_after_sync); - return exit_code; + if (err == ERR_HELP) { // Returned by --help and --version, so success. + return EXIT_SUCCESS; + } + return EXIT_FAILURE; } - os->set_exit_code(0); main_started = true; // Ease up compatibility. ResourceLoader::set_abort_on_missing_resources(false); - Main::start(); + int ret = Main::start(); + os->set_exit_code(ret); os->get_main_loop()->initialize(); #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_project_manager_hint() && FileAccess::exists("/tmp/preload.zip")) { @@ -140,5 +138,5 @@ extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) { // We are inside an animation frame, we want to immediately draw on the newly setup canvas. main_loop_callback(); - return 0; + return os->get_exit_code(); } diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 074d40bf4b..79078456a5 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -903,8 +903,7 @@ static BOOL CALLBACK _MonitorEnumProcPos(HMONITOR hMonitor, HDC hdcMonitor, LPRE static BOOL CALLBACK _MonitorEnumProcOrigin(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) { EnumPosData *data = (EnumPosData *)dwData; - data->pos.x = MIN(data->pos.x, lprcMonitor->left); - data->pos.y = MIN(data->pos.y, lprcMonitor->top); + data->pos = data->pos.min(Point2(lprcMonitor->left, lprcMonitor->top)); return TRUE; } @@ -1637,8 +1636,7 @@ void DisplayServerWindows::window_set_current_screen(int p_screen, WindowID p_wi Size2i wsize = window_get_size(p_window); wpos += srect.position; - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - wsize.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - wsize.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - wsize / 3); window_set_position(wpos, p_window); } } @@ -5076,8 +5074,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, Rect2i srect = screen_get_usable_rect(rq_screen); Point2i wpos = p_rect.position; if (srect != Rect2i()) { - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - p_rect.size.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - p_rect.size.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - p_rect.size / 3); } WindowRect.left = wpos.x; diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index f86ecd87fb..5f41b4e568 100644 --- a/platform/windows/godot_windows.cpp +++ b/platform/windows/godot_windows.cpp @@ -171,13 +171,15 @@ int widechar_main(int argc, wchar_t **argv) { delete[] argv_utf8; if (err == ERR_HELP) { // Returned by --help and --version, so success. - return 0; + return EXIT_SUCCESS; } - return 255; + return EXIT_FAILURE; } - if (Main::start()) { + if (Main::start() == EXIT_SUCCESS) { os.run(); + } else { + os.set_exit_code(EXIT_FAILURE); } Main::cleanup(); diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp index 722858b674..e9bab274c6 100644 --- a/scene/2d/camera_2d.cpp +++ b/scene/2d/camera_2d.cpp @@ -54,7 +54,12 @@ void Camera2D::_update_scroll() { if (is_current()) { ERR_FAIL_COND(custom_viewport && !ObjectDB::get_instance(custom_viewport_id)); - Transform2D xform = get_camera_transform(); + Transform2D xform; + if (is_physics_interpolated_and_enabled()) { + xform = _interpolation_data.xform_prev.interpolate_with(_interpolation_data.xform_curr, Engine::get_singleton()->get_physics_interpolation_fraction()); + } else { + xform = get_camera_transform(); + } viewport->set_canvas_transform(xform); @@ -68,15 +73,26 @@ void Camera2D::_update_scroll() { } void Camera2D::_update_process_callback() { - if (_is_editing_in_editor()) { + if (is_physics_interpolated_and_enabled()) { + set_process_internal(is_current()); + set_physics_process_internal(is_current()); + +#ifdef TOOLS_ENABLED + if (process_callback == CAMERA2D_PROCESS_IDLE) { + WARN_PRINT_ONCE("Camera2D overridden to physics process mode due to use of physics interpolation."); + } +#endif + } else if (_is_editing_in_editor()) { set_process_internal(false); set_physics_process_internal(false); - } else if (process_callback == CAMERA2D_PROCESS_IDLE) { - set_process_internal(true); - set_physics_process_internal(false); } else { - set_process_internal(false); - set_physics_process_internal(true); + if (process_callback == CAMERA2D_PROCESS_IDLE) { + set_process_internal(true); + set_physics_process_internal(false); + } else { + set_process_internal(false); + set_physics_process_internal(true); + } } } @@ -161,8 +177,15 @@ Transform2D Camera2D::get_camera_transform() { } } + // FIXME: There is a bug here, introduced before physics interpolation. + // Smoothing occurs rather confusingly during the call to get_camera_transform(). + // It may be called MULTIPLE TIMES on certain frames, + // therefore smoothing is not currently applied only once per frame / tick, + // which will result in some haphazard results. if (position_smoothing_enabled && !_is_editing_in_editor()) { - real_t c = position_smoothing_speed * (process_callback == CAMERA2D_PROCESS_PHYSICS ? get_physics_process_delta_time() : get_process_delta_time()); + bool physics_process = (process_callback == CAMERA2D_PROCESS_PHYSICS) || is_physics_interpolated_and_enabled(); + real_t delta = physics_process ? get_physics_process_delta_time() : get_process_delta_time(); + real_t c = position_smoothing_speed * delta; smoothed_camera_pos = ((camera_pos - smoothed_camera_pos) * c) + smoothed_camera_pos; ret_camera_pos = smoothed_camera_pos; //camera_pos=camera_pos*(1.0-position_smoothing_speed)+new_camera_pos*position_smoothing_speed; @@ -223,17 +246,52 @@ Transform2D Camera2D::get_camera_transform() { return xform.affine_inverse(); } +void Camera2D::_ensure_update_interpolation_data() { + // The "curr -> previous" update can either occur + // on NOTIFICATION_INTERNAL_PHYSICS_PROCESS, OR + // on NOTIFICATION_TRANSFORM_CHANGED, + // if NOTIFICATION_TRANSFORM_CHANGED takes place earlier than + // NOTIFICATION_INTERNAL_PHYSICS_PROCESS on a tick. + // This is to ensure that the data keeps flowing, but the new data + // doesn't overwrite before prev has been set. + + // Keep the data flowing. + uint64_t tick = Engine::get_singleton()->get_physics_frames(); + if (_interpolation_data.last_update_physics_tick != tick) { + _interpolation_data.xform_prev = _interpolation_data.xform_curr; + _interpolation_data.last_update_physics_tick = tick; + } +} + void Camera2D::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_INTERNAL_PROCESS: - case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + case NOTIFICATION_INTERNAL_PROCESS: { _update_scroll(); } break; + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + if (is_physics_interpolated_and_enabled()) { + _ensure_update_interpolation_data(); + _interpolation_data.xform_curr = get_camera_transform(); + } else { + _update_scroll(); + } + } break; + + case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: { + // Force the limits etc. to update. + _interpolation_data.xform_curr = get_camera_transform(); + _interpolation_data.xform_prev = _interpolation_data.xform_curr; + } break; + case NOTIFICATION_TRANSFORM_CHANGED: { - if (!position_smoothing_enabled || _is_editing_in_editor()) { + if ((!position_smoothing_enabled && !is_physics_interpolated_and_enabled()) || _is_editing_in_editor()) { _update_scroll(); } + if (is_physics_interpolated_and_enabled()) { + _ensure_update_interpolation_data(); + _interpolation_data.xform_curr = get_camera_transform(); + } } break; case NOTIFICATION_ENTER_TREE: { @@ -260,6 +318,15 @@ void Camera2D::_notification(int p_what) { _update_process_callback(); first = true; _update_scroll(); + + // Note that NOTIFICATION_RESET_PHYSICS_INTERPOLATION + // is automatically called before this because Camera2D is inherited + // from CanvasItem. However, the camera transform is not up to date + // until this point, so we do an extra manual reset. + if (is_physics_interpolated_and_enabled()) { + _interpolation_data.xform_curr = get_camera_transform(); + _interpolation_data.xform_prev = _interpolation_data.xform_curr; + } } break; case NOTIFICATION_EXIT_TREE: { @@ -431,12 +498,17 @@ void Camera2D::_make_current(Object *p_which) { queue_redraw(); - if (p_which == this) { + bool was_current = viewport->get_camera_2d() == this; + bool is_current = p_which == this; + + if (is_current) { viewport->_camera_2d_set(this); - } else { - if (viewport->get_camera_2d() == this) { - viewport->_camera_2d_set(nullptr); - } + } else if (was_current) { + viewport->_camera_2d_set(nullptr); + } + + if (is_current != was_current) { + _update_process_callback(); } } @@ -456,6 +528,7 @@ void Camera2D::make_current() { _make_current(this); } _update_scroll(); + _update_process_callback(); } void Camera2D::clear_current() { @@ -468,6 +541,8 @@ void Camera2D::clear_current() { if (!custom_viewport || ObjectDB::get_instance(custom_viewport_id)) { viewport->assign_next_enabled_camera_2d(group_name); } + + _update_process_callback(); } bool Camera2D::is_current() const { diff --git a/scene/2d/camera_2d.h b/scene/2d/camera_2d.h index 5693d05ee5..bf25267aa8 100644 --- a/scene/2d/camera_2d.h +++ b/scene/2d/camera_2d.h @@ -102,6 +102,14 @@ protected: Camera2DProcessCallback process_callback = CAMERA2D_PROCESS_IDLE; + struct InterpolationData { + Transform2D xform_curr; + Transform2D xform_prev; + uint32_t last_update_physics_tick = 0; + } _interpolation_data; + + void _ensure_update_interpolation_data(); + Size2 _get_camera_screen_size() const; protected: diff --git a/scene/2d/light_2d.cpp b/scene/2d/light_2d.cpp index c03786caef..79732d7861 100644 --- a/scene/2d/light_2d.cpp +++ b/scene/2d/light_2d.cpp @@ -197,6 +197,10 @@ Light2D::BlendMode Light2D::get_blend_mode() const { return blend_mode; } +void Light2D::_physics_interpolated_changed() { + RenderingServer::get_singleton()->canvas_light_set_interpolated(canvas_light, is_physics_interpolated()); +} + void Light2D::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { @@ -212,6 +216,17 @@ void Light2D::_notification(int p_what) { _update_light_visibility(); } break; + case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: { + if (is_visible_in_tree() && is_physics_interpolated()) { + // Explicitly make sure the transform is up to date in RenderingServer before + // resetting. This is necessary because NOTIFICATION_TRANSFORM_CHANGED + // is normally deferred, and a client change to transform will not always be sent + // before the reset, so we need to guarantee this. + RS::get_singleton()->canvas_light_set_transform(canvas_light, get_global_transform()); + RS::get_singleton()->canvas_light_reset_physics_interpolation(canvas_light); + } + } break; + case NOTIFICATION_EXIT_TREE: { RS::get_singleton()->canvas_light_attach_to_canvas(canvas_light, RID()); _update_light_visibility(); diff --git a/scene/2d/light_2d.h b/scene/2d/light_2d.h index 3c1171deae..8a0c2a2a92 100644 --- a/scene/2d/light_2d.h +++ b/scene/2d/light_2d.h @@ -74,6 +74,7 @@ private: void _update_light_visibility(); virtual void owner_changed_notify() override; + virtual void _physics_interpolated_changed() override; protected: _FORCE_INLINE_ RID _get_light() const { return canvas_light; } diff --git a/scene/2d/light_occluder_2d.cpp b/scene/2d/light_occluder_2d.cpp index 61f5d5cd46..092c987ac0 100644 --- a/scene/2d/light_occluder_2d.cpp +++ b/scene/2d/light_occluder_2d.cpp @@ -158,6 +158,10 @@ void LightOccluder2D::_poly_changed() { #endif } +void LightOccluder2D::_physics_interpolated_changed() { + RenderingServer::get_singleton()->canvas_light_occluder_set_interpolated(occluder, is_physics_interpolated()); +} + void LightOccluder2D::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_CANVAS: { @@ -199,6 +203,17 @@ void LightOccluder2D::_notification(int p_what) { case NOTIFICATION_EXIT_CANVAS: { RS::get_singleton()->canvas_light_occluder_attach_to_canvas(occluder, RID()); } break; + + case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: { + if (is_visible_in_tree() && is_physics_interpolated()) { + // Explicitly make sure the transform is up to date in RenderingServer before + // resetting. This is necessary because NOTIFICATION_TRANSFORM_CHANGED + // is normally deferred, and a client change to transform will not always be sent + // before the reset, so we need to guarantee this. + RS::get_singleton()->canvas_light_occluder_set_transform(occluder, get_global_transform()); + RS::get_singleton()->canvas_light_occluder_reset_physics_interpolation(occluder); + } + } break; } } diff --git a/scene/2d/light_occluder_2d.h b/scene/2d/light_occluder_2d.h index dd3130394e..4c499d0465 100644 --- a/scene/2d/light_occluder_2d.h +++ b/scene/2d/light_occluder_2d.h @@ -86,6 +86,8 @@ class LightOccluder2D : public Node2D { bool sdf_collision = false; void _poly_changed(); + virtual void _physics_interpolated_changed() override; + protected: void _notification(int p_what); static void _bind_methods(); diff --git a/scene/2d/parallax_2d.cpp b/scene/2d/parallax_2d.cpp index f516fd41ac..7e47e0f061 100644 --- a/scene/2d/parallax_2d.cpp +++ b/scene/2d/parallax_2d.cpp @@ -129,6 +129,7 @@ void Parallax2D::_update_repeat() { Point2 repeat_scale = repeat_size * get_scale(); RenderingServer::get_singleton()->canvas_set_item_repeat(get_canvas_item(), repeat_scale, repeat_times); + RenderingServer::get_singleton()->canvas_item_set_interpolated(get_canvas_item(), false); } void Parallax2D::set_scroll_scale(const Size2 &p_scale) { @@ -287,4 +288,6 @@ void Parallax2D::_bind_methods() { } Parallax2D::Parallax2D() { + // Parallax2D is always updated every frame so there is no need to interpolate. + set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_OFF); } diff --git a/scene/2d/parallax_layer.cpp b/scene/2d/parallax_layer.cpp index 3dd0d7b61c..e2a7e9e154 100644 --- a/scene/2d/parallax_layer.cpp +++ b/scene/2d/parallax_layer.cpp @@ -73,6 +73,7 @@ void ParallaxLayer::_update_mirroring() { RID ci = get_canvas_item(); Point2 mirrorScale = mirroring * get_scale(); RenderingServer::get_singleton()->canvas_set_item_mirroring(c, ci, mirrorScale); + RenderingServer::get_singleton()->canvas_item_set_interpolated(ci, false); } } @@ -162,4 +163,6 @@ void ParallaxLayer::_bind_methods() { } ParallaxLayer::ParallaxLayer() { + // ParallaxLayer is always updated every frame so there is no need to interpolate. + set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_OFF); } diff --git a/scene/2d/sprite_2d.cpp b/scene/2d/sprite_2d.cpp index 3c19dd0020..5745a59297 100644 --- a/scene/2d/sprite_2d.cpp +++ b/scene/2d/sprite_2d.cpp @@ -374,8 +374,7 @@ bool Sprite2D::is_pixel_opaque(const Point2 &p_point) const { q.y = texture->get_size().height - q.y - 1; } } else { - q.x = MIN(q.x, texture->get_size().width - 1); - q.y = MIN(q.y, texture->get_size().height - 1); + q = q.min(texture->get_size() - Vector2(1, 1)); } return texture->is_pixel_opaque((int)q.x, (int)q.y); diff --git a/scene/2d/tile_map_layer.cpp b/scene/2d/tile_map_layer.cpp index c7aa4c2be4..84c4c43a5a 100644 --- a/scene/2d/tile_map_layer.cpp +++ b/scene/2d/tile_map_layer.cpp @@ -356,6 +356,14 @@ void TileMapLayer::_rendering_update() { // Drawing the tile in the canvas item. TileMap::draw_tile(ci, local_tile_pos - rendering_quadrant->canvas_items_position, tile_set, cell_data.cell.source_id, cell_data.cell.get_atlas_coords(), cell_data.cell.alternative_tile, -1, get_self_modulate(), tile_data, random_animation_offset); } + + // Reset physics interpolation for any recreated canvas items. + if (is_physics_interpolated_and_enabled() && is_visible_in_tree()) { + for (const RID &ci : rendering_quadrant->canvas_items) { + rs->canvas_item_reset_physics_interpolation(ci); + } + } + } else { // Free the quadrant. for (int i = 0; i < rendering_quadrant->canvas_items.size(); i++) { @@ -453,6 +461,15 @@ void TileMapLayer::_rendering_notification(int p_what) { } } } + } else if (p_what == NOTIFICATION_RESET_PHYSICS_INTERPOLATION) { + for (const KeyValue<Vector2i, Ref<RenderingQuadrant>> &kv : rendering_quadrant_map) { + for (const RID &ci : kv.value->canvas_items) { + if (ci.is_null()) { + continue; + } + rs->canvas_item_reset_physics_interpolation(ci); + } + } } } diff --git a/scene/3d/decal.cpp b/scene/3d/decal.cpp index 6878df21d8..caabb4225f 100644 --- a/scene/3d/decal.cpp +++ b/scene/3d/decal.cpp @@ -31,7 +31,7 @@ #include "decal.h" void Decal::set_size(const Vector3 &p_size) { - size = Vector3(MAX(0.001, p_size.x), MAX(0.001, p_size.y), MAX(0.001, p_size.z)); + size = p_size.max(Vector3(0.001, 0.001, 0.001)); RS::get_singleton()->decal_set_size(decal, size); update_gizmos(); } diff --git a/scene/3d/fog_volume.cpp b/scene/3d/fog_volume.cpp index 12ca1888c4..8af386f282 100644 --- a/scene/3d/fog_volume.cpp +++ b/scene/3d/fog_volume.cpp @@ -73,9 +73,7 @@ bool FogVolume::_get(const StringName &p_name, Variant &r_property) const { void FogVolume::set_size(const Vector3 &p_size) { size = p_size; - size.x = MAX(0.0, size.x); - size.y = MAX(0.0, size.y); - size.z = MAX(0.0, size.z); + size = size.max(Vector3()); RS::get_singleton()->fog_volume_set_size(_get_volume(), size); update_gizmos(); } diff --git a/scene/3d/gpu_particles_collision_3d.cpp b/scene/3d/gpu_particles_collision_3d.cpp index cbc75801b0..8fd5f25749 100644 --- a/scene/3d/gpu_particles_collision_3d.cpp +++ b/scene/3d/gpu_particles_collision_3d.cpp @@ -330,7 +330,7 @@ void GPUParticlesCollisionSDF3D::_find_closest_distance(const Vector3 &p_pos, co Vector3 center = p_bvh[p_bvh_cell].bounds.position + he; Vector3 rel = (p_pos - center).abs(); - Vector3 closest(MIN(rel.x, he.x), MIN(rel.y, he.y), MIN(rel.z, he.z)); + Vector3 closest = rel.min(he); float d = rel.distance_to(closest); if (d >= r_closest_distance) { @@ -382,9 +382,7 @@ Vector3i GPUParticlesCollisionSDF3D::get_estimated_cell_size() const { float cell_size = aabb.get_longest_axis_size() / float(subdiv); Vector3i sdf_size = Vector3i(aabb.size / cell_size); - sdf_size.x = MAX(1, sdf_size.x); - sdf_size.y = MAX(1, sdf_size.y); - sdf_size.z = MAX(1, sdf_size.z); + sdf_size = sdf_size.max(Vector3i(1, 1, 1)); return sdf_size; } @@ -397,9 +395,7 @@ Ref<Image> GPUParticlesCollisionSDF3D::bake() { float cell_size = aabb.get_longest_axis_size() / float(subdiv); Vector3i sdf_size = Vector3i(aabb.size / cell_size); - sdf_size.x = MAX(1, sdf_size.x); - sdf_size.y = MAX(1, sdf_size.y); - sdf_size.z = MAX(1, sdf_size.z); + sdf_size = sdf_size.max(Vector3i(1, 1, 1)); if (bake_begin_function) { bake_begin_function(100); diff --git a/scene/3d/occluder_instance_3d.cpp b/scene/3d/occluder_instance_3d.cpp index 8d995a8785..2f77185d0d 100644 --- a/scene/3d/occluder_instance_3d.cpp +++ b/scene/3d/occluder_instance_3d.cpp @@ -236,7 +236,7 @@ void BoxOccluder3D::set_size(const Vector3 &p_size) { return; } - size = Vector3(MAX(p_size.x, 0.0f), MAX(p_size.y, 0.0f), MAX(p_size.z, 0.0f)); + size = p_size.max(Vector3()); _update(); } diff --git a/scene/3d/voxel_gi.cpp b/scene/3d/voxel_gi.cpp index eb8569fa30..938d6e5699 100644 --- a/scene/3d/voxel_gi.cpp +++ b/scene/3d/voxel_gi.cpp @@ -294,7 +294,7 @@ VoxelGI::Subdiv VoxelGI::get_subdiv() const { void VoxelGI::set_size(const Vector3 &p_size) { // Prevent very small size dimensions as these breaks baking if other size dimensions are set very high. - size = Vector3(MAX(1.0, p_size.x), MAX(1.0, p_size.y), MAX(1.0, p_size.z)); + size = p_size.max(Vector3(1.0, 1.0, 1.0)); update_gizmos(); } diff --git a/scene/animation/animation_blend_space_1d.cpp b/scene/animation/animation_blend_space_1d.cpp index 4cbd9b1d76..36343edd11 100644 --- a/scene/animation/animation_blend_space_1d.cpp +++ b/scene/animation/animation_blend_space_1d.cpp @@ -33,12 +33,17 @@ #include "animation_blend_tree.h" void AnimationNodeBlendSpace1D::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::FLOAT, blend_position)); r_list->push_back(PropertyInfo(Variant::INT, closest, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); - r_list->push_back(PropertyInfo(Variant::FLOAT, length_internal, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); } Variant AnimationNodeBlendSpace1D::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + if (p_parameter == closest) { return -1; } else { @@ -272,9 +277,9 @@ void AnimationNodeBlendSpace1D::_add_blend_point(int p_index, const Ref<Animatio } } -double AnimationNodeBlendSpace1D::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { if (blend_points_used == 0) { - return 0.0; + return NodeTimeInfo(); } AnimationMixer::PlaybackInfo pi = p_playback_info; @@ -287,8 +292,7 @@ double AnimationNodeBlendSpace1D::_process(const AnimationMixer::PlaybackInfo p_ double blend_pos = get_parameter(blend_position); int cur_closest = get_parameter(closest); - double cur_length_internal = get_parameter(length_internal); - double max_time_remaining = 0.0; + NodeTimeInfo mind; if (blend_mode == BLEND_MODE_INTERPOLATED) { int point_lower = -1; @@ -341,12 +345,17 @@ double AnimationNodeBlendSpace1D::_process(const AnimationMixer::PlaybackInfo p_ } // actually blend the animations now - + bool first = true; + double max_weight = 0.0; for (int i = 0; i < blend_points_used; i++) { if (i == point_lower || i == point_higher) { pi.weight = weights[i]; - double remaining = blend_node(blend_points[i].node, blend_points[i].name, pi, FILTER_IGNORE, true, p_test_only); - max_time_remaining = MAX(max_time_remaining, remaining); + NodeTimeInfo t = blend_node(blend_points[i].node, blend_points[i].name, pi, FILTER_IGNORE, true, p_test_only); + if (first || pi.weight > max_weight) { + max_weight = pi.weight; + mind = t; + first = false; + } } else if (sync) { pi.weight = 0; blend_node(blend_points[i].node, blend_points[i].name, pi, FILTER_IGNORE, true, p_test_only); @@ -365,7 +374,7 @@ double AnimationNodeBlendSpace1D::_process(const AnimationMixer::PlaybackInfo p_ } if (new_closest != cur_closest && new_closest != -1) { - double from = 0.0; + NodeTimeInfo from; if (blend_mode == BLEND_MODE_DISCRETE_CARRY && cur_closest != -1) { //for ping-pong loop Ref<AnimationNodeAnimation> na_c = static_cast<Ref<AnimationNodeAnimation>>(blend_points[cur_closest].node); @@ -376,18 +385,17 @@ double AnimationNodeBlendSpace1D::_process(const AnimationMixer::PlaybackInfo p_ //see how much animation remains pi.seeked = false; pi.weight = 0; - from = cur_length_internal - blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, p_test_only); + from = blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, p_test_only); } - pi.time = from; + pi.time = from.position; pi.seeked = true; pi.weight = 1.0; - max_time_remaining = blend_node(blend_points[new_closest].node, blend_points[new_closest].name, pi, FILTER_IGNORE, true, p_test_only); - cur_length_internal = from + max_time_remaining; + mind = blend_node(blend_points[new_closest].node, blend_points[new_closest].name, pi, FILTER_IGNORE, true, p_test_only); cur_closest = new_closest; } else { pi.weight = 1.0; - max_time_remaining = blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, p_test_only); + mind = blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, p_test_only); } if (sync) { @@ -402,8 +410,7 @@ double AnimationNodeBlendSpace1D::_process(const AnimationMixer::PlaybackInfo p_ } set_parameter(closest, cur_closest); - set_parameter(length_internal, cur_length_internal); - return max_time_remaining; + return mind; } String AnimationNodeBlendSpace1D::get_caption() const { diff --git a/scene/animation/animation_blend_space_1d.h b/scene/animation/animation_blend_space_1d.h index 40679d55ef..64ae4d0505 100644 --- a/scene/animation/animation_blend_space_1d.h +++ b/scene/animation/animation_blend_space_1d.h @@ -68,7 +68,6 @@ protected: StringName blend_position = "blend_position"; StringName closest = "closest"; - StringName length_internal = "length_internal"; BlendMode blend_mode = BLEND_MODE_INTERPOLATED; @@ -114,7 +113,7 @@ public: void set_use_sync(bool p_sync); bool is_using_sync() const; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; String get_caption() const override; Ref<AnimationNode> get_child_by_name(const StringName &p_name) const override; diff --git a/scene/animation/animation_blend_space_2d.cpp b/scene/animation/animation_blend_space_2d.cpp index d5c6253e9d..2634248231 100644 --- a/scene/animation/animation_blend_space_2d.cpp +++ b/scene/animation/animation_blend_space_2d.cpp @@ -34,16 +34,19 @@ #include "core/math/geometry_2d.h" void AnimationNodeBlendSpace2D::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::VECTOR2, blend_position)); r_list->push_back(PropertyInfo(Variant::INT, closest, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); - r_list->push_back(PropertyInfo(Variant::FLOAT, length_internal, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); } Variant AnimationNodeBlendSpace2D::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + if (p_parameter == closest) { return -1; - } else if (p_parameter == length_internal) { - return 0; } else { return Vector2(); } @@ -442,19 +445,18 @@ void AnimationNodeBlendSpace2D::_blend_triangle(const Vector2 &p_pos, const Vect r_weights[2] = w; } -double AnimationNodeBlendSpace2D::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { _update_triangles(); Vector2 blend_pos = get_parameter(blend_position); int cur_closest = get_parameter(closest); - double cur_length_internal = get_parameter(length_internal); - double mind = 0.0; //time of min distance point + NodeTimeInfo mind; //time of min distance point AnimationMixer::PlaybackInfo pi = p_playback_info; if (blend_mode == BLEND_MODE_INTERPOLATED) { if (triangles.size() == 0) { - return 0; + return NodeTimeInfo(); } Vector2 best_point; @@ -500,7 +502,7 @@ double AnimationNodeBlendSpace2D::_process(const AnimationMixer::PlaybackInfo p_ } } - ERR_FAIL_COND_V(blend_triangle == -1, 0); //should never reach here + ERR_FAIL_COND_V(blend_triangle == -1, NodeTimeInfo()); //should never reach here int triangle_points[3]; for (int j = 0; j < 3; j++) { @@ -509,15 +511,17 @@ double AnimationNodeBlendSpace2D::_process(const AnimationMixer::PlaybackInfo p_ first = true; + bool found = false; + double max_weight = 0.0; for (int i = 0; i < blend_points_used; i++) { - bool found = false; for (int j = 0; j < 3; j++) { if (i == triangle_points[j]) { //blend with the given weight pi.weight = blend_weights[j]; - double t = blend_node(blend_points[i].node, blend_points[i].name, pi, FILTER_IGNORE, true, p_test_only); - if (first || t < mind) { + NodeTimeInfo t = blend_node(blend_points[i].node, blend_points[i].name, pi, FILTER_IGNORE, true, p_test_only); + if (first || pi.weight > max_weight) { mind = t; + max_weight = pi.weight; first = false; } found = true; @@ -543,7 +547,7 @@ double AnimationNodeBlendSpace2D::_process(const AnimationMixer::PlaybackInfo p_ } if (new_closest != cur_closest && new_closest != -1) { - double from = 0.0; + NodeTimeInfo from; if (blend_mode == BLEND_MODE_DISCRETE_CARRY && cur_closest != -1) { //for ping-pong loop Ref<AnimationNodeAnimation> na_c = static_cast<Ref<AnimationNodeAnimation>>(blend_points[cur_closest].node); @@ -554,14 +558,13 @@ double AnimationNodeBlendSpace2D::_process(const AnimationMixer::PlaybackInfo p_ //see how much animation remains pi.seeked = false; pi.weight = 0; - from = cur_length_internal - blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, p_test_only); + from = blend_node(blend_points[cur_closest].node, blend_points[cur_closest].name, pi, FILTER_IGNORE, true, p_test_only); } - pi.time = from; + pi.time = from.position; pi.seeked = true; pi.weight = 1.0; mind = blend_node(blend_points[new_closest].node, blend_points[new_closest].name, pi, FILTER_IGNORE, true, p_test_only); - cur_length_internal = from + mind; cur_closest = new_closest; } else { pi.weight = 1.0; @@ -580,7 +583,6 @@ double AnimationNodeBlendSpace2D::_process(const AnimationMixer::PlaybackInfo p_ } set_parameter(closest, cur_closest); - set_parameter(length_internal, cur_length_internal); return mind; } diff --git a/scene/animation/animation_blend_space_2d.h b/scene/animation/animation_blend_space_2d.h index 33a821d80c..c26ff2bce0 100644 --- a/scene/animation/animation_blend_space_2d.h +++ b/scene/animation/animation_blend_space_2d.h @@ -65,7 +65,6 @@ protected: StringName blend_position = "blend_position"; StringName closest = "closest"; - StringName length_internal = "length_internal"; Vector2 max_space = Vector2(1, 1); Vector2 min_space = Vector2(-1, -1); Vector2 snap = Vector2(0.1, 0.1); @@ -129,7 +128,7 @@ public: void set_y_label(const String &p_label); String get_y_label() const; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; virtual String get_caption() const override; Vector2 get_closest_point(const Vector2 &p_point); diff --git a/scene/animation/animation_blend_tree.cpp b/scene/animation/animation_blend_tree.cpp index 4a01b2ad65..71f9c45eea 100644 --- a/scene/animation/animation_blend_tree.cpp +++ b/scene/animation/animation_blend_tree.cpp @@ -44,7 +44,26 @@ StringName AnimationNodeAnimation::get_animation() const { Vector<String> (*AnimationNodeAnimation::get_editable_animation_list)() = nullptr; void AnimationNodeAnimation::get_parameter_list(List<PropertyInfo> *r_list) const { - r_list->push_back(PropertyInfo(Variant::FLOAT, time, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); + AnimationNode::get_parameter_list(r_list); +} + +AnimationNode::NodeTimeInfo AnimationNodeAnimation::get_node_time_info() const { + NodeTimeInfo nti; + if (!process_state->tree->has_animation(animation)) { + return nti; + } + + if (use_custom_timeline) { + nti.length = timeline_length; + nti.loop_mode = loop_mode; + } else { + Ref<Animation> anim = process_state->tree->get_animation(animation); + nti.length = (double)anim->get_length(); + nti.loop_mode = anim->get_loop_mode(); + } + nti.position = get_parameter(current_position); + + return nti; } void AnimationNodeAnimation::_validate_property(PropertyInfo &p_property) const { @@ -62,11 +81,34 @@ void AnimationNodeAnimation::_validate_property(PropertyInfo &p_property) const p_property.hint_string = anims; } } + + if (!use_custom_timeline) { + if (p_property.name == "timeline_length" || p_property.name == "start_offset" || p_property.name == "loop_mode" || p_property.name == "stretch_time_scale") { + p_property.usage = PROPERTY_USAGE_NONE; + } + } } -double AnimationNodeAnimation::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { - double cur_time = get_parameter(time); +AnimationNode::NodeTimeInfo AnimationNodeAnimation::process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { + process_state->is_testing = p_test_only; + AnimationMixer::PlaybackInfo pi = p_playback_info; + if (p_playback_info.seeked) { + pi.delta = get_node_time_info().position - p_playback_info.time; + } else { + pi.time = get_node_time_info().position + (backward ? -p_playback_info.delta : p_playback_info.delta); + } + + NodeTimeInfo nti = _process(pi, p_test_only); + + if (!p_test_only) { + set_node_time_info(nti); + } + + return nti; +} + +AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { if (!process_state->tree->has_animation(animation)) { AnimationNodeBlendTree *tree = Object::cast_to<AnimationNodeBlendTree>(node_state.parent); if (tree) { @@ -77,87 +119,129 @@ double AnimationNodeAnimation::_process(const AnimationMixer::PlaybackInfo p_pla make_invalid(vformat(RTR("Animation not found: '%s'"), animation)); } - return 0; + return NodeTimeInfo(); } Ref<Animation> anim = process_state->tree->get_animation(animation); double anim_size = (double)anim->get_length(); - double step = 0.0; - double prev_time = cur_time; + + NodeTimeInfo cur_nti = get_node_time_info(); + double cur_len = cur_nti.length; + double cur_time = p_playback_info.time; + double cur_delta = p_playback_info.delta; + + Animation::LoopMode cur_loop_mode = cur_nti.loop_mode; + double prev_time = cur_nti.position; + Animation::LoopedFlag looped_flag = Animation::LOOPED_FLAG_NONE; bool node_backward = play_mode == PLAY_MODE_BACKWARD; - double p_time = p_playback_info.time; bool p_seek = p_playback_info.seeked; bool p_is_external_seeking = p_playback_info.is_external_seeking; - if (p_playback_info.seeked) { - step = p_time - cur_time; - cur_time = p_time; + bool is_just_looped = false; + + // 1. Progress for AnimationNode. + if (cur_loop_mode != Animation::LOOP_NONE) { + if (cur_loop_mode == Animation::LOOP_LINEAR) { + if (!Math::is_zero_approx(cur_len)) { + if (prev_time <= cur_len && cur_time > cur_len) { + is_just_looped = true; // Don't break with negative timescale since remain will not be 0. + } + cur_time = Math::fposmod(cur_time, cur_len); + } + backward = false; + } else { + if (!Math::is_zero_approx(cur_len)) { + if (prev_time >= 0 && cur_time < 0) { + backward = !backward; + } else if (prev_time <= cur_len && cur_time > cur_len) { + backward = !backward; + is_just_looped = true; // Don't break with negative timescale since remain will not be 0. + } + cur_time = Math::pingpong(cur_time, cur_len); + } + } } else { - p_time *= backward ? -1.0 : 1.0; - cur_time = cur_time + p_time; - step = p_time; + if (cur_time < 0) { + cur_delta += cur_time; + cur_time = 0; + } else if (cur_time > cur_len) { + cur_delta += cur_time - cur_len; + cur_time = cur_len; + } + backward = false; + // If ended, don't progress AnimationNode. So set delta to 0. + if (!Math::is_zero_approx(cur_delta)) { + if (play_mode == PLAY_MODE_FORWARD) { + if (prev_time >= cur_len) { + cur_delta = 0; + } + } else { + if (prev_time <= 0) { + cur_delta = 0; + } + } + } } - bool is_looping = false; - if (anim->get_loop_mode() == Animation::LOOP_PINGPONG) { + // 2. For return, store "AnimationNode" time info here, not "Animation" time info as below. + NodeTimeInfo nti; + nti.length = cur_len; + nti.position = cur_time; + nti.delta = cur_delta; + nti.loop_mode = cur_loop_mode; + nti.is_just_looped = is_just_looped; + + // 3. Progress for Animation. + double prev_playback_time = prev_time - start_offset; + double cur_playback_time = cur_time - start_offset; + if (stretch_time_scale) { + double mlt = anim_size / cur_len; + cur_playback_time *= mlt; + cur_delta *= mlt; + } + if (cur_loop_mode == Animation::LOOP_LINEAR) { if (!Math::is_zero_approx(anim_size)) { - if (prev_time >= 0 && cur_time < 0) { - backward = !backward; + prev_playback_time = Math::fposmod(prev_playback_time, anim_size); + cur_playback_time = Math::fposmod(cur_playback_time, anim_size); + if (prev_playback_time >= 0 && cur_playback_time < 0) { looped_flag = node_backward ? Animation::LOOPED_FLAG_END : Animation::LOOPED_FLAG_START; } - if (prev_time <= anim_size && cur_time > anim_size) { - backward = !backward; + if (prev_playback_time <= anim_size && cur_playback_time > anim_size) { looped_flag = node_backward ? Animation::LOOPED_FLAG_START : Animation::LOOPED_FLAG_END; } - cur_time = Math::pingpong(cur_time, anim_size); } - is_looping = true; - } else if (anim->get_loop_mode() == Animation::LOOP_LINEAR) { + } else if (cur_loop_mode == Animation::LOOP_PINGPONG) { if (!Math::is_zero_approx(anim_size)) { - if (prev_time >= 0 && cur_time < 0) { + if (Math::fposmod(cur_playback_time, anim_size * 2.0) >= anim_size) { + cur_delta = -cur_delta; // Needed for retrieveing discrete keys correctly. + } + prev_playback_time = Math::pingpong(prev_playback_time, anim_size); + cur_playback_time = Math::pingpong(cur_playback_time, anim_size); + if (prev_playback_time >= 0 && cur_playback_time < 0) { looped_flag = node_backward ? Animation::LOOPED_FLAG_END : Animation::LOOPED_FLAG_START; } - if (prev_time <= anim_size && cur_time > anim_size) { + if (prev_playback_time <= anim_size && cur_playback_time > anim_size) { looped_flag = node_backward ? Animation::LOOPED_FLAG_START : Animation::LOOPED_FLAG_END; } - cur_time = Math::fposmod(cur_time, anim_size); } - backward = false; - is_looping = true; } else { - if (cur_time < 0) { - step += cur_time; - cur_time = 0; - } else if (cur_time > anim_size) { - step += anim_size - cur_time; - cur_time = anim_size; - } - backward = false; - - // If ended, don't progress animation. So set delta to 0. - if (p_time > 0) { - if (play_mode == PLAY_MODE_FORWARD) { - if (prev_time >= anim_size) { - step = 0; - } - } else { - if (prev_time <= 0) { - step = 0; - } - } + if (cur_playback_time < 0) { + cur_playback_time = 0; + } else if (cur_playback_time > anim_size) { + cur_playback_time = anim_size; } // Emit start & finish signal. Internally, the detections are the same for backward. // We should use call_deferred since the track keys are still being processed. if (process_state->tree && !p_test_only) { // AnimationTree uses seek to 0 "internally" to process the first key of the animation, which is used as the start detection. - if (p_seek && !p_is_external_seeking && cur_time == 0) { + if (p_seek && !p_is_external_seeking && cur_playback_time == 0) { process_state->tree->call_deferred(SNAME("emit_signal"), "animation_started", animation); } // Finished. - if (prev_time < anim_size && cur_time >= anim_size) { + if (prev_time - start_offset < anim_size && cur_playback_time >= anim_size) { process_state->tree->call_deferred(SNAME("emit_signal"), "animation_finished", animation); } } @@ -166,19 +250,18 @@ double AnimationNodeAnimation::_process(const AnimationMixer::PlaybackInfo p_pla if (!p_test_only) { AnimationMixer::PlaybackInfo pi = p_playback_info; if (play_mode == PLAY_MODE_FORWARD) { - pi.time = cur_time; - pi.delta = step; + pi.time = cur_playback_time; + pi.delta = cur_delta; } else { - pi.time = anim_size - cur_time; - pi.delta = -step; + pi.time = anim_size - cur_playback_time; + pi.delta = -cur_delta; } pi.weight = 1.0; pi.looped_flag = looped_flag; blend_animation(animation, pi); } - set_parameter(time, cur_time); - return is_looping ? HUGE_LENGTH : anim_size - cur_time; + return nti; } String AnimationNodeAnimation::get_caption() const { @@ -201,6 +284,48 @@ bool AnimationNodeAnimation::is_backward() const { return backward; } +void AnimationNodeAnimation::set_use_custom_timeline(bool p_use_custom_timeline) { + use_custom_timeline = p_use_custom_timeline; + notify_property_list_changed(); +} + +bool AnimationNodeAnimation::is_using_custom_timeline() const { + return use_custom_timeline; +} + +void AnimationNodeAnimation::set_timeline_length(double p_length) { + timeline_length = p_length; +} + +double AnimationNodeAnimation::get_timeline_length() const { + return timeline_length; +} + +void AnimationNodeAnimation::set_stretch_time_scale(bool p_strech_time_scale) { + stretch_time_scale = p_strech_time_scale; + notify_property_list_changed(); +} + +bool AnimationNodeAnimation::is_stretching_time_scale() const { + return stretch_time_scale; +} + +void AnimationNodeAnimation::set_start_offset(double p_offset) { + start_offset = p_offset; +} + +double AnimationNodeAnimation::get_start_offset() const { + return start_offset; +} + +void AnimationNodeAnimation::set_loop_mode(Animation::LoopMode p_loop_mode) { + loop_mode = p_loop_mode; +} + +Animation::LoopMode AnimationNodeAnimation::get_loop_mode() const { + return loop_mode; +} + void AnimationNodeAnimation::_bind_methods() { ClassDB::bind_method(D_METHOD("set_animation", "name"), &AnimationNodeAnimation::set_animation); ClassDB::bind_method(D_METHOD("get_animation"), &AnimationNodeAnimation::get_animation); @@ -208,8 +333,28 @@ void AnimationNodeAnimation::_bind_methods() { ClassDB::bind_method(D_METHOD("set_play_mode", "mode"), &AnimationNodeAnimation::set_play_mode); ClassDB::bind_method(D_METHOD("get_play_mode"), &AnimationNodeAnimation::get_play_mode); + ClassDB::bind_method(D_METHOD("set_use_custom_timeline", "use_custom_timeline"), &AnimationNodeAnimation::set_use_custom_timeline); + ClassDB::bind_method(D_METHOD("is_using_custom_timeline"), &AnimationNodeAnimation::is_using_custom_timeline); + + ClassDB::bind_method(D_METHOD("set_timeline_length", "timeline_length"), &AnimationNodeAnimation::set_timeline_length); + ClassDB::bind_method(D_METHOD("get_timeline_length"), &AnimationNodeAnimation::get_timeline_length); + + ClassDB::bind_method(D_METHOD("set_stretch_time_scale", "stretch_time_scale"), &AnimationNodeAnimation::set_stretch_time_scale); + ClassDB::bind_method(D_METHOD("is_stretching_time_scale"), &AnimationNodeAnimation::is_stretching_time_scale); + + ClassDB::bind_method(D_METHOD("set_start_offset", "start_offset"), &AnimationNodeAnimation::set_start_offset); + ClassDB::bind_method(D_METHOD("get_start_offset"), &AnimationNodeAnimation::get_start_offset); + + ClassDB::bind_method(D_METHOD("set_loop_mode", "loop_mode"), &AnimationNodeAnimation::set_loop_mode); + ClassDB::bind_method(D_METHOD("get_loop_mode"), &AnimationNodeAnimation::get_loop_mode); + ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "animation"), "set_animation", "get_animation"); ADD_PROPERTY(PropertyInfo(Variant::INT, "play_mode", PROPERTY_HINT_ENUM, "Forward,Backward"), "set_play_mode", "get_play_mode"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_custom_timeline"), "set_use_custom_timeline", "is_using_custom_timeline"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "timeline_length", PROPERTY_HINT_RANGE, "0.001,60,0.001,or_greater,or_less,hide_slider,suffix:s"), "set_timeline_length", "get_timeline_length"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "stretch_time_scale"), "set_stretch_time_scale", "is_stretching_time_scale"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "start_offset", PROPERTY_HINT_RANGE, "-60,60,0.001,or_greater,or_less,hide_slider,suffix:s"), "set_start_offset", "get_start_offset"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_mode", PROPERTY_HINT_ENUM, "None,Linear,Ping-Pong"), "set_loop_mode", "get_loop_mode"); BIND_ENUM_CONSTANT(PLAY_MODE_FORWARD); BIND_ENUM_CONSTANT(PLAY_MODE_BACKWARD); @@ -240,16 +385,21 @@ AnimationNodeSync::AnimationNodeSync() { //////////////////////////////////////////////////////// void AnimationNodeOneShot::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::BOOL, active, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY)); r_list->push_back(PropertyInfo(Variant::BOOL, internal_active, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY)); r_list->push_back(PropertyInfo(Variant::INT, request, PROPERTY_HINT_ENUM, ",Fire,Abort,Fade Out")); - r_list->push_back(PropertyInfo(Variant::FLOAT, time, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); - r_list->push_back(PropertyInfo(Variant::FLOAT, remaining, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); + r_list->push_back(PropertyInfo(Variant::FLOAT, fade_in_remaining, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); r_list->push_back(PropertyInfo(Variant::FLOAT, fade_out_remaining, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); r_list->push_back(PropertyInfo(Variant::FLOAT, time_to_restart, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); } Variant AnimationNodeOneShot::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + if (p_parameter == request) { return ONE_SHOT_REQUEST_NONE; } else if (p_parameter == active || p_parameter == internal_active) { @@ -262,6 +412,10 @@ Variant AnimationNodeOneShot::get_parameter_default_value(const StringName &p_pa } bool AnimationNodeOneShot::is_parameter_read_only(const StringName &p_parameter) const { + if (AnimationNode::is_parameter_read_only(p_parameter)) { + return true; + } + if (p_parameter == active || p_parameter == internal_active) { return true; } @@ -332,6 +486,14 @@ AnimationNodeOneShot::MixMode AnimationNodeOneShot::get_mix_mode() const { return mix; } +void AnimationNodeOneShot::set_break_loop_at_end(bool p_enable) { + break_loop_at_end = p_enable; +} + +bool AnimationNodeOneShot::is_loop_broken_at_end() const { + return break_loop_at_end; +} + String AnimationNodeOneShot::get_caption() const { return "OneShot"; } @@ -340,14 +502,14 @@ bool AnimationNodeOneShot::has_filter() const { return true; } -double AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { OneShotRequest cur_request = static_cast<OneShotRequest>((int)get_parameter(request)); bool cur_active = get_parameter(active); bool cur_internal_active = get_parameter(internal_active); - double cur_time = get_parameter(time); - double cur_remaining = get_parameter(remaining); - double cur_fade_out_remaining = get_parameter(fade_out_remaining); + NodeTimeInfo cur_nti = get_node_time_info(); double cur_time_to_restart = get_parameter(time_to_restart); + double cur_fade_in_remaining = get_parameter(fade_in_remaining); + double cur_fade_out_remaining = get_parameter(fade_out_remaining); set_parameter(request, ONE_SHOT_REQUEST_NONE); @@ -356,6 +518,8 @@ double AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playb bool is_fading_out = cur_active == true && cur_internal_active == false; double p_time = p_playback_info.time; + double p_delta = p_playback_info.delta; + double abs_delta = Math::abs(p_delta); bool p_seek = p_playback_info.seeked; bool p_is_external_seeking = p_playback_info.is_external_seeking; @@ -374,6 +538,7 @@ double AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playb // Request fading. is_fading_out = true; cur_fade_out_remaining = fade_out; + cur_fade_in_remaining = 0; } else { // Shot is ended, do nothing. is_shooting = false; @@ -382,7 +547,7 @@ double AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playb set_parameter(time_to_restart, -1); } else if (!do_start && !cur_active) { if (cur_time_to_restart >= 0.0 && !p_seek) { - cur_time_to_restart -= p_time; + cur_time_to_restart -= abs_delta; if (cur_time_to_restart < 0) { do_start = true; // Restart. } @@ -413,8 +578,11 @@ double AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playb } if (do_start) { - cur_time = 0; os_seek = true; + if (!cur_internal_active) { + cur_fade_in_remaining = fade_in; // If already active, don't fade-in again. + } + cur_internal_active = true; set_parameter(request, ONE_SHOT_REQUEST_NONE); set_parameter(internal_active, true); set_parameter(active, true); @@ -422,20 +590,17 @@ double AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playb real_t blend = 1.0; bool use_blend = sync; - if (cur_time < fade_in) { + + if (cur_fade_in_remaining > 0) { if (fade_in > 0) { use_blend = true; - blend = cur_time / fade_in; + blend = (fade_in - cur_fade_in_remaining) / fade_in; if (fade_in_curve.is_valid()) { blend = fade_in_curve->sample(blend); } } else { blend = 0; // Should not happen. } - } else if (!do_start && !is_fading_out && cur_remaining <= fade_out) { - is_fading_out = true; - cur_fade_out_remaining = cur_remaining; - set_parameter(internal_active, false); } if (is_fading_out) { @@ -451,34 +616,36 @@ double AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playb } AnimationMixer::PlaybackInfo pi = p_playback_info; - double main_rem = 0.0; + NodeTimeInfo main_nti; if (mix == MIX_MODE_ADD) { pi.weight = 1.0; - main_rem = blend_input(0, pi, FILTER_IGNORE, sync, p_test_only); + main_nti = blend_input(0, pi, FILTER_IGNORE, sync, p_test_only); } else { pi.seeked &= use_blend; pi.weight = 1.0 - blend; - main_rem = blend_input(0, pi, FILTER_BLEND, sync, p_test_only); // Unlike below, processing this edge is a corner case. + main_nti = blend_input(0, pi, FILTER_BLEND, sync, p_test_only); // Unlike below, processing this edge is a corner case. } + pi = p_playback_info; - if (os_seek) { - pi.time = cur_time; + if (do_start) { + pi.time = 0; + } else if (os_seek) { + pi.time = cur_nti.position; } pi.seeked = os_seek; pi.weight = Math::is_zero_approx(blend) ? CMP_EPSILON : blend; - double os_rem = blend_input(1, pi, FILTER_PASS, true, p_test_only); // Blend values must be more than CMP_EPSILON to process discrete keys in edge. - if (do_start) { - cur_remaining = os_rem; + NodeTimeInfo os_nti = blend_input(1, pi, FILTER_PASS, true, p_test_only); // Blend values must be more than CMP_EPSILON to process discrete keys in edge. + + if (cur_fade_in_remaining <= 0 && !do_start && !is_fading_out && os_nti.get_remain(break_loop_at_end) <= fade_out) { + is_fading_out = true; + cur_fade_out_remaining = os_nti.get_remain(break_loop_at_end); + cur_fade_in_remaining = 0; + set_parameter(internal_active, false); } - if (p_seek) { - cur_time = p_time; - } else { - cur_time += p_time; - cur_remaining = os_rem; - cur_fade_out_remaining -= p_time; - if (cur_remaining <= 0 || (is_fading_out && cur_fade_out_remaining <= 0)) { + if (!p_seek) { + if (os_nti.get_remain(break_loop_at_end) <= 0 || (is_fading_out && cur_fade_out_remaining <= 0)) { set_parameter(internal_active, false); set_parameter(active, false); if (auto_restart) { @@ -486,13 +653,17 @@ double AnimationNodeOneShot::_process(const AnimationMixer::PlaybackInfo p_playb set_parameter(time_to_restart, restart_sec); } } + double d = Math::abs(os_nti.delta); + if (!do_start) { + cur_fade_in_remaining = MAX(0, cur_fade_in_remaining - d); // Don't consider seeked delta by restart. + } + cur_fade_out_remaining = MAX(0, cur_fade_out_remaining - d); } - set_parameter(time, cur_time); - set_parameter(remaining, cur_remaining); + set_parameter(fade_in_remaining, cur_fade_in_remaining); set_parameter(fade_out_remaining, cur_fade_out_remaining); - return MAX(main_rem, cur_remaining); + return cur_internal_active ? os_nti : main_nti; } void AnimationNodeOneShot::_bind_methods() { @@ -508,6 +679,9 @@ void AnimationNodeOneShot::_bind_methods() { ClassDB::bind_method(D_METHOD("set_fadeout_curve", "curve"), &AnimationNodeOneShot::set_fade_out_curve); ClassDB::bind_method(D_METHOD("get_fadeout_curve"), &AnimationNodeOneShot::get_fade_out_curve); + ClassDB::bind_method(D_METHOD("set_break_loop_at_end", "enable"), &AnimationNodeOneShot::set_break_loop_at_end); + ClassDB::bind_method(D_METHOD("is_loop_broken_at_end"), &AnimationNodeOneShot::is_loop_broken_at_end); + ClassDB::bind_method(D_METHOD("set_autorestart", "active"), &AnimationNodeOneShot::set_auto_restart_enabled); ClassDB::bind_method(D_METHOD("has_autorestart"), &AnimationNodeOneShot::is_auto_restart_enabled); @@ -526,6 +700,7 @@ void AnimationNodeOneShot::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "fadein_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_fadein_curve", "get_fadein_curve"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fadeout_time", PROPERTY_HINT_RANGE, "0,60,0.01,or_greater,suffix:s"), "set_fadeout_time", "get_fadeout_time"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "fadeout_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_fadeout_curve", "get_fadeout_curve"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "break_loop_at_end"), "set_break_loop_at_end", "is_loop_broken_at_end"); ADD_GROUP("Auto Restart", "autorestart_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autorestart"), "set_autorestart", "has_autorestart"); @@ -550,10 +725,16 @@ AnimationNodeOneShot::AnimationNodeOneShot() { //////////////////////////////////////////////// void AnimationNodeAdd2::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::FLOAT, add_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); } Variant AnimationNodeAdd2::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + return 0; } @@ -565,16 +746,16 @@ bool AnimationNodeAdd2::has_filter() const { return true; } -double AnimationNodeAdd2::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeAdd2::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { double amount = get_parameter(add_amount); AnimationMixer::PlaybackInfo pi = p_playback_info; pi.weight = 1.0; - double rem0 = blend_input(0, pi, FILTER_IGNORE, sync, p_test_only); + NodeTimeInfo nti = blend_input(0, pi, FILTER_IGNORE, sync, p_test_only); pi.weight = amount; blend_input(1, pi, FILTER_PASS, sync, p_test_only); - return rem0; + return nti; } void AnimationNodeAdd2::_bind_methods() { @@ -588,10 +769,16 @@ AnimationNodeAdd2::AnimationNodeAdd2() { //////////////////////////////////////////////// void AnimationNodeAdd3::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::FLOAT, add_amount, PROPERTY_HINT_RANGE, "-1,1,0.01,or_less,or_greater")); } Variant AnimationNodeAdd3::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + return 0; } @@ -603,18 +790,18 @@ bool AnimationNodeAdd3::has_filter() const { return true; } -double AnimationNodeAdd3::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeAdd3::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { double amount = get_parameter(add_amount); AnimationMixer::PlaybackInfo pi = p_playback_info; pi.weight = MAX(0, -amount); blend_input(0, pi, FILTER_PASS, sync, p_test_only); pi.weight = 1.0; - double rem0 = blend_input(1, pi, FILTER_IGNORE, sync, p_test_only); + NodeTimeInfo nti = blend_input(1, pi, FILTER_IGNORE, sync, p_test_only); pi.weight = MAX(0, amount); blend_input(2, pi, FILTER_PASS, sync, p_test_only); - return rem0; + return nti; } void AnimationNodeAdd3::_bind_methods() { @@ -629,10 +816,16 @@ AnimationNodeAdd3::AnimationNodeAdd3() { ///////////////////////////////////////////// void AnimationNodeBlend2::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); } Variant AnimationNodeBlend2::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + return 0; // For blend amount. } @@ -640,16 +833,16 @@ String AnimationNodeBlend2::get_caption() const { return "Blend2"; } -double AnimationNodeBlend2::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeBlend2::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { double amount = get_parameter(blend_amount); AnimationMixer::PlaybackInfo pi = p_playback_info; pi.weight = 1.0 - amount; - double rem0 = blend_input(0, pi, FILTER_BLEND, sync, p_test_only); + NodeTimeInfo nti0 = blend_input(0, pi, FILTER_BLEND, sync, p_test_only); pi.weight = amount; - double rem1 = blend_input(1, pi, FILTER_PASS, sync, p_test_only); + NodeTimeInfo nti1 = blend_input(1, pi, FILTER_PASS, sync, p_test_only); - return amount > 0.5 ? rem1 : rem0; // Hacky but good enough. + return amount > 0.5 ? nti1 : nti0; // Hacky but good enough. } bool AnimationNodeBlend2::has_filter() const { @@ -667,10 +860,16 @@ AnimationNodeBlend2::AnimationNodeBlend2() { ////////////////////////////////////// void AnimationNodeBlend3::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "-1,1,0.01,or_less,or_greater")); } Variant AnimationNodeBlend3::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + return 0; // For blend amount. } @@ -678,18 +877,18 @@ String AnimationNodeBlend3::get_caption() const { return "Blend3"; } -double AnimationNodeBlend3::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeBlend3::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { double amount = get_parameter(blend_amount); AnimationMixer::PlaybackInfo pi = p_playback_info; pi.weight = MAX(0, -amount); - double rem0 = blend_input(0, pi, FILTER_IGNORE, sync, p_test_only); + NodeTimeInfo nti0 = blend_input(0, pi, FILTER_IGNORE, sync, p_test_only); pi.weight = 1.0 - ABS(amount); - double rem1 = blend_input(1, pi, FILTER_IGNORE, sync, p_test_only); + NodeTimeInfo nti1 = blend_input(1, pi, FILTER_IGNORE, sync, p_test_only); pi.weight = MAX(0, amount); - double rem2 = blend_input(2, pi, FILTER_IGNORE, sync, p_test_only); + NodeTimeInfo nti2 = blend_input(2, pi, FILTER_IGNORE, sync, p_test_only); - return amount > 0.5 ? rem2 : (amount < -0.5 ? rem0 : rem1); // Hacky but good enough. + return amount > 0.5 ? nti2 : (amount < -0.5 ? nti0 : nti1); // Hacky but good enough. } void AnimationNodeBlend3::_bind_methods() { @@ -704,10 +903,16 @@ AnimationNodeBlend3::AnimationNodeBlend3() { //////////////////////////////////////////////// void AnimationNodeSub2::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::FLOAT, sub_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); } Variant AnimationNodeSub2::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + return 0; } @@ -719,7 +924,7 @@ bool AnimationNodeSub2::has_filter() const { return true; } -double AnimationNodeSub2::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeSub2::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { double amount = get_parameter(sub_amount); AnimationMixer::PlaybackInfo pi = p_playback_info; @@ -742,10 +947,16 @@ AnimationNodeSub2::AnimationNodeSub2() { ///////////////////////////////// void AnimationNodeTimeScale::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::FLOAT, scale, PROPERTY_HINT_RANGE, "-32,32,0.01,or_less,or_greater")); } Variant AnimationNodeTimeScale::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + return 1.0; // Initial timescale. } @@ -753,13 +964,13 @@ String AnimationNodeTimeScale::get_caption() const { return "TimeScale"; } -double AnimationNodeTimeScale::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeTimeScale::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { double cur_scale = get_parameter(scale); AnimationMixer::PlaybackInfo pi = p_playback_info; pi.weight = 1.0; if (!pi.seeked) { - pi.time *= cur_scale; + pi.delta *= cur_scale; } return blend_input(0, pi, FILTER_IGNORE, true, p_test_only); @@ -775,10 +986,16 @@ AnimationNodeTimeScale::AnimationNodeTimeScale() { //////////////////////////////////// void AnimationNodeTimeSeek::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::FLOAT, seek_pos_request, PROPERTY_HINT_RANGE, "-1,3600,0.01,or_greater")); // It will be reset to -1 after seeking the position immediately. } Variant AnimationNodeTimeSeek::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + return -1.0; // Initial seek request. } @@ -786,7 +1003,7 @@ String AnimationNodeTimeSeek::get_caption() const { return "TimeSeek"; } -double AnimationNodeTimeSeek::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeTimeSeek::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { double cur_seek_pos = get_parameter(seek_pos_request); AnimationMixer::PlaybackInfo pi = p_playback_info; @@ -833,6 +1050,8 @@ bool AnimationNodeTransition::_set(const StringName &p_path, const Variant &p_va set_input_name(which, p_value); } else if (what == "auto_advance") { set_input_as_auto_advance(which, p_value); + } else if (what == "break_loop_at_end") { + set_input_break_loop_at_end(which, p_value); } else if (what == "reset") { set_input_reset(which, p_value); } else { @@ -858,6 +1077,8 @@ bool AnimationNodeTransition::_get(const StringName &p_path, Variant &r_ret) con r_ret = get_input_name(which); } else if (what == "auto_advance") { r_ret = is_input_set_as_auto_advance(which); + } else if (what == "break_loop_at_end") { + r_ret = is_input_loop_broken_at_end(which); } else if (what == "reset") { r_ret = is_input_reset(which); } else { @@ -868,6 +1089,7 @@ bool AnimationNodeTransition::_get(const StringName &p_path, Variant &r_ret) con } void AnimationNodeTransition::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); String anims; for (int i = 0; i < get_input_count(); i++) { if (i > 0) { @@ -880,12 +1102,16 @@ void AnimationNodeTransition::get_parameter_list(List<PropertyInfo> *r_list) con r_list->push_back(PropertyInfo(Variant::STRING, transition_request, PROPERTY_HINT_ENUM, anims)); // For transition request. It will be cleared after setting the value immediately. r_list->push_back(PropertyInfo(Variant::INT, current_index, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY)); // To avoid finding the index every frame, use this internally. r_list->push_back(PropertyInfo(Variant::INT, prev_index, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); - r_list->push_back(PropertyInfo(Variant::FLOAT, time, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); r_list->push_back(PropertyInfo(Variant::FLOAT, prev_xfading, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE)); } Variant AnimationNodeTransition::get_parameter_default_value(const StringName &p_parameter) const { - if (p_parameter == time || p_parameter == prev_xfading) { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + + if (p_parameter == prev_xfading) { return 0.0; } else if (p_parameter == prev_index || p_parameter == current_index) { return -1; @@ -895,6 +1121,10 @@ Variant AnimationNodeTransition::get_parameter_default_value(const StringName &p } bool AnimationNodeTransition::is_parameter_read_only(const StringName &p_parameter) const { + if (AnimationNode::is_parameter_read_only(p_parameter)) { + return true; + } + if (p_parameter == current_state || p_parameter == current_index) { return true; } @@ -947,6 +1177,16 @@ bool AnimationNodeTransition::is_input_set_as_auto_advance(int p_input) const { return input_data[p_input].auto_advance; } +void AnimationNodeTransition::set_input_break_loop_at_end(int p_input, bool p_enable) { + ERR_FAIL_INDEX(p_input, get_input_count()); + input_data.write[p_input].break_loop_at_end = p_enable; +} + +bool AnimationNodeTransition::is_input_loop_broken_at_end(int p_input) const { + ERR_FAIL_INDEX_V(p_input, get_input_count(), false); + return input_data[p_input].break_loop_at_end; +} + void AnimationNodeTransition::set_input_reset(int p_input, bool p_enable) { ERR_FAIL_INDEX(p_input, get_input_count()); input_data.write[p_input].reset = p_enable; @@ -981,12 +1221,12 @@ bool AnimationNodeTransition::is_allow_transition_to_self() const { return allow_transition_to_self; } -double AnimationNodeTransition::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeTransition::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { String cur_transition_request = get_parameter(transition_request); int cur_current_index = get_parameter(current_index); int cur_prev_index = get_parameter(prev_index); - double cur_time = get_parameter(time); + NodeTimeInfo cur_nti = get_node_time_info(); double cur_prev_xfading = get_parameter(prev_xfading); bool switched = false; @@ -1052,7 +1292,6 @@ double AnimationNodeTransition::_process(const AnimationMixer::PlaybackInfo p_pl // Special case for restart. if (restart) { - set_parameter(time, 0); pi.time = 0; pi.seeked = true; pi.weight = 1.0; @@ -1061,16 +1300,12 @@ double AnimationNodeTransition::_process(const AnimationMixer::PlaybackInfo p_pl if (switched) { cur_prev_xfading = xfade_time; - cur_time = 0; } if (cur_current_index < 0 || cur_current_index >= get_input_count() || cur_prev_index >= get_input_count()) { - return 0; + return NodeTimeInfo(); } - double rem = 0.0; - double abs_time = Math::abs(p_time); - if (sync) { pi.weight = 0; for (int i = 0; i < get_input_count(); i++) { @@ -1081,20 +1316,11 @@ double AnimationNodeTransition::_process(const AnimationMixer::PlaybackInfo p_pl } if (cur_prev_index < 0) { // Process current animation, check for transition. - pi.weight = 1.0; - rem = blend_input(cur_current_index, pi, FILTER_IGNORE, true, p_test_only); - - if (p_seek) { - cur_time = abs_time; - } else { - cur_time += abs_time; - } - - if (input_data[cur_current_index].auto_advance && rem <= xfade_time) { + cur_nti = blend_input(cur_current_index, pi, FILTER_IGNORE, true, p_test_only); + if (input_data[cur_current_index].auto_advance && cur_nti.get_remain(input_data[cur_current_index].break_loop_at_end) <= xfade_time) { set_parameter(transition_request, get_input_name((cur_current_index + 1) % get_input_count())); } - } else { // Cross-fading from prev to current. real_t blend = 0.0; @@ -1117,33 +1343,30 @@ double AnimationNodeTransition::_process(const AnimationMixer::PlaybackInfo p_pl pi.time = 0; pi.seeked = true; } - rem = blend_input(cur_current_index, pi, FILTER_IGNORE, true, p_test_only); + cur_nti = blend_input(cur_current_index, pi, FILTER_IGNORE, true, p_test_only); pi = p_playback_info; pi.seeked &= use_blend; pi.weight = blend; blend_input(cur_prev_index, pi, FILTER_IGNORE, true, p_test_only); - if (p_seek) { - cur_time = abs_time; - } else { - cur_time += abs_time; - cur_prev_xfading -= abs_time; - if (cur_prev_xfading < 0) { + if (!p_seek) { + if (cur_prev_xfading <= 0) { set_parameter(prev_index, -1); } + cur_prev_xfading -= Math::abs(p_playback_info.delta); } } - set_parameter(time, cur_time); set_parameter(prev_xfading, cur_prev_xfading); - return rem; + return cur_nti; } void AnimationNodeTransition::_get_property_list(List<PropertyInfo> *p_list) const { for (int i = 0; i < get_input_count(); i++) { p_list->push_back(PropertyInfo(Variant::STRING, "input_" + itos(i) + "/name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL)); p_list->push_back(PropertyInfo(Variant::BOOL, "input_" + itos(i) + "/auto_advance", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL)); + p_list->push_back(PropertyInfo(Variant::BOOL, "input_" + itos(i) + "/break_loop_at_end", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL)); p_list->push_back(PropertyInfo(Variant::BOOL, "input_" + itos(i) + "/reset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL)); } } @@ -1154,6 +1377,9 @@ void AnimationNodeTransition::_bind_methods() { ClassDB::bind_method(D_METHOD("set_input_as_auto_advance", "input", "enable"), &AnimationNodeTransition::set_input_as_auto_advance); ClassDB::bind_method(D_METHOD("is_input_set_as_auto_advance", "input"), &AnimationNodeTransition::is_input_set_as_auto_advance); + ClassDB::bind_method(D_METHOD("set_input_break_loop_at_end", "input", "enable"), &AnimationNodeTransition::set_input_break_loop_at_end); + ClassDB::bind_method(D_METHOD("is_input_loop_broken_at_end", "input"), &AnimationNodeTransition::is_input_loop_broken_at_end); + ClassDB::bind_method(D_METHOD("set_input_reset", "input", "enable"), &AnimationNodeTransition::set_input_reset); ClassDB::bind_method(D_METHOD("is_input_reset", "input"), &AnimationNodeTransition::is_input_reset); @@ -1181,7 +1407,7 @@ String AnimationNodeOutput::get_caption() const { return "Output"; } -double AnimationNodeOutput::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeOutput::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { AnimationMixer::PlaybackInfo pi = p_playback_info; pi.weight = 1.0; return blend_input(0, pi, FILTER_IGNORE, true, p_test_only); @@ -1400,10 +1626,10 @@ String AnimationNodeBlendTree::get_caption() const { return "BlendTree"; } -double AnimationNodeBlendTree::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeBlendTree::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { Ref<AnimationNodeOutput> output = nodes[SceneStringNames::get_singleton()->output].node; node_state.connections = nodes[SceneStringNames::get_singleton()->output].connections; - ERR_FAIL_COND_V(output.is_null(), 0); + ERR_FAIL_COND_V(output.is_null(), NodeTimeInfo()); AnimationMixer::PlaybackInfo pi = p_playback_info; pi.weight = 1.0; diff --git a/scene/animation/animation_blend_tree.h b/scene/animation/animation_blend_tree.h index cf0884b892..c7ef7ed624 100644 --- a/scene/animation/animation_blend_tree.h +++ b/scene/animation/animation_blend_tree.h @@ -37,7 +37,12 @@ class AnimationNodeAnimation : public AnimationRootNode { GDCLASS(AnimationNodeAnimation, AnimationRootNode); StringName animation; - StringName time = "time"; + + bool use_custom_timeline = false; + double timeline_length = 1.0; + Animation::LoopMode loop_mode = Animation::LOOP_NONE; + bool stretch_time_scale = true; + double start_offset = 0.0; uint64_t last_version = 0; bool skip = false; @@ -50,10 +55,13 @@ public: void get_parameter_list(List<PropertyInfo> *r_list) const override; + virtual NodeTimeInfo get_node_time_info() const override; // Wrapper of get_parameter(). + static Vector<String> (*get_editable_animation_list)(); virtual String get_caption() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; void set_animation(const StringName &p_name); StringName get_animation() const; @@ -64,6 +72,21 @@ public: void set_backward(bool p_backward); bool is_backward() const; + void set_use_custom_timeline(bool p_use_custom_timeline); + bool is_using_custom_timeline() const; + + void set_timeline_length(double p_length); + double get_timeline_length() const; + + void set_stretch_time_scale(bool p_strech_time_scale); + bool is_stretching_time_scale() const; + + void set_start_offset(double p_offset); + double get_start_offset() const; + + void set_loop_mode(Animation::LoopMode p_loop_mode); + Animation::LoopMode get_loop_mode() const; + AnimationNodeAnimation(); protected: @@ -118,12 +141,12 @@ private: double auto_restart_delay = 1.0; double auto_restart_random_delay = 0.0; MixMode mix = MIX_MODE_BLEND; + bool break_loop_at_end = false; StringName request = PNAME("request"); StringName active = PNAME("active"); StringName internal_active = PNAME("internal_active"); - StringName time = "time"; - StringName remaining = "remaining"; + StringName fade_in_remaining = "fade_in_remaining"; StringName fade_out_remaining = "fade_out_remaining"; StringName time_to_restart = "time_to_restart"; @@ -160,8 +183,11 @@ public: void set_mix_mode(MixMode p_mix); MixMode get_mix_mode() const; + void set_break_loop_at_end(bool p_enable); + bool is_loop_broken_at_end() const; + virtual bool has_filter() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeOneShot(); }; @@ -184,7 +210,7 @@ public: virtual String get_caption() const override; virtual bool has_filter() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeAdd2(); }; @@ -204,7 +230,7 @@ public: virtual String get_caption() const override; virtual bool has_filter() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeAdd3(); }; @@ -222,7 +248,7 @@ public: virtual Variant get_parameter_default_value(const StringName &p_parameter) const override; virtual String get_caption() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; virtual bool has_filter() const override; AnimationNodeBlend2(); @@ -242,7 +268,7 @@ public: virtual String get_caption() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeBlend3(); }; @@ -261,7 +287,7 @@ public: virtual String get_caption() const override; virtual bool has_filter() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeSub2(); }; @@ -280,7 +306,7 @@ public: virtual String get_caption() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeTimeScale(); }; @@ -299,7 +325,7 @@ public: virtual String get_caption() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeTimeSeek(); }; @@ -309,11 +335,11 @@ class AnimationNodeTransition : public AnimationNodeSync { struct InputData { bool auto_advance = false; + bool break_loop_at_end = false; bool reset = true; }; Vector<InputData> input_data; - StringName time = "time"; StringName prev_xfading = "prev_xfading"; StringName prev_index = "prev_index"; StringName current_index = PNAME("current_index"); @@ -351,6 +377,9 @@ public: void set_input_as_auto_advance(int p_input, bool p_enable); bool is_input_set_as_auto_advance(int p_input) const; + void set_input_break_loop_at_end(int p_input, bool p_enable); + bool is_input_loop_broken_at_end(int p_input) const; + void set_input_reset(int p_input, bool p_enable); bool is_input_reset(int p_input) const; @@ -363,7 +392,7 @@ public: void set_allow_transition_to_self(bool p_enable); bool is_allow_transition_to_self() const; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeTransition(); }; @@ -373,7 +402,7 @@ class AnimationNodeOutput : public AnimationNode { public: virtual String get_caption() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; AnimationNodeOutput(); }; @@ -445,7 +474,7 @@ public: void get_node_connections(List<NodeConnection> *r_connections) const; virtual String get_caption() const override; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; void get_node_list(List<StringName> *r_list); diff --git a/scene/animation/animation_node_state_machine.cpp b/scene/animation/animation_node_state_machine.cpp index ec44641484..0484694555 100644 --- a/scene/animation/animation_node_state_machine.cpp +++ b/scene/animation/animation_node_state_machine.cpp @@ -101,12 +101,22 @@ float AnimationNodeStateMachineTransition::get_xfade_time() const { void AnimationNodeStateMachineTransition::set_xfade_curve(const Ref<Curve> &p_curve) { xfade_curve = p_curve; + emit_changed(); } Ref<Curve> AnimationNodeStateMachineTransition::get_xfade_curve() const { return xfade_curve; } +void AnimationNodeStateMachineTransition::set_break_loop_at_end(bool p_enable) { + break_loop_at_end = p_enable; + emit_changed(); +} + +bool AnimationNodeStateMachineTransition::is_loop_broken_at_end() const { + return break_loop_at_end; +} + void AnimationNodeStateMachineTransition::set_reset(bool p_reset) { reset = p_reset; emit_changed(); @@ -141,6 +151,9 @@ void AnimationNodeStateMachineTransition::_bind_methods() { ClassDB::bind_method(D_METHOD("set_xfade_curve", "curve"), &AnimationNodeStateMachineTransition::set_xfade_curve); ClassDB::bind_method(D_METHOD("get_xfade_curve"), &AnimationNodeStateMachineTransition::get_xfade_curve); + ClassDB::bind_method(D_METHOD("set_break_loop_at_end", "enable"), &AnimationNodeStateMachineTransition::set_break_loop_at_end); + ClassDB::bind_method(D_METHOD("is_loop_broken_at_end"), &AnimationNodeStateMachineTransition::is_loop_broken_at_end); + ClassDB::bind_method(D_METHOD("set_reset", "reset"), &AnimationNodeStateMachineTransition::set_reset); ClassDB::bind_method(D_METHOD("is_reset"), &AnimationNodeStateMachineTransition::is_reset); @@ -153,6 +166,7 @@ void AnimationNodeStateMachineTransition::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "xfade_time", PROPERTY_HINT_RANGE, "0,240,0.01,suffix:s"), "set_xfade_time", "get_xfade_time"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "xfade_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_xfade_curve", "get_xfade_curve"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "break_loop_at_end"), "set_break_loop_at_end", "is_loop_broken_at_end"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "reset"), "set_reset", "is_reset"); ADD_PROPERTY(PropertyInfo(Variant::INT, "priority", PROPERTY_HINT_RANGE, "0,32,1"), "set_priority", "get_priority"); @@ -310,19 +324,19 @@ TypedArray<StringName> AnimationNodeStateMachinePlayback::_get_travel_path() con } float AnimationNodeStateMachinePlayback::get_current_play_pos() const { - return pos_current; + return current_nti.position; } float AnimationNodeStateMachinePlayback::get_current_length() const { - return len_current; + return current_nti.length; } float AnimationNodeStateMachinePlayback::get_fade_from_play_pos() const { - return pos_fade_from; + return fadeing_from_nti.position; } float AnimationNodeStateMachinePlayback::get_fade_from_length() const { - return len_fade_from; + return fadeing_from_nti.length; } float AnimationNodeStateMachinePlayback::get_fading_time() const { @@ -665,21 +679,22 @@ bool AnimationNodeStateMachinePlayback::_make_travel_path(AnimationTree *p_tree, } } -double AnimationNodeStateMachinePlayback::process(const String &p_base_path, AnimationNodeStateMachine *p_state_machine, const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { - double rem = _process(p_base_path, p_state_machine, p_playback_info, p_test_only); +AnimationNode::NodeTimeInfo AnimationNodeStateMachinePlayback::process(const String &p_base_path, AnimationNodeStateMachine *p_state_machine, const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { + AnimationNode::NodeTimeInfo nti = _process(p_base_path, p_state_machine, p_playback_info, p_test_only); start_request = StringName(); next_request = false; stop_request = false; reset_request_on_teleport = false; - return rem; + return nti; } -double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, AnimationNodeStateMachine *p_state_machine, const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeStateMachinePlayback::_process(const String &p_base_path, AnimationNodeStateMachine *p_state_machine, const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { _set_base_path(p_base_path); AnimationTree *tree = p_state_machine->process_state->tree; double p_time = p_playback_info.time; + double p_delta = p_playback_info.delta; bool p_seek = p_playback_info.seeked; bool p_is_external_seeking = p_playback_info.is_external_seeking; @@ -690,8 +705,8 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An if (p_state_machine->get_state_machine_type() != AnimationNodeStateMachine::STATE_MACHINE_TYPE_GROUPED) { path.clear(); _clear_path_children(tree, p_state_machine, p_test_only); - _start(p_state_machine); } + _start(p_state_machine); reset_request = true; } else { // Reset current state. @@ -705,11 +720,11 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An travel_request = StringName(); path.clear(); playing = false; - return 0; + return AnimationNode::NodeTimeInfo(); } if (!playing && start_request != StringName() && travel_request != StringName()) { - return 0; + return AnimationNode::NodeTimeInfo(); } // Process start/travel request. @@ -732,7 +747,7 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An _start(p_state_machine); } else { StringName node = start_request; - ERR_FAIL_V_MSG(0, "No such node: '" + node + "'"); + ERR_FAIL_V_MSG(AnimationNode::NodeTimeInfo(), "No such node: '" + node + "'"); } } @@ -766,7 +781,7 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An teleport_request = true; } } else { - ERR_FAIL_V_MSG(0, "No such node: '" + temp_travel_request + "'"); + ERR_FAIL_V_MSG(AnimationNode::NodeTimeInfo(), "No such node: '" + temp_travel_request + "'"); } } } @@ -777,16 +792,14 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An teleport_request = false; // Clear fadeing on teleport. fading_from = StringName(); + fadeing_from_nti = AnimationNode::NodeTimeInfo(); fading_pos = 0; // Init current length. - pos_current = 0; // Overwritten suddenly in main process. - pi.time = 0; pi.seeked = true; pi.is_external_seeking = false; pi.weight = 0; - - len_current = p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, true); + current_nti = p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, true); // Don't process first node if not necessary, insteads process next node. _transition_to_next_recursive(tree, p_state_machine, p_test_only); } @@ -795,7 +808,7 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An if (!p_state_machine->states.has(current)) { playing = false; // Current does not exist. _set_current(p_state_machine, StringName()); - return 0; + return AnimationNode::NodeTimeInfo(); } // Special case for grouped state machine Start/End to make priority with parent blend (means don't treat Start and End states as RESET animations). @@ -813,7 +826,7 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An fading_from = StringName(); } else { if (!p_seek) { - fading_pos += p_time; + fading_pos += Math::abs(p_delta); } fade_blend = MIN(1.0, fading_pos / fading_time); } @@ -829,18 +842,14 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An } // Main process. - double rem = 0.0; pi = p_playback_info; pi.weight = fade_blend; if (reset_request) { reset_request = false; pi.time = 0; pi.seeked = true; - len_current = p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, p_test_only); - rem = len_current; - } else { - rem = p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, p_test_only); // Blend values must be more than CMP_EPSILON to process discrete keys in edge. } + current_nti = p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, p_test_only); // Blend values must be more than CMP_EPSILON to process discrete keys in edge. // Cross-fade process. if (fading_from != StringName()) { @@ -852,7 +861,6 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An fade_blend_inv = 1.0; } - float fading_from_rem = 0.0; pi = p_playback_info; pi.weight = fade_blend_inv; if (_reset_request_for_fading_from) { @@ -860,57 +868,41 @@ double AnimationNodeStateMachinePlayback::_process(const String &p_base_path, An pi.time = 0; pi.seeked = true; } - fading_from_rem = p_state_machine->blend_node(p_state_machine->states[fading_from].node, fading_from, pi, AnimationNode::FILTER_IGNORE, true, p_test_only); // Blend values must be more than CMP_EPSILON to process discrete keys in edge. - - // Guess playback position. - if (fading_from_rem > len_fade_from) { /// Weird but ok. - len_fade_from = fading_from_rem; - } - pos_fade_from = len_fade_from - fading_from_rem; + fadeing_from_nti = p_state_machine->blend_node(p_state_machine->states[fading_from].node, fading_from, pi, AnimationNode::FILTER_IGNORE, true, p_test_only); // Blend values must be more than CMP_EPSILON to process discrete keys in edge. if (fading_pos >= fading_time) { - fading_from = StringName(); // Finish fading. + // Finish fading. + fading_from = StringName(); + fadeing_from_nti = AnimationNode::NodeTimeInfo(); } } - // Guess playback position. - if (rem > len_current) { // Weird but ok. - len_current = rem; - } - pos_current = len_current - rem; - // Find next and see when to transition. _transition_to_next_recursive(tree, p_state_machine, p_test_only); // Predict remaining time. - double remain = rem; // If we can't predict the end of state machine, the time remaining must be INFINITY. - if (p_state_machine->get_state_machine_type() == AnimationNodeStateMachine::STATE_MACHINE_TYPE_NESTED) { // There is no next transition. if (!p_state_machine->has_transition_from(current)) { if (fading_from != StringName()) { - remain = MAX(rem, fading_time - fading_pos); - } else { - remain = rem; + return current_nti.get_remain() > fadeing_from_nti.get_remain() ? current_nti : fadeing_from_nti; } - return remain; + return current_nti; } } if (current == p_state_machine->end_node) { - if (fading_from != StringName()) { - remain = MAX(0, fading_time - fading_pos); - } else { - remain = 0; + if (fading_from != StringName() && fadeing_from_nti.get_remain() > 0) { + return fadeing_from_nti; } - return remain; + return AnimationNode::NodeTimeInfo(); } if (!is_end()) { - return HUGE_LENGTH; + current_nti.is_infinity = true; } - return remain; + return current_nti; } bool AnimationNodeStateMachinePlayback::_transition_to_next_recursive(AnimationTree *p_tree, AnimationNodeStateMachine *p_state_machine, bool p_test_only) { @@ -952,6 +944,7 @@ bool AnimationNodeStateMachinePlayback::_transition_to_next_recursive(AnimationT p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, p_test_only); } fading_from = StringName(); + fadeing_from_nti = AnimationNode::NodeTimeInfo(); fading_time = 0; fading_pos = 0; } @@ -968,11 +961,10 @@ bool AnimationNodeStateMachinePlayback::_transition_to_next_recursive(AnimationT _reset_request_for_fading_from = reset_request; // To avoid processing doubly, it must be reset in the fading process within _process(). reset_request = next.is_reset; - pos_fade_from = pos_current; - len_fade_from = len_current; + fadeing_from_nti = current_nti; if (next.switch_mode == AnimationNodeStateMachineTransition::SWITCH_MODE_SYNC) { - pi.time = MIN(pos_current, len_current); + pi.time = current_nti.position; pi.seeked = true; pi.is_external_seeking = false; pi.weight = 0; @@ -980,24 +972,11 @@ bool AnimationNodeStateMachinePlayback::_transition_to_next_recursive(AnimationT } // Just get length to find next recursive. - double rem = 0.0; pi.time = 0; pi.is_external_seeking = false; pi.weight = 0; - if (next.is_reset) { - pi.seeked = true; - len_current = p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, true); // Just retrieve remain length, don't process. - rem = len_current; - } else { - pi.seeked = false; - rem = p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, true); // Just retrieve remain length, don't process. - } - - // Guess playback position. - if (rem > len_current) { // Weird but ok. - len_current = rem; - } - pos_current = len_current - rem; + pi.seeked = next.is_reset; + current_nti = p_state_machine->blend_node(p_state_machine->states[current].node, current, pi, AnimationNode::FILTER_IGNORE, true, true); // Just retrieve remain length, don't process. // Fading must be processed. if (fading_time) { @@ -1028,6 +1007,7 @@ bool AnimationNodeStateMachinePlayback::_can_transition_to_next(AnimationTree *p playback->_next_main(); // Then, fadeing should be end. fading_from = StringName(); + fadeing_from_nti = AnimationNode::NodeTimeInfo(); fading_pos = 0; } else { return true; @@ -1039,7 +1019,7 @@ bool AnimationNodeStateMachinePlayback::_can_transition_to_next(AnimationTree *p } if (current != p_state_machine->start_node && p_next.switch_mode == AnimationNodeStateMachineTransition::SWITCH_MODE_AT_END) { - return pos_current >= len_current - p_next.xfade; + return current_nti.get_remain(p_next.break_loop_at_end) <= p_next.xfade; } return true; } @@ -1084,6 +1064,7 @@ AnimationNodeStateMachinePlayback::NextInfo AnimationNodeStateMachinePlayback::_ next.curve = ref_transition->get_xfade_curve(); next.switch_mode = ref_transition->get_switch_mode(); next.is_reset = ref_transition->is_reset(); + next.break_loop_at_end = ref_transition->is_loop_broken_at_end(); } } } else { @@ -1113,6 +1094,7 @@ AnimationNodeStateMachinePlayback::NextInfo AnimationNodeStateMachinePlayback::_ next.curve = ref_transition->get_xfade_curve(); next.switch_mode = ref_transition->get_switch_mode(); next.is_reset = ref_transition->is_reset(); + next.break_loop_at_end = ref_transition->is_loop_broken_at_end(); } } @@ -1233,6 +1215,7 @@ AnimationNodeStateMachinePlayback::AnimationNodeStateMachinePlayback() { /////////////////////////////////////////////////////// void AnimationNodeStateMachine::get_parameter_list(List<PropertyInfo> *r_list) const { + AnimationNode::get_parameter_list(r_list); r_list->push_back(PropertyInfo(Variant::OBJECT, playback, PROPERTY_HINT_RESOURCE_TYPE, "AnimationNodeStateMachinePlayback", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_ALWAYS_DUPLICATE)); // Don't store this object in .tres, it always needs to be made as unique object. List<StringName> advance_conditions; for (int i = 0; i < transitions.size(); i++) { @@ -1249,6 +1232,11 @@ void AnimationNodeStateMachine::get_parameter_list(List<PropertyInfo> *r_list) c } Variant AnimationNodeStateMachine::get_parameter_default_value(const StringName &p_parameter) const { + Variant ret = AnimationNode::get_parameter_default_value(p_parameter); + if (ret != Variant()) { + return ret; + } + if (p_parameter == playback) { Ref<AnimationNodeStateMachinePlayback> p; p.instantiate(); @@ -1259,6 +1247,10 @@ Variant AnimationNodeStateMachine::get_parameter_default_value(const StringName } bool AnimationNodeStateMachine::is_parameter_read_only(const StringName &p_parameter) const { + if (AnimationNode::is_parameter_read_only(p_parameter)) { + return true; + } + if (p_parameter == playback) { return true; } @@ -1622,9 +1614,9 @@ Vector2 AnimationNodeStateMachine::get_graph_offset() const { return graph_offset; } -double AnimationNodeStateMachine::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNodeStateMachine::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { Ref<AnimationNodeStateMachinePlayback> playback_new = get_parameter(playback); - ERR_FAIL_COND_V(playback_new.is_null(), 0.0); + ERR_FAIL_COND_V(playback_new.is_null(), AnimationNode::NodeTimeInfo()); playback_new->_set_grouped(state_machine_type == STATE_MACHINE_TYPE_GROUPED); if (p_test_only) { playback_new = playback_new->duplicate(); // Don't process original when testing. diff --git a/scene/animation/animation_node_state_machine.h b/scene/animation/animation_node_state_machine.h index fa0eca5877..8078ffb26d 100644 --- a/scene/animation/animation_node_state_machine.h +++ b/scene/animation/animation_node_state_machine.h @@ -57,6 +57,7 @@ private: StringName advance_condition_name; float xfade_time = 0.0; Ref<Curve> xfade_curve; + bool break_loop_at_end = false; bool reset = true; int priority = 1; String advance_expression; @@ -85,6 +86,9 @@ public: void set_xfade_time(float p_xfade); float get_xfade_time() const; + void set_break_loop_at_end(bool p_enable); + bool is_loop_broken_at_end() const; + void set_reset(bool p_reset); bool is_reset() const; @@ -210,7 +214,7 @@ public: void set_graph_offset(const Vector2 &p_offset); Vector2 get_graph_offset() const; - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false) override; virtual String get_caption() const override; virtual Ref<AnimationNode> get_child_by_name(const StringName &p_name) const override; @@ -246,6 +250,7 @@ class AnimationNodeStateMachinePlayback : public Resource { Ref<Curve> curve; AnimationNodeStateMachineTransition::SwitchMode switch_mode; bool is_reset; + bool break_loop_at_end; }; struct ChildStateMachineInfo { @@ -257,18 +262,14 @@ class AnimationNodeStateMachinePlayback : public Resource { Ref<AnimationNodeStateMachineTransition> default_transition; String base_path; - double len_fade_from = 0.0; - double pos_fade_from = 0.0; - - double len_current = 0.0; - double pos_current = 0.0; - + AnimationNode::NodeTimeInfo current_nti; StringName current; Ref<Curve> current_curve; Ref<AnimationNodeStateMachineTransition> group_start_transition; Ref<AnimationNodeStateMachineTransition> group_end_transition; + AnimationNode::NodeTimeInfo fadeing_from_nti; StringName fading_from; float fading_time = 0.0; float fading_pos = 0.0; @@ -301,8 +302,8 @@ class AnimationNodeStateMachinePlayback : public Resource { bool _travel_children(AnimationTree *p_tree, AnimationNodeStateMachine *p_state_machine, const String &p_path, bool p_is_allow_transition_to_self, bool p_is_parent_same_state, bool p_test_only); void _start_children(AnimationTree *p_tree, AnimationNodeStateMachine *p_state_machine, const String &p_path, bool p_test_only); - double process(const String &p_base_path, AnimationNodeStateMachine *p_state_machine, const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only); - double _process(const String &p_base_path, AnimationNodeStateMachine *p_state_machine, const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only); + AnimationNode::NodeTimeInfo process(const String &p_base_path, AnimationNodeStateMachine *p_state_machine, const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only); + AnimationNode::NodeTimeInfo _process(const String &p_base_path, AnimationNodeStateMachine *p_state_machine, const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only); bool _check_advance_condition(const Ref<AnimationNodeStateMachine> p_state_machine, const Ref<AnimationNodeStateMachineTransition> p_transition) const; bool _transition_to_next_recursive(AnimationTree *p_tree, AnimationNodeStateMachine *p_state_machine, bool p_test_only); diff --git a/scene/animation/animation_tree.cpp b/scene/animation/animation_tree.cpp index 2c2d8387f3..2d0bd9c258 100644 --- a/scene/animation/animation_tree.cpp +++ b/scene/animation/animation_tree.cpp @@ -46,6 +46,10 @@ void AnimationNode::get_parameter_list(List<PropertyInfo> *r_list) const { r_list->push_back(PropertyInfo::from_dict(d)); } } + + r_list->push_back(PropertyInfo(Variant::FLOAT, current_length, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_READ_ONLY)); + r_list->push_back(PropertyInfo(Variant::FLOAT, current_position, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_READ_ONLY)); + r_list->push_back(PropertyInfo(Variant::FLOAT, current_delta, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_READ_ONLY)); } Variant AnimationNode::get_parameter_default_value(const StringName &p_parameter) const { @@ -56,8 +60,15 @@ Variant AnimationNode::get_parameter_default_value(const StringName &p_parameter bool AnimationNode::is_parameter_read_only(const StringName &p_parameter) const { bool ret = false; - GDVIRTUAL_CALL(_is_parameter_read_only, p_parameter, ret); - return ret; + if (GDVIRTUAL_CALL(_is_parameter_read_only, p_parameter, ret) && ret) { + return true; + } + + if (p_parameter == current_length || p_parameter == current_position || p_parameter == current_delta) { + return true; + } + + return false; } void AnimationNode::set_parameter(const StringName &p_name, const Variant &p_value) { @@ -81,6 +92,20 @@ Variant AnimationNode::get_parameter(const StringName &p_name) const { return process_state->tree->property_map[path].first; } +void AnimationNode::set_node_time_info(const NodeTimeInfo &p_node_time_info) { + set_parameter(current_length, p_node_time_info.length); + set_parameter(current_position, p_node_time_info.position); + set_parameter(current_delta, p_node_time_info.delta); +} + +AnimationNode::NodeTimeInfo AnimationNode::get_node_time_info() const { + NodeTimeInfo nti; + nti.length = get_parameter(current_length); + nti.position = get_parameter(current_position); + nti.delta = get_parameter(current_delta); + return nti; +} + void AnimationNode::get_child_nodes(List<ChildNode> *r_child_nodes) { Dictionary cn; if (GDVIRTUAL_CALL(_get_child_nodes, cn)) { @@ -101,11 +126,11 @@ void AnimationNode::blend_animation(const StringName &p_animation, AnimationMixe process_state->tree->make_animation_instance(p_animation, p_playback_info); } -double AnimationNode::_pre_process(ProcessState *p_process_state, AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNode::_pre_process(ProcessState *p_process_state, AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { process_state = p_process_state; - double t = process(p_playback_info, p_test_only); + NodeTimeInfo nti = process(p_playback_info, p_test_only); process_state = nullptr; - return t; + return nti; } void AnimationNode::make_invalid(const String &p_reason) { @@ -122,11 +147,11 @@ AnimationTree *AnimationNode::get_animation_tree() const { return process_state->tree; } -double AnimationNode::blend_input(int p_input, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter, bool p_sync, bool p_test_only) { - ERR_FAIL_INDEX_V(p_input, inputs.size(), 0); +AnimationNode::NodeTimeInfo AnimationNode::blend_input(int p_input, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter, bool p_sync, bool p_test_only) { + ERR_FAIL_INDEX_V(p_input, inputs.size(), NodeTimeInfo()); AnimationNodeBlendTree *blend_tree = Object::cast_to<AnimationNodeBlendTree>(node_state.parent); - ERR_FAIL_NULL_V(blend_tree, 0); + ERR_FAIL_NULL_V(blend_tree, NodeTimeInfo()); // Update connections. StringName current_name = blend_tree->get_node_name(Ref<AnimationNode>(this)); @@ -136,32 +161,31 @@ double AnimationNode::blend_input(int p_input, AnimationMixer::PlaybackInfo p_pl StringName node_name = node_state.connections[p_input]; if (!blend_tree->has_node(node_name)) { make_invalid(vformat(RTR("Nothing connected to input '%s' of node '%s'."), get_input_name(p_input), current_name)); - return 0; + return NodeTimeInfo(); } Ref<AnimationNode> node = blend_tree->get_node(node_name); - ERR_FAIL_COND_V(node.is_null(), 0); + ERR_FAIL_COND_V(node.is_null(), NodeTimeInfo()); real_t activity = 0.0; Vector<AnimationTree::Activity> *activity_ptr = process_state->tree->input_activity_map.getptr(node_state.base_path); - double ret = _blend_node(node, node_name, nullptr, p_playback_info, p_filter, p_sync, p_test_only, &activity); + NodeTimeInfo nti = _blend_node(node, node_name, nullptr, p_playback_info, p_filter, p_sync, p_test_only, &activity); if (activity_ptr && p_input < activity_ptr->size()) { activity_ptr->write[p_input].last_pass = process_state->last_pass; activity_ptr->write[p_input].activity = activity; } - return ret; + return nti; } -double AnimationNode::blend_node(Ref<AnimationNode> p_node, const StringName &p_subpath, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter, bool p_sync, bool p_test_only) { - ERR_FAIL_COND_V(p_node.is_null(), 0); - +AnimationNode::NodeTimeInfo AnimationNode::blend_node(Ref<AnimationNode> p_node, const StringName &p_subpath, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter, bool p_sync, bool p_test_only) { + ERR_FAIL_COND_V(p_node.is_null(), NodeTimeInfo()); p_node->node_state.connections.clear(); return _blend_node(p_node, p_subpath, this, p_playback_info, p_filter, p_sync, p_test_only, nullptr); } -double AnimationNode::_blend_node(Ref<AnimationNode> p_node, const StringName &p_subpath, AnimationNode *p_new_parent, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter, bool p_sync, bool p_test_only, real_t *r_activity) { - ERR_FAIL_NULL_V(process_state, 0); +AnimationNode::NodeTimeInfo AnimationNode::_blend_node(Ref<AnimationNode> p_node, const StringName &p_subpath, AnimationNode *p_new_parent, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter, bool p_sync, bool p_test_only, real_t *r_activity) { + ERR_FAIL_NULL_V(process_state, NodeTimeInfo()); int blend_count = node_state.track_weights.size(); @@ -261,7 +285,7 @@ double AnimationNode::_blend_node(Ref<AnimationNode> p_node, const StringName &p new_parent = p_new_parent; new_path = String(node_state.base_path) + String(p_subpath) + "/"; } else { - ERR_FAIL_NULL_V(node_state.parent, 0); + ERR_FAIL_NULL_V(node_state.parent, NodeTimeInfo()); new_parent = node_state.parent; new_path = String(new_parent->node_state.base_path) + String(p_subpath) + "/"; } @@ -271,7 +295,7 @@ double AnimationNode::_blend_node(Ref<AnimationNode> p_node, const StringName &p p_node->node_state.base_path = new_path; p_node->node_state.parent = new_parent; if (!p_playback_info.seeked && !p_sync && !any_valid) { - p_playback_info.time = 0.0; + p_playback_info.delta = 0.0; return p_node->_pre_process(process_state, p_playback_info, p_test_only); } return p_node->_pre_process(process_state, p_playback_info, p_test_only); @@ -328,15 +352,31 @@ int AnimationNode::find_input(const String &p_name) const { return idx; } -double AnimationNode::process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { +AnimationNode::NodeTimeInfo AnimationNode::process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { process_state->is_testing = p_test_only; - return _process(p_playback_info, p_test_only); + + AnimationMixer::PlaybackInfo pi = p_playback_info; + if (p_playback_info.seeked) { + pi.delta = get_node_time_info().position - p_playback_info.time; + } else { + pi.time = get_node_time_info().position + p_playback_info.delta; + } + + NodeTimeInfo nti = _process(pi, p_test_only); + + if (!p_test_only) { + set_node_time_info(nti); + } + + return nti; } -double AnimationNode::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { - double ret = 0; - GDVIRTUAL_CALL(_process, p_playback_info.time, p_playback_info.seeked, p_playback_info.is_external_seeking, p_test_only, ret); - return ret; +AnimationNode::NodeTimeInfo AnimationNode::_process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only) { + double r_ret = 0.0; + GDVIRTUAL_CALL(_process, p_playback_info.time, p_playback_info.seeked, p_playback_info.is_external_seeking, p_test_only, r_ret); + NodeTimeInfo nti; + nti.delta = r_ret; + return nti; } void AnimationNode::set_filter_path(const NodePath &p_path, bool p_enable) { @@ -432,7 +472,8 @@ double AnimationNode::blend_node_ex(const StringName &p_sub_path, Ref<AnimationN info.seeked = p_seek; info.is_external_seeking = p_is_external_seeking; info.weight = p_blend; - return blend_node(p_node, p_sub_path, info, p_filter, p_sync, p_test_only); + NodeTimeInfo nti = blend_node(p_node, p_sub_path, info, p_filter, p_sync, p_test_only); + return nti.length - nti.position; } double AnimationNode::blend_input_ex(int p_input, double p_time, bool p_seek, bool p_is_external_seeking, real_t p_blend, FilterAction p_filter, bool p_sync, bool p_test_only) { @@ -441,8 +482,37 @@ double AnimationNode::blend_input_ex(int p_input, double p_time, bool p_seek, bo info.seeked = p_seek; info.is_external_seeking = p_is_external_seeking; info.weight = p_blend; - return blend_input(p_input, info, p_filter, p_sync, p_test_only); + NodeTimeInfo nti = blend_input(p_input, info, p_filter, p_sync, p_test_only); + return nti.length - nti.position; +} + +#ifdef TOOLS_ENABLED +void AnimationNode::get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const { + const String pf = p_function; + if (p_idx == 0) { + if (pf == "find_input") { + for (const AnimationNode::Input &E : inputs) { + r_options->push_back(E.name.quote()); + } + } else if (pf == "get_parameter" || pf == "set_parameter") { + bool is_setter = pf == "set_parameter"; + List<PropertyInfo> parameters; + get_parameter_list(¶meters); + for (const PropertyInfo &E : parameters) { + if (is_setter && is_parameter_read_only(E.name)) { + continue; + } + r_options->push_back(E.name.quote()); + } + } else if (pf == "set_filter_path" || pf == "is_path_filtered") { + for (const KeyValue<NodePath, bool> &E : filter) { + r_options->push_back(String(E.key).quote()); + } + } + } + Resource::get_argument_options(p_function, p_idx, r_options); } +#endif void AnimationNode::_bind_methods() { ClassDB::bind_method(D_METHOD("add_input", "name"), &AnimationNode::add_input); @@ -568,11 +638,12 @@ bool AnimationTree::_blend_pre_process(double p_delta, int p_track_count, const if (started) { // If started, seek. pi.seeked = true; + pi.delta = p_delta; root_animation_node->_pre_process(&process_state, pi, false); started = false; } else { pi.seeked = false; - pi.time = p_delta; + pi.delta = p_delta; root_animation_node->_pre_process(&process_state, pi, false); } } diff --git a/scene/animation/animation_tree.h b/scene/animation/animation_tree.h index 87928e4d20..2a58822275 100644 --- a/scene/animation/animation_tree.h +++ b/scene/animation/animation_tree.h @@ -63,6 +63,34 @@ public: HashMap<NodePath, bool> filter; bool filter_enabled = false; + // To propagate information from upstream for use in estimation of playback progress. + // These values must be taken from the result of blend_node() or blend_input() and must be essentially read-only. + // For example, if you want to change the position, you need to change the pi.time value of PlaybackInfo passed to blend_input(pi) and get the result. + struct NodeTimeInfo { + // Retain the previous frame values. These are stored into the AnimationTree's Map and exposing them as read-only values. + double length = 0.0; + double position = 0.0; + double delta = 0.0; + + // Needs internally to estimate remain time, the previous frame values are not retained. + Animation::LoopMode loop_mode = Animation::LOOP_NONE; + bool is_just_looped = false; // For breaking loop, it is true when just looped. + bool is_infinity = false; // For unpredictable state machine's end. + + bool is_looping() { + return loop_mode != Animation::LOOP_NONE; + } + double get_remain(bool p_break_loop = false) { + if ((is_looping() && !p_break_loop) || is_infinity) { + return HUGE_LENGTH; + } + if (p_break_loop && is_just_looped) { + return 0; + } + return length - position; + } + }; + // Temporary state for blending process which needs to be stored in each AnimationNodes. struct NodeState { StringName base_path; @@ -84,16 +112,23 @@ public: Array _get_filters() const; void _set_filters(const Array &p_filters); friend class AnimationNodeBlendTree; - double _blend_node(Ref<AnimationNode> p_node, const StringName &p_subpath, AnimationNode *p_new_parent, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter = FILTER_IGNORE, bool p_sync = true, bool p_test_only = false, real_t *r_activity = nullptr); - double _pre_process(ProcessState *p_process_state, AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false); + + // The time information is passed from upstream to downstream by AnimationMixer::PlaybackInfo::p_playback_info until AnimationNodeAnimation processes it. + // Conversely, AnimationNodeAnimation returns the processed result as NodeTimeInfo from downstream to upstream. + NodeTimeInfo _blend_node(Ref<AnimationNode> p_node, const StringName &p_subpath, AnimationNode *p_new_parent, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter = FILTER_IGNORE, bool p_sync = true, bool p_test_only = false, real_t *r_activity = nullptr); + NodeTimeInfo _pre_process(ProcessState *p_process_state, AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false); protected: - virtual double _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false); - double process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false); + StringName current_length = "current_length"; + StringName current_position = "current_position"; + StringName current_delta = "current_delta"; + + virtual NodeTimeInfo process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false); // To organize time information. Virtualizing for especially AnimationNodeAnimation needs to take "backward" into account. + virtual NodeTimeInfo _process(const AnimationMixer::PlaybackInfo p_playback_info, bool p_test_only = false); // Main process. void blend_animation(const StringName &p_animation, AnimationMixer::PlaybackInfo p_playback_info); - double blend_node(Ref<AnimationNode> p_node, const StringName &p_subpath, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter = FILTER_IGNORE, bool p_sync = true, bool p_test_only = false); - double blend_input(int p_input, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter = FILTER_IGNORE, bool p_sync = true, bool p_test_only = false); + NodeTimeInfo blend_node(Ref<AnimationNode> p_node, const StringName &p_subpath, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter = FILTER_IGNORE, bool p_sync = true, bool p_test_only = false); + NodeTimeInfo blend_input(int p_input, AnimationMixer::PlaybackInfo p_playback_info, FilterAction p_filter = FILTER_IGNORE, bool p_sync = true, bool p_test_only = false); // Bind-able methods to expose for compatibility, moreover AnimationMixer::PlaybackInfo is not exposed. void blend_animation_ex(const StringName &p_animation, double p_time, double p_delta, bool p_seeked, bool p_is_external_seeking, real_t p_blend, Animation::LoopedFlag p_looped_flag = Animation::LOOPED_FLAG_NONE); @@ -124,6 +159,9 @@ public: void set_parameter(const StringName &p_name, const Variant &p_value); Variant get_parameter(const StringName &p_name) const; + void set_node_time_info(const NodeTimeInfo &p_node_time_info); // Wrapper of set_parameter(). + virtual NodeTimeInfo get_node_time_info() const; // Wrapper of get_parameter(). + struct ChildNode { StringName name; Ref<AnimationNode> node; @@ -151,6 +189,10 @@ public: virtual bool has_filter() const; +#ifdef TOOLS_ENABLED + virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override; +#endif + virtual Ref<AnimationNode> get_child_by_name(const StringName &p_name) const; Ref<AnimationNode> find_node_by_path(const String &p_name) const; diff --git a/scene/gui/aspect_ratio_container.cpp b/scene/gui/aspect_ratio_container.cpp index 94240ccead..711dd13781 100644 --- a/scene/gui/aspect_ratio_container.cpp +++ b/scene/gui/aspect_ratio_container.cpp @@ -46,8 +46,7 @@ Size2 AspectRatioContainer::get_minimum_size() const { continue; } Size2 minsize = c->get_combined_minimum_size(); - ms.width = MAX(ms.width, minsize.width); - ms.height = MAX(ms.height, minsize.height); + ms = ms.max(minsize); } return ms; } @@ -144,8 +143,7 @@ void AspectRatioContainer::_notification(int p_what) { } break; } child_size *= scale_factor; - child_size.x = MAX(child_size.x, child_minsize.x); - child_size.y = MAX(child_size.y, child_minsize.y); + child_size = child_size.max(child_minsize); float align_x = 0.5; switch (alignment_horizontal) { diff --git a/scene/gui/center_container.cpp b/scene/gui/center_container.cpp index 7a860cdea7..acdeae289b 100644 --- a/scene/gui/center_container.cpp +++ b/scene/gui/center_container.cpp @@ -47,8 +47,7 @@ Size2 CenterContainer::get_minimum_size() const { continue; } Size2 minsize = c->get_combined_minimum_size(); - ms.width = MAX(ms.width, minsize.width); - ms.height = MAX(ms.height, minsize.height); + ms = ms.max(minsize); } return ms; diff --git a/scene/gui/check_box.cpp b/scene/gui/check_box.cpp index 46c20e4a1c..af6696834e 100644 --- a/scene/gui/check_box.cpp +++ b/scene/gui/check_box.cpp @@ -36,28 +36,28 @@ Size2 CheckBox::get_icon_size() const { Size2 tex_size = Size2(0, 0); if (!theme_cache.checked.is_null()) { - tex_size = Size2(theme_cache.checked->get_width(), theme_cache.checked->get_height()); + tex_size = theme_cache.checked->get_size(); } if (!theme_cache.unchecked.is_null()) { - tex_size = Size2(MAX(tex_size.width, theme_cache.unchecked->get_width()), MAX(tex_size.height, theme_cache.unchecked->get_height())); + tex_size = tex_size.max(theme_cache.unchecked->get_size()); } if (!theme_cache.radio_checked.is_null()) { - tex_size = Size2(MAX(tex_size.width, theme_cache.radio_checked->get_width()), MAX(tex_size.height, theme_cache.radio_checked->get_height())); + tex_size = tex_size.max(theme_cache.radio_checked->get_size()); } if (!theme_cache.radio_unchecked.is_null()) { - tex_size = Size2(MAX(tex_size.width, theme_cache.radio_unchecked->get_width()), MAX(tex_size.height, theme_cache.radio_unchecked->get_height())); + tex_size = tex_size.max(theme_cache.radio_unchecked->get_size()); } if (!theme_cache.checked_disabled.is_null()) { - tex_size = Size2(MAX(tex_size.width, theme_cache.checked_disabled->get_width()), MAX(tex_size.height, theme_cache.checked_disabled->get_height())); + tex_size = tex_size.max(theme_cache.checked_disabled->get_size()); } if (!theme_cache.unchecked_disabled.is_null()) { - tex_size = Size2(MAX(tex_size.width, theme_cache.unchecked_disabled->get_width()), MAX(tex_size.height, theme_cache.unchecked_disabled->get_height())); + tex_size = tex_size.max(theme_cache.unchecked_disabled->get_size()); } if (!theme_cache.radio_checked_disabled.is_null()) { - tex_size = Size2(MAX(tex_size.width, theme_cache.radio_checked_disabled->get_width()), MAX(tex_size.height, theme_cache.radio_checked_disabled->get_height())); + tex_size = tex_size.max(theme_cache.radio_checked_disabled->get_size()); } if (!theme_cache.radio_unchecked_disabled.is_null()) { - tex_size = Size2(MAX(tex_size.width, theme_cache.radio_unchecked_disabled->get_width()), MAX(tex_size.height, theme_cache.radio_unchecked_disabled->get_height())); + tex_size = tex_size.max(theme_cache.radio_unchecked_disabled->get_size()); } return tex_size; } diff --git a/scene/gui/check_button.cpp b/scene/gui/check_button.cpp index ca2ef220fd..ab3b74a3c3 100644 --- a/scene/gui/check_button.cpp +++ b/scene/gui/check_button.cpp @@ -57,10 +57,10 @@ Size2 CheckButton::get_icon_size() const { Size2 tex_size = Size2(0, 0); if (!on_tex.is_null()) { - tex_size = Size2(on_tex->get_width(), on_tex->get_height()); + tex_size = on_tex->get_size(); } if (!off_tex.is_null()) { - tex_size = Size2(MAX(tex_size.width, off_tex->get_width()), MAX(tex_size.height, off_tex->get_height())); + tex_size = tex_size.max(off_tex->get_size()); } return tex_size; diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 632e6af2ce..4f90504e35 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -3398,9 +3398,16 @@ void CodeEdit::_filter_code_completion_candidates_impl() { int offset = option.default_value.get_type() == Variant::COLOR ? line_height : 0; if (in_string != -1) { + // The completion string may have a literal behind it, which should be removed before re-quoting. + String literal; + if (option.insert_text.substr(1).is_quoted()) { + literal = option.display.left(1); + option.display = option.display.substr(1); + option.insert_text = option.insert_text.substr(1); + } String quote = single_quote ? "'" : "\""; - option.display = option.display.unquote().quote(quote); - option.insert_text = option.insert_text.unquote().quote(quote); + option.display = literal + (option.display.unquote().quote(quote)); + option.insert_text = literal + (option.insert_text.unquote().quote(quote)); } if (option.display.length() == 0) { diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index d65399446c..62bb14459d 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -1653,8 +1653,7 @@ Size2 Control::get_custom_minimum_size() const { void Control::_update_minimum_size_cache() { Size2 minsize = get_minimum_size(); - minsize.x = MAX(minsize.x, data.custom_minimum_size.x); - minsize.y = MAX(minsize.y, data.custom_minimum_size.y); + minsize = minsize.max(data.custom_minimum_size); data.minimum_size_cache = minsize; data.minimum_size_valid = true; @@ -3690,6 +3689,8 @@ void Control::_bind_methods() { Control::Control() { data.theme_owner = memnew(ThemeOwner(this)); + + set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_OFF); } Control::~Control() { diff --git a/scene/gui/dialogs.cpp b/scene/gui/dialogs.cpp index 3746b667f6..c13a2e281a 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -252,8 +252,7 @@ Size2 AcceptDialog::_get_contents_minimum_size() const { } Size2 child_minsize = c->get_combined_minimum_size(); - content_minsize.x = MAX(child_minsize.x, content_minsize.x); - content_minsize.y = MAX(child_minsize.y, content_minsize.y); + content_minsize = child_minsize.max(content_minsize); } // Then we take the background panel as it provides the offsets, diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index a53dffd0d2..40ac87160c 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -671,8 +671,8 @@ void FileDialog::update_file_list() { item = dir_access->get_next(); } - dirs.sort_custom<NaturalNoCaseComparator>(); - files.sort_custom<NaturalNoCaseComparator>(); + dirs.sort_custom<FileNoCaseComparator>(); + files.sort_custom<FileNoCaseComparator>(); while (!dirs.is_empty()) { String &dir_name = dirs.front()->get(); diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index 056872a4fe..8bce5c0caa 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -181,8 +181,7 @@ void GraphEditMinimap::gui_input(const Ref<InputEvent> &p_ev) { if (is_resizing) { // Prevent setting minimap wider than GraphEdit. Vector2 new_minimap_size; - new_minimap_size.width = MIN(get_size().width - mm->get_relative().x, ge->get_size().width - 2.0 * minimap_padding.x); - new_minimap_size.height = MIN(get_size().height - mm->get_relative().y, ge->get_size().height - 2.0 * minimap_padding.y); + new_minimap_size = (get_size() - mm->get_relative()).min(ge->get_size() - 2.0 * minimap_padding); ge->set_minimap_size(new_minimap_size); queue_redraw(); diff --git a/scene/gui/graph_element.cpp b/scene/gui/graph_element.cpp index 7fa5b0ceec..9eb9578b2c 100644 --- a/scene/gui/graph_element.cpp +++ b/scene/gui/graph_element.cpp @@ -74,8 +74,7 @@ Size2 GraphElement::get_minimum_size() const { Size2i size = child->get_combined_minimum_size(); - minsize.width = MAX(minsize.width, size.width); - minsize.height = MAX(minsize.height, size.height); + minsize = minsize.max(size); } return minsize; diff --git a/scene/gui/panel_container.cpp b/scene/gui/panel_container.cpp index 306ef4936f..ef85919859 100644 --- a/scene/gui/panel_container.cpp +++ b/scene/gui/panel_container.cpp @@ -44,8 +44,7 @@ Size2 PanelContainer::get_minimum_size() const { } Size2 minsize = c->get_combined_minimum_size(); - ms.width = MAX(ms.width, minsize.width); - ms.height = MAX(ms.height, minsize.height); + ms = ms.max(minsize); } if (theme_cache.panel_style.is_valid()) { diff --git a/scene/gui/popup.cpp b/scene/gui/popup.cpp index 0c5a882044..0927404947 100644 --- a/scene/gui/popup.cpp +++ b/scene/gui/popup.cpp @@ -224,8 +224,7 @@ Size2 PopupPanel::_get_contents_minimum_size() const { } Size2 cms = c->get_combined_minimum_size(); - ms.x = MAX(cms.x, ms.x); - ms.y = MAX(cms.y, ms.y); + ms = cms.max(ms); } return ms + theme_cache.panel_style->get_minimum_size(); diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index c66b30f130..b2617e6fc7 100644 --- a/scene/gui/progress_bar.cpp +++ b/scene/gui/progress_bar.cpp @@ -35,15 +35,13 @@ Size2 ProgressBar::get_minimum_size() const { Size2 minimum_size = theme_cache.background_style->get_minimum_size(); - minimum_size.height = MAX(minimum_size.height, theme_cache.fill_style->get_minimum_size().height); - minimum_size.width = MAX(minimum_size.width, theme_cache.fill_style->get_minimum_size().width); + minimum_size = minimum_size.max(theme_cache.fill_style->get_minimum_size()); if (show_percentage) { String txt = "100%"; TextLine tl = TextLine(txt, theme_cache.font, theme_cache.font_size); minimum_size.height = MAX(minimum_size.height, theme_cache.background_style->get_minimum_size().height + tl.get_size().y); } else { // this is needed, else the progressbar will collapse - minimum_size.width = MAX(minimum_size.width, 1); - minimum_size.height = MAX(minimum_size.height, 1); + minimum_size = minimum_size.max(Size2(1, 1)); } return minimum_size; } diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index cb7ccb583e..1baf71dd07 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -951,7 +951,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o case ITEM_IMAGE: { ItemImage *img = static_cast<ItemImage *>(it); if (img->pad) { - Size2 pad_size = Size2(MIN(rect.size.x, img->image->get_width()), MIN(rect.size.y, img->image->get_height())); + Size2 pad_size = rect.size.min(img->image->get_size()); Vector2 pad_off = (rect.size - pad_size) / 2; img->image->draw_rect(ci, Rect2(p_ofs + rect.position + off + pad_off, pad_size), false, img->color); } else { diff --git a/scene/gui/scroll_container.cpp b/scene/gui/scroll_container.cpp index 89d308de3f..1447d2f002 100644 --- a/scene/gui/scroll_container.cpp +++ b/scene/gui/scroll_container.cpp @@ -56,8 +56,7 @@ Size2 ScrollContainer::get_minimum_size() const { Size2 child_min_size = c->get_combined_minimum_size(); - largest_child_min_size.x = MAX(largest_child_min_size.x, child_min_size.x); - largest_child_min_size.y = MAX(largest_child_min_size.y, child_min_size.y); + largest_child_min_size = largest_child_min_size.max(child_min_size); } if (horizontal_scroll_mode == SCROLL_MODE_DISABLED) { diff --git a/scene/gui/subviewport_container.cpp b/scene/gui/subviewport_container.cpp index 0d33774e20..f6cfe6ab18 100644 --- a/scene/gui/subviewport_container.cpp +++ b/scene/gui/subviewport_container.cpp @@ -45,8 +45,7 @@ Size2 SubViewportContainer::get_minimum_size() const { } Size2 minsize = c->get_size(); - ms.width = MAX(ms.width, minsize.width); - ms.height = MAX(ms.height, minsize.height); + ms = ms.max(minsize); } return ms; diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index 741b38a35a..df3c9631f9 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -871,8 +871,7 @@ Size2 TabContainer::get_minimum_size() const { } Size2 cms = c->get_combined_minimum_size(); - largest_child_min_size.x = MAX(largest_child_min_size.x, cms.x); - largest_child_min_size.y = MAX(largest_child_min_size.y, cms.y); + largest_child_min_size = largest_child_min_size.max(cms); } ms.y += largest_child_min_size.y; diff --git a/scene/gui/texture_button.cpp b/scene/gui/texture_button.cpp index dcbb25c41d..0b197c8c02 100644 --- a/scene/gui/texture_button.cpp +++ b/scene/gui/texture_button.cpp @@ -103,8 +103,8 @@ bool TextureButton::has_point(const Point2 &p_point) const { point *= scale; // finally, we need to check if the point is inside a rectangle with a position >= 0,0 and a size <= mask_size - rect.position = Point2(MAX(0, _texture_region.position.x), MAX(0, _texture_region.position.y)); - rect.size = Size2(MIN(mask_size.x, _texture_region.size.x), MIN(mask_size.y, _texture_region.size.y)); + rect.position = Point2().max(_texture_region.position); + rect.size = mask_size.min(_texture_region.size); } if (!rect.has_point(point)) { diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp index 248260a7d2..5261cbe3eb 100644 --- a/scene/gui/texture_progress_bar.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -249,8 +249,7 @@ Point2 TextureProgressBar::get_relative_center() { p += rad_center_off; p.x /= progress->get_width(); p.y /= progress->get_height(); - p.x = CLAMP(p.x, 0, 1); - p.y = CLAMP(p.y, 0, 1); + p = p.clamp(Point2(), Point2(1, 1)); return p; } diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 2d0da075ba..768c83954b 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -336,6 +336,19 @@ void CanvasItem::_notification(int p_what) { get_parent()->connect(SNAME("child_order_changed"), callable_mp(get_viewport(), &Viewport::canvas_parent_mark_dirty).bind(get_parent()), CONNECT_REFERENCE_COUNTED); } + // If using physics interpolation, reset for this node only, + // as a helper, as in most cases, users will want items reset when + // adding to the tree. + // In cases where they move immediately after adding, + // there will be little cost in having two resets as these are cheap, + // and it is worth it for convenience. + // Do not propagate to children, as each child of an added branch + // receives its own NOTIFICATION_ENTER_TREE, and this would + // cause unnecessary duplicate resets. + if (is_physics_interpolated_and_enabled()) { + notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION); + } + } break; case NOTIFICATION_EXIT_TREE: { ERR_MAIN_THREAD_GUARD; @@ -360,6 +373,12 @@ void CanvasItem::_notification(int p_what) { } } break; + case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: { + if (is_visible_in_tree() && is_physics_interpolated()) { + RenderingServer::get_singleton()->canvas_item_reset_physics_interpolation(canvas_item); + } + } break; + case NOTIFICATION_VISIBILITY_CHANGED: { ERR_MAIN_THREAD_GUARD; @@ -921,6 +940,10 @@ void CanvasItem::_notify_transform(CanvasItem *p_node) { } } +void CanvasItem::_physics_interpolated_changed() { + RenderingServer::get_singleton()->canvas_item_set_interpolated(canvas_item, is_physics_interpolated()); +} + Rect2 CanvasItem::get_viewport_rect() const { ERR_READ_THREAD_GUARD_V(Rect2()); ERR_FAIL_COND_V(!is_inside_tree(), Rect2()); diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h index 03b01f7ef7..383edeec93 100644 --- a/scene/main/canvas_item.h +++ b/scene/main/canvas_item.h @@ -140,6 +140,8 @@ private: void _notify_transform(CanvasItem *p_node); + virtual void _physics_interpolated_changed() override; + static CanvasItem *current_item_drawn; friend class Viewport; void _refresh_texture_repeat_cache() const; diff --git a/scene/main/node.cpp b/scene/main/node.cpp index 906c397b5c..142736e388 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -99,6 +99,14 @@ void Node::_notification(int p_notification) { } } + if (data.physics_interpolation_mode == PHYSICS_INTERPOLATION_MODE_INHERIT) { + bool interpolate = true; // Root node default is for interpolation to be on. + if (data.parent) { + interpolate = data.parent->is_physics_interpolated(); + } + _propagate_physics_interpolated(interpolate); + } + // Update auto translate mode. if (data.auto_translate_mode == AUTO_TRANSLATE_MODE_INHERIT && !data.parent) { ERR_PRINT("The root node can't be set to Inherit auto translate mode, reverting to Always instead."); @@ -395,6 +403,36 @@ void Node::_propagate_exit_tree() { data.depth = -1; } +void Node::_propagate_physics_interpolated(bool p_interpolated) { + switch (data.physics_interpolation_mode) { + case PHYSICS_INTERPOLATION_MODE_INHERIT: + // Keep the parent p_interpolated. + break; + case PHYSICS_INTERPOLATION_MODE_OFF: { + p_interpolated = false; + } break; + case PHYSICS_INTERPOLATION_MODE_ON: { + p_interpolated = true; + } break; + } + + // No change? No need to propagate further. + if (data.physics_interpolated == p_interpolated) { + return; + } + + data.physics_interpolated = p_interpolated; + + // Allow a call to the RenderingServer etc. in derived classes. + _physics_interpolated_changed(); + + data.blocked++; + for (KeyValue<StringName, Node *> &K : data.children) { + K.value->_propagate_physics_interpolated(p_interpolated); + } + data.blocked--; +} + void Node::move_child(Node *p_child, int p_index) { ERR_FAIL_COND_MSG(data.inside_tree && !Thread::is_main_thread(), "Moving child node positions inside the SceneTree is only allowed from the main thread. Use call_deferred(\"move_child\",child,index)."); ERR_FAIL_NULL(p_child); @@ -507,6 +545,8 @@ void Node::move_child_notify(Node *p_child) { void Node::owner_changed_notify() { } +void Node::_physics_interpolated_changed() {} + void Node::set_physics_process(bool p_process) { ERR_THREAD_GUARD if (data.physics_process == p_process) { @@ -821,6 +861,42 @@ bool Node::_can_process(bool p_paused) const { } } +void Node::set_physics_interpolation_mode(PhysicsInterpolationMode p_mode) { + ERR_THREAD_GUARD + if (data.physics_interpolation_mode == p_mode) { + return; + } + + data.physics_interpolation_mode = p_mode; + + bool interpolate = true; // Default for root node. + + switch (p_mode) { + case PHYSICS_INTERPOLATION_MODE_INHERIT: { + if (is_inside_tree() && data.parent) { + interpolate = data.parent->is_physics_interpolated(); + } + } break; + case PHYSICS_INTERPOLATION_MODE_OFF: { + interpolate = false; + } break; + case PHYSICS_INTERPOLATION_MODE_ON: { + interpolate = true; + } break; + } + + // If swapping from interpolated to non-interpolated, use this as an extra means to cause a reset. + if (is_physics_interpolated() && !interpolate) { + reset_physics_interpolation(); + } + + _propagate_physics_interpolated(interpolate); +} + +void Node::reset_physics_interpolation() { + propagate_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION); +} + bool Node::_is_enabled() const { ProcessMode process_mode; @@ -2737,25 +2813,9 @@ Node *Node::_duplicate(int p_flags, HashMap<const Node *, Node *> *r_duplimap) c } } else { - // If property points to a node which is owned by a node we are duplicating, update its path. - if (value.get_type() == Variant::OBJECT) { - Node *property_node = Object::cast_to<Node>(value); - if (property_node && is_ancestor_of(property_node)) { - value = current_node->get_node_or_null(get_path_to(property_node)); - } - } else if (value.get_type() == Variant::ARRAY) { - Array arr = value; - if (arr.get_typed_builtin() == Variant::OBJECT) { - for (int i = 0; i < arr.size(); i++) { - Node *property_node = Object::cast_to<Node>(arr[i]); - if (property_node && is_ancestor_of(property_node)) { - arr[i] = current_node->get_node_or_null(get_path_to(property_node)); - } - } - value = arr; - } + if (value.get_type() != Variant::OBJECT && (value.get_type() != Variant::ARRAY || static_cast<Array>(value).get_typed_builtin() != Variant::OBJECT)) { + current_node->set(name, value); } - current_node->set(name, value); } } } @@ -2771,6 +2831,8 @@ Node *Node::duplicate(int p_flags) const { _duplicate_signals(this, dupe); } + _duplicate_properties_node(this, this, dupe); + return dupe; } @@ -2792,6 +2854,8 @@ Node *Node::duplicate_from_editor(HashMap<const Node *, Node *> &r_duplimap, con // if the emitter node comes later in tree order than the receiver _duplicate_signals(this, dupe); + _duplicate_properties_node(this, this, dupe); + return dupe; } @@ -2844,6 +2908,44 @@ void Node::remap_nested_resources(Ref<Resource> p_resource, const HashMap<Ref<Re } #endif +// Duplicates properties that store a Node. +// This has to be called after nodes have been duplicated since +// only then do we get a full picture of how the duplicated node tree looks like. +void Node::_duplicate_properties_node(const Node *p_root, const Node *p_original, Node *p_copy) const { + List<PropertyInfo> props; + p_copy->get_property_list(&props); + for (const PropertyInfo &E : props) { + if (!(E.usage & PROPERTY_USAGE_STORAGE)) { + continue; + } + String name = E.name; + Variant value = p_original->get(name).duplicate(true); + if (value.get_type() == Variant::OBJECT) { + Node *property_node = Object::cast_to<Node>(value); + if (property_node && (p_root == property_node || p_root->is_ancestor_of(property_node))) { + value = p_copy->get_node_or_null(p_original->get_path_to(property_node)); + p_copy->set(name, value); + } + } else if (value.get_type() == Variant::ARRAY) { + Array arr = value; + if (arr.get_typed_builtin() == Variant::OBJECT) { + for (int i = 0; i < arr.size(); i++) { + Node *property_node = Object::cast_to<Node>(arr[i]); + if (property_node && (p_root == property_node || p_root->is_ancestor_of(property_node))) { + arr[i] = p_copy->get_node_or_null(p_original->get_path_to(property_node)); + } + } + value = arr; + p_copy->set(name, value); + } + } + } + + for (int i = 0; i < p_copy->get_child_count(); i++) { + _duplicate_properties_node(p_root, p_original->get_child(i), p_copy->get_child(i)); + } +} + // Duplication of signals must happen after all the node descendants have been copied, // because re-targeting of connections from some descendant to another is not possible // if the emitter node comes later in tree order than the receiver @@ -3489,6 +3591,12 @@ void Node::_bind_methods() { ClassDB::bind_method(D_METHOD("set_physics_process_internal", "enable"), &Node::set_physics_process_internal); ClassDB::bind_method(D_METHOD("is_physics_processing_internal"), &Node::is_physics_processing_internal); + ClassDB::bind_method(D_METHOD("set_physics_interpolation_mode", "mode"), &Node::set_physics_interpolation_mode); + ClassDB::bind_method(D_METHOD("get_physics_interpolation_mode"), &Node::get_physics_interpolation_mode); + ClassDB::bind_method(D_METHOD("is_physics_interpolated"), &Node::is_physics_interpolated); + ClassDB::bind_method(D_METHOD("is_physics_interpolated_and_enabled"), &Node::is_physics_interpolated_and_enabled); + ClassDB::bind_method(D_METHOD("reset_physics_interpolation"), &Node::reset_physics_interpolation); + ClassDB::bind_method(D_METHOD("set_auto_translate_mode", "mode"), &Node::set_auto_translate_mode); ClassDB::bind_method(D_METHOD("get_auto_translate_mode"), &Node::get_auto_translate_mode); @@ -3594,6 +3702,7 @@ void Node::_bind_methods() { BIND_CONSTANT(NOTIFICATION_POST_ENTER_TREE); BIND_CONSTANT(NOTIFICATION_DISABLED); BIND_CONSTANT(NOTIFICATION_ENABLED); + BIND_CONSTANT(NOTIFICATION_RESET_PHYSICS_INTERPOLATION); BIND_CONSTANT(NOTIFICATION_EDITOR_PRE_SAVE); BIND_CONSTANT(NOTIFICATION_EDITOR_POST_SAVE); @@ -3633,6 +3742,10 @@ void Node::_bind_methods() { BIND_BITFIELD_FLAG(FLAG_PROCESS_THREAD_MESSAGES_PHYSICS); BIND_BITFIELD_FLAG(FLAG_PROCESS_THREAD_MESSAGES_ALL); + BIND_ENUM_CONSTANT(PHYSICS_INTERPOLATION_MODE_INHERIT); + BIND_ENUM_CONSTANT(PHYSICS_INTERPOLATION_MODE_ON); + BIND_ENUM_CONSTANT(PHYSICS_INTERPOLATION_MODE_OFF); + BIND_ENUM_CONSTANT(DUPLICATE_SIGNALS); BIND_ENUM_CONSTANT(DUPLICATE_GROUPS); BIND_ENUM_CONSTANT(DUPLICATE_SCRIPTS); @@ -3674,6 +3787,9 @@ void Node::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "process_thread_group_order"), "set_process_thread_group_order", "get_process_thread_group_order"); ADD_PROPERTY(PropertyInfo(Variant::INT, "process_thread_messages", PROPERTY_HINT_FLAGS, "Process,Physics Process"), "set_process_thread_messages", "get_process_thread_messages"); + ADD_GROUP("Physics Interpolation", "physics_interpolation_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "physics_interpolation_mode", PROPERTY_HINT_ENUM, "Inherit,Off,On"), "set_physics_interpolation_mode", "get_physics_interpolation_mode"); + ADD_GROUP("Auto Translate", "auto_translate_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "auto_translate_mode", PROPERTY_HINT_ENUM, "Inherit,Always,Disabled"), "set_auto_translate_mode", "get_auto_translate_mode"); @@ -3708,6 +3824,35 @@ String Node::_get_name_num_separator() { Node::Node() { orphan_node_count++; + + // Default member initializer for bitfield is a C++20 extension, so: + + data.process_mode = PROCESS_MODE_INHERIT; + data.physics_interpolation_mode = PHYSICS_INTERPOLATION_MODE_INHERIT; + + data.physics_process = false; + data.process = false; + + data.physics_process_internal = false; + data.process_internal = false; + + data.input = false; + data.shortcut_input = false; + data.unhandled_input = false; + data.unhandled_key_input = false; + + data.physics_interpolated = false; + + data.parent_owned = false; + data.in_constructor = true; + data.use_placeholder = false; + + data.display_folded = false; + data.editable_instance = false; + + data.inside_tree = false; + data.ready_notified = false; // This is a small hack, so if a node is added during _ready() to the tree, it correctly gets the _ready() notification. + data.ready_first = true; } Node::~Node() { diff --git a/scene/main/node.h b/scene/main/node.h index 57a7278e73..f49eeec9cd 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -66,7 +66,9 @@ protected: }; public: - enum ProcessMode { + // N.B. Any enum stored as a bitfield should be specified as UNSIGNED to work around + // some compilers trying to store it as signed, and requiring 1 more bit than necessary. + enum ProcessMode : unsigned int { PROCESS_MODE_INHERIT, // same as parent node PROCESS_MODE_PAUSABLE, // process only if not paused PROCESS_MODE_WHEN_PAUSED, // process only if paused @@ -86,6 +88,12 @@ public: FLAG_PROCESS_THREAD_MESSAGES_ALL = 3, }; + enum PhysicsInterpolationMode : unsigned int { + PHYSICS_INTERPOLATION_MODE_INHERIT, + PHYSICS_INTERPOLATION_MODE_ON, + PHYSICS_INTERPOLATION_MODE_OFF, + }; + enum DuplicateFlags { DUPLICATE_SIGNALS = 1, DUPLICATE_GROUPS = 2, @@ -170,9 +178,7 @@ private: int blocked = 0; // Safeguard that throws an error when attempting to modify the tree in a harmful way while being traversed. StringName name; SceneTree *tree = nullptr; - bool inside_tree = false; - bool ready_notified = false; // This is a small hack, so if a node is added during _ready() to the tree, it correctly gets the _ready() notification. - bool ready_first = true; + #ifdef TOOLS_ENABLED NodePath import_path; // Path used when imported, used by scene editors to keep tracking. #endif @@ -184,7 +190,6 @@ private: List<Node *>::Element *OW = nullptr; // Owned element. List<Node *> owned; - ProcessMode process_mode = PROCESS_MODE_INHERIT; Node *process_owner = nullptr; ProcessThreadGroup process_thread_group = PROCESS_THREAD_GROUP_INHERIT; Node *process_thread_group_owner = nullptr; @@ -196,26 +201,39 @@ private: Variant rpc_config; // Variables used to properly sort the node when processing, ignored otherwise. - // TODO: Should move all the stuff below to bits. - bool physics_process = false; - bool process = false; int process_priority = 0; int physics_process_priority = 0; - bool physics_process_internal = false; - bool process_internal = false; + // Keep bitpacked values together to get better packing. + ProcessMode process_mode : 3; + PhysicsInterpolationMode physics_interpolation_mode : 2; + + bool physics_process : 1; + bool process : 1; + + bool physics_process_internal : 1; + bool process_internal : 1; + + bool input : 1; + bool shortcut_input : 1; + bool unhandled_input : 1; + bool unhandled_key_input : 1; - bool input = false; - bool shortcut_input = false; - bool unhandled_input = false; - bool unhandled_key_input = false; + // Physics interpolation can be turned on and off on a per node basis. + // This only takes effect when the SceneTree (or project setting) physics interpolation + // is switched on. + bool physics_interpolated : 1; - bool parent_owned = false; - bool in_constructor = true; - bool use_placeholder = false; + bool parent_owned : 1; + bool in_constructor : 1; + bool use_placeholder : 1; - bool display_folded = false; - bool editable_instance = false; + bool display_folded : 1; + bool editable_instance : 1; + + bool inside_tree : 1; + bool ready_notified : 1; + bool ready_first : 1; AutoTranslateMode auto_translate_mode = AUTO_TRANSLATE_MODE_INHERIT; mutable bool is_auto_translating = true; @@ -243,10 +261,12 @@ private: void _propagate_ready(); void _propagate_exit_tree(); void _propagate_after_exit_tree(); + void _propagate_physics_interpolated(bool p_interpolated); void _propagate_process_owner(Node *p_owner, int p_pause_notification, int p_enabled_notification); void _propagate_groups_dirty(); Array _get_node_and_resource(const NodePath &p_path); + void _duplicate_properties_node(const Node *p_root, const Node *p_original, Node *p_copy) const; void _duplicate_signals(const Node *p_original, Node *p_copy) const; Node *_duplicate(int p_flags, HashMap<const Node *, Node *> *r_duplimap = nullptr) const; @@ -295,6 +315,8 @@ protected: void _notification(int p_notification); + virtual void _physics_interpolated_changed(); + virtual void add_child_notify(Node *p_child); virtual void remove_child_notify(Node *p_child); virtual void move_child_notify(Node *p_child); @@ -339,7 +361,7 @@ protected: public: enum { - // you can make your own, but don't use the same numbers as other notifications in other nodes + // You can make your own, but don't use the same numbers as other notifications in other nodes. NOTIFICATION_ENTER_TREE = 10, NOTIFICATION_EXIT_TREE = 11, NOTIFICATION_MOVED_IN_PARENT = 12, @@ -360,8 +382,8 @@ public: NOTIFICATION_POST_ENTER_TREE = 27, NOTIFICATION_DISABLED = 28, NOTIFICATION_ENABLED = 29, - //keep these linked to node - + NOTIFICATION_RESET_PHYSICS_INTERPOLATION = 2001, // A GodotSpace Odyssey. + // Keep these linked to Node. NOTIFICATION_WM_MOUSE_ENTER = 1002, NOTIFICATION_WM_MOUSE_EXIT = 1003, NOTIFICATION_WM_WINDOW_FOCUS_IN = 1004, @@ -613,6 +635,13 @@ public: ProcessMode get_process_mode() const; bool can_process() const; bool can_process_notification(int p_what) const; + + void set_physics_interpolation_mode(PhysicsInterpolationMode p_mode); + PhysicsInterpolationMode get_physics_interpolation_mode() const { return data.physics_interpolation_mode; } + _FORCE_INLINE_ bool is_physics_interpolated() const { return data.physics_interpolated; } + _FORCE_INLINE_ bool is_physics_interpolated_and_enabled() const { return is_inside_tree() && get_tree()->is_physics_interpolation_enabled() && is_physics_interpolated(); } + void reset_physics_interpolation(); + bool is_enabled() const; bool is_ready() const; @@ -742,6 +771,7 @@ VARIANT_ENUM_CAST(Node::ProcessMode); VARIANT_ENUM_CAST(Node::ProcessThreadGroup); VARIANT_BITFIELD_CAST(Node::ProcessThreadMessages); VARIANT_ENUM_CAST(Node::InternalMode); +VARIANT_ENUM_CAST(Node::PhysicsInterpolationMode); VARIANT_ENUM_CAST(Node::AutoTranslateMode); typedef HashSet<Node *, Node::Comparator> NodeSet; diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index cb16f425b5..0c77a60af6 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -451,6 +451,30 @@ void SceneTree::initialize() { root->_set_tree(this); } +void SceneTree::set_physics_interpolation_enabled(bool p_enabled) { + // We never want interpolation in the editor. + if (Engine::get_singleton()->is_editor_hint()) { + p_enabled = false; + } + + if (p_enabled == _physics_interpolation_enabled) { + return; + } + + _physics_interpolation_enabled = p_enabled; + RenderingServer::get_singleton()->set_physics_interpolation_enabled(p_enabled); +} + +bool SceneTree::is_physics_interpolation_enabled() const { + return _physics_interpolation_enabled; +} + +void SceneTree::iteration_prepare() { + if (_physics_interpolation_enabled) { + RenderingServer::get_singleton()->tick(); + } +} + bool SceneTree::physics_process(double p_time) { root_lock++; @@ -1606,6 +1630,9 @@ void SceneTree::_bind_methods() { ClassDB::bind_method(D_METHOD("get_frame"), &SceneTree::get_frame); ClassDB::bind_method(D_METHOD("quit", "exit_code"), &SceneTree::quit, DEFVAL(EXIT_SUCCESS)); + ClassDB::bind_method(D_METHOD("set_physics_interpolation_enabled", "enabled"), &SceneTree::set_physics_interpolation_enabled); + ClassDB::bind_method(D_METHOD("is_physics_interpolation_enabled"), &SceneTree::is_physics_interpolation_enabled); + ClassDB::bind_method(D_METHOD("queue_delete", "obj"), &SceneTree::queue_delete); MethodInfo mi; @@ -1657,6 +1684,7 @@ void SceneTree::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "current_scene", PROPERTY_HINT_RESOURCE_TYPE, "Node", PROPERTY_USAGE_NONE), "set_current_scene", "get_current_scene"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "root", PROPERTY_HINT_RESOURCE_TYPE, "Node", PROPERTY_USAGE_NONE), "", "get_root"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "multiplayer_poll"), "set_multiplayer_poll_enabled", "is_multiplayer_poll_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "physics_interpolation"), "set_physics_interpolation_enabled", "is_physics_interpolation_enabled"); ADD_SIGNAL(MethodInfo("tree_changed")); ADD_SIGNAL(MethodInfo("tree_process_mode_changed")); //editor only signal, but due to API hash it can't be removed in run-time @@ -1748,6 +1776,8 @@ SceneTree::SceneTree() { root->set_as_audio_listener_3d(true); #endif // _3D_DISABLED + set_physics_interpolation_enabled(GLOBAL_DEF("physics/common/physics_interpolation", false)); + // Initialize network state. set_multiplayer(MultiplayerAPI::create_default_interface()); diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h index a7515f0243..603438cc5f 100644 --- a/scene/main/scene_tree.h +++ b/scene/main/scene_tree.h @@ -139,6 +139,8 @@ private: HashMap<StringName, Group> group_map; bool _quit = false; + bool _physics_interpolation_enabled = false; + StringName tree_changed_name = "tree_changed"; StringName node_added_name = "node_added"; StringName node_removed_name = "node_removed"; @@ -313,6 +315,8 @@ public: virtual void initialize() override; + virtual void iteration_prepare() override; + virtual bool physics_process(double p_time) override; virtual bool process(double p_time) override; @@ -425,6 +429,9 @@ public: void set_disable_node_threading(bool p_disable); //default texture settings + void set_physics_interpolation_enabled(bool p_enabled); + bool is_physics_interpolation_enabled() const; + SceneTree(); ~SceneTree(); }; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index c8d2d71c2a..41f3ff108c 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -2747,8 +2747,7 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) { Size2i min_size = gui.currently_dragged_subwindow->get_min_size(); Size2i min_size_clamped = gui.currently_dragged_subwindow->get_clamped_minimum_size(); - min_size_clamped.x = MAX(min_size_clamped.x, 1); - min_size_clamped.y = MAX(min_size_clamped.y, 1); + min_size_clamped = min_size_clamped.max(Size2i(1, 1)); Rect2i r = gui.subwindow_resize_from_rect; @@ -2809,8 +2808,7 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) { Size2i max_size = gui.currently_dragged_subwindow->get_max_size(); if ((max_size.x > 0 || max_size.y > 0) && (max_size.x >= min_size.x && max_size.y >= min_size.y)) { - max_size.x = MAX(max_size.x, 1); - max_size.y = MAX(max_size.y, 1); + max_size = max_size.max(Size2i(1, 1)); if (r.size.x > max_size.x) { r.size.x = max_size.x; diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 6632dd9033..bced493d98 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -1033,8 +1033,7 @@ void Window::_update_window_size() { } if (embedder) { - size.x = MAX(size.x, 1); - size.y = MAX(size.y, 1); + size = size.max(Size2i(1, 1)); embedder->_sub_window_update(this); } else if (window_id != DisplayServer::INVALID_WINDOW_ID) { @@ -1545,8 +1544,7 @@ Size2 Window::_get_contents_minimum_size() const { Point2i pos = c->get_position(); Size2i min = c->get_combined_minimum_size(); - max.x = MAX(pos.x + min.x, max.x); - max.y = MAX(pos.y + min.y, max.y); + max = max.max(pos + min); } } @@ -1703,7 +1701,7 @@ void Window::popup_centered_clamped(const Size2i &p_size, float p_fallback_ratio Vector2i size_ratio = parent_rect.size * p_fallback_ratio; Rect2i popup_rect; - popup_rect.size = Vector2i(MIN(size_ratio.x, expected_size.x), MIN(size_ratio.y, expected_size.y)); + popup_rect.size = size_ratio.min(expected_size); popup_rect.size = _clamp_window_size(popup_rect.size); if (parent_rect != Rect2()) { diff --git a/scene/resources/2d/polygon_path_finder.cpp b/scene/resources/2d/polygon_path_finder.cpp index 617a53f0a3..3aa292431d 100644 --- a/scene/resources/2d/polygon_path_finder.cpp +++ b/scene/resources/2d/polygon_path_finder.cpp @@ -64,8 +64,7 @@ void PolygonPathFinder::setup(const Vector<Vector2> &p_points, const Vector<int> points.write[i].pos = p_points[i]; points.write[i].penalty = 0; - outside_point.x = i == 0 ? p_points[0].x : (MAX(p_points[i].x, outside_point.x)); - outside_point.y = i == 0 ? p_points[0].y : (MAX(p_points[i].y, outside_point.y)); + outside_point = i == 0 ? p_points[0] : p_points[i].max(outside_point); if (i == 0) { bounds.position = points[i].pos; diff --git a/scene/resources/2d/tile_set.cpp b/scene/resources/2d/tile_set.cpp index d09723d765..66d97398b9 100644 --- a/scene/resources/2d/tile_set.cpp +++ b/scene/resources/2d/tile_set.cpp @@ -4650,7 +4650,7 @@ Ref<Texture2D> TileSetAtlasSource::get_texture() const { void TileSetAtlasSource::set_margins(Vector2i p_margins) { if (p_margins.x < 0 || p_margins.y < 0) { WARN_PRINT("Atlas source margins should be positive."); - margins = Vector2i(MAX(0, p_margins.x), MAX(0, p_margins.y)); + margins = p_margins.max(Vector2i()); } else { margins = p_margins; } @@ -4666,7 +4666,7 @@ Vector2i TileSetAtlasSource::get_margins() const { void TileSetAtlasSource::set_separation(Vector2i p_separation) { if (p_separation.x < 0 || p_separation.y < 0) { WARN_PRINT("Atlas source separation should be positive."); - separation = Vector2i(MAX(0, p_separation.x), MAX(0, p_separation.y)); + separation = p_separation.max(Vector2i()); } else { separation = p_separation; } @@ -4682,7 +4682,7 @@ Vector2i TileSetAtlasSource::get_separation() const { void TileSetAtlasSource::set_texture_region_size(Vector2i p_tile_size) { if (p_tile_size.x <= 0 || p_tile_size.y <= 0) { WARN_PRINT("Atlas source tile_size should be strictly positive."); - texture_region_size = Vector2i(MAX(1, p_tile_size.x), MAX(1, p_tile_size.y)); + texture_region_size = p_tile_size.max(Vector2i(1, 1)); } else { texture_region_size = p_tile_size; } diff --git a/scene/resources/3d/primitive_meshes.cpp b/scene/resources/3d/primitive_meshes.cpp index c2b2a6d68b..ee772f960a 100644 --- a/scene/resources/3d/primitive_meshes.cpp +++ b/scene/resources/3d/primitive_meshes.cpp @@ -2845,10 +2845,8 @@ void TextMesh::_generate_glyph_mesh_data(const GlyphMeshKey &p_key, const Glyph for (int j = 0; j < gl_data.contours[i].size(); j++) { int next = (j + 1 == gl_data.contours[i].size()) ? 0 : (j + 1); - gl_data.min_p.x = MIN(gl_data.min_p.x, gl_data.contours[i][j].point.x); - gl_data.min_p.y = MIN(gl_data.min_p.y, gl_data.contours[i][j].point.y); - gl_data.max_p.x = MAX(gl_data.max_p.x, gl_data.contours[i][j].point.x); - gl_data.max_p.y = MAX(gl_data.max_p.y, gl_data.contours[i][j].point.y); + gl_data.min_p = gl_data.min_p.min(gl_data.contours[i][j].point); + gl_data.max_p = gl_data.max_p.max(gl_data.contours[i][j].point); length += (gl_data.contours[i][next].point - gl_data.contours[i][j].point).length(); inp.GetPoint(j) = gl_data.contours[i][j].point; diff --git a/scene/resources/animation.h b/scene/resources/animation.h index d3df8d03e5..c72327e464 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -75,6 +75,7 @@ public: LOOP_PINGPONG, }; + // LoopedFlag is used in Animataion to "process the keys at both ends correct". enum LoopedFlag { LOOPED_FLAG_NONE, LOOPED_FLAG_END, @@ -187,6 +188,7 @@ private: }; /* BEZIER TRACK */ + struct BezierKey { Vector2 in_handle; // Relative (x always <0) Vector2 out_handle; // Relative (x always >0) @@ -223,7 +225,7 @@ private: } }; - /* AUDIO TRACK */ + /* ANIMATION TRACK */ struct AnimationTrack : public Track { Vector<TKey<StringName>> values; diff --git a/scene/resources/navigation_polygon.cpp b/scene/resources/navigation_polygon.cpp index e830153330..274b13a487 100644 --- a/scene/resources/navigation_polygon.cpp +++ b/scene/resources/navigation_polygon.cpp @@ -251,8 +251,7 @@ void NavigationPolygon::make_polygons_from_outlines() { } const Vector2 *r = ol.ptr(); for (int j = 0; j < olsize; j++) { - outside_point.x = MAX(r[j].x, outside_point.x); - outside_point.y = MAX(r[j].y, outside_point.y); + outside_point = outside_point.max(r[j]); } } diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp index c7c2ddbb18..5cd9ec7ad0 100644 --- a/scene/resources/visual_shader.cpp +++ b/scene/resources/visual_shader.cpp @@ -4710,68 +4710,61 @@ String VisualShaderNodeExpression::get_expression() const { return expression; } +bool VisualShaderNodeExpression::_is_valid_identifier_char(char32_t p_c) const { + return p_c == '_' || (p_c >= 'A' && p_c <= 'Z') || (p_c >= 'a' && p_c <= 'z') || (p_c >= '0' && p_c <= '9'); +} + +String VisualShaderNodeExpression::_replace_port_names(const Vector<Pair<String, String>> &p_pairs, const String &p_expression) const { + String _expression = p_expression; + + for (const Pair<String, String> &pair : p_pairs) { + String from = pair.first; + String to = pair.second; + int search_idx = 0; + int len = from.length(); + + while (true) { + int index = _expression.find(from, search_idx); + if (index == -1) { + break; + } + + int left_index = index - 1; + int right_index = index + len; + bool left_correct = left_index <= 0 || !_is_valid_identifier_char(_expression[left_index]); + bool right_correct = right_index >= _expression.length() || !_is_valid_identifier_char(_expression[right_index]); + + if (left_correct && right_correct) { + _expression = _expression.erase(index, len); + _expression = _expression.insert(index, to); + + search_idx = index + to.length(); + } else { + search_idx = index + len; + } + } + } + + return _expression; +} + String VisualShaderNodeExpression::generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview) const { String _expression = expression; _expression = _expression.insert(0, "\n"); _expression = _expression.replace("\n", "\n "); - static Vector<String> pre_symbols; - if (pre_symbols.is_empty()) { - pre_symbols.push_back(" "); - pre_symbols.push_back(","); - pre_symbols.push_back(";"); - pre_symbols.push_back("{"); - pre_symbols.push_back("["); - pre_symbols.push_back("]"); - pre_symbols.push_back("("); - pre_symbols.push_back(" "); - pre_symbols.push_back("-"); - pre_symbols.push_back("*"); - pre_symbols.push_back("/"); - pre_symbols.push_back("+"); - pre_symbols.push_back("="); - pre_symbols.push_back("&"); - pre_symbols.push_back("|"); - pre_symbols.push_back("!"); - } - - static Vector<String> post_symbols; - if (post_symbols.is_empty()) { - post_symbols.push_back(" "); - post_symbols.push_back("\n"); - post_symbols.push_back(","); - post_symbols.push_back(";"); - post_symbols.push_back("}"); - post_symbols.push_back("["); - post_symbols.push_back("]"); - post_symbols.push_back(")"); - post_symbols.push_back(" "); - post_symbols.push_back("."); - post_symbols.push_back("-"); - post_symbols.push_back("*"); - post_symbols.push_back("/"); - post_symbols.push_back("+"); - post_symbols.push_back("="); - post_symbols.push_back("&"); - post_symbols.push_back("|"); - post_symbols.push_back("!"); - } - + Vector<Pair<String, String>> input_port_names; for (int i = 0; i < get_input_port_count(); i++) { - for (int j = 0; j < pre_symbols.size(); j++) { - for (int k = 0; k < post_symbols.size(); k++) { - _expression = _expression.replace(pre_symbols[j] + get_input_port_name(i) + post_symbols[k], pre_symbols[j] + p_input_vars[i] + post_symbols[k]); - } - } + input_port_names.push_back(Pair<String, String>(get_input_port_name(i), p_input_vars[i])); } + _expression = _replace_port_names(input_port_names, _expression); + + Vector<Pair<String, String>> output_port_names; for (int i = 0; i < get_output_port_count(); i++) { - for (int j = 0; j < pre_symbols.size(); j++) { - for (int k = 0; k < post_symbols.size(); k++) { - _expression = _expression.replace(pre_symbols[j] + get_output_port_name(i) + post_symbols[k], pre_symbols[j] + p_output_vars[i] + post_symbols[k]); - } - } + output_port_names.push_back(Pair<String, String>(get_output_port_name(i), p_output_vars[i])); } + _expression = _replace_port_names(output_port_names, _expression); String output_initializer; diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h index 09ea9a8890..f02ada7ee8 100644 --- a/scene/resources/visual_shader.h +++ b/scene/resources/visual_shader.h @@ -819,6 +819,10 @@ public: class VisualShaderNodeExpression : public VisualShaderNodeGroupBase { GDCLASS(VisualShaderNodeExpression, VisualShaderNodeGroupBase); +private: + bool _is_valid_identifier_char(char32_t p_c) const; + String _replace_port_names(const Vector<Pair<String, String>> &p_pairs, const String &p_expression) const; + protected: String expression = ""; diff --git a/servers/physics_3d/godot_shape_3d.cpp b/servers/physics_3d/godot_shape_3d.cpp index 872d26aff6..ea389ff59c 100644 --- a/servers/physics_3d/godot_shape_3d.cpp +++ b/servers/physics_3d/godot_shape_3d.cpp @@ -2016,9 +2016,7 @@ void GodotHeightMapShape3D::_get_cell(const Vector3 &p_point, int &r_x, int &r_y Vector3 pos_local = shape_aabb.position + local_origin; Vector3 clamped_point(p_point); - clamped_point.x = CLAMP(p_point.x, pos_local.x, pos_local.x + shape_aabb.size.x); - clamped_point.y = CLAMP(p_point.y, pos_local.y, pos_local.y + shape_aabb.size.y); - clamped_point.z = CLAMP(p_point.z, pos_local.z, pos_local.z + shape_aabb.size.z); + clamped_point = p_point.clamp(pos_local, pos_local + shape_aabb.size); r_x = (clamped_point.x < 0.0) ? (clamped_point.x - 0.5) : (clamped_point.x + 0.5); r_y = (clamped_point.y < 0.0) ? (clamped_point.y - 0.5) : (clamped_point.y + 0.5); diff --git a/servers/rendering/renderer_canvas_cull.cpp b/servers/rendering/renderer_canvas_cull.cpp index 8e342375f8..e32164ea98 100644 --- a/servers/rendering/renderer_canvas_cull.cpp +++ b/servers/rendering/renderer_canvas_cull.cpp @@ -32,6 +32,7 @@ #include "core/config/project_settings.h" #include "core/math/geometry_2d.h" +#include "core/math/transform_interpolator.h" #include "renderer_viewport.h" #include "rendering_server_default.h" #include "rendering_server_globals.h" @@ -81,7 +82,7 @@ void _collect_ysort_children(RendererCanvasCull::Item *p_canvas_item, Transform2 if (r_items) { r_items[r_index] = child_items[i]; child_items[i]->ysort_xform = p_transform; - child_items[i]->ysort_pos = p_transform.xform(child_items[i]->xform.columns[2]); + child_items[i]->ysort_pos = p_transform.xform(child_items[i]->xform_curr.columns[2]); child_items[i]->material_owner = child_items[i]->use_parent_material ? p_material_owner : nullptr; child_items[i]->ysort_modulate = p_modulate; child_items[i]->ysort_index = r_index; @@ -98,7 +99,7 @@ void _collect_ysort_children(RendererCanvasCull::Item *p_canvas_item, Transform2 r_index++; if (child_items[i]->sort_y) { - _collect_ysort_children(child_items[i], p_transform * child_items[i]->xform, child_items[i]->use_parent_material ? p_material_owner : child_items[i], p_modulate * child_items[i]->modulate, r_items, r_index, abs_z); + _collect_ysort_children(child_items[i], p_transform * child_items[i]->xform_curr, child_items[i]->use_parent_material ? p_material_owner : child_items[i], p_modulate * child_items[i]->modulate, r_items, r_index, abs_z); } } } @@ -244,7 +245,14 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 } } - Transform2D xform = ci->xform; + Transform2D final_xform; + if (!_interpolation_data.interpolation_enabled || !ci->interpolated) { + final_xform = ci->xform_curr; + } else { + real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); + TransformInterpolator::interpolate_transform_2d(ci->xform_prev, ci->xform_curr, final_xform, f); + } + Transform2D parent_xform = p_parent_xform; Point2 repeat_size = p_repeat_size; @@ -258,19 +266,19 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 ci->repeat_times = repeat_times; if (repeat_size.x || repeat_size.y) { - rect.size += repeat_size * repeat_times / ci->xform.get_scale(); + rect.size += repeat_size * repeat_times / final_xform.get_scale(); rect.position -= repeat_size * (repeat_times / 2); } } if (snapping_2d_transforms_to_pixel) { - xform.columns[2] = xform.columns[2].round(); + final_xform.columns[2] = final_xform.columns[2].round(); parent_xform.columns[2] = parent_xform.columns[2].round(); } - xform = parent_xform * xform; + final_xform = parent_xform * final_xform; - Rect2 global_rect = xform.xform(rect); + Rect2 global_rect = final_xform.xform(rect); global_rect.position += p_clip_rect.position; if (ci->use_parent_material && p_material_owner) { @@ -324,7 +332,7 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 child_item_count = ci->ysort_children_count + 1; child_items = (Item **)alloca(child_item_count * sizeof(Item *)); - ci->ysort_xform = ci->xform.affine_inverse(); + ci->ysort_xform = final_xform.affine_inverse(); ci->ysort_pos = Vector2(); ci->ysort_modulate = Color(1, 1, 1, 1); ci->ysort_index = 0; @@ -337,7 +345,7 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 sorter.sort(child_items, child_item_count); for (i = 0; i < child_item_count; i++) { - _cull_canvas_item(child_items[i], xform * child_items[i]->ysort_xform, p_clip_rect, modulate * child_items[i]->ysort_modulate, child_items[i]->ysort_parent_abs_z_index, r_z_list, r_z_last_list, (Item *)ci->final_clip_owner, (Item *)child_items[i]->material_owner, false, p_canvas_cull_mask, repeat_size, repeat_times); + _cull_canvas_item(child_items[i], final_xform * child_items[i]->ysort_xform, p_clip_rect, modulate * child_items[i]->ysort_modulate, child_items[i]->ysort_parent_abs_z_index, r_z_list, r_z_last_list, (Item *)ci->final_clip_owner, (Item *)child_items[i]->material_owner, false, p_canvas_cull_mask, repeat_size, repeat_times); } } else { RendererCanvasRender::Item *canvas_group_from = nullptr; @@ -347,7 +355,7 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 canvas_group_from = r_z_last_list[zidx]; } - _attach_canvas_item_for_draw(ci, p_canvas_clip, r_z_list, r_z_last_list, xform, p_clip_rect, global_rect, modulate, p_z, p_material_owner, use_canvas_group, canvas_group_from); + _attach_canvas_item_for_draw(ci, p_canvas_clip, r_z_list, r_z_last_list, final_xform, p_clip_rect, global_rect, modulate, p_z, p_material_owner, use_canvas_group, canvas_group_from); } } else { RendererCanvasRender::Item *canvas_group_from = nullptr; @@ -361,14 +369,14 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 if (!child_items[i]->behind && !use_canvas_group) { continue; } - _cull_canvas_item(child_items[i], xform, p_clip_rect, modulate, p_z, r_z_list, r_z_last_list, (Item *)ci->final_clip_owner, p_material_owner, true, p_canvas_cull_mask, repeat_size, repeat_times); + _cull_canvas_item(child_items[i], final_xform, p_clip_rect, modulate, p_z, r_z_list, r_z_last_list, (Item *)ci->final_clip_owner, p_material_owner, true, p_canvas_cull_mask, repeat_size, repeat_times); } - _attach_canvas_item_for_draw(ci, p_canvas_clip, r_z_list, r_z_last_list, xform, p_clip_rect, global_rect, modulate, p_z, p_material_owner, use_canvas_group, canvas_group_from); + _attach_canvas_item_for_draw(ci, p_canvas_clip, r_z_list, r_z_last_list, final_xform, p_clip_rect, global_rect, modulate, p_z, p_material_owner, use_canvas_group, canvas_group_from); for (int i = 0; i < child_item_count; i++) { if (child_items[i]->behind || use_canvas_group) { continue; } - _cull_canvas_item(child_items[i], xform, p_clip_rect, modulate, p_z, r_z_list, r_z_last_list, (Item *)ci->final_clip_owner, p_material_owner, true, p_canvas_cull_mask, repeat_size, repeat_times); + _cull_canvas_item(child_items[i], final_xform, p_clip_rect, modulate, p_z, r_z_list, r_z_last_list, (Item *)ci->final_clip_owner, p_material_owner, true, p_canvas_cull_mask, repeat_size, repeat_times); } } } @@ -512,7 +520,16 @@ void RendererCanvasCull::canvas_item_set_transform(RID p_item, const Transform2D Item *canvas_item = canvas_item_owner.get_or_null(p_item); ERR_FAIL_NULL(canvas_item); - canvas_item->xform = p_transform; + if (_interpolation_data.interpolation_enabled && canvas_item->interpolated) { + if (!canvas_item->on_interpolate_transform_list) { + _interpolation_data.canvas_item_transform_update_list_curr->push_back(p_item); + canvas_item->on_interpolate_transform_list = true; + } else { + DEV_ASSERT(_interpolation_data.canvas_item_transform_update_list_curr->size() > 0); + } + } + + canvas_item->xform_curr = p_transform; } void RendererCanvasCull::canvas_item_set_visibility_layer(RID p_item, uint32_t p_visibility_layer) { @@ -1622,6 +1639,26 @@ bool RendererCanvasCull::canvas_item_get_debug_redraw() const { return debug_redraw; } +void RendererCanvasCull::canvas_item_set_interpolated(RID p_item, bool p_interpolated) { + Item *canvas_item = canvas_item_owner.get_or_null(p_item); + ERR_FAIL_NULL(canvas_item); + canvas_item->interpolated = p_interpolated; +} + +void RendererCanvasCull::canvas_item_reset_physics_interpolation(RID p_item) { + Item *canvas_item = canvas_item_owner.get_or_null(p_item); + ERR_FAIL_NULL(canvas_item); + canvas_item->xform_prev = canvas_item->xform_curr; +} + +// Useful especially for origin shifting. +void RendererCanvasCull::canvas_item_transform_physics_interpolation(RID p_item, const Transform2D &p_transform) { + Item *canvas_item = canvas_item_owner.get_or_null(p_item); + ERR_FAIL_NULL(canvas_item); + canvas_item->xform_prev = p_transform * canvas_item->xform_prev; + canvas_item->xform_curr = p_transform * canvas_item->xform_curr; +} + void RendererCanvasCull::canvas_item_set_canvas_group_mode(RID p_item, RS::CanvasGroupMode p_mode, float p_clear_margin, bool p_fit_empty, float p_fit_margin, bool p_blur_mipmaps) { Item *canvas_item = canvas_item_owner.get_or_null(p_item); ERR_FAIL_NULL(canvas_item); @@ -1720,7 +1757,16 @@ void RendererCanvasCull::canvas_light_set_transform(RID p_light, const Transform RendererCanvasRender::Light *clight = canvas_light_owner.get_or_null(p_light); ERR_FAIL_NULL(clight); - clight->xform = p_transform; + if (_interpolation_data.interpolation_enabled && clight->interpolated) { + if (!clight->on_interpolate_transform_list) { + _interpolation_data.canvas_light_transform_update_list_curr->push_back(p_light); + clight->on_interpolate_transform_list = true; + } else { + DEV_ASSERT(_interpolation_data.canvas_light_transform_update_list_curr->size() > 0); + } + } + + clight->xform_curr = p_transform; } void RendererCanvasCull::canvas_light_set_texture(RID p_light, RID p_texture) { @@ -1839,6 +1885,25 @@ void RendererCanvasCull::canvas_light_set_shadow_smooth(RID p_light, float p_smo clight->shadow_smooth = p_smooth; } +void RendererCanvasCull::canvas_light_set_interpolated(RID p_light, bool p_interpolated) { + RendererCanvasRender::Light *clight = canvas_light_owner.get_or_null(p_light); + ERR_FAIL_NULL(clight); + clight->interpolated = p_interpolated; +} + +void RendererCanvasCull::canvas_light_reset_physics_interpolation(RID p_light) { + RendererCanvasRender::Light *clight = canvas_light_owner.get_or_null(p_light); + ERR_FAIL_NULL(clight); + clight->xform_prev = clight->xform_curr; +} + +void RendererCanvasCull::canvas_light_transform_physics_interpolation(RID p_light, const Transform2D &p_transform) { + RendererCanvasRender::Light *clight = canvas_light_owner.get_or_null(p_light); + ERR_FAIL_NULL(clight); + clight->xform_prev = p_transform * clight->xform_prev; + clight->xform_curr = p_transform * clight->xform_curr; +} + RID RendererCanvasCull::canvas_light_occluder_allocate() { return canvas_light_occluder_owner.allocate_rid(); } @@ -1911,7 +1976,16 @@ void RendererCanvasCull::canvas_light_occluder_set_transform(RID p_occluder, con RendererCanvasRender::LightOccluderInstance *occluder = canvas_light_occluder_owner.get_or_null(p_occluder); ERR_FAIL_NULL(occluder); - occluder->xform = p_xform; + if (_interpolation_data.interpolation_enabled && occluder->interpolated) { + if (!occluder->on_interpolate_transform_list) { + _interpolation_data.canvas_light_occluder_transform_update_list_curr->push_back(p_occluder); + occluder->on_interpolate_transform_list = true; + } else { + DEV_ASSERT(_interpolation_data.canvas_light_occluder_transform_update_list_curr->size() > 0); + } + } + + occluder->xform_curr = p_xform; } void RendererCanvasCull::canvas_light_occluder_set_light_mask(RID p_occluder, int p_mask) { @@ -1921,6 +1995,25 @@ void RendererCanvasCull::canvas_light_occluder_set_light_mask(RID p_occluder, in occluder->light_mask = p_mask; } +void RendererCanvasCull::canvas_light_occluder_set_interpolated(RID p_occluder, bool p_interpolated) { + RendererCanvasRender::LightOccluderInstance *occluder = canvas_light_occluder_owner.get_or_null(p_occluder); + ERR_FAIL_NULL(occluder); + occluder->interpolated = p_interpolated; +} + +void RendererCanvasCull::canvas_light_occluder_reset_physics_interpolation(RID p_occluder) { + RendererCanvasRender::LightOccluderInstance *occluder = canvas_light_occluder_owner.get_or_null(p_occluder); + ERR_FAIL_NULL(occluder); + occluder->xform_prev = occluder->xform_curr; +} + +void RendererCanvasCull::canvas_light_occluder_transform_physics_interpolation(RID p_occluder, const Transform2D &p_transform) { + RendererCanvasRender::LightOccluderInstance *occluder = canvas_light_occluder_owner.get_or_null(p_occluder); + ERR_FAIL_NULL(occluder); + occluder->xform_prev = p_transform * occluder->xform_prev; + occluder->xform_curr = p_transform * occluder->xform_curr; +} + RID RendererCanvasCull::canvas_occluder_polygon_allocate() { return canvas_light_occluder_polygon_owner.allocate_rid(); } @@ -2075,6 +2168,7 @@ bool RendererCanvasCull::free(RID p_rid) { } else if (canvas_item_owner.owns(p_rid)) { Item *canvas_item = canvas_item_owner.get_or_null(p_rid); ERR_FAIL_NULL_V(canvas_item, true); + _interpolation_data.notify_free_canvas_item(p_rid, *canvas_item); if (canvas_item->parent.is_valid()) { if (canvas_owner.owns(canvas_item->parent)) { @@ -2114,6 +2208,7 @@ bool RendererCanvasCull::free(RID p_rid) { } else if (canvas_light_owner.owns(p_rid)) { RendererCanvasRender::Light *canvas_light = canvas_light_owner.get_or_null(p_rid); ERR_FAIL_NULL_V(canvas_light, true); + _interpolation_data.notify_free_canvas_light(p_rid, *canvas_light); if (canvas_light->canvas.is_valid()) { Canvas *canvas = canvas_owner.get_or_null(canvas_light->canvas); @@ -2129,6 +2224,7 @@ bool RendererCanvasCull::free(RID p_rid) { } else if (canvas_light_occluder_owner.owns(p_rid)) { RendererCanvasRender::LightOccluderInstance *occluder = canvas_light_occluder_owner.get_or_null(p_rid); ERR_FAIL_NULL_V(occluder, true); + _interpolation_data.notify_free_canvas_light_occluder(p_rid, *occluder); if (occluder->polygon.is_valid()) { LightOccluderPolygon *occluder_poly = canvas_light_occluder_polygon_owner.get_or_null(occluder->polygon); @@ -2186,6 +2282,81 @@ void RendererCanvasCull::finalize() { _free_rids(canvas_light_occluder_polygon_owner, "CanvasLightOccluderPolygon"); } +void RendererCanvasCull::tick() { + if (_interpolation_data.interpolation_enabled) { + update_interpolation_tick(true); + } +} + +void RendererCanvasCull::update_interpolation_tick(bool p_process) { +#define GODOT_UPDATE_INTERPOLATION_TICK(m_list_prev, m_list_curr, m_type, m_owner_list) \ + /* Detect any that were on the previous transform list that are no longer active. */ \ + for (unsigned int n = 0; n < _interpolation_data.m_list_prev->size(); n++) { \ + const RID &rid = (*_interpolation_data.m_list_prev)[n]; \ + m_type *item = m_owner_list.get_or_null(rid); \ + /* no longer active? (either the instance deleted or no longer being transformed) */ \ + if (item && !item->on_interpolate_transform_list) { \ + item->xform_prev = item->xform_curr; \ + } \ + } \ + /* and now for any in the transform list (being actively interpolated), */ \ + /* keep the previous transform value up to date and ready for next tick */ \ + if (p_process) { \ + for (unsigned int n = 0; n < _interpolation_data.m_list_curr->size(); n++) { \ + const RID &rid = (*_interpolation_data.m_list_curr)[n]; \ + m_type *item = m_owner_list.get_or_null(rid); \ + if (item) { \ + item->xform_prev = item->xform_curr; \ + item->on_interpolate_transform_list = false; \ + } \ + } \ + } \ + SWAP(_interpolation_data.m_list_curr, _interpolation_data.m_list_prev); \ + _interpolation_data.m_list_curr->clear(); + + GODOT_UPDATE_INTERPOLATION_TICK(canvas_item_transform_update_list_prev, canvas_item_transform_update_list_curr, Item, canvas_item_owner); + GODOT_UPDATE_INTERPOLATION_TICK(canvas_light_transform_update_list_prev, canvas_light_transform_update_list_curr, RendererCanvasRender::Light, canvas_light_owner); + GODOT_UPDATE_INTERPOLATION_TICK(canvas_light_occluder_transform_update_list_prev, canvas_light_occluder_transform_update_list_curr, RendererCanvasRender::LightOccluderInstance, canvas_light_occluder_owner); + +#undef GODOT_UPDATE_INTERPOLATION_TICK +} + +void RendererCanvasCull::InterpolationData::notify_free_canvas_item(RID p_rid, RendererCanvasCull::Item &r_canvas_item) { + r_canvas_item.on_interpolate_transform_list = false; + + if (!interpolation_enabled) { + return; + } + + // If the instance was on any of the lists, remove. + canvas_item_transform_update_list_curr->erase_multiple_unordered(p_rid); + canvas_item_transform_update_list_prev->erase_multiple_unordered(p_rid); +} + +void RendererCanvasCull::InterpolationData::notify_free_canvas_light(RID p_rid, RendererCanvasRender::Light &r_canvas_light) { + r_canvas_light.on_interpolate_transform_list = false; + + if (!interpolation_enabled) { + return; + } + + // If the instance was on any of the lists, remove. + canvas_light_transform_update_list_curr->erase_multiple_unordered(p_rid); + canvas_light_transform_update_list_prev->erase_multiple_unordered(p_rid); +} + +void RendererCanvasCull::InterpolationData::notify_free_canvas_light_occluder(RID p_rid, RendererCanvasRender::LightOccluderInstance &r_canvas_light_occluder) { + r_canvas_light_occluder.on_interpolate_transform_list = false; + + if (!interpolation_enabled) { + return; + } + + // If the instance was on any of the lists, remove. + canvas_light_occluder_transform_update_list_curr->erase_multiple_unordered(p_rid); + canvas_light_occluder_transform_update_list_prev->erase_multiple_unordered(p_rid); +} + RendererCanvasCull::RendererCanvasCull() { z_list = (RendererCanvasRender::Item **)memalloc(z_range * sizeof(RendererCanvasRender::Item *)); z_last_list = (RendererCanvasRender::Item **)memalloc(z_range * sizeof(RendererCanvasRender::Item *)); diff --git a/servers/rendering/renderer_canvas_cull.h b/servers/rendering/renderer_canvas_cull.h index d733a7bed2..961506ca28 100644 --- a/servers/rendering/renderer_canvas_cull.h +++ b/servers/rendering/renderer_canvas_cull.h @@ -271,6 +271,10 @@ public: void canvas_item_set_debug_redraw(bool p_enabled); bool canvas_item_get_debug_redraw() const; + void canvas_item_set_interpolated(RID p_item, bool p_interpolated); + void canvas_item_reset_physics_interpolation(RID p_item); + void canvas_item_transform_physics_interpolation(RID p_item, const Transform2D &p_transform); + RID canvas_light_allocate(); void canvas_light_initialize(RID p_rid); @@ -297,6 +301,10 @@ public: void canvas_light_set_shadow_color(RID p_light, const Color &p_color); void canvas_light_set_shadow_smooth(RID p_light, float p_smooth); + void canvas_light_set_interpolated(RID p_light, bool p_interpolated); + void canvas_light_reset_physics_interpolation(RID p_light); + void canvas_light_transform_physics_interpolation(RID p_light, const Transform2D &p_transform); + RID canvas_light_occluder_allocate(); void canvas_light_occluder_initialize(RID p_rid); @@ -307,6 +315,10 @@ public: void canvas_light_occluder_set_transform(RID p_occluder, const Transform2D &p_xform); void canvas_light_occluder_set_light_mask(RID p_occluder, int p_mask); + void canvas_light_occluder_set_interpolated(RID p_occluder, bool p_interpolated); + void canvas_light_occluder_reset_physics_interpolation(RID p_occluder); + void canvas_light_occluder_transform_physics_interpolation(RID p_occluder, const Transform2D &p_transform); + RID canvas_occluder_polygon_allocate(); void canvas_occluder_polygon_initialize(RID p_rid); @@ -336,6 +348,32 @@ public: void finalize(); + /* INTERPOLATION */ + + void tick(); + void update_interpolation_tick(bool p_process = true); + void set_physics_interpolation_enabled(bool p_enabled) { _interpolation_data.interpolation_enabled = p_enabled; } + + struct InterpolationData { + void notify_free_canvas_item(RID p_rid, RendererCanvasCull::Item &r_canvas_item); + void notify_free_canvas_light(RID p_rid, RendererCanvasRender::Light &r_canvas_light); + void notify_free_canvas_light_occluder(RID p_rid, RendererCanvasRender::LightOccluderInstance &r_canvas_light_occluder); + + LocalVector<RID> canvas_item_transform_update_lists[2]; + LocalVector<RID> *canvas_item_transform_update_list_curr = &canvas_item_transform_update_lists[0]; + LocalVector<RID> *canvas_item_transform_update_list_prev = &canvas_item_transform_update_lists[1]; + + LocalVector<RID> canvas_light_transform_update_lists[2]; + LocalVector<RID> *canvas_light_transform_update_list_curr = &canvas_light_transform_update_lists[0]; + LocalVector<RID> *canvas_light_transform_update_list_prev = &canvas_light_transform_update_lists[1]; + + LocalVector<RID> canvas_light_occluder_transform_update_lists[2]; + LocalVector<RID> *canvas_light_occluder_transform_update_list_curr = &canvas_light_occluder_transform_update_lists[0]; + LocalVector<RID> *canvas_light_occluder_transform_update_list_prev = &canvas_light_occluder_transform_update_lists[1]; + + bool interpolation_enabled = false; + } _interpolation_data; + RendererCanvasCull(); ~RendererCanvasCull(); }; diff --git a/servers/rendering/renderer_canvas_render.h b/servers/rendering/renderer_canvas_render.h index cf8f6dcb2e..4a56548932 100644 --- a/servers/rendering/renderer_canvas_render.h +++ b/servers/rendering/renderer_canvas_render.h @@ -51,9 +51,12 @@ public: }; struct Light { - bool enabled; + bool enabled : 1; + bool on_interpolate_transform_list : 1; + bool interpolated : 1; Color color; - Transform2D xform; + Transform2D xform_curr; + Transform2D xform_prev; float height; float energy; float scale; @@ -97,6 +100,8 @@ public: Light() { version = 0; enabled = true; + on_interpolate_transform_list = false; + interpolated = true; color = Color(1, 1, 1); shadow_color = Color(0, 0, 0, 0); height = 0; @@ -307,11 +312,17 @@ public: Rect2 rect; }; - Transform2D xform; - bool clip; - bool visible; - bool behind; - bool update_when_visible; + // For interpolation we store the current local xform, + // and the previous xform from the previous tick. + Transform2D xform_curr; + Transform2D xform_prev; + + bool clip : 1; + bool visible : 1; + bool behind : 1; + bool update_when_visible : 1; + bool on_interpolate_transform_list : 1; + bool interpolated : 1; struct CanvasGroup { RS::CanvasGroupMode mode; @@ -472,6 +483,8 @@ public: texture_filter = RS::CANVAS_ITEM_TEXTURE_FILTER_DEFAULT; texture_repeat = RS::CANVAS_ITEM_TEXTURE_REPEAT_DEFAULT; repeat_source = false; + on_interpolate_transform_list = false; + interpolated = true; } virtual ~Item() { clear(); @@ -487,12 +500,15 @@ public: virtual void canvas_render_items(RID p_to_render_target, Item *p_item_list, const Color &p_modulate, Light *p_light_list, Light *p_directional_list, const Transform2D &p_canvas_transform, RS::CanvasItemTextureFilter p_default_filter, RS::CanvasItemTextureRepeat p_default_repeat, bool p_snap_2d_vertices_to_pixel, bool &r_sdf_used, RenderingMethod::RenderInfo *r_render_info = nullptr) = 0; struct LightOccluderInstance { - bool enabled; + bool enabled : 1; + bool on_interpolate_transform_list : 1; + bool interpolated : 1; RID canvas; RID polygon; RID occluder; Rect2 aabb_cache; - Transform2D xform; + Transform2D xform_curr; + Transform2D xform_prev; Transform2D xform_cache; int light_mask; bool sdf_collision; @@ -502,6 +518,8 @@ public: LightOccluderInstance() { enabled = true; + on_interpolate_transform_list = false; + interpolated = false; sdf_collision = false; next = nullptr; light_mask = 1; diff --git a/servers/rendering/renderer_rd/environment/fog.cpp b/servers/rendering/renderer_rd/environment/fog.cpp index 1d144bedcf..48537a97d9 100644 --- a/servers/rendering/renderer_rd/environment/fog.cpp +++ b/servers/rendering/renderer_rd/environment/fog.cpp @@ -509,9 +509,7 @@ Vector3i Fog::_point_get_position_in_froxel_volume(const Vector3 &p_point, float fog_position.z = Math::pow(float(fog_position.z), float(1.0 / volumetric_fog_detail_spread)); fog_position = fog_position * fog_size - Vector3(0.5, 0.5, 0.5); - fog_position.x = CLAMP(fog_position.x, 0.0, fog_size.x); - fog_position.y = CLAMP(fog_position.y, 0.0, fog_size.y); - fog_position.z = CLAMP(fog_position.z, 0.0, fog_size.z); + fog_position = fog_position.clamp(Vector3(), fog_size); return Vector3i(fog_position); } @@ -680,8 +678,8 @@ void Fog::volumetric_fog_update(const VolumetricFogSettings &p_settings, const P max = Vector3i(1, 1, 1); for (int j = 0; j < 8; j++) { - min = Vector3i(MIN(min.x, points[j].x), MIN(min.y, points[j].y), MIN(min.z, points[j].z)); - max = Vector3i(MAX(max.x, points[j].x), MAX(max.y, points[j].y), MAX(max.z, points[j].z)); + min = min.min(points[j]); + max = max.max(points[j]); } kernel_size = max - min; diff --git a/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp index ac93aca6bb..2f307c62f3 100644 --- a/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp +++ b/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp @@ -2835,6 +2835,11 @@ RenderForwardMobile::~RenderForwardMobile() { for (const RID &rid : scene_state.uniform_buffers) { RD::get_singleton()->free(rid); } + for (uint32_t i = 0; i < RENDER_LIST_MAX; i++) { + if (scene_state.instance_buffer[i].is_valid()) { + RD::get_singleton()->free(scene_state.instance_buffer[i]); + } + } RD::get_singleton()->free(scene_state.lightmap_buffer); RD::get_singleton()->free(scene_state.lightmap_capture_buffer); memdelete_arr(scene_state.lightmap_captures); diff --git a/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp b/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp index 673afc53e5..6f56711151 100644 --- a/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp @@ -34,6 +34,7 @@ #include "core/math/geometry_2d.h" #include "core/math/math_defs.h" #include "core/math/math_funcs.h" +#include "core/math/transform_interpolator.h" #include "renderer_compositor_rd.h" #include "servers/rendering/renderer_rd/storage_rd/material_storage.h" #include "servers/rendering/renderer_rd/storage_rd/particles_storage.h" @@ -427,7 +428,7 @@ void RendererCanvasRenderRD::_render_item(RD::DrawListID p_draw_list, RID p_rend Transform2D base_transform = p_canvas_transform_inverse * p_item->final_transform; if (p_offset.x || p_offset.y) { - base_transform *= Transform2D(0, p_offset / p_item->xform.get_scale()); + base_transform *= Transform2D(0, p_offset / p_item->xform_curr.get_scale()); // TODO: Interpolate or explain why not needed. } Transform2D draw_transform; @@ -1366,7 +1367,15 @@ void RendererCanvasRenderRD::canvas_render_items(RID p_to_render_target, Item *p ERR_CONTINUE(!clight); } - Vector2 canvas_light_pos = p_canvas_transform.xform(l->xform.get_origin()); //convert light position to canvas coordinates, as all computation is done in canvas coords to avoid precision loss + Transform2D final_xform; + if (!RSG::canvas->_interpolation_data.interpolation_enabled || !l->interpolated) { + final_xform = l->xform_curr; + } else { + real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); + TransformInterpolator::interpolate_transform_2d(l->xform_prev, l->xform_curr, final_xform, f); + } + // Convert light position to canvas coordinates, as all computation is done in canvas coordinates to avoid precision loss. + Vector2 canvas_light_pos = p_canvas_transform.xform(final_xform.get_origin()); state.light_uniforms[index].position[0] = canvas_light_pos.x; state.light_uniforms[index].position[1] = canvas_light_pos.y; diff --git a/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp b/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp index 8dc74820e2..c5d74d395f 100644 --- a/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp +++ b/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp @@ -128,6 +128,13 @@ void RenderSceneBuffersRD::cleanup() { free_named_texture(E.value); } named_textures.clear(); + + // Clear weight_buffer / blur textures. + for (const WeightBuffers &weight_buffer : weight_buffers) { + if (weight_buffer.weight.is_valid()) { + RD::get_singleton()->free(weight_buffer.weight); + } + } } void RenderSceneBuffersRD::configure(const RenderSceneBuffersConfiguration *p_config) { diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp index dd94982f1a..f3ce432495 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp @@ -3637,8 +3637,7 @@ void TextureStorage::_render_target_allocate_sdf(RenderTarget *rt) { } rt->process_size = size * scale / 100; - rt->process_size.x = MAX(rt->process_size.x, 1); - rt->process_size.y = MAX(rt->process_size.y, 1); + rt->process_size = rt->process_size.max(Size2i(1, 1)); tformat.format = RD::DATA_FORMAT_R16G16_SINT; tformat.width = rt->process_size.width; diff --git a/servers/rendering/renderer_scene_occlusion_cull.h b/servers/rendering/renderer_scene_occlusion_cull.h index 565b393094..149d7b1cdb 100644 --- a/servers/rendering/renderer_scene_occlusion_cull.h +++ b/servers/rendering/renderer_scene_occlusion_cull.h @@ -65,7 +65,7 @@ public: return false; } - Vector3 closest_point = Vector3(CLAMP(p_cam_position.x, p_bounds[0], p_bounds[3]), CLAMP(p_cam_position.y, p_bounds[1], p_bounds[4]), CLAMP(p_cam_position.z, p_bounds[2], p_bounds[5])); + Vector3 closest_point = p_cam_position.clamp(Vector3(p_bounds[0], p_bounds[1], p_bounds[2]), Vector3(p_bounds[3], p_bounds[4], p_bounds[5])); if (closest_point == p_cam_position) { return false; diff --git a/servers/rendering/renderer_viewport.cpp b/servers/rendering/renderer_viewport.cpp index baa198626c..bafabf16b8 100644 --- a/servers/rendering/renderer_viewport.cpp +++ b/servers/rendering/renderer_viewport.cpp @@ -31,6 +31,7 @@ #include "renderer_viewport.h" #include "core/config/project_settings.h" +#include "core/math/transform_interpolator.h" #include "core/object/worker_thread_pool.h" #include "renderer_canvas_cull.h" #include "renderer_scene_cull.h" @@ -339,7 +340,14 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { if (!F->enabled) { continue; } - F->xform_cache = xf * F->xform; + + if (!RSG::canvas->_interpolation_data.interpolation_enabled || !F->interpolated) { + F->xform_cache = xf * F->xform_curr; + } else { + real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); + TransformInterpolator::interpolate_transform_2d(F->xform_prev, F->xform_curr, F->xform_cache, f); + F->xform_cache = xf * F->xform_cache; + } if (sdf_rect.intersects_transformed(F->xform_cache, F->aabb_cache)) { F->next = occluders; @@ -378,7 +386,14 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { Vector2 offset = tsize / 2.0; cl->rect_cache = Rect2(-offset + cl->texture_offset, tsize); - cl->xform_cache = xf * cl->xform; + + if (!RSG::canvas->_interpolation_data.interpolation_enabled || !cl->interpolated) { + cl->xform_cache = xf * cl->xform_curr; + } else { + real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); + TransformInterpolator::interpolate_transform_2d(cl->xform_prev, cl->xform_curr, cl->xform_cache, f); + cl->xform_cache = xf * cl->xform_cache; + } if (clip_rect.intersects_transformed(cl->xform_cache, cl->rect_cache)) { cl->filter_next_ptr = lights; @@ -386,7 +401,7 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { Transform2D scale; scale.scale(cl->rect_cache.size); scale.columns[2] = cl->rect_cache.position; - cl->light_shader_xform = xf * cl->xform * scale; + cl->light_shader_xform = cl->xform_cache * scale; if (cl->use_shadow) { cl->shadows_next_ptr = lights_with_shadow; if (lights_with_shadow == nullptr) { @@ -406,7 +421,13 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { if (cl->enabled) { cl->filter_next_ptr = directional_lights; directional_lights = cl; - cl->xform_cache = xf * cl->xform; + if (!RSG::canvas->_interpolation_data.interpolation_enabled || !cl->interpolated) { + cl->xform_cache = xf * cl->xform_curr; + } else { + real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); + TransformInterpolator::interpolate_transform_2d(cl->xform_prev, cl->xform_curr, cl->xform_cache, f); + cl->xform_cache = xf * cl->xform_cache; + } cl->xform_cache.columns[2] = Vector2(); //translation is pointless if (cl->use_shadow) { cl->shadows_next_ptr = directional_lights_with_shadow; @@ -441,7 +462,13 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { if (!F->enabled) { continue; } - F->xform_cache = xf * F->xform; + if (!RSG::canvas->_interpolation_data.interpolation_enabled || !F->interpolated) { + F->xform_cache = xf * F->xform_curr; + } else { + real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); + TransformInterpolator::interpolate_transform_2d(F->xform_prev, F->xform_curr, F->xform_cache, f); + F->xform_cache = xf * F->xform_cache; + } if (shadow_rect.intersects_transformed(F->xform_cache, F->aabb_cache)) { F->next = occluders; occluders = F; @@ -521,7 +548,13 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { if (!F->enabled) { continue; } - F->xform_cache = xf * F->xform; + if (!RSG::canvas->_interpolation_data.interpolation_enabled || !F->interpolated) { + F->xform_cache = xf * F->xform_curr; + } else { + real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); + TransformInterpolator::interpolate_transform_2d(F->xform_prev, F->xform_curr, F->xform_cache, f); + F->xform_cache = xf * F->xform_cache; + } Transform2D localizer = F->xform_cache.affine_inverse(); for (int j = 0; j < point_count; j++) { diff --git a/servers/rendering/rendering_server_default.cpp b/servers/rendering/rendering_server_default.cpp index b4c539dcff..f614070f68 100644 --- a/servers/rendering/rendering_server_default.cpp +++ b/servers/rendering/rendering_server_default.cpp @@ -370,6 +370,16 @@ void RenderingServerDefault::_thread_loop() { _finish(); } +/* INTERPOLATION */ + +void RenderingServerDefault::tick() { + RSG::canvas->tick(); +} + +void RenderingServerDefault::set_physics_interpolation_enabled(bool p_enabled) { + RSG::canvas->set_physics_interpolation_enabled(p_enabled); +} + /* EVENT QUEUING */ void RenderingServerDefault::sync() { diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h index 99fd683e1d..139624c777 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -926,6 +926,10 @@ public: FUNC1(canvas_item_set_debug_redraw, bool) FUNC0RC(bool, canvas_item_get_debug_redraw) + FUNC2(canvas_item_set_interpolated, RID, bool) + FUNC1(canvas_item_reset_physics_interpolation, RID) + FUNC2(canvas_item_transform_physics_interpolation, RID, const Transform2D &) + FUNCRIDSPLIT(canvas_light) FUNC2(canvas_light_set_mode, RID, CanvasLightMode) @@ -952,6 +956,10 @@ public: FUNC2(canvas_light_set_shadow_color, RID, const Color &) FUNC2(canvas_light_set_shadow_smooth, RID, float) + FUNC2(canvas_light_set_interpolated, RID, bool) + FUNC1(canvas_light_reset_physics_interpolation, RID) + FUNC2(canvas_light_transform_physics_interpolation, RID, const Transform2D &) + FUNCRIDSPLIT(canvas_light_occluder) FUNC2(canvas_light_occluder_attach_to_canvas, RID, RID) FUNC2(canvas_light_occluder_set_enabled, RID, bool) @@ -960,6 +968,10 @@ public: FUNC2(canvas_light_occluder_set_transform, RID, const Transform2D &) FUNC2(canvas_light_occluder_set_light_mask, RID, int) + FUNC2(canvas_light_occluder_set_interpolated, RID, bool) + FUNC1(canvas_light_occluder_reset_physics_interpolation, RID) + FUNC2(canvas_light_occluder_transform_physics_interpolation, RID, const Transform2D &) + FUNCRIDSPLIT(canvas_occluder_polygon) FUNC3(canvas_occluder_polygon_set_shape, RID, const Vector<Vector2> &, bool) @@ -1021,6 +1033,11 @@ public: } } + /* INTERPOLATION */ + + virtual void tick() override; + virtual void set_physics_interpolation_enabled(bool p_enabled) override; + /* EVENT QUEUING */ virtual void request_frame_drawn_callback(const Callable &p_callable) override; diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index 1ed0424839..994f6ad8b4 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -3219,6 +3219,9 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("canvas_item_set_modulate", "item", "color"), &RenderingServer::canvas_item_set_modulate); ClassDB::bind_method(D_METHOD("canvas_item_set_self_modulate", "item", "color"), &RenderingServer::canvas_item_set_self_modulate); ClassDB::bind_method(D_METHOD("canvas_item_set_draw_behind_parent", "item", "enabled"), &RenderingServer::canvas_item_set_draw_behind_parent); + ClassDB::bind_method(D_METHOD("canvas_item_set_interpolated", "item", "interpolated"), &RenderingServer::canvas_item_set_interpolated); + ClassDB::bind_method(D_METHOD("canvas_item_reset_physics_interpolation", "item"), &RenderingServer::canvas_item_reset_physics_interpolation); + ClassDB::bind_method(D_METHOD("canvas_item_transform_physics_interpolation", "item", "transform"), &RenderingServer::canvas_item_transform_physics_interpolation); /* Primitives */ @@ -3302,6 +3305,9 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("canvas_light_set_shadow_color", "light", "color"), &RenderingServer::canvas_light_set_shadow_color); ClassDB::bind_method(D_METHOD("canvas_light_set_shadow_smooth", "light", "smooth"), &RenderingServer::canvas_light_set_shadow_smooth); ClassDB::bind_method(D_METHOD("canvas_light_set_blend_mode", "light", "mode"), &RenderingServer::canvas_light_set_blend_mode); + ClassDB::bind_method(D_METHOD("canvas_light_set_interpolated", "light", "interpolated"), &RenderingServer::canvas_light_set_interpolated); + ClassDB::bind_method(D_METHOD("canvas_light_reset_physics_interpolation", "light"), &RenderingServer::canvas_light_reset_physics_interpolation); + ClassDB::bind_method(D_METHOD("canvas_light_transform_physics_interpolation", "light", "transform"), &RenderingServer::canvas_light_transform_physics_interpolation); BIND_ENUM_CONSTANT(CANVAS_LIGHT_MODE_POINT); BIND_ENUM_CONSTANT(CANVAS_LIGHT_MODE_DIRECTIONAL); @@ -3324,6 +3330,9 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("canvas_light_occluder_set_as_sdf_collision", "occluder", "enable"), &RenderingServer::canvas_light_occluder_set_as_sdf_collision); ClassDB::bind_method(D_METHOD("canvas_light_occluder_set_transform", "occluder", "transform"), &RenderingServer::canvas_light_occluder_set_transform); ClassDB::bind_method(D_METHOD("canvas_light_occluder_set_light_mask", "occluder", "mask"), &RenderingServer::canvas_light_occluder_set_light_mask); + ClassDB::bind_method(D_METHOD("canvas_light_occluder_set_interpolated", "occluder", "interpolated"), &RenderingServer::canvas_light_occluder_set_interpolated); + ClassDB::bind_method(D_METHOD("canvas_light_occluder_reset_physics_interpolation", "occluder"), &RenderingServer::canvas_light_occluder_reset_physics_interpolation); + ClassDB::bind_method(D_METHOD("canvas_light_occluder_transform_physics_interpolation", "occluder", "transform"), &RenderingServer::canvas_light_occluder_transform_physics_interpolation); /* CANVAS LIGHT OCCLUDER POLYGON */ diff --git a/servers/rendering_server.h b/servers/rendering_server.h index a67ae0a66c..a3a77bc57b 100644 --- a/servers/rendering_server.h +++ b/servers/rendering_server.h @@ -1476,6 +1476,10 @@ public: virtual void canvas_item_set_debug_redraw(bool p_enabled) = 0; virtual bool canvas_item_get_debug_redraw() const = 0; + virtual void canvas_item_set_interpolated(RID p_item, bool p_interpolated) = 0; + virtual void canvas_item_reset_physics_interpolation(RID p_item) = 0; + virtual void canvas_item_transform_physics_interpolation(RID p_item, const Transform2D &p_transform) = 0; + /* CANVAS LIGHT */ virtual RID canvas_light_create() = 0; @@ -1523,6 +1527,10 @@ public: virtual void canvas_light_set_shadow_color(RID p_light, const Color &p_color) = 0; virtual void canvas_light_set_shadow_smooth(RID p_light, float p_smooth) = 0; + virtual void canvas_light_set_interpolated(RID p_light, bool p_interpolated) = 0; + virtual void canvas_light_reset_physics_interpolation(RID p_light) = 0; + virtual void canvas_light_transform_physics_interpolation(RID p_light, const Transform2D &p_transform) = 0; + /* CANVAS LIGHT OCCLUDER */ virtual RID canvas_light_occluder_create() = 0; @@ -1533,6 +1541,10 @@ public: virtual void canvas_light_occluder_set_transform(RID p_occluder, const Transform2D &p_xform) = 0; virtual void canvas_light_occluder_set_light_mask(RID p_occluder, int p_mask) = 0; + virtual void canvas_light_occluder_set_interpolated(RID p_occluder, bool p_interpolated) = 0; + virtual void canvas_light_occluder_reset_physics_interpolation(RID p_occluder) = 0; + virtual void canvas_light_occluder_transform_physics_interpolation(RID p_occluder, const Transform2D &p_transform) = 0; + /* CANVAS LIGHT OCCLUDER POLYGON */ virtual RID canvas_occluder_polygon_create() = 0; @@ -1604,6 +1616,11 @@ public: virtual void free(RID p_rid) = 0; // Free RIDs associated with the rendering server. + /* INTERPOLATION */ + + virtual void tick() = 0; + virtual void set_physics_interpolation_enabled(bool p_enabled) = 0; + /* EVENT QUEUING */ virtual void request_frame_drawn_callback(const Callable &p_callable) = 0; diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index da742d0183..027dcac92d 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -336,6 +336,14 @@ TEST_CASE("[String] Natural compare function test") { CHECK(a.naturalnocasecmp_to("img10.png") < 0); } +TEST_CASE("[String] File compare function test") { + String a = "_img2.png"; + + CHECK(a.nocasecmp_to("img10.png") > 0); + CHECK_MESSAGE(a.filenocasecmp_to("img10.png") < 0, "Should sort before letters."); + CHECK_MESSAGE(a.filenocasecmp_to(".img10.png") > 0, "Should sort after period."); +} + TEST_CASE("[String] hex_encode_buffer") { static const uint8_t u8str[] = { 0x45, 0xE3, 0x81, 0x8A, 0x8F, 0xE3 }; String s = String::hex_encode_buffer(u8str, 6); diff --git a/thirdparty/README.md b/thirdparty/README.md index d86c14cd14..67c92e03e4 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -540,7 +540,7 @@ File extracted from upstream release tarball: ## meshoptimizer - Upstream: https://github.com/zeux/meshoptimizer -- Version: git (c21d3be6ddf627f8ca852ba4b6db9903b0557858, 2023) +- Version: 0.20 (c21d3be6ddf627f8ca852ba4b6db9903b0557858, 2023) - License: MIT Files extracted from upstream repository: |