summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/object/object.cpp2
-rw-r--r--core/variant/variant_op.h24
-rw-r--r--doc/classes/@GlobalScope.xml4
-rw-r--r--doc/classes/Color.xml170
-rw-r--r--doc/classes/Curve2D.xml16
-rw-r--r--doc/classes/Curve3D.xml4
-rw-r--r--doc/classes/Dictionary.xml167
-rw-r--r--doc/classes/Object.xml16
-rw-r--r--doc/classes/RID.xml13
-rw-r--r--doc/classes/RichTextLabel.xml6
-rw-r--r--doc/classes/Vector2.xml4
-rw-r--r--doc/classes/Vector3.xml4
-rwxr-xr-xdoc/tools/make_rst.py319
-rw-r--r--drivers/gles3/shaders/canvas.glsl12
-rw-r--r--drivers/gles3/shaders/stdlib_inc.glsl19
-rw-r--r--editor/code_editor.cpp263
-rw-r--r--editor/create_dialog.cpp3
-rw-r--r--editor/editor_file_system.cpp57
-rw-r--r--editor/plugins/animation_player_editor_plugin.cpp4
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp16
-rw-r--r--editor/plugins/node_3d_editor_plugin.h2
-rw-r--r--editor/scene_tree_dock.cpp3
-rw-r--r--editor/shader_globals_editor.cpp43
-rw-r--r--modules/raycast/SCsub7
-rw-r--r--modules/raycast/config.py14
-rw-r--r--platform/linuxbsd/joypad_linux.cpp2
-rw-r--r--platform/windows/detect.py11
-rw-r--r--scene/2d/path_2d.cpp2
-rw-r--r--scene/3d/xr_nodes.cpp40
-rw-r--r--scene/3d/xr_nodes.h2
-rw-r--r--scene/animation/animation_player.cpp2
-rw-r--r--scene/debugger/scene_debugger.cpp24
-rw-r--r--scene/debugger/scene_debugger.h1
-rw-r--r--scene/gui/graph_node.cpp66
-rw-r--r--scene/gui/line_edit.cpp69
-rw-r--r--scene/gui/line_edit.h3
-rw-r--r--scene/gui/range.cpp2
-rw-r--r--scene/gui/rich_text_label.cpp31
-rw-r--r--scene/gui/rich_text_label.h2
-rw-r--r--scene/resources/curve.cpp309
-rw-r--r--scene/resources/curve.h15
-rw-r--r--tests/scene/test_curve.h5
42 files changed, 1101 insertions, 677 deletions
diff --git a/core/object/object.cpp b/core/object/object.cpp
index d27e0d7621..105f9560d6 100644
--- a/core/object/object.cpp
+++ b/core/object/object.cpp
@@ -1473,6 +1473,8 @@ void Object::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_indexed", "property_path"), &Object::_get_indexed_bind);
ClassDB::bind_method(D_METHOD("get_property_list"), &Object::_get_property_list_bind);
ClassDB::bind_method(D_METHOD("get_method_list"), &Object::_get_method_list_bind);
+ ClassDB::bind_method(D_METHOD("property_can_revert", "property"), &Object::property_can_revert);
+ ClassDB::bind_method(D_METHOD("property_get_revert", "property"), &Object::property_get_revert);
ClassDB::bind_method(D_METHOD("notification", "what", "reversed"), &Object::notification, DEFVAL(false));
ClassDB::bind_method(D_METHOD("to_string"), &Object::to_string);
ClassDB::bind_method(D_METHOD("get_instance_id"), &Object::get_instance_id);
diff --git a/core/variant/variant_op.h b/core/variant/variant_op.h
index ec1ce67445..34858540ec 100644
--- a/core/variant/variant_op.h
+++ b/core/variant/variant_op.h
@@ -890,10 +890,12 @@ public:
static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
const String &a = *VariantGetInternalPtr<String>::get_ptr(&p_left);
*r_ret = do_mod(a, &r_valid);
- r_valid = true;
}
static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
- *VariantGetInternalPtr<String>::get_ptr(r_ret) = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), nullptr);
+ bool valid = true;
+ String result = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), &valid);
+ ERR_FAIL_COND_MSG(!valid, result);
+ *VariantGetInternalPtr<String>::get_ptr(r_ret) = result;
}
static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
PtrToArg<String>::encode(do_mod(PtrToArg<String>::convert(left), nullptr), r_ret);
@@ -913,10 +915,12 @@ public:
static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
const String &a = *VariantGetInternalPtr<String>::get_ptr(&p_left);
*r_ret = do_mod(a, *VariantGetInternalPtr<Array>::get_ptr(&p_right), &r_valid);
- r_valid = true;
}
static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
- *VariantGetInternalPtr<String>::get_ptr(r_ret) = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), *VariantGetInternalPtr<Array>::get_ptr(right), nullptr);
+ bool valid = true;
+ String result = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), *VariantGetInternalPtr<Array>::get_ptr(right), &valid);
+ ERR_FAIL_COND_MSG(!valid, result);
+ *VariantGetInternalPtr<String>::get_ptr(r_ret) = result;
}
static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
PtrToArg<String>::encode(do_mod(PtrToArg<String>::convert(left), PtrToArg<Array>::convert(right), nullptr), r_ret);
@@ -939,10 +943,12 @@ public:
static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
const String &a = *VariantGetInternalPtr<String>::get_ptr(&p_left);
*r_ret = do_mod(a, p_right.get_validated_object(), &r_valid);
- r_valid = true;
}
static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
- *VariantGetInternalPtr<String>::get_ptr(r_ret) = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), right->get_validated_object(), nullptr);
+ bool valid = true;
+ String result = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), right->get_validated_object(), &valid);
+ ERR_FAIL_COND_MSG(!valid, result);
+ *VariantGetInternalPtr<String>::get_ptr(r_ret) = result;
}
static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
PtrToArg<String>::encode(do_mod(PtrToArg<String>::convert(left), PtrToArg<Object *>::convert(right), nullptr), r_ret);
@@ -965,10 +971,12 @@ public:
static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
const String &a = *VariantGetInternalPtr<String>::get_ptr(&p_left);
*r_ret = do_mod(a, *VariantGetInternalPtr<T>::get_ptr(&p_right), &r_valid);
- r_valid = true;
}
static inline void validated_evaluate(const Variant *left, const Variant *right, Variant *r_ret) {
- *VariantGetInternalPtr<String>::get_ptr(r_ret) = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), *VariantGetInternalPtr<T>::get_ptr(right), nullptr);
+ bool valid = true;
+ String result = do_mod(*VariantGetInternalPtr<String>::get_ptr(left), *VariantGetInternalPtr<T>::get_ptr(right), &valid);
+ ERR_FAIL_COND_MSG(!valid, result);
+ *VariantGetInternalPtr<String>::get_ptr(r_ret) = result;
}
static void ptr_evaluate(const void *left, const void *right, void *r_ret) {
PtrToArg<String>::encode(do_mod(PtrToArg<String>::convert(left), PtrToArg<T>::convert(right), nullptr), r_ret);
diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml
index 977bcb78e9..4a6b85f229 100644
--- a/doc/classes/@GlobalScope.xml
+++ b/doc/classes/@GlobalScope.xml
@@ -115,7 +115,7 @@
<param index="3" name="end" type="float" />
<param index="4" name="t" type="float" />
<description>
- Returns the derivative at the given [param t] on a one-dimensional [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bezier curve[/url] defined by the given [param control_1], [param control_2], and [param end] points.
+ Returns the derivative at the given [param t] on a one-dimensional [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bézier curve[/url] defined by the given [param control_1], [param control_2], and [param end] points.
</description>
</method>
<method name="bezier_interpolate">
@@ -126,7 +126,7 @@
<param index="3" name="end" type="float" />
<param index="4" name="t" type="float" />
<description>
- Returns the point at the given [param t] on a one-dimensional [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bezier curve[/url] defined by the given [param control_1], [param control_2], and [param end] points.
+ Returns the point at the given [param t] on a one-dimensional [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bézier curve[/url] defined by the given [param control_1], [param control_2], and [param end] points.
</description>
</method>
<method name="bytes_to_var">
diff --git a/doc/classes/Color.xml b/doc/classes/Color.xml
index 90fbdcc622..4d78433915 100644
--- a/doc/classes/Color.xml
+++ b/doc/classes/Color.xml
@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="Color" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
- Color in RGBA format using floats on the range of 0 to 1.
+ Color built-in type, in RGBA format.
</brief_description>
<description>
- A color represented by red, green, blue, and alpha (RGBA) components. The alpha component is often used for opacity. Values are in floating-point and usually range from 0 to 1. Some properties (such as CanvasItem.modulate) may accept values greater than 1 (overbright or HDR colors).
- You can also create a color from standardized color names by using the string constructor or directly using the color constants defined here. The standardized color set is based on the [url=https://en.wikipedia.org/wiki/X11_color_names]X11 color names[/url].
- If you want to supply values in a range of 0 to 255, you should use [method @GDScript.Color8].
+ A color represented in RGBA format by red ([member r]), green ([member g]), blue ([member b]), and alpha ([member a]) components. Each component is a 16-bit floating-point value, usually ranging from 0 to 1. Some properties (such as [member CanvasItem.modulate]) may support values greater than 1, for overbright or High Dynamic Range colors. If you want to supply values in a range of 0 to 255, you should use [method @GDScript.Color8].
+ Colors can also be created by name from a set of standardized colors, through the [String] constructor, [method from_string], or by directly fetching the color constants documented here. The standardized color set is based on the [url=https://en.wikipedia.org/wiki/X11_color_names]X11 color names[/url], with the addition of [constant TRANSPARENT].
[b]Note:[/b] In a boolean context, a Color will evaluate to [code]false[/code] if it's equal to [code]Color(0, 0, 0, 1)[/code] (opaque black). Otherwise, a Color will always evaluate to [code]true[/code].
[url=https://raw.githubusercontent.com/godotengine/godot-docs/master/img/color_constants.png]Color constants cheatsheet[/url]
</description>
@@ -19,7 +18,8 @@
<constructor name="Color">
<return type="Color" />
<description>
- Constructs a default-initialized [Color] with all components set to [code]0[/code].
+ Constructs a default [Color] from opaque black. This is the same as [constant BLACK].
+ [b]Note:[/b] in C#, constructs an empty color with all of its components set to [code]0.0[/code] (transparent black).
</description>
</constructor>
<constructor name="Color">
@@ -27,10 +27,10 @@
<param index="0" name="from" type="Color" />
<param index="1" name="alpha" type="float" />
<description>
- Constructs a [Color] from an existing color, but with a custom alpha value.
+ Constructs a [Color] from the existing color, with [member a] set to the given [param alpha] value.
[codeblocks]
[gdscript]
- var red = Color(Color.red, 0.2) # 20% opaque red.
+ var red = Color(Color.RED, 0.2) # 20% opaque red.
[/gdscript]
[csharp]
var red = new Color(Colors.Red, 0.2f); // 20% opaque red.
@@ -49,7 +49,7 @@
<return type="Color" />
<param index="0" name="code" type="String" />
<description>
- Constructs a [Color] either from an HTML color code or from a standardized color name. Supported color names are the same as the constants.
+ Constructs a [Color] either from an HTML color code or from a standardized color name. The supported color names are the same as the constants.
</description>
</constructor>
<constructor name="Color">
@@ -57,7 +57,7 @@
<param index="0" name="code" type="String" />
<param index="1" name="alpha" type="float" />
<description>
- Constructs a [Color] either from an HTML color code or from a standardized color name, with [param alpha] on the range of 0 to 1. Supported color names are the same as the constants.
+ Constructs a [Color] either from an HTML color code or from a standardized color name, with [param alpha] on the range of 0.0 to 1.0. The supported color names are the same as the constants.
</description>
</constructor>
<constructor name="Color">
@@ -66,7 +66,7 @@
<param index="1" name="g" type="float" />
<param index="2" name="b" type="float" />
<description>
- Constructs a [Color] from RGB values, typically between 0 and 1. Alpha will be 1.
+ Constructs a [Color] from RGB values, typically between 0.0 and 1.0. [member a] is set to 1.0.
[codeblocks]
[gdscript]
var color = Color(0.2, 1.0, 0.7) # Similar to `Color8(51, 255, 178, 255)`
@@ -84,7 +84,7 @@
<param index="2" name="b" type="float" />
<param index="3" name="a" type="float" />
<description>
- Constructs a [Color] from RGBA values, typically between 0 and 1.
+ Constructs a [Color] from RGBA values, typically between 0.0 and 1.0.
[codeblocks]
[gdscript]
var color = Color(0.2, 1.0, 0.7, 0.8) # Similar to `Color8(51, 255, 178, 204)`
@@ -101,7 +101,7 @@
<return type="Color" />
<param index="0" name="over" type="Color" />
<description>
- Returns a new color resulting from blending this color over another. If the color is opaque, the result is also opaque. The second color may have a range of alpha values.
+ Returns a new color resulting from overlaying this color over the given color. In a painting program, you can imagine it as the [param over] color painted over this colour (including alpha).
[codeblocks]
[gdscript]
var bg = Color(0.0, 1.0, 0.0, 0.5) # Green with alpha of 50%
@@ -128,7 +128,7 @@
<return type="Color" />
<param index="0" name="amount" type="float" />
<description>
- Returns a new color resulting from making this color darker by the specified percentage (ratio from 0 to 1).
+ Returns a new color resulting from making this color darker by the specified [param amount] (ratio from 0.0 to 1.0). See also [method lightened].
[codeblocks]
[gdscript]
var green = Color(0.0, 1.0, 0.0)
@@ -148,7 +148,7 @@
<param index="2" name="v" type="float" />
<param index="3" name="alpha" type="float" default="1.0" />
<description>
- Constructs a color from an [url=https://en.wikipedia.org/wiki/HSL_and_HSV]HSV profile[/url]. [param h] (hue), [param s] (saturation), and [param v] (value) are typically between 0 and 1.
+ Constructs a color from an [url=https://en.wikipedia.org/wiki/HSL_and_HSV]HSV profile[/url]. The hue ([param h]), saturation ([param s]), and value ([param v]) are typically between 0.0 and 1.0.
[codeblocks]
[gdscript]
var color = Color.from_hsv(0.58, 0.5, 0.79, 0.8)
@@ -166,7 +166,7 @@
<param index="2" name="l" type="float" />
<param index="3" name="alpha" type="float" default="1.0" />
<description>
- Constructs a color from an [url=https://bottosson.github.io/posts/colorpicker/]OK HSL profile[/url]. [param h] (hue), [param s] (saturation), and [param l] (lightness) are typically between 0 and 1.
+ Constructs a color from an [url=https://bottosson.github.io/posts/colorpicker/]OK HSL profile[/url]. The hue ([param h]), saturation ([param s]), and lightness ([param l]) are typically between 0.0 and 1.0.
[codeblocks]
[gdscript]
var color = Color.from_ok_hsl(0.58, 0.5, 0.79, 0.8)
@@ -189,14 +189,13 @@
<param index="0" name="str" type="String" />
<param index="1" name="default" type="Color" />
<description>
- Creates a [Color] from string, which can be either a HTML color code or a named color. Fallbacks to [param default] if the string does not denote any valid color.
+ Creates a [Color] from the given string, which can be either an HTML color code or a named color (case-insensitive). Returns [param default] if the color cannot be inferred from the string.
</description>
</method>
<method name="get_luminance" qualifiers="const">
<return type="float" />
<description>
- Returns the luminance of the color in the [code][0.0, 1.0][/code] range.
- This is useful when determining light or dark color. Colors with a luminance smaller than 0.5 can be generally considered dark.
+ Returns the light intensity of the color, as a value between 0.0 and 1.0 (inclusive). This is useful when determining light or dark color. Colors with a luminance smaller than 0.5 can be generally considered dark.
[b]Note:[/b] [method get_luminance] relies on the color being in the linear color space to return an accurate relative luminance value. If the color is in the sRGB color space, use [method srgb_to_linear] to convert it to the linear color space first.
</description>
</method>
@@ -204,9 +203,12 @@
<return type="Color" />
<param index="0" name="hex" type="int" />
<description>
- Returns the [Color] associated with the provided integer number, with 8 bits per channel in ARGB order. The integer should be 32-bit. Best used with hexadecimal notation.
+ Returns the [Color] associated with the provided [param hex] integer in 32-bit ARGB format (8 bits per channel, alpha channel first).
+ In GDScript and C#, the [int] is best visualized with hexadecimal notation ([code]"0x"[/code] prefix).
[codeblock]
- modulate = Color.hex(0xffff0000) # red
+ var red = Color.hex(0xffff0000)
+ var dark_cyan = Color.hex(0xff008b8b)
+ var my_color = Color.hex(0xa4bbefd2)
[/codeblock]
</description>
</method>
@@ -214,25 +216,27 @@
<return type="Color" />
<param index="0" name="hex" type="int" />
<description>
- Same as [method hex], but takes 64-bit integer and the color uses 16 bits per channel.
+ Returns the [Color] associated with the provided [param hex] integer in 64-bit ARGB format (16 bits per channel, alpha channel first).
+ In GDScript and C#, the [int] is best visualized with hexadecimal notation ([code]"0x"[/code] prefix).
</description>
</method>
<method name="html" qualifiers="static">
<return type="Color" />
<param index="0" name="rgba" type="String" />
<description>
- Returns a new color from [param rgba], an HTML hexadecimal color string. [param rgba] is not case sensitive, and may be prefixed with a '#' character.
- [param rgba] must be a valid three-digit or six-digit hexadecimal color string, and may contain an alpha channel value. If [param rgba] does not contain an alpha channel value, an alpha channel value of 1.0 is applied.
- If [param rgba] is invalid a Color(0.0, 0.0, 0.0, 1.0) is returned.
- [b]Note:[/b] This method is not implemented in C#, but the same functionality is provided in the class constructor.
+ Returns a new color from [param rgba], an HTML hexadecimal color string. [param rgba] is not case-sensitive, and may be prefixed by a hash sign ([code]#[/code]).
+ [param rgba] must be a valid three-digit or six-digit hexadecimal color string, and may contain an alpha channel value. If [param rgba] does not contain an alpha channel value, an alpha channel value of 1.0 is applied. If [param rgba] is invalid, returns an empty color.
+ [b]Note:[/b] In C#, this method is not implemented. The same functionality is provided by the Color constructor.
[codeblocks]
[gdscript]
- var green = Color.html("#00FF00FF") # set green to Color(0.0, 1.0, 0.0, 1.0)
- var blue = Color.html("#0000FF") # set blue to Color(0.0, 0.0, 1.0, 1.0)
+ var blue = Color.html("#0000ff") # blue is Color(0.0, 0.0, 1.0, 1.0)
+ var green = Color.html("#0F0") # green is Color(0.0, 1.0, 0.0, 1.0)
+ var col = Color.html("663399cc") # col is Color(0.4, 0.2, 0.6, 0.8)
[/gdscript]
[csharp]
- var green = new Color("#00FF00FF"); // set green to Color(0.0, 1.0, 0.0, 1.0)
- var blue = new Color("#0000FF"); // set blue to Color(0.0, 0.0, 1.0, 1.0)
+ var blue = new Color("#0000ff"); // blue is Color(0.0, 0.0, 1.0, 1.0)
+ var green = new Color("#0F0"); // green is Color(0.0, 1.0, 0.0, 1.0)
+ var col = new Color("663399cc"); // col is Color(0.4, 0.2, 0.6, 0.8)
[/csharp]
[/codeblocks]
</description>
@@ -241,24 +245,26 @@
<return type="bool" />
<param index="0" name="color" type="String" />
<description>
- Returns [code]true[/code] if [param color] is a valid HTML hexadecimal color string. [param color] is not case sensitive, and may be prefixed with a '#' character.
- For a string to be valid it must be three-digit or six-digit hexadecimal, and may contain an alpha channel value.
+ Returns [code]true[/code] if [param color] is a valid HTML hexadecimal color string. The string must be a hexadecimal value (case-insensitive) of either 3, 4, 6 or 8 digits, and may be prefixed by a hash sign ([code]#[/code]). This method is identical to [method String.is_valid_html_color].
[codeblocks]
[gdscript]
- var result = Color.html_is_valid("#55aaFF") # result is true
- result = Color.html_is_valid("#55AAFF20") # result is true
- result = Color.html_is_valid("55AAFF") # result is true
- result = Color.html_is_valid("#F2C") # result is true
- result = Color.html_is_valid("#AABBC) # result is false
- result = Color.html_is_valid("#55aaFF5") # result is false
+ Color.html_is_valid("#55aaFF") # Returns true
+ Color.html_is_valid("#55AAFF20") # Returns true
+ Color.html_is_valid("55AAFF") # Returns true
+ Color.html_is_valid("#F2C") # Returns true
+
+ Color.html_is_valid("#AABBC) # Returns false
+ Color.html_is_valid("#55aaFF5") # Returns false
[/gdscript]
[csharp]
- var result = Color.HtmlIsValid("#55AAFF"); // result is true
- result = Color.HtmlIsValid("#55AAFF20"); // result is true
- result = Color.HtmlIsValid("55AAFF); // result is true
- result = Color.HtmlIsValid("#F2C"); // result is true
- result = Color.HtmlIsValid("#AABBC"); // result is false
- result = Color.HtmlIsValid("#55aaFF5"); // result is false
+ // This method is not available in C#. Use `StringExtensions.IsValidHtmlColor()`, instead.
+ "#55AAFF".IsValidHtmlColor(); // Returns true
+ "#55AAFF20".IsValidHtmlColor(); // Returns true
+ "55AAFF".IsValidHtmlColor(); // Returns true
+ "#F2C".IsValidHtmlColor(); // Returns true
+
+ "#AABBC".IsValidHtmlColor(); // Returns false
+ "#55aaFF5".IsValidHtmlColor(); // Returns false
[/csharp]
[/codeblocks]
</description>
@@ -266,13 +272,15 @@
<method name="inverted" qualifiers="const">
<return type="Color" />
<description>
- Returns the inverted color [code](1 - r, 1 - g, 1 - b, a)[/code].
+ Returns the color with its [member r], [member g], and [member b] components inverted ([code](1 - r, 1 - g, 1 - b, a)[/code]).
[codeblocks]
[gdscript]
+ var black = Color.WHITE.inverted()
var color = Color(0.3, 0.4, 0.9)
var inverted_color = color.inverted() # Equivalent to `Color(0.7, 0.6, 0.1)`
[/gdscript]
[csharp]
+ var black = Colors.White.Inverted();
var color = new Color(0.3f, 0.4f, 0.9f);
Color invertedColor = color.Inverted(); // Equivalent to `new Color(0.7f, 0.6f, 0.1f)`
[/csharp]
@@ -291,17 +299,23 @@
<param index="0" name="to" type="Color" />
<param index="1" name="weight" type="float" />
<description>
- Returns the linear interpolation with another color. The interpolation factor [param weight] is between 0 and 1.
+ Returns the linear interpolation between this color's components and [param to]'s components. The interpolation factor [param weight] should be between 0.0 and 1.0 (inclusive). See also [method @GlobalScope.lerp].
[codeblocks]
[gdscript]
- var c1 = Color(1.0, 0.0, 0.0)
- var c2 = Color(0.0, 1.0, 0.0)
- var lerp_color = c1.lerp(c2, 0.5) # Equivalent to `Color(0.5, 0.5, 0.0)`
+ var red = Color(1.0, 0.0, 0.0)
+ var aqua = Color(0.0, 1.0, 0.8)
+
+ red.lerp(aqua, 0.2) # Returns Color(0.8, 0.2, 0.16)
+ red.lerp(aqua, 0.5) # Returns Color(0.5, 0.5, 0.4)
+ red.lerp(aqua, 1.0) # Returns Color(0.0, 1.0, 0.8)
[/gdscript]
[csharp]
- var c1 = new Color(1.0f, 0.0f, 0.0f);
- var c2 = new Color(0.0f, 1.0f, 0.0f);
- Color lerpColor = c1.Lerp(c2, 0.5f); // Equivalent to `new Color(0.5f, 0.5f, 0.0f)`
+ var red = new Color(1.0f, 0.0f, 0.0f);
+ var aqua = new Color(0.0f, 1.0f, 0.8f);
+
+ red.Lerp(aqua, 0.2f); // Returns Color(0.8f, 0.2f, 0.16f)
+ red.Lerp(aqua, 0.5f); // Returns Color(0.5f, 0.5f, 0.4f)
+ red.Lerp(aqua, 1.0f); // Returns Color(0.0f, 1.0f, 0.8f)
[/csharp]
[/codeblocks]
</description>
@@ -310,15 +324,15 @@
<return type="Color" />
<param index="0" name="amount" type="float" />
<description>
- Returns a new color resulting from making this color lighter by the specified percentage (ratio from 0 to 1).
+ Returns a new color resulting from making this color lighter by the specified [param amount], which should be a ratio from 0.0 to 1.0. See also [method darkened].
[codeblocks]
[gdscript]
var green = Color(0.0, 1.0, 0.0)
- var lightgreen = green.lightened(0.2) # 20% lighter than regular green
+ var light_green = green.lightened(0.2) # 20% lighter than regular green
[/gdscript]
[csharp]
var green = new Color(0.0f, 1.0f, 0.0f);
- Color lightgreen = green.Lightened(0.2f); // 20% lighter than regular green
+ Color lightGreen = green.Lightened(0.2f); // 20% lighter than regular green
[/csharp]
[/codeblocks]
</description>
@@ -326,19 +340,19 @@
<method name="linear_to_srgb" qualifiers="const">
<return type="Color" />
<description>
- Returns the color converted to the [url=https://en.wikipedia.org/wiki/SRGB]sRGB[/url] color space. This assumes the original color is in the linear color space. See also [method srgb_to_linear] which performs the opposite operation.
+ Returns the color converted to the [url=https://en.wikipedia.org/wiki/SRGB]sRGB[/url] color space. This method assumes the original color is in the linear color space. See also [method srgb_to_linear] which performs the opposite operation.
</description>
</method>
<method name="srgb_to_linear" qualifiers="const">
<return type="Color" />
<description>
- Returns the color converted to the linear color space. This assumes the original color is in the sRGB color space. See also [method linear_to_srgb] which performs the opposite operation.
+ Returns the color converted to the linear color space. This method assumes the original color already is in the sRGB color space. See also [method linear_to_srgb] which performs the opposite operation.
</description>
</method>
<method name="to_abgr32" qualifiers="const">
<return type="int" />
<description>
- Returns the color converted to a 32-bit integer in ABGR format (each byte represents a color channel). ABGR is the reversed version of the default format.
+ Returns the color converted to a 32-bit integer in ABGR format (each component is 8 bits). ABGR is the reversed version of the default RGBA format.
[codeblocks]
[gdscript]
var color = Color(1, 0.5, 0.2)
@@ -354,7 +368,7 @@
<method name="to_abgr64" qualifiers="const">
<return type="int" />
<description>
- Returns the color converted to a 64-bit integer in ABGR format (each word represents a color channel). ABGR is the reversed version of the default format.
+ Returns the color converted to a 64-bit integer in ABGR format (each component is 16 bits). ABGR is the reversed version of the default RGBA format.
[codeblocks]
[gdscript]
var color = Color(1, 0.5, 0.2)
@@ -370,7 +384,7 @@
<method name="to_argb32" qualifiers="const">
<return type="int" />
<description>
- Returns the color converted to a 32-bit integer in ARGB format (each byte represents a color channel). ARGB is more compatible with DirectX.
+ Returns the color converted to a 32-bit integer in ARGB format (each component is 8 bits). ARGB is more compatible with DirectX.
[codeblocks]
[gdscript]
var color = Color(1, 0.5, 0.2)
@@ -386,7 +400,7 @@
<method name="to_argb64" qualifiers="const">
<return type="int" />
<description>
- Returns the color converted to a 64-bit integer in ARGB format (each word represents a color channel). ARGB is more compatible with DirectX.
+ Returns the color converted to a 64-bit integer in ARGB format (each component is 16 bits). ARGB is more compatible with DirectX.
[codeblocks]
[gdscript]
var color = Color(1, 0.5, 0.2)
@@ -403,18 +417,18 @@
<return type="String" />
<param index="0" name="with_alpha" type="bool" default="true" />
<description>
- Returns the color converted to an HTML hexadecimal color string in RGBA format (ex: [code]ff34f822[/code]).
- Setting [param with_alpha] to [code]false[/code] excludes alpha from the hexadecimal string (and uses RGB instead of RGBA format).
+ Returns the color converted to an HTML hexadecimal color [String] in RGBA format, without the hash ([code]#[/code]) prefix.
+ Setting [param with_alpha] to [code]false[/code], excludes alpha from the hexadecimal string, using RGB format instead of RGBA format.
[codeblocks]
[gdscript]
- var color = Color(1, 1, 1, 0.5)
- var with_alpha = color.to_html() # Returns "ffffff7f"
- var without_alpha = color.to_html(false) # Returns "ffffff"
+ var white = Color(1, 1, 1, 0.5)
+ var with_alpha = white.to_html() # Returns "ffffff7f"
+ var without_alpha = white.to_html(false) # Returns "ffffff"
[/gdscript]
[csharp]
- var color = new Color(1, 1, 1, 0.5f);
- String withAlpha = color.ToHtml(); // Returns "ffffff7f"
- String withoutAlpha = color.ToHtml(false); // Returns "ffffff"
+ var white = new Color(1, 1, 1, 0.5f);
+ string withAlpha = white.ToHtml(); // Returns "ffffff7f"
+ string withoutAlpha = white.ToHtml(false); // Returns "ffffff"
[/csharp]
[/codeblocks]
</description>
@@ -422,7 +436,7 @@
<method name="to_rgba32" qualifiers="const">
<return type="int" />
<description>
- Returns the color converted to a 32-bit integer in RGBA format (each byte represents a color channel). RGBA is Godot's default format.
+ Returns the color converted to a 32-bit integer in RGBA format (each component is 8 bits). RGBA is Godot's default format.
[codeblocks]
[gdscript]
var color = Color(1, 0.5, 0.2)
@@ -438,7 +452,7 @@
<method name="to_rgba64" qualifiers="const">
<return type="int" />
<description>
- Returns the color converted to a 64-bit integer in RGBA format (each word represents a color channel). RGBA is Godot's default format.
+ Returns the color converted to a 64-bit integer in RGBA format (each component is 16 bits). RGBA is Godot's default format.
[codeblocks]
[gdscript]
var color = Color(1, 0.5, 0.2)
@@ -457,19 +471,19 @@
The color's alpha component, typically on the range of 0 to 1. A value of 0 means that the color is fully transparent. A value of 1 means that the color is fully opaque.
</member>
<member name="a8" type="int" setter="" getter="" default="255">
- Wrapper for [member a] that uses the range 0 to 255 instead of 0 to 1.
+ Wrapper for [member a] that uses the range 0 to 255, instead of 0 to 1.
</member>
<member name="b" type="float" setter="" getter="" default="0.0">
The color's blue component, typically on the range of 0 to 1.
</member>
<member name="b8" type="int" setter="" getter="" default="0">
- Wrapper for [member b] that uses the range 0 to 255 instead of 0 to 1.
+ Wrapper for [member b] that uses the range 0 to 255, instead of 0 to 1.
</member>
<member name="g" type="float" setter="" getter="" default="0.0">
The color's green component, typically on the range of 0 to 1.
</member>
<member name="g8" type="int" setter="" getter="" default="0">
- Wrapper for [member g] that uses the range 0 to 255 instead of 0 to 1.
+ Wrapper for [member g] that uses the range 0 to 255, instead of 0 to 1.
</member>
<member name="h" type="float" setter="" getter="" default="0.0">
The HSV hue of this color, on the range 0 to 1.
@@ -478,7 +492,7 @@
The color's red component, typically on the range of 0 to 1.
</member>
<member name="r8" type="int" setter="" getter="" default="0">
- Wrapper for [member r] that uses the range 0 to 255 instead of 0 to 1.
+ Wrapper for [member r] that uses the range 0 to 255, instead of 0 to 1.
</member>
<member name="s" type="float" setter="" getter="" default="0.0">
The HSV saturation of this color, on the range 0 to 1.
@@ -510,7 +524,7 @@
Bisque color.
</constant>
<constant name="BLACK" value="Color(0, 0, 0, 1)">
- Black color.
+ Black color. In GDScript, this is the default value of any color.
</constant>
<constant name="BLANCHED_ALMOND" value="Color(1, 0.921569, 0.803922, 1)">
Blanched almond color.
@@ -932,7 +946,7 @@
<return type="bool" />
<param index="0" name="right" type="Color" />
<description>
- Returns [code]true[/code] if the colors are not equal.
+ Returns [code]true[/code] if the colors are not exactly equal.
[b]Note:[/b] Due to floating-point precision errors, consider using [method is_equal_approx] instead, which is more reliable.
</description>
</operator>
@@ -1004,7 +1018,7 @@
<return type="float" />
<param index="0" name="index" type="int" />
<description>
- Access color components using their index. [code]c[0][/code] is equivalent to [code]c.r[/code], [code]c[1][/code] is equivalent to [code]c.g[/code], [code]c[2][/code] is equivalent to [code]c.b[/code], and [code]c[3][/code] is equivalent to [code]c.a[/code].
+ Access color components using their index. [code][0][/code] is equivalent to [member r], [code][1][/code] is equivalent to [member g], [code][2][/code] is equivalent to [member b], and [code][3][/code] is equivalent to [member a].
</description>
</operator>
<operator name="operator unary+">
@@ -1016,7 +1030,7 @@
<operator name="operator unary-">
<return type="Color" />
<description>
- Inverts the given color. This is equivalent to [code]Color.WHITE - c[/code] or [code]Color(1 - c.r, 1 - c.g, 1 - c.b, 1 - c.a)[/code].
+ Inverts the given color. This is equivalent to [code]Color.WHITE - c[/code] or [code]Color(1 - c.r, 1 - c.g, 1 - c.b, 1 - c.a)[/code]. Unlike with [method inverted], the [member a] component is inverted, too.
</description>
</operator>
</operators>
diff --git a/doc/classes/Curve2D.xml b/doc/classes/Curve2D.xml
index ccdf085319..b5e75dff68 100644
--- a/doc/classes/Curve2D.xml
+++ b/doc/classes/Curve2D.xml
@@ -51,7 +51,7 @@
<return type="Vector2" />
<param index="0" name="to_point" type="Vector2" />
<description>
- Returns the closest baked point (in curve's local space) to [param to_point].
+ Returns the closest point on baked segments (in curve's local space) to [param to_point].
[param to_point] must be in this curve's local space.
</description>
</method>
@@ -94,7 +94,7 @@
</method>
<method name="sample_baked" qualifiers="const">
<return type="Vector2" />
- <param index="0" name="offset" type="float" />
+ <param index="0" name="offset" type="float" default="0.0" />
<param index="1" name="cubic" type="bool" default="false" />
<description>
Returns a point within the curve at position [param offset], where [param offset] is measured as a pixel distance along the curve.
@@ -104,13 +104,10 @@
</method>
<method name="sample_baked_with_rotation" qualifiers="const">
<return type="Transform2D" />
- <param index="0" name="offset" type="float" />
+ <param index="0" name="offset" type="float" default="0.0" />
<param index="1" name="cubic" type="bool" default="false" />
- <param index="2" name="loop" type="bool" default="true" />
- <param index="3" name="lookahead" type="float" default="4.0" />
<description>
Similar to [method sample_baked], but returns [Transform2D] that includes a rotation along the curve. Returns empty transform if length of the curve is [code]0[/code].
- Use [param loop] to smooth the tangent at the end of the curve. [param lookahead] defines the distance to a nearby point for calculating the tangent vector.
[codeblock]
var transform = curve.sample_baked_with_rotation(offset)
position = transform.get_origin()
@@ -160,6 +157,13 @@
[param tolerance_degrees] controls how many degrees the midpoint of a segment may deviate from the real curve, before the segment has to be subdivided.
</description>
</method>
+ <method name="tessellate_even_length" qualifiers="const">
+ <return type="PackedVector2Array" />
+ <param index="0" name="max_stages" type="int" default="5" />
+ <param index="1" name="tolerance_length" type="float" default="20.0" />
+ <description>
+ </description>
+ </method>
</methods>
<members>
<member name="bake_interval" type="float" setter="set_bake_interval" getter="get_bake_interval" default="5.0">
diff --git a/doc/classes/Curve3D.xml b/doc/classes/Curve3D.xml
index 851996131f..cfe2036499 100644
--- a/doc/classes/Curve3D.xml
+++ b/doc/classes/Curve3D.xml
@@ -114,7 +114,7 @@
</method>
<method name="sample_baked" qualifiers="const">
<return type="Vector3" />
- <param index="0" name="offset" type="float" />
+ <param index="0" name="offset" type="float" default="0.0" />
<param index="1" name="cubic" type="bool" default="false" />
<description>
Returns a point within the curve at position [param offset], where [param offset] is measured as a distance in 3D units along the curve.
@@ -134,7 +134,7 @@
</method>
<method name="sample_baked_with_rotation" qualifiers="const">
<return type="Transform3D" />
- <param index="0" name="offset" type="float" />
+ <param index="0" name="offset" type="float" default="0.0" />
<param index="1" name="cubic" type="bool" default="false" />
<param index="2" name="apply_tilt" type="bool" default="false" />
<description>
diff --git a/doc/classes/Dictionary.xml b/doc/classes/Dictionary.xml
index 9842288d35..92225b816f 100644
--- a/doc/classes/Dictionary.xml
+++ b/doc/classes/Dictionary.xml
@@ -4,9 +4,8 @@
Dictionary type.
</brief_description>
<description>
- Dictionary type. Associative container, which contains values referenced by unique keys. Dictionaries are composed of pairs of keys (which must be unique) and values. Dictionaries will preserve the insertion order when adding elements. In other programming languages, this data structure is sometimes referred to as a hash map or associative array.
+ Dictionary type. Associative container, which contains values referenced by unique keys. Dictionaries are composed of pairs of keys (which must be unique) and values. Dictionaries will preserve the insertion order when adding new entries. In other programming languages, this data structure is sometimes referred to as a hash map or associative array.
You can define a dictionary by placing a comma-separated list of [code]key: value[/code] pairs in curly braces [code]{}[/code].
- Erasing elements while iterating over them [b]is not supported[/b] and will result in undefined behavior.
[b]Note:[/b] Dictionaries are always passed by reference. To get a copy of a dictionary which can be modified independently of the original dictionary, use [method duplicate].
Creating a dictionary:
[codeblocks]
@@ -40,10 +39,10 @@
};
[/csharp]
[/codeblocks]
- You can access a dictionary's values by referencing the appropriate key. In the above example, [code]points_dict["White"][/code] will return [code]50[/code]. You can also write [code]points_dict.White[/code], which is equivalent. However, you'll have to use the bracket syntax if the key you're accessing the dictionary with isn't a fixed string (such as a number or variable).
+ You can access a dictionary's value by referencing its corresponding key. In the above example, [code]points_dict["White"][/code] will return [code]50[/code]. You can also write [code]points_dict.White[/code], which is equivalent. However, you'll have to use the bracket syntax if the key you're accessing the dictionary with isn't a fixed string (such as a number or variable).
[codeblocks]
[gdscript]
- export(String, "White", "Yellow", "Orange") var my_color
+ @export(String, "White", "Yellow", "Orange") var my_color
var points_dict = {"White": 50, "Yellow": 75, "Orange": 100}
func _ready():
# We can't use dot syntax here as `my_color` is a variable.
@@ -69,7 +68,9 @@
Dictionaries can contain more complex data:
[codeblocks]
[gdscript]
- my_dict = {"First Array": [1, 2, 3, 4]} # Assigns an Array to a String key.
+ var my_dict = {
+ "First Array": [1, 2, 3, 4] # Assigns an Array to a String key.
+ }
[/gdscript]
[csharp]
var myDict = new Godot.Collections.Dictionary
@@ -91,7 +92,7 @@
{"Yellow", 75},
{"Orange", 100}
};
- pointsDict["blue"] = 150; // Add "Blue" as a key and assign 150 as its value.
+ pointsDict["Blue"] = 150; // Add "Blue" as a key and assign 150 as its value.
[/csharp]
[/codeblocks]
Finally, dictionaries can contain different types of keys and values in the same dictionary:
@@ -118,63 +119,23 @@
};
[/csharp]
[/codeblocks]
- [b]Note:[/b] Unlike [Array]s, you can't compare dictionaries directly:
+ The keys of a dictionary can be iterated with the [code]for[/code] keyword:
[codeblocks]
[gdscript]
- var array1 = [1, 2, 3]
- var array2 = [1, 2, 3]
-
- func compare_arrays():
- print(array1 == array2) # Will print true.
-
- var dict1 = {"a": 1, "b": 2, "c": 3}
- var dict2 = {"a": 1, "b": 2, "c": 3}
-
- func compare_dictionaries():
- print(dict1 == dict2) # Will NOT print true.
+ var groceries = {"Orange": 20, "Apple": 2, "Banana": 4}
+ for fruit in groceries:
+ var amount = groceries[fruit]
[/gdscript]
[csharp]
- // You have to use GD.Hash().
-
- public Godot.Collections.Array array1 = new Godot.Collections.Array{1, 2, 3};
- public Godot.Collections.Array array2 = new Godot.Collections.Array{1, 2, 3};
-
- public void CompareArrays()
+ var groceries = new Godot.Collections.Dictionary{{"Orange", 20}, {"Apple", 2}, {"Banana", 4}};
+ foreach (var (fruit, amount) in groceries)
{
- GD.Print(array1 == array2); // Will print FALSE!!
- GD.Print(GD.Hash(array1) == GD.Hash(array2)); // Will print true.
- }
-
- public Godot.Collections.Dictionary dict1 = new Godot.Collections.Dictionary{{"a", 1}, {"b", 2}, {"c", 3}};
- public Godot.Collections.Dictionary dict2 = new Godot.Collections.Dictionary{{"a", 1}, {"b", 2}, {"c", 3}};
-
- public void CompareDictionaries()
- {
- GD.Print(dict1 == dict2); // Will NOT print true.
+ // `fruit` is the key, `amount` is the value.
}
[/csharp]
[/codeblocks]
- You need to first calculate the dictionary's hash with [method hash] before you can compare them:
- [codeblocks]
- [gdscript]
- var dict1 = {"a": 1, "b": 2, "c": 3}
- var dict2 = {"a": 1, "b": 2, "c": 3}
-
- func compare_dictionaries():
- print(dict1.hash() == dict2.hash()) # Will print true.
- [/gdscript]
- [csharp]
- // You have to use GD.Hash().
- public Godot.Collections.Dictionary dict1 = new Godot.Collections.Dictionary{{"a", 1}, {"b", 2}, {"c", 3}};
- public Godot.Collections.Dictionary dict2 = new Godot.Collections.Dictionary{{"a", 1}, {"b", 2}, {"c", 3}};
-
- public void CompareDictionaries()
- {
- GD.Print(GD.Hash(dict1) == GD.Hash(dict2)); // Will print true.
- }
- [/csharp]
- [/codeblocks]
- [b]Note:[/b] When declaring a dictionary with [code]const[/code], the dictionary itself can still be mutated by defining the values of individual keys. Using [code]const[/code] will only prevent assigning the constant with another value after it was initialized.
+ [b]Note:[/b] Erasing elements while iterating over dictionaries is [b]not[/b] supported and will result in unpredictable behavior.
+ [b]Note:[/b] When declaring a dictionary with [code]const[/code], the dictionary becomes read-only. A read-only Dictionary's entries cannot be overriden at run-time. This does [i]not[/i] affect nested [Array] and [Dictionary] values.
</description>
<tutorials>
<link title="GDScript basics: Dictionary">$DOCS_URL/tutorials/scripting/gdscript/gdscript_basics.html#dictionary</link>
@@ -192,7 +153,7 @@
<return type="Dictionary" />
<param index="0" name="from" type="Dictionary" />
<description>
- Constructs a [Dictionary] as a copy of the given [Dictionary].
+ Returns the same array as [param from]. If you need a copy of the array, use [method duplicate].
</description>
</constructor>
</constructors>
@@ -200,30 +161,30 @@
<method name="clear">
<return type="void" />
<description>
- Clear the dictionary, removing all key/value pairs.
+ Clears the dictionary, removing all entries from it.
</description>
</method>
<method name="duplicate" qualifiers="const">
<return type="Dictionary" />
<param index="0" name="deep" type="bool" default="false" />
<description>
- Creates a copy of the dictionary, and returns it. The [param deep] parameter causes inner dictionaries and arrays to be copied recursively, but does not apply to objects.
+ Creates and returns a new copy of the dictionary. If [param deep] is [code]true[/code], inner [Dictionary] and [Array] keys and values are also copied, recursively.
</description>
</method>
<method name="erase">
<return type="bool" />
<param index="0" name="key" type="Variant" />
<description>
- Erase a dictionary key/value pair by key. Returns [code]true[/code] if the given key was present in the dictionary, [code]false[/code] otherwise.
- [b]Note:[/b] Don't erase elements while iterating over the dictionary. You can iterate over the [method keys] array instead.
+ Removes the dictionary entry by key, if it exists. Returns [code]true[/code] if the given [param key] existed in the dictionary, otherwise [code]false[/code].
+ [b]Note:[/b] Do not erase entries while iterating over the dictionary. You can iterate over the [method keys] array instead.
</description>
</method>
<method name="find_key" qualifiers="const">
<return type="Variant" />
<param index="0" name="value" type="Variant" />
<description>
- Returns the first key whose associated value is equal to [param value], or [code]null[/code] if no such value is found.
- [b]Note:[/b] [code]null[/code] is also a valid key. If you have it in your [Dictionary], the [method find_key] method can give misleading results.
+ Finds and returns the first key whose associated value is equal to [param value], or [code]null[/code] if it is not found.
+ [b]Note:[/b] [code]null[/code] is also a valid key. If inside the dictionary, [method find_key] may give misleading results.
</description>
</method>
<method name="get" qualifiers="const">
@@ -231,72 +192,89 @@
<param index="0" name="key" type="Variant" />
<param index="1" name="default" type="Variant" default="null" />
<description>
- Returns the current value for the specified key in the [Dictionary]. If the key does not exist, the method returns the value of the optional default argument, or [code]null[/code] if it is omitted.
+ Returns the corresponding value for the given [param key] in the dictionary. If the [param key] does not exist, returns [param default], or [code]null[/code] if the parameter is omitted.
</description>
</method>
<method name="has" qualifiers="const">
<return type="bool" />
<param index="0" name="key" type="Variant" />
<description>
- Returns [code]true[/code] if the dictionary has a given key.
- [b]Note:[/b] This is equivalent to using the [code]in[/code] operator as follows:
+ Returns [code]true[/code] if the dictionary contains an entry with the given [param key].
[codeblocks]
[gdscript]
- # Will evaluate to `true`.
- if "godot" in {"godot": "engine"}:
- pass
+ var my_dict = {
+ "Godot" : 4,
+ 210 : null,
+ }
+
+ print(my_dict.has("Godot")) # Prints true
+ print(my_dict.has(210)) # Prints true
+ print(my_dict.has(4)) # Prints false
[/gdscript]
[csharp]
- // You have to use Contains() here as an alternative to GDScript's `in` operator.
- if (new Godot.Collections.Dictionary{{"godot", "engine"}}.Contains("godot"))
+ var myDict = new Godot.Collections.Dictionary
{
- // I am executed.
- }
+ { "Godot", 4 },
+ { 210, default },
+ };
+
+ GD.Print(myDict.Contains("Godot")); // Prints true
+ GD.Print(myDict.Contains(210)); // Prints true
+ GD.Print(myDict.Contains(4)); // Prints false
[/csharp]
[/codeblocks]
- This method (like the [code]in[/code] operator) will evaluate to [code]true[/code] as long as the key exists, even if the associated value is [code]null[/code].
+ In GDScript, this is equivalent to the [code]in[/code] operator:
+ [codeblock]
+ if "Godot" in {"Godot": 4}:
+ print("The key is here!") # Will be printed.
+ [/codeblock]
+ [b]Note:[/b] This method returns [code]true[/code] as long as the [param key] exists, even if its corresponding value is [code]null[/code].
</description>
</method>
<method name="has_all" qualifiers="const">
<return type="bool" />
<param index="0" name="keys" type="Array" />
<description>
- Returns [code]true[/code] if the dictionary has all the keys in the given array.
+ Returns [code]true[/code] if the dictionary contains all keys in the given [param keys] array.
+ [codeblock]
+ var data = {"width" : 10, "height" : 20}
+ data.has_all(["height", "width"]) # Returns true
+ [/codeblock]
</description>
</method>
<method name="hash" qualifiers="const">
<return type="int" />
<description>
- Returns a hashed 32-bit integer value representing the dictionary contents. This can be used to compare dictionaries by value:
+ Returns a hashed 32-bit integer value representing the dictionary contents.
[codeblocks]
[gdscript]
- var dict1 = {0: 10}
- var dict2 = {0: 10}
- # The line below prints `true`, whereas it would have printed `false` if both variables were compared directly.
- print(dict1.hash() == dict2.hash())
+ var dict1 = {"A": 10, "B": 2}
+ var dict2 = {"A": 10, "B": 2}
+
+ print(dict1.hash() == dict2.hash()) # Prints true
[/gdscript]
[csharp]
- var dict1 = new Godot.Collections.Dictionary{{0, 10}};
- var dict2 = new Godot.Collections.Dictionary{{0, 10}};
- // The line below prints `true`, whereas it would have printed `false` if both variables were compared directly.
- // Dictionary has no Hash() method. Use GD.Hash() instead.
- GD.Print(GD.Hash(dict1) == GD.Hash(dict2));
+ var dict1 = new Godot.Collections.Dictionary{{"A", 10}, {"B", 2}};
+ var dict2 = new Godot.Collections.Dictionary{{"A", 10}, {"B", 2}};
+
+ // Godot.Collections.Dictionary has no Hash() method. Use GD.Hash() instead.
+ GD.Print(GD.Hash(dict1) == GD.Hash(dict2)); // Prints true
[/csharp]
[/codeblocks]
- [b]Note:[/b] Dictionaries with the same keys/values but in a different order will have a different hash.
- [b]Note:[/b] Dictionaries with equal content will always produce identical hash values. However, the reverse is not true. Returning identical hash values does [i]not[/i] imply the dictionaries are equal, because different dictionaries can have identical hash values due to hash collisions.
+ [b]Note:[/b] Dictionaries with the same entries but in a different order will not have the same hash.
+ [b]Note:[/b] Dictionaries with equal hash values are [i]not[/i] guaranteed to be the same, because of hash collisions. On the countrary, dictionaries with different hash values are guaranteed to be different.
</description>
</method>
<method name="is_empty" qualifiers="const">
<return type="bool" />
<description>
- Returns [code]true[/code] if the dictionary is empty.
+ Returns [code]true[/code] if the dictionary is empty (its size is [code]0[/code]). See also [method size].
</description>
</method>
<method name="keys" qualifiers="const">
<return type="Array" />
<description>
- Returns the list of keys in the [Dictionary].
+ Returns the list of keys in the dictionary.
</description>
</method>
<method name="merge">
@@ -304,19 +282,19 @@
<param index="0" name="dictionary" type="Dictionary" />
<param index="1" name="overwrite" type="bool" default="false" />
<description>
- Adds elements from [param dictionary] to this [Dictionary]. By default, duplicate keys will not be copied over, unless [param overwrite] is [code]true[/code].
+ Adds entries from [param dictionary] to this dictionary. By default, duplicate keys are not copied over, unless [param overwrite] is [code]true[/code].
</description>
</method>
<method name="size" qualifiers="const">
<return type="int" />
<description>
- Returns the number of keys in the dictionary.
+ Returns the number of entries in the dictionary. Empty dictionaries ([code]{ }[/code]) always return [code]0[/code]. See also [method is_empty].
</description>
</method>
<method name="values" qualifiers="const">
<return type="Array" />
<description>
- Returns the list of values in the [Dictionary].
+ Returns the list of values in this dictionary.
</description>
</method>
</methods>
@@ -325,21 +303,22 @@
<return type="bool" />
<param index="0" name="right" type="Dictionary" />
<description>
- Returns [code]true[/code] if the dictionaries differ, i.e. their key or value lists are different (including the order).
+ Returns [code]true[/code] if the two dictionaries do not contain the same keys and values.
</description>
</operator>
<operator name="operator ==">
<return type="bool" />
<param index="0" name="right" type="Dictionary" />
<description>
- Returns [code]true[/code] if both dictionaries have the same contents, i.e. their keys list and value list are equal.
+ Returns [code]true[/code] if the two dictionaries contain the same keys and values. The order of the entries does not matter.
+ [b]Note:[/b] In C#, by convention, this operator compares by [b]reference[/b]. If you need to compare by value, iterate over both dictionaries.
</description>
</operator>
<operator name="operator []">
<return type="Variant" />
<param index="0" name="key" type="Variant" />
<description>
- Returns a value at the given [param key] or [code]null[/code] and error if the key does not exist. For safe access, use [method get] or [method has].
+ Returns the corresponding value for the given [param key] in the dictionary. If the entry does not exist, fails and returns [code]null[/code]. For safe access, use [method get] or [method has].
</description>
</operator>
</operators>
diff --git a/doc/classes/Object.xml b/doc/classes/Object.xml
index bf15f96291..5e834b3d91 100644
--- a/doc/classes/Object.xml
+++ b/doc/classes/Object.xml
@@ -764,6 +764,22 @@
Emits the [signal property_list_changed] signal. This is mainly used to refresh the editor, so that the Inspector and editor plugins are properly updated.
</description>
</method>
+ <method name="property_can_revert" qualifiers="const">
+ <return type="bool" />
+ <param index="0" name="property" type="StringName" />
+ <description>
+ Returns [code]true[/code] if the given [param property] has a custom default value. Use [method property_get_revert] to get the [param property]'s default value.
+ [b]Note:[/b] This method is used by the Inspector dock to display a revert icon. The object must implement [method _property_can_revert] to customize the default value. If [method _property_can_revert] is not implemented, this method returns [code]false[/code].
+ </description>
+ </method>
+ <method name="property_get_revert" qualifiers="const">
+ <return type="Variant" />
+ <param index="0" name="property" type="StringName" />
+ <description>
+ Returns the custom default value of the given [param property]. Use [method property_can_revert] to check if the [param property] has a custom default value.
+ [b]Note:[/b] This method is used by the Inspector dock to display a revert icon. The object must implement [method _property_get_revert] to customize the default value. If [method _property_get_revert] is not implemented, this method returns [code]null[/code].
+ </description>
+ </method>
<method name="remove_meta">
<return type="void" />
<param index="0" name="name" type="StringName" />
diff --git a/doc/classes/RID.xml b/doc/classes/RID.xml
index a6523e4c8b..2d40a7be57 100644
--- a/doc/classes/RID.xml
+++ b/doc/classes/RID.xml
@@ -4,7 +4,8 @@
Handle for a [Resource]'s unique ID.
</brief_description>
<description>
- The RID type is used to access the unique integer ID of a resource. They are opaque, which means they do not grant access to the associated resource by themselves. They are used by and with the low-level Server classes such as [RenderingServer].
+ The RID [Variant] type is used to access a low-level resource by its unique ID. RIDs are opaque, which means they do not grant access to the resource by themselves. They are used by the low-level server classes, such as [DisplayServer], [RenderingServer], [TextServer], etc.
+ A low-level resource may correspond to a high-level [Resource], such as [Texture] or [Mesh].
</description>
<tutorials>
</tutorials>
@@ -27,13 +28,13 @@
<method name="get_id" qualifiers="const">
<return type="int" />
<description>
- Returns the ID of the referenced resource.
+ Returns the ID of the referenced low-level resource.
</description>
</method>
<method name="is_valid" qualifiers="const">
<return type="bool" />
<description>
- Returns [code]true[/code] if [RID] is valid.
+ Returns [code]true[/code] if the [RID] is not [code]0[/code].
</description>
</method>
</methods>
@@ -42,36 +43,42 @@
<return type="bool" />
<param index="0" name="right" type="RID" />
<description>
+ Returns [code]true[/code] if the [RID]s are not equal.
</description>
</operator>
<operator name="operator &lt;">
<return type="bool" />
<param index="0" name="right" type="RID" />
<description>
+ Returns [code]true[/code] if the [RID]'s ID is less than [param right]'s ID.
</description>
</operator>
<operator name="operator &lt;=">
<return type="bool" />
<param index="0" name="right" type="RID" />
<description>
+ Returns [code]true[/code] if the [RID]'s ID is less than or equal to [param right]'s ID.
</description>
</operator>
<operator name="operator ==">
<return type="bool" />
<param index="0" name="right" type="RID" />
<description>
+ Returns [code]true[/code] if both [RID]s are equal, which means they both refer to the same low-level resource.
</description>
</operator>
<operator name="operator &gt;">
<return type="bool" />
<param index="0" name="right" type="RID" />
<description>
+ Returns [code]true[/code] if the [RID]'s ID is greater than [param right]'s ID.
</description>
</operator>
<operator name="operator &gt;=">
<return type="bool" />
<param index="0" name="right" type="RID" />
<description>
+ Returns [code]true[/code] if the [RID]'s ID is greater than or equal to [param right]'s ID.
</description>
</operator>
</operators>
diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml
index cb2481f705..e222894647 100644
--- a/doc/classes/RichTextLabel.xml
+++ b/doc/classes/RichTextLabel.xml
@@ -399,6 +399,12 @@
Scrolls the window's top line to match first line of the [param paragraph].
</description>
</method>
+ <method name="scroll_to_selection">
+ <return type="void" />
+ <description>
+ Scrolls to the beginning of the current selection.
+ </description>
+ </method>
<method name="select_all">
<return type="void" />
<description>
diff --git a/doc/classes/Vector2.xml b/doc/classes/Vector2.xml
index 4156030d77..1cd73688ee 100644
--- a/doc/classes/Vector2.xml
+++ b/doc/classes/Vector2.xml
@@ -93,7 +93,7 @@
<param index="2" name="end" type="Vector2" />
<param index="3" name="t" type="float" />
<description>
- Returns the derivative at the given [param t] on the [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bezier curve[/url] defined by this vector and the given [param control_1], [param control_2], and [param end] points.
+ Returns the derivative at the given [param t] on the [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bézier curve[/url] defined by this vector and the given [param control_1], [param control_2], and [param end] points.
</description>
</method>
<method name="bezier_interpolate" qualifiers="const">
@@ -103,7 +103,7 @@
<param index="2" name="end" type="Vector2" />
<param index="3" name="t" type="float" />
<description>
- Returns the point at the given [param t] on the [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bezier curve[/url] defined by this vector and the given [param control_1], [param control_2], and [param end] points.
+ Returns the point at the given [param t] on the [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bézier curve[/url] defined by this vector and the given [param control_1], [param control_2], and [param end] points.
</description>
</method>
<method name="bounce" qualifiers="const">
diff --git a/doc/classes/Vector3.xml b/doc/classes/Vector3.xml
index 2896408505..1d7eda6bb9 100644
--- a/doc/classes/Vector3.xml
+++ b/doc/classes/Vector3.xml
@@ -69,7 +69,7 @@
<param index="2" name="end" type="Vector3" />
<param index="3" name="t" type="float" />
<description>
- Returns the derivative at the given [param t] on the [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bezier curve[/url] defined by this vector and the given [param control_1], [param control_2], and [param end] points.
+ Returns the derivative at the given [param t] on the [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bézier curve[/url] defined by this vector and the given [param control_1], [param control_2], and [param end] points.
</description>
</method>
<method name="bezier_interpolate" qualifiers="const">
@@ -79,7 +79,7 @@
<param index="2" name="end" type="Vector3" />
<param index="3" name="t" type="float" />
<description>
- Returns the point at the given [param t] on the [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bezier curve[/url] defined by this vector and the given [param control_1], [param control_2], and [param end] points.
+ Returns the point at the given [param t] on the [url=https://en.wikipedia.org/wiki/B%C3%A9zier_curve]Bézier curve[/url] defined by this vector and the given [param control_1], [param control_2], and [param end] points.
</description>
</method>
<method name="bounce" qualifiers="const">
diff --git a/doc/tools/make_rst.py b/doc/tools/make_rst.py
index e5a0bbb008..8960c66acc 100755
--- a/doc/tools/make_rst.py
+++ b/doc/tools/make_rst.py
@@ -240,7 +240,7 @@ class State:
enum_def = class_def.enums[enum]
else:
- enum_def = EnumDef(enum, is_bitfield)
+ enum_def = EnumDef(enum, TypeName("int", enum), is_bitfield)
class_def.enums[enum] = enum_def
enum_def.values[constant_name] = constant_def
@@ -458,9 +458,10 @@ class ConstantDef(DefinitionBase):
class EnumDef(DefinitionBase):
- def __init__(self, name: str, bitfield: bool) -> None:
+ def __init__(self, name: str, type_name: TypeName, bitfield: bool) -> None:
super().__init__("enum", name)
+ self.type_name = type_name
self.values: OrderedDict[str, ConstantDef] = OrderedDict()
self.is_bitfield = bitfield
@@ -754,7 +755,8 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f".. _class_{class_name}:\n\n")
f.write(make_heading(class_name, "=", False))
- # Inheritance tree
+ ### INHERITANCE TREE ###
+
# Ascendants
if class_def.inherits:
inherits = class_def.inherits.strip()
@@ -788,6 +790,8 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(make_type(child, state))
f.write("\n\n")
+ ### INTRODUCTION ###
+
has_description = False
# Brief description
@@ -800,7 +804,9 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
if class_def.description is not None and class_def.description.strip() != "":
has_description = True
+ f.write(".. rst-class:: classref-introduction-group\n\n")
f.write(make_heading("Description", "-"))
+
f.write(f"{format_text_block(class_def.description.strip(), class_def, state)}\n\n")
if not has_description:
@@ -814,14 +820,22 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
# Online tutorials
if len(class_def.tutorials) > 0:
+ f.write(".. rst-class:: classref-introduction-group\n\n")
f.write(make_heading("Tutorials", "-"))
+
for url, title in class_def.tutorials:
f.write(f"- {make_link(url, title)}\n\n")
- # Properties overview
+ ### REFERENCE TABLES ###
+
+ # Reused container for reference tables.
ml: List[Tuple[Optional[str], ...]] = []
+
+ # Properties reference table
if len(class_def.properties) > 0:
+ f.write(".. rst-class:: classref-reftable-group\n\n")
f.write(make_heading("Properties", "-"))
+
ml = []
for property_def in class_def.properties.values():
type_rst = property_def.type_name.to_rst(state)
@@ -833,76 +847,108 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
else:
ref = f":ref:`{property_def.name}<class_{class_name}_property_{property_def.name}>`"
ml.append((type_rst, ref, default))
+
format_table(f, ml, True)
- # Constructors, Methods, Operators overview
+ # Constructors, Methods, Operators reference tables
if len(class_def.constructors) > 0:
+ f.write(".. rst-class:: classref-reftable-group\n\n")
f.write(make_heading("Constructors", "-"))
+
ml = []
for method_list in class_def.constructors.values():
for m in method_list:
ml.append(make_method_signature(class_def, m, "constructor", state))
+
format_table(f, ml)
if len(class_def.methods) > 0:
+ f.write(".. rst-class:: classref-reftable-group\n\n")
f.write(make_heading("Methods", "-"))
+
ml = []
for method_list in class_def.methods.values():
for m in method_list:
ml.append(make_method_signature(class_def, m, "method", state))
+
format_table(f, ml)
if len(class_def.operators) > 0:
+ f.write(".. rst-class:: classref-reftable-group\n\n")
f.write(make_heading("Operators", "-"))
+
ml = []
for method_list in class_def.operators.values():
for m in method_list:
ml.append(make_method_signature(class_def, m, "operator", state))
+
format_table(f, ml)
- # Theme properties
+ # Theme properties reference table
if len(class_def.theme_items) > 0:
+ f.write(".. rst-class:: classref-reftable-group\n\n")
f.write(make_heading("Theme Properties", "-"))
- pl: List[Tuple[Optional[str], ...]] = []
+
+ ml = []
for theme_item_def in class_def.theme_items.values():
ref = f":ref:`{theme_item_def.name}<class_{class_name}_theme_{theme_item_def.data_name}_{theme_item_def.name}>`"
- pl.append((theme_item_def.type_name.to_rst(state), ref, theme_item_def.default_value))
- format_table(f, pl, True)
+ ml.append((theme_item_def.type_name.to_rst(state), ref, theme_item_def.default_value))
+
+ format_table(f, ml, True)
- # Signals
+ ### DETAILED DESCRIPTIONS ###
+
+ # Signal descriptions
if len(class_def.signals) > 0:
+ f.write(make_separator(True))
+ f.write(".. rst-class:: classref-descriptions-group\n\n")
f.write(make_heading("Signals", "-"))
+
index = 0
for signal in class_def.signals.values():
if index != 0:
- f.write("----\n\n")
+ f.write(make_separator())
+
+ # Create signal signature and anchor point.
f.write(f".. _class_{class_name}_signal_{signal.name}:\n\n")
+ f.write(".. rst-class:: classref-signal\n\n")
+
_, signature = make_method_signature(class_def, signal, "", state)
- f.write(f"- {signature}\n\n")
+ f.write(f"{signature}\n\n")
+
+ # Add signal description, or a call to action if it's missing.
if signal.description is not None and signal.description.strip() != "":
f.write(f"{format_text_block(signal.description.strip(), signal, state)}\n\n")
+ else:
+ f.write(".. container:: contribute\n\n\t")
+ f.write(
+ translate(
+ "There is currently no description for this signal. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!"
+ )
+ + "\n\n"
+ )
index += 1
- # Enums
+ # Enumeration descriptions
if len(class_def.enums) > 0:
+ f.write(make_separator(True))
+ f.write(".. rst-class:: classref-descriptions-group\n\n")
f.write(make_heading("Enumerations", "-"))
+
index = 0
for e in class_def.enums.values():
if index != 0:
- f.write("----\n\n")
+ f.write(make_separator())
+
+ # Create enumeration signature and anchor point.
f.write(f".. _enum_{class_name}_{e.name}:\n\n")
- # Sphinx seems to divide the bullet list into individual <ul> tags if we weave the labels into it.
- # As such I'll put them all above the list. Won't be perfect but better than making the list visually broken.
- # As to why I'm not modifying the reference parser to directly link to the _enum label:
- # If somebody gets annoyed enough to fix it, all existing references will magically improve.
- for value in e.values.values():
- f.write(f".. _class_{class_name}_constant_{value.name}:\n\n")
+ f.write(".. rst-class:: classref-enumeration\n\n")
if e.is_bitfield:
f.write(f"flags **{e.name}**:\n\n")
@@ -910,45 +956,66 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
f.write(f"enum **{e.name}**:\n\n")
for value in e.values.values():
- f.write(f"- **{value.name}** = **{value.value}**")
+ # Also create signature and anchor point for each enum constant.
+
+ f.write(f".. _class_{class_name}_constant_{value.name}:\n\n")
+ f.write(".. rst-class:: classref-enumeration-constant\n\n")
+
+ f.write(f"{e.type_name.to_rst(state)} **{value.name}** = ``{value.value}``\n\n")
+
+ # Add enum constant description.
+
if value.text is not None and value.text.strip() != "":
- # If value.text contains a bullet point list, each entry needs additional indentation
- f.write(f" --- {indent_bullets(format_text_block(value.text.strip(), value, state))}")
+ f.write(f"{format_text_block(value.text.strip(), value, state)}")
f.write("\n\n")
index += 1
- # Constants
+ # Constant descriptions
if len(class_def.constants) > 0:
+ f.write(make_separator(True))
+ f.write(".. rst-class:: classref-descriptions-group\n\n")
f.write(make_heading("Constants", "-"))
- # Sphinx seems to divide the bullet list into individual <ul> tags if we weave the labels into it.
- # As such I'll put them all above the list. Won't be perfect but better than making the list visually broken.
+
for constant in class_def.constants.values():
+ # Create constant signature and anchor point.
+
f.write(f".. _class_{class_name}_constant_{constant.name}:\n\n")
+ f.write(".. rst-class:: classref-constant\n\n")
+
+ f.write(f"**{constant.name}** = ``{constant.value}``\n\n")
+
+ # Add enum constant description.
- for constant in class_def.constants.values():
- f.write(f"- **{constant.name}** = **{constant.value}**")
if constant.text is not None and constant.text.strip() != "":
- f.write(f" --- {format_text_block(constant.text.strip(), constant, state)}")
+ f.write(f"{format_text_block(constant.text.strip(), constant, state)}")
f.write("\n\n")
- # Annotations
+ # Annotation descriptions
if len(class_def.annotations) > 0:
+ f.write(make_separator(True))
f.write(make_heading("Annotations", "-"))
+
index = 0
for method_list in class_def.annotations.values(): # type: ignore
for i, m in enumerate(method_list):
if index != 0:
- f.write("----\n\n")
+ f.write(make_separator())
+
+ # Create annotation signature and anchor point.
if i == 0:
f.write(f".. _class_{class_name}_annotation_{m.name}:\n\n")
+ f.write(".. rst-class:: classref-annotation\n\n")
+
_, signature = make_method_signature(class_def, m, "", state)
- f.write(f"- {signature}\n\n")
+ f.write(f"{signature}\n\n")
+
+ # Add annotation description, or a call to action if it's missing.
if m.description is not None and m.description.strip() != "":
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
@@ -965,7 +1032,10 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
# Property descriptions
if any(not p.overrides for p in class_def.properties.values()) > 0:
+ f.write(make_separator(True))
+ f.write(".. rst-class:: classref-descriptions-group\n\n")
f.write(make_heading("Property Descriptions", "-"))
+
index = 0
for property_def in class_def.properties.values():
@@ -973,22 +1043,36 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
continue
if index != 0:
- f.write("----\n\n")
+ f.write(make_separator())
+
+ # Create property signature and anchor point.
f.write(f".. _class_{class_name}_property_{property_def.name}:\n\n")
- f.write(f"- {property_def.type_name.to_rst(state)} **{property_def.name}**\n\n")
+ f.write(".. rst-class:: classref-property\n\n")
- info: List[Tuple[Optional[str], ...]] = []
- # Not using translate() for now as it breaks table formatting.
+ property_default = ""
if property_def.default_value is not None:
- info.append(("*Default*", property_def.default_value))
+ property_default = f" = {property_def.default_value}"
+ f.write(f"{property_def.type_name.to_rst(state)} **{property_def.name}**{property_default}\n\n")
+
+ # Create property setter and getter records.
+
+ property_setget = ""
+
if property_def.setter is not None and not property_def.setter.startswith("_"):
- info.append(("*Setter*", f"{property_def.setter}(value)"))
+ property_setter = make_setter_signature(class_def, property_def, state)
+ property_setget += f"- {property_setter}\n"
+
if property_def.getter is not None and not property_def.getter.startswith("_"):
- info.append(("*Getter*", f"{property_def.getter}()"))
+ property_getter = make_getter_signature(class_def, property_def, state)
+ property_setget += f"- {property_getter}\n"
+
+ if property_setget != "":
+ f.write(".. rst-class:: classref-property-setget\n\n")
+ f.write(property_setget)
+ f.write("\n")
- if len(info) > 0:
- format_table(f, info)
+ # Add property description, or a call to action if it's missing.
if property_def.text is not None and property_def.text.strip() != "":
f.write(f"{format_text_block(property_def.text.strip(), property_def, state)}\n\n")
@@ -1005,19 +1089,28 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
# Constructor, Method, Operator descriptions
if len(class_def.constructors) > 0:
+ f.write(make_separator(True))
+ f.write(".. rst-class:: classref-descriptions-group\n\n")
f.write(make_heading("Constructor Descriptions", "-"))
+
index = 0
for method_list in class_def.constructors.values():
for i, m in enumerate(method_list):
if index != 0:
- f.write("----\n\n")
+ f.write(make_separator())
+
+ # Create constructor signature and anchor point.
if i == 0:
f.write(f".. _class_{class_name}_constructor_{m.name}:\n\n")
+ f.write(".. rst-class:: classref-constructor\n\n")
+
ret_type, signature = make_method_signature(class_def, m, "", state)
- f.write(f"- {ret_type} {signature}\n\n")
+ f.write(f"{ret_type} {signature}\n\n")
+
+ # Add constructor description, or a call to action if it's missing.
if m.description is not None and m.description.strip() != "":
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
@@ -1033,19 +1126,28 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
index += 1
if len(class_def.methods) > 0:
+ f.write(make_separator(True))
+ f.write(".. rst-class:: classref-descriptions-group\n\n")
f.write(make_heading("Method Descriptions", "-"))
+
index = 0
for method_list in class_def.methods.values():
for i, m in enumerate(method_list):
if index != 0:
- f.write("----\n\n")
+ f.write(make_separator())
+
+ # Create method signature and anchor point.
if i == 0:
f.write(f".. _class_{class_name}_method_{m.name}:\n\n")
+ f.write(".. rst-class:: classref-method\n\n")
+
ret_type, signature = make_method_signature(class_def, m, "", state)
- f.write(f"- {ret_type} {signature}\n\n")
+ f.write(f"{ret_type} {signature}\n\n")
+
+ # Add method description, or a call to action if it's missing.
if m.description is not None and m.description.strip() != "":
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
@@ -1061,20 +1163,31 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
index += 1
if len(class_def.operators) > 0:
+ f.write(make_separator(True))
+ f.write(".. rst-class:: classref-descriptions-group\n\n")
f.write(make_heading("Operator Descriptions", "-"))
+
index = 0
for method_list in class_def.operators.values():
for i, m in enumerate(method_list):
if index != 0:
- f.write("----\n\n")
- out = f".. _class_{class_name}_operator_{sanitize_operator_name(m.name, state)}"
+ f.write(make_separator())
+
+ # Create operator signature and anchor point.
+
+ operator_anchor = f".. _class_{class_name}_operator_{sanitize_operator_name(m.name, state)}"
for parameter in m.parameters:
- out += f"_{parameter.type_name.type_name}"
- out += f":\n\n"
- f.write(out)
+ operator_anchor += f"_{parameter.type_name.type_name}"
+ operator_anchor += f":\n\n"
+ f.write(operator_anchor)
+
+ f.write(".. rst-class:: classref-operator\n\n")
+
ret_type, signature = make_method_signature(class_def, m, "", state)
- f.write(f"- {ret_type} {signature}\n\n")
+ f.write(f"{ret_type} {signature}\n\n")
+
+ # Add operator description, or a call to action if it's missing.
if m.description is not None and m.description.strip() != "":
f.write(f"{format_text_block(m.description.strip(), m, state)}\n\n")
@@ -1091,23 +1204,27 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
# Theme property descriptions
if len(class_def.theme_items) > 0:
+ f.write(make_separator(True))
+ f.write(".. rst-class:: classref-descriptions-group\n\n")
f.write(make_heading("Theme Property Descriptions", "-"))
+
index = 0
for theme_item_def in class_def.theme_items.values():
if index != 0:
- f.write("----\n\n")
+ f.write(make_separator())
+
+ # Create theme property signature and anchor point.
f.write(f".. _class_{class_name}_theme_{theme_item_def.data_name}_{theme_item_def.name}:\n\n")
- f.write(f"- {theme_item_def.type_name.to_rst(state)} **{theme_item_def.name}**\n\n")
+ f.write(".. rst-class:: classref-themeproperty\n\n")
- info = []
+ theme_item_default = ""
if theme_item_def.default_value is not None:
- # Not using translate() for now as it breaks table formatting.
- info.append(("*Default*", theme_item_def.default_value))
+ theme_item_default = f" = {theme_item_def.default_value}"
+ f.write(f"{theme_item_def.type_name.to_rst(state)} **{theme_item_def.name}**{theme_item_default}\n\n")
- if len(info) > 0:
- format_table(f, info)
+ # Add theme property description, or a call to action if it's missing.
if theme_item_def.text is not None and theme_item_def.text.strip() != "":
f.write(f"{format_text_block(theme_item_def.text.strip(), theme_item_def, state)}\n\n")
@@ -1216,6 +1333,39 @@ def make_method_signature(
return ret_type, out
+def make_setter_signature(class_def: ClassDef, property_def: PropertyDef, state: State) -> str:
+ if property_def.setter is None:
+ return ""
+
+ # If setter is a method available as a method definition, we use that.
+ if property_def.setter in class_def.methods:
+ setter = class_def.methods[property_def.setter][0]
+ # Otherwise we fake it with the information we have available.
+ else:
+ setter_params: List[ParameterDef] = []
+ setter_params.append(ParameterDef("value", property_def.type_name, None))
+ setter = MethodDef(property_def.setter, TypeName("void"), setter_params, None, None)
+
+ ret_type, signature = make_method_signature(class_def, setter, "", state)
+ return f"{ret_type} {signature}"
+
+
+def make_getter_signature(class_def: ClassDef, property_def: PropertyDef, state: State) -> str:
+ if property_def.getter is None:
+ return ""
+
+ # If getter is a method available as a method definition, we use that.
+ if property_def.getter in class_def.methods:
+ getter = class_def.methods[property_def.getter][0]
+ # Otherwise we fake it with the information we have available.
+ else:
+ getter_params: List[ParameterDef] = []
+ getter = MethodDef(property_def.getter, property_def.type_name, getter_params, None, None)
+
+ ret_type, signature = make_method_signature(class_def, getter, "", state)
+ return f"{ret_type} {signature}"
+
+
def make_heading(title: str, underline: str, l10n: bool = True) -> str:
if l10n:
new_title = translate(title)
@@ -1247,6 +1397,14 @@ def make_footer() -> str:
)
+def make_separator(section_level: bool = False) -> str:
+ separator_class = "item"
+ if section_level:
+ separator_class = "section"
+
+ return f".. rst-class:: classref-{separator_class}-separator\n\n----\n\n"
+
+
def make_link(url: str, title: str) -> str:
match = GODOT_DOCS_PATTERN.search(url)
if match:
@@ -1409,8 +1567,8 @@ def format_text_block(
# Tag is a reference to a class.
if tag_text in state.classes:
if tag_text == state.current_class:
- # Don't create a link to the same class, format it as inline code.
- tag_text = f"``{tag_text}``"
+ # Don't create a link to the same class, format it as strong emphasis.
+ tag_text = f"**{tag_text}**"
else:
tag_text = make_type(tag_text, state)
escape_pre = True
@@ -1872,6 +2030,11 @@ def format_table(f: TextIO, data: List[Tuple[Optional[str], ...]], remove_empty_
if len(data) == 0:
return
+ f.write(".. table::\n")
+ f.write(" :widths: auto\n\n")
+
+ # Calculate the width of each column first, we will use this information
+ # to properly format RST-style tables.
column_sizes = [0] * len(data[0])
for row in data:
for i, text in enumerate(row):
@@ -1879,14 +2042,21 @@ def format_table(f: TextIO, data: List[Tuple[Optional[str], ...]], remove_empty_
if text_length > column_sizes[i]:
column_sizes[i] = text_length
+ # Each table row is wrapped in two separators, consecutive rows share the same separator.
+ # All separators, or rather borders, have the same shape and content. We compose it once,
+ # then reuse it.
+
sep = ""
for size in column_sizes:
if size == 0 and remove_empty_columns:
continue
- sep += "+" + "-" * (size + 2)
+ sep += "+" + "-" * (size + 2) # Content of each cell is padded by 1 on each side.
sep += "+\n"
- f.write(sep)
+ # Draw the first separator.
+ f.write(f" {sep}")
+
+ # Draw each row and close it with a separator.
for row in data:
row_text = "|"
for i, text in enumerate(row):
@@ -1894,8 +2064,10 @@ def format_table(f: TextIO, data: List[Tuple[Optional[str], ...]], remove_empty_
continue
row_text += f' {(text or "").ljust(column_sizes[i])} |'
row_text += "\n"
- f.write(row_text)
- f.write(sep)
+
+ f.write(f" {row_text}")
+ f.write(f" {sep}")
+
f.write("\n")
@@ -1957,24 +2129,5 @@ def sanitize_operator_name(dirty_name: str, state: State) -> str:
return clear_name
-def indent_bullets(text: str) -> str:
- # Take the text and check each line for a bullet point represented by "-".
- # Where found, indent the given line by a further "\t".
- # Used to properly indent bullet points contained in the description for enum values.
- # Ignore the first line - text will be prepended to it so bullet points wouldn't work anyway.
- bullet_points = "-"
-
- lines = text.splitlines(keepends=True)
- for line_index, line in enumerate(lines[1:], start=1):
- pos = 0
- while pos < len(line) and line[pos] == "\t":
- pos += 1
-
- if pos < len(line) and line[pos] in bullet_points:
- lines[line_index] = f"{line[:pos]}\t{line[pos:]}"
-
- return "".join(lines)
-
-
if __name__ == "__main__":
main()
diff --git a/drivers/gles3/shaders/canvas.glsl b/drivers/gles3/shaders/canvas.glsl
index 60139de472..a61ea1587d 100644
--- a/drivers/gles3/shaders/canvas.glsl
+++ b/drivers/gles3/shaders/canvas.glsl
@@ -288,11 +288,9 @@ vec3 light_normal_compute(vec3 light_vec, vec3 normal, vec3 base_color, vec3 lig
#endif
-#define SHADOW_TEST(m_uv) \
- { \
- highp float sd = SHADOW_DEPTH(m_uv); \
- shadow += step(sd, shadow_uv.z / shadow_uv.w); \
- }
+/* clang-format off */
+#define SHADOW_TEST(m_uv) { highp float sd = SHADOW_DEPTH(m_uv); shadow += step(sd, shadow_uv.z / shadow_uv.w); }
+/* clang-format on */
//float distance = length(shadow_pos);
vec4 light_shadow_compute(uint light_base, vec4 light_color, vec4 shadow_uv
@@ -332,7 +330,7 @@ vec4 light_shadow_compute(uint light_base, vec4 light_color, vec4 shadow_uv
shadow /= 13.0;
}
- vec4 shadow_color = unpackUnorm4x8(light_array[light_base].shadow_color);
+ vec4 shadow_color = godot_unpackUnorm4x8(light_array[light_base].shadow_color);
#ifdef LIGHT_CODE_USED
shadow_color.rgb *= shadow_modulate;
#endif
@@ -499,7 +497,7 @@ void main() {
if (specular_shininess_used || (using_light && normal_used && bool(draw_data[draw_data_instance].flags & FLAGS_DEFAULT_SPECULAR_MAP_USED))) {
specular_shininess = texture(specular_texture, uv);
- specular_shininess *= unpackUnorm4x8(draw_data[draw_data_instance].specular_shininess);
+ specular_shininess *= godot_unpackUnorm4x8(draw_data[draw_data_instance].specular_shininess);
specular_shininess_used = true;
} else {
specular_shininess = vec4(1.0);
diff --git a/drivers/gles3/shaders/stdlib_inc.glsl b/drivers/gles3/shaders/stdlib_inc.glsl
index d819940b1d..8d4a24cc1f 100644
--- a/drivers/gles3/shaders/stdlib_inc.glsl
+++ b/drivers/gles3/shaders/stdlib_inc.glsl
@@ -39,23 +39,32 @@ vec2 unpackSnorm2x16(uint p) {
return clamp((v - 32767.0) * vec2(0.00003051851), vec2(-1.0), vec2(1.0));
}
-uint packUnorm4x8(vec4 v) {
+#endif
+
+// Compatibility renames. These are exposed with the "godot_" prefix
+// to work around an Adreno bug which was exposing these ES310 functions
+// in ES300 shaders. Internally, we must use the "godot_" prefix, but user shaders
+// will be mapped automatically.
+uint godot_packUnorm4x8(vec4 v) {
uvec4 uv = uvec4(round(clamp(v, vec4(0.0), vec4(1.0)) * 255.0));
return uv.x | (uv.y << uint(8)) | (uv.z << uint(16)) | (uv.w << uint(24));
}
-vec4 unpackUnorm4x8(uint p) {
+vec4 godot_unpackUnorm4x8(uint p) {
return vec4(float(p & uint(0xff)), float((p >> uint(8)) & uint(0xff)), float((p >> uint(16)) & uint(0xff)), float(p >> uint(24))) * 0.00392156862; // 1.0 / 255.0
}
-uint packSnorm4x8(vec4 v) {
+uint godot_packSnorm4x8(vec4 v) {
uvec4 uv = uvec4(round(clamp(v, vec4(-1.0), vec4(1.0)) * 127.0) + 127.0);
return uv.x | uv.y << uint(8) | uv.z << uint(16) | uv.w << uint(24);
}
-vec4 unpackSnorm4x8(uint p) {
+vec4 godot_unpackSnorm4x8(uint p) {
vec4 v = vec4(float(p & uint(0xff)), float((p >> uint(8)) & uint(0xff)), float((p >> uint(16)) & uint(0xff)), float(p >> uint(24)));
return clamp((v - vec4(127.0)) * vec4(0.00787401574), vec4(-1.0), vec4(1.0));
}
-#endif
+#define packUnorm4x8 godot_packUnorm4x8
+#define unpackUnorm4x8 godot_unpackUnorm4x8
+#define packSnorm4x8 godot_packSnorm4x8
+#define unpackSnorm4x8 godot_unpackSnorm4x8
diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp
index 65cb083ac7..926c01b334 100644
--- a/editor/code_editor.cpp
+++ b/editor/code_editor.cpp
@@ -33,6 +33,7 @@
#include "core/input/input.h"
#include "core/os/keyboard.h"
#include "core/string/string_builder.h"
+#include "core/templates/pair.h"
#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
#include "editor/plugins/script_editor_plugin.h"
@@ -1290,90 +1291,98 @@ void CodeTextEditor::convert_case(CaseStyle p_case) {
void CodeTextEditor::move_lines_up() {
text_editor->begin_complex_operation();
- Vector<int> carets_to_remove;
-
Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
+
+ // Lists of carets representing each group
+ Vector<Vector<int>> caret_groups;
+ Vector<Pair<int, int>> group_borders;
+
+ // Search for groups of carets and their selections residing on the same lines
for (int i = 0; i < caret_edit_order.size(); i++) {
int c = caret_edit_order[i];
- int cl = text_editor->get_caret_line(c);
- bool swaped_caret = false;
- for (int j = i + 1; j < caret_edit_order.size(); j++) {
- if (text_editor->has_selection(caret_edit_order[j])) {
- if (text_editor->get_selection_from_line() == cl) {
- carets_to_remove.push_back(caret_edit_order[j]);
- continue;
- }
+ Vector<int> new_group{ c };
+ Pair<int, int> group_border;
+ if (text_editor->has_selection(c)) {
+ group_border.first = text_editor->get_selection_from_line(c);
+ group_border.second = text_editor->get_selection_to_line(c);
+ } else {
+ group_border.first = text_editor->get_caret_line(c);
+ group_border.second = text_editor->get_caret_line(c);
+ }
- if (text_editor->get_selection_to_line() == cl) {
- if (text_editor->has_selection(c)) {
- if (text_editor->get_selection_to_line(c) != cl) {
- text_editor->select(cl + 1, 0, text_editor->get_selection_to_line(c), text_editor->get_selection_to_column(c), c);
- break;
- }
- }
+ for (int j = i; j < caret_edit_order.size() - 1; j++) {
+ int c_current = caret_edit_order[j];
+ int c_next = caret_edit_order[j + 1];
- carets_to_remove.push_back(c);
- i = j - 1;
- swaped_caret = true;
- break;
- }
+ int next_start_pos = text_editor->has_selection(c_next) ? text_editor->get_selection_from_line(c_next) : text_editor->get_caret_line(c_next);
+ int next_end_pos = text_editor->has_selection(c_next) ? text_editor->get_selection_to_line(c_next) : text_editor->get_caret_line(c_next);
+
+ int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current);
+
+ i = j;
+ if (next_end_pos != current_start_pos && next_end_pos + 1 != current_start_pos) {
break;
}
-
- if (text_editor->get_caret_line(caret_edit_order[j]) == cl) {
- carets_to_remove.push_back(caret_edit_order[j]);
- i = j;
- continue;
+ group_border.first = next_start_pos;
+ new_group.push_back(c_next);
+ // If the last caret is added to the current group there is no need to process it again
+ if (j + 1 == caret_edit_order.size() - 1) {
+ i++;
}
- break;
}
+ group_borders.push_back(group_border);
+ caret_groups.push_back(new_group);
+ }
- if (swaped_caret) {
+ for (int i = group_borders.size() - 1; i >= 0; i--) {
+ if (group_borders[i].first - 1 < 0) {
continue;
}
- if (text_editor->has_selection(c)) {
+ // If the group starts overlapping with the upper group don't move it
+ if (i < group_borders.size() - 1 && group_borders[i].first - 1 <= group_borders[i + 1].second) {
+ continue;
+ }
+
+ // We have to remember caret positions and selections prior to line swapping
+ Vector<Vector<int>> caret_group_parameters;
+
+ for (int j = 0; j < caret_groups[i].size(); j++) {
+ int c = caret_groups[i][j];
+ int cursor_line = text_editor->get_caret_line(c);
+ int cursor_column = text_editor->get_caret_column(c);
+
+ if (!text_editor->has_selection(c)) {
+ caret_group_parameters.push_back(Vector<int>{ -1, -1, -1, -1, cursor_line, cursor_column });
+ continue;
+ }
int from_line = text_editor->get_selection_from_line(c);
int from_col = text_editor->get_selection_from_column(c);
int to_line = text_editor->get_selection_to_line(c);
int to_column = text_editor->get_selection_to_column(c);
- int cursor_line = text_editor->get_caret_line(c);
-
- for (int j = from_line; j <= to_line; j++) {
- int line_id = j;
- int next_id = j - 1;
+ caret_group_parameters.push_back(Vector<int>{ from_line, from_col, to_line, to_column, cursor_line, cursor_column });
+ }
- if (line_id == 0 || next_id < 0) {
- return;
- }
+ for (int line_id = group_borders[i].first; line_id <= group_borders[i].second; line_id++) {
+ text_editor->unfold_line(line_id);
+ text_editor->unfold_line(line_id - 1);
- text_editor->unfold_line(line_id);
- text_editor->unfold_line(next_id);
+ text_editor->swap_lines(line_id - 1, line_id);
+ }
- text_editor->swap_lines(line_id, next_id);
- text_editor->set_caret_line(next_id, c == 0, true, 0, c);
- }
- int from_line_up = from_line > 0 ? from_line - 1 : from_line;
- int to_line_up = to_line > 0 ? to_line - 1 : to_line;
- int cursor_line_up = cursor_line > 0 ? cursor_line - 1 : cursor_line;
- text_editor->select(from_line_up, from_col, to_line_up, to_column, c);
- text_editor->set_caret_line(cursor_line_up, c == 0, true, 0, c);
- } else {
- int line_id = text_editor->get_caret_line(c);
- int next_id = line_id - 1;
+ for (int j = 0; j < caret_groups[i].size(); j++) {
+ int c = caret_groups[i][j];
+ Vector<int> caret_parameters = caret_group_parameters[j];
+ text_editor->set_caret_line(caret_parameters[4] - 1, c == 0, true, 0, c);
+ text_editor->set_caret_column(caret_parameters[5], c == 0, c);
- if (line_id == 0 || next_id < 0) {
- return;
+ if (caret_parameters[0] >= 0) {
+ text_editor->select(caret_parameters[0] - 1, caret_parameters[1], caret_parameters[2] - 1, caret_parameters[3], c);
}
-
- text_editor->unfold_line(line_id);
- text_editor->unfold_line(next_id);
-
- text_editor->swap_lines(line_id, next_id);
- text_editor->set_caret_line(next_id, c == 0, true, 0, c);
}
}
+
text_editor->end_complex_operation();
text_editor->merge_overlapping_carets();
text_editor->queue_redraw();
@@ -1382,95 +1391,97 @@ void CodeTextEditor::move_lines_up() {
void CodeTextEditor::move_lines_down() {
text_editor->begin_complex_operation();
- Vector<int> carets_to_remove;
-
Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order();
+
+ // Lists of carets representing each group
+ Vector<Vector<int>> caret_groups;
+ Vector<Pair<int, int>> group_borders;
+
+ // Search for groups of carets and their selections residing on the same lines
for (int i = 0; i < caret_edit_order.size(); i++) {
int c = caret_edit_order[i];
- int cl = text_editor->get_caret_line(c);
- bool swaped_caret = false;
- for (int j = i + 1; j < caret_edit_order.size(); j++) {
- if (text_editor->has_selection(caret_edit_order[j])) {
- if (text_editor->get_selection_from_line() == cl) {
- carets_to_remove.push_back(caret_edit_order[j]);
- continue;
- }
+ Vector<int> new_group{ c };
+ Pair<int, int> group_border;
+ if (text_editor->has_selection(c)) {
+ group_border.first = text_editor->get_selection_from_line(c);
+ group_border.second = text_editor->get_selection_to_line(c);
+ } else {
+ group_border.first = text_editor->get_caret_line(c);
+ group_border.second = text_editor->get_caret_line(c);
+ }
- if (text_editor->get_selection_to_line() == cl) {
- if (text_editor->has_selection(c)) {
- if (text_editor->get_selection_to_line(c) != cl) {
- text_editor->select(cl + 1, 0, text_editor->get_selection_to_line(c), text_editor->get_selection_to_column(c), c);
- break;
- }
- }
+ for (int j = i; j < caret_edit_order.size() - 1; j++) {
+ int c_current = caret_edit_order[j];
+ int c_next = caret_edit_order[j + 1];
- carets_to_remove.push_back(c);
- i = j - 1;
- swaped_caret = true;
- break;
+ int next_start_pos = text_editor->has_selection(c_next) ? text_editor->get_selection_from_line(c_next) : text_editor->get_caret_line(c_next);
+ int next_end_pos = text_editor->has_selection(c_next) ? text_editor->get_selection_to_line(c_next) : text_editor->get_caret_line(c_next);
+
+ int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current);
+
+ i = j;
+ if (next_end_pos == current_start_pos || next_end_pos + 1 == current_start_pos) {
+ group_border.first = next_start_pos;
+ new_group.push_back(c_next);
+ // If the last caret is added to the current group there is no need to process it again
+ if (j + 1 == caret_edit_order.size() - 1) {
+ i++;
}
+ } else {
break;
}
-
- if (text_editor->get_caret_line(caret_edit_order[j]) == cl) {
- carets_to_remove.push_back(caret_edit_order[j]);
- i = j;
- continue;
- }
- break;
}
+ group_borders.push_back(group_border);
+ caret_groups.push_back(new_group);
+ }
- if (swaped_caret) {
+ for (int i = 0; i < group_borders.size(); i++) {
+ if (group_borders[i].second + 1 > text_editor->get_line_count() - 1) {
continue;
}
- if (text_editor->has_selection(c)) {
- int from_line = text_editor->get_selection_from_line(c);
- int from_col = text_editor->get_selection_from_column(c);
- int to_line = text_editor->get_selection_to_line(c);
- int to_column = text_editor->get_selection_to_column(c);
- int cursor_line = text_editor->get_caret_line(c);
-
- for (int l = to_line; l >= from_line; l--) {
- int line_id = l;
- int next_id = l + 1;
-
- if (line_id == text_editor->get_line_count() - 1 || next_id > text_editor->get_line_count()) {
- continue;
- }
-
- text_editor->unfold_line(line_id);
- text_editor->unfold_line(next_id);
+ // If the group starts overlapping with the upper group don't move it
+ if (i > 0 && group_borders[i].second + 1 >= group_borders[i - 1].first) {
+ continue;
+ }
- text_editor->swap_lines(line_id, next_id);
- text_editor->set_caret_line(next_id, c == 0, true, 0, c);
- }
- int from_line_down = from_line < text_editor->get_line_count() ? from_line + 1 : from_line;
- int to_line_down = to_line < text_editor->get_line_count() ? to_line + 1 : to_line;
- int cursor_line_down = cursor_line < text_editor->get_line_count() ? cursor_line + 1 : cursor_line;
- text_editor->select(from_line_down, from_col, to_line_down, to_column, c);
- text_editor->set_caret_line(cursor_line_down, c == 0, true, 0, c);
- } else {
- int line_id = text_editor->get_caret_line(c);
- int next_id = line_id + 1;
+ // We have to remember caret positions and selections prior to line swapping
+ Vector<Vector<int>> caret_group_parameters;
- if (line_id == text_editor->get_line_count() - 1 || next_id > text_editor->get_line_count()) {
- continue;
+ for (int j = 0; j < caret_groups[i].size(); j++) {
+ int c = caret_groups[i][j];
+ int cursor_line = text_editor->get_caret_line(c);
+ int cursor_column = text_editor->get_caret_column(c);
+
+ if (text_editor->has_selection(c)) {
+ int from_line = text_editor->get_selection_from_line(c);
+ int from_col = text_editor->get_selection_from_column(c);
+ int to_line = text_editor->get_selection_to_line(c);
+ int to_column = text_editor->get_selection_to_column(c);
+ caret_group_parameters.push_back(Vector<int>{ from_line, from_col, to_line, to_column, cursor_line, cursor_column });
+ } else {
+ caret_group_parameters.push_back(Vector<int>{ -1, -1, -1, -1, cursor_line, cursor_column });
}
+ }
+ for (int line_id = group_borders[i].second; line_id >= group_borders[i].first; line_id--) {
text_editor->unfold_line(line_id);
- text_editor->unfold_line(next_id);
+ text_editor->unfold_line(line_id + 1);
- text_editor->swap_lines(line_id, next_id);
- text_editor->set_caret_line(next_id, c == 0, true, 0, c);
+ text_editor->swap_lines(line_id + 1, line_id);
}
- }
- // Sort and remove backwards to preserve indexes.
- carets_to_remove.sort();
- for (int i = carets_to_remove.size() - 1; i >= 0; i--) {
- text_editor->remove_caret(carets_to_remove[i]);
+ for (int j = 0; j < caret_groups[i].size(); j++) {
+ int c = caret_groups[i][j];
+ Vector<int> caret_parameters = caret_group_parameters[j];
+ text_editor->set_caret_line(caret_parameters[4] + 1, c == 0, true, 0, c);
+ text_editor->set_caret_column(caret_parameters[5], c == 0, c);
+
+ if (caret_parameters[0] >= 0) {
+ text_editor->select(caret_parameters[0] + 1, caret_parameters[1], caret_parameters[2] + 1, caret_parameters[3], c);
+ }
+ }
}
text_editor->merge_overlapping_carets();
diff --git a/editor/create_dialog.cpp b/editor/create_dialog.cpp
index 785476d75b..2adab089e4 100644
--- a/editor/create_dialog.cpp
+++ b/editor/create_dialog.cpp
@@ -275,7 +275,8 @@ void CreateDialog::_configure_search_option_item(TreeItem *r_item, const String
r_item->set_text(0, "\"" + p_type + "\"");
} else if (script_type) {
r_item->set_metadata(0, p_type);
- r_item->set_text(0, p_type + " (" + ScriptServer::get_global_class_path(p_type).get_file() + ")");
+ r_item->set_text(0, p_type);
+ r_item->set_suffix(0, "(" + ScriptServer::get_global_class_path(p_type).get_file() + ")");
} else {
r_item->set_metadata(0, custom_type_parents[p_type]);
r_item->set_text(0, p_type);
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index 5d137ce290..678ec04b9d 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -1642,6 +1642,7 @@ Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector
String importer_name;
HashMap<String, HashMap<StringName, Variant>> source_file_options;
+ HashMap<String, ResourceUID::ID> uids;
HashMap<String, String> base_paths;
for (int i = 0; i < p_files.size(); i++) {
Ref<ConfigFile> config;
@@ -1657,6 +1658,15 @@ Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector
ERR_FAIL_V(ERR_FILE_CORRUPT);
}
+ ResourceUID::ID uid = ResourceUID::INVALID_ID;
+
+ if (config->has_section_key("remap", "uid")) {
+ String uidt = config->get_value("remap", "uid");
+ uid = ResourceUID::get_singleton()->text_to_id(uidt);
+ }
+
+ uids[p_files[i]] = uid;
+
source_file_options[p_files[i]] = HashMap<StringName, Variant>();
importer_name = file_importer_name;
@@ -1701,6 +1711,7 @@ Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector
const String &file = E.key;
String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(file);
Vector<String> dest_paths;
+ ResourceUID::ID uid = uids[file];
{
Ref<FileAccess> f = FileAccess::open(file + ".import", FileAccess::WRITE);
ERR_FAIL_COND_V_MSG(f.is_null(), ERR_FILE_CANT_OPEN, "Cannot open import file '" + file + ".import'.");
@@ -1717,6 +1728,12 @@ Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector
f->store_line("type=\"" + importer->get_resource_type() + "\"");
}
+ if (uid == ResourceUID::INVALID_ID) {
+ uid = ResourceUID::get_singleton()->create_id();
+ }
+
+ f->store_line("uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\""); // Store in readable format.
+
if (err == OK) {
String path = base_path + "." + importer->get_save_extension();
f->store_line("path=\"" + path + "\"");
@@ -1782,12 +1799,19 @@ Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector
fs->files[cpos]->modified_time = FileAccess::get_modified_time(file);
fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(file + ".import");
fs->files[cpos]->deps = _get_dependencies(file);
+ fs->files[cpos]->uid = uid;
fs->files[cpos]->type = importer->get_resource_type();
if (fs->files[cpos]->type == "" && textfile_extensions.has(file.get_extension())) {
fs->files[cpos]->type = "TextFile";
}
fs->files[cpos]->import_valid = err == OK;
+ if (ResourceUID::get_singleton()->has_id(uid)) {
+ ResourceUID::get_singleton()->set_id(uid, file);
+ } else {
+ ResourceUID::get_singleton()->add_id(uid, file);
+ }
+
//if file is currently up, maybe the source it was loaded from changed, so import math must be updated for it
//to reload properly
Ref<Resource> r = ResourceCache::get_ref(file);
@@ -2074,6 +2098,8 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
importing = true;
EditorProgress pr("reimport", TTR("(Re)Importing Assets"), p_files.size());
+ Vector<String> reloads;
+
Vector<ImportFile> reimport_files;
HashSet<String> groups_to_reimport;
@@ -2089,22 +2115,26 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
String group_file = ResourceFormatImporter::get_singleton()->get_import_group_file(file);
if (group_file_cache.has(file)) {
- //maybe the file itself is a group!
+ // Maybe the file itself is a group!
groups_to_reimport.insert(file);
- //groups do not belong to groups
+ // Groups do not belong to groups.
+ group_file = String();
+ } else if (groups_to_reimport.has(file)) {
+ // Groups do not belong to groups.
group_file = String();
} else if (!group_file.is_empty()) {
- //it's a group file, add group to import and skip this file
+ // It's a group file, add group to import and skip this file.
groups_to_reimport.insert(group_file);
} else {
- //it's a regular file
+ // It's a regular file.
ImportFile ifile;
ifile.path = file;
ResourceFormatImporter::get_singleton()->get_import_order_threads_and_importer(file, ifile.order, ifile.threaded, ifile.importer);
+ reloads.push_back(file);
reimport_files.push_back(ifile);
}
- //group may have changed, so also update group reference
+ // Group may have changed, so also update group reference.
EditorFileSystemDirectory *fs = nullptr;
int cpos = -1;
if (_find_file(file, &fs, cpos)) {
@@ -2118,10 +2148,14 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
int from = 0;
for (int i = 0; i < reimport_files.size(); i++) {
+ if (groups_to_reimport.has(reimport_files[i].path)) {
+ continue;
+ }
+
if (use_multiple_threads && reimport_files[i].threaded) {
if (i + 1 == reimport_files.size() || reimport_files[i + 1].importer != reimport_files[from].importer) {
if (from - i == 0) {
- //single file, do not use threads
+ // Single file, do not use threads.
pr.step(reimport_files[i].path.get_file(), i);
_reimport_file(reimport_files[i].path);
} else {
@@ -2159,20 +2193,25 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
}
}
- //reimport groups
+ // Reimport groups.
+
+ from = reimport_files.size();
if (groups_to_reimport.size()) {
HashMap<String, Vector<String>> group_files;
_find_group_files(filesystem, group_files, groups_to_reimport);
for (const KeyValue<String, Vector<String>> &E : group_files) {
+ pr.step(E.key.get_file(), from++);
Error err = _reimport_group(E.key, E.value);
+ reloads.push_back(E.key);
+ reloads.append_array(E.value);
if (err == OK) {
_reimport_file(E.key);
}
}
}
- ResourceUID::get_singleton()->update_cache(); //after reimporting, update the cache
+ ResourceUID::get_singleton()->update_cache(); // After reimporting, update the cache.
_save_filesystem_cache();
importing = false;
@@ -2180,7 +2219,7 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
emit_signal(SNAME("filesystem_changed"));
}
- emit_signal(SNAME("resources_reimported"), p_files);
+ emit_signal(SNAME("resources_reimported"), reloads);
}
Error EditorFileSystem::_resource_import(const String &p_path) {
diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp
index 338688f274..5183a738ae 100644
--- a/editor/plugins/animation_player_editor_plugin.cpp
+++ b/editor/plugins/animation_player_editor_plugin.cpp
@@ -216,8 +216,8 @@ void AnimationPlayerEditor::_play_from_pressed() {
player->stop(); //so it won't blend with itself
}
ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");
- player->play(current);
player->seek(time);
+ player->play(current);
}
//unstop
@@ -254,8 +254,8 @@ void AnimationPlayerEditor::_play_bw_from_pressed() {
player->stop(); //so it won't blend with itself
}
ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");
- player->play(current, -1, -1, true);
player->seek(time);
+ player->play(current, -1, -1, true);
}
//unstop
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index 4194fd831b..c97de80a76 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -92,6 +92,9 @@ void ViewportNavigationControl::_notification(int p_what) {
if (!is_connected("mouse_exited", callable_mp(this, &ViewportNavigationControl::_on_mouse_exited))) {
connect("mouse_exited", callable_mp(this, &ViewportNavigationControl::_on_mouse_exited));
}
+ if (!is_connected("mouse_entered", callable_mp(this, &ViewportNavigationControl::_on_mouse_entered))) {
+ connect("mouse_entered", callable_mp(this, &ViewportNavigationControl::_on_mouse_entered));
+ }
} break;
case NOTIFICATION_DRAW: {
@@ -112,7 +115,7 @@ void ViewportNavigationControl::_draw() {
float radius = get_size().x / 2.0;
const bool focused = focused_index != -1;
- draw_circle(center, radius, Color(0.5, 0.5, 0.5, focused ? 0.25 : 0.05));
+ draw_circle(center, radius, Color(0.5, 0.5, 0.5, focused || hovered ? 0.35 : 0.15));
const Color c = focused ? Color(0.9, 0.9, 0.9, 0.9) : Color(0.5, 0.5, 0.5, 0.25);
@@ -123,6 +126,9 @@ void ViewportNavigationControl::_draw() {
}
void ViewportNavigationControl::_process_click(int p_index, Vector2 p_position, bool p_pressed) {
+ hovered = false;
+ queue_redraw();
+
if (focused_index != -1 && focused_index != p_index) {
return;
}
@@ -233,7 +239,13 @@ void ViewportNavigationControl::_update_navigation() {
}
}
+void ViewportNavigationControl::_on_mouse_entered() {
+ hovered = true;
+ queue_redraw();
+}
+
void ViewportNavigationControl::_on_mouse_exited() {
+ hovered = false;
queue_redraw();
}
@@ -5133,7 +5145,7 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p
// Prevent visible spacing between frame time labels.
top_right_vbox->add_theme_constant_override("separation", 0);
- const int navigation_control_size = 200;
+ const int navigation_control_size = 150;
position_control = memnew(ViewportNavigationControl);
position_control->set_navigation_mode(Node3DEditorViewport::NAVIGATION_MOVE);
diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h
index 04fc030f2b..ed555d86c3 100644
--- a/editor/plugins/node_3d_editor_plugin.h
+++ b/editor/plugins/node_3d_editor_plugin.h
@@ -930,6 +930,7 @@ class ViewportNavigationControl : public Control {
Node3DEditorViewport *viewport = nullptr;
Vector2i focused_mouse_start;
Vector2 focused_pos;
+ bool hovered = false;
int focused_index = -1;
Node3DEditorViewport::NavigationMode nav_mode = Node3DEditorViewport::NavigationMode::NAVIGATION_NONE;
@@ -939,6 +940,7 @@ protected:
void _notification(int p_what);
virtual void gui_input(const Ref<InputEvent> &p_event) override;
void _draw();
+ void _on_mouse_entered();
void _on_mouse_exited();
void _process_click(int p_index, Vector2 p_position, bool p_pressed);
void _process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position);
diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp
index 0ebd8e610a..96688a3614 100644
--- a/editor/scene_tree_dock.cpp
+++ b/editor/scene_tree_dock.cpp
@@ -2453,9 +2453,12 @@ void SceneTreeDock::_new_scene_from(String p_file) {
Node *copy = base->duplicate_from_editor(duplimap);
if (copy) {
+ // Handle Unique Nodes.
for (int i = 0; i < copy->get_child_count(false); i++) {
_set_node_owner_recursive(copy->get_child(i, false), copy);
}
+ // Root node cannot ever be unique name in its own Scene!
+ copy->set_unique_name_in_owner(false);
Ref<PackedScene> sdata = memnew(PackedScene);
Error err = sdata->pack(copy);
diff --git a/editor/shader_globals_editor.cpp b/editor/shader_globals_editor.cpp
index 22a1d49422..b778262fed 100644
--- a/editor/shader_globals_editor.cpp
+++ b/editor/shader_globals_editor.cpp
@@ -69,28 +69,12 @@ static const char *global_var_type_names[RS::GLOBAL_VAR_TYPE_MAX] = {
class ShaderGlobalsEditorInterface : public Object {
GDCLASS(ShaderGlobalsEditorInterface, Object)
- void _var_changed() {
- emit_signal(SNAME("var_changed"));
- }
-
-protected:
- static void _bind_methods() {
- ClassDB::bind_method("_var_changed", &ShaderGlobalsEditorInterface::_var_changed);
- ADD_SIGNAL(MethodInfo("var_changed"));
- }
-
- bool _set(const StringName &p_name, const Variant &p_value) {
- Variant existing = RS::get_singleton()->global_shader_parameter_get(p_name);
-
- if (existing.get_type() == Variant::NIL) {
- return false;
- }
-
+ void _set_var(const StringName &p_name, const Variant &p_value, const Variant &p_prev_value) {
Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo();
undo_redo->create_action(TTR("Set Shader Global Variable"));
undo_redo->add_do_method(RS::get_singleton(), "global_shader_parameter_set", p_name, p_value);
- undo_redo->add_undo_method(RS::get_singleton(), "global_shader_parameter_set", p_name, existing);
+ undo_redo->add_undo_method(RS::get_singleton(), "global_shader_parameter_set", p_name, p_prev_value);
RS::GlobalShaderParameterType type = RS::get_singleton()->global_shader_parameter_get_type(p_name);
Dictionary gv;
gv["type"] = global_var_type_names[type];
@@ -111,8 +95,29 @@ protected:
undo_redo->add_do_method(this, "_var_changed");
undo_redo->add_undo_method(this, "_var_changed");
block_update = true;
- undo_redo->commit_action(false);
+ undo_redo->commit_action();
block_update = false;
+ }
+
+ void _var_changed() {
+ emit_signal(SNAME("var_changed"));
+ }
+
+protected:
+ static void _bind_methods() {
+ ClassDB::bind_method("_set_var", &ShaderGlobalsEditorInterface::_set_var);
+ ClassDB::bind_method("_var_changed", &ShaderGlobalsEditorInterface::_var_changed);
+ ADD_SIGNAL(MethodInfo("var_changed"));
+ }
+
+ bool _set(const StringName &p_name, const Variant &p_value) {
+ Variant existing = RS::get_singleton()->global_shader_parameter_get(p_name);
+
+ if (existing.get_type() == Variant::NIL) {
+ return false;
+ }
+
+ call_deferred("_set_var", p_name, p_value, existing);
return true;
}
diff --git a/modules/raycast/SCsub b/modules/raycast/SCsub
index 20b05816e1..51d75d45b0 100644
--- a/modules/raycast/SCsub
+++ b/modules/raycast/SCsub
@@ -67,7 +67,7 @@ if env["builtin_embree"]:
env_raycast.AppendUnique(CPPDEFINES=["NDEBUG"]) # No assert() even in debug builds.
if not env.msvc:
- if env["arch"] == "x86_64":
+ if env["arch"] in ["x86_64", "x86_32"]:
env_raycast.Append(CPPFLAGS=["-msse2", "-mxsave"])
if env["platform"] == "windows":
@@ -87,10 +87,13 @@ if env["builtin_embree"]:
env_thirdparty.disable_warnings()
env_thirdparty.add_source_files(thirdparty_obj, thirdparty_sources)
- if env["arch"] == "arm64" or env.msvc:
+ if env["arch"] != "x86_64" or env.msvc:
# Embree needs those, it will automatically use SSE2NEON in ARM
env_thirdparty.Append(CPPDEFINES=["__SSE2__", "__SSE__"])
+ if env["platform"] == "web":
+ env_thirdparty.Append(CPPFLAGS=["-msimd128"])
+
if not env.msvc:
env_thirdparty.Append(
CPPFLAGS=[
diff --git a/modules/raycast/config.py b/modules/raycast/config.py
index 833ad50018..f4243f01c5 100644
--- a/modules/raycast/config.py
+++ b/modules/raycast/config.py
@@ -1,9 +1,13 @@
def can_build(env, platform):
- # Depends on Embree library, which only supports x86_64 and arm64.
- if platform == "windows":
- return env["arch"] == "x86_64" # TODO build for Windows on ARM
-
- return env["arch"] in ["x86_64", "arm64"]
+ # Supported architectures depend on the Embree library.
+ # No ARM32 support planned.
+ if env["arch"] == "arm32":
+ return False
+ # x86_32 only seems supported on Windows for now.
+ if env["arch"] == "x86_32" and platform != "windows":
+ return False
+ # The rest works, even wasm32!
+ return True
def configure(env):
diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp
index bc018e366b..4eaf6965c9 100644
--- a/platform/linuxbsd/joypad_linux.cpp
+++ b/platform/linuxbsd/joypad_linux.cpp
@@ -218,8 +218,8 @@ void JoypadLinux::monitor_joypads() {
}
}
closedir(input_directory);
+ usleep(1000000); // 1s
}
- usleep(1000000); // 1s
}
void JoypadLinux::close_joypads() {
diff --git a/platform/windows/detect.py b/platform/windows/detect.py
index 705e83dace..1b55574b19 100644
--- a/platform/windows/detect.py
+++ b/platform/windows/detect.py
@@ -188,6 +188,7 @@ def get_opts():
BoolVariable("use_llvm", "Use the LLVM compiler", False),
BoolVariable("use_static_cpp", "Link MinGW/MSVC C++ runtime libraries statically", True),
BoolVariable("use_asan", "Use address sanitizer (ASAN)", False),
+ BoolVariable("debug_crt", "Compile with MSVC's debug CRT (/MDd)", False),
]
@@ -339,10 +340,14 @@ def configure_msvc(env, vcvars_msvc_config):
## Compile/link flags
- if env["use_static_cpp"]:
- env.AppendUnique(CCFLAGS=["/MT"])
+ if env["debug_crt"]:
+ # Always use dynamic runtime, static debug CRT breaks thread_local.
+ env.AppendUnique(CCFLAGS=["/MDd"])
else:
- env.AppendUnique(CCFLAGS=["/MD"])
+ if env["use_static_cpp"]:
+ env.AppendUnique(CCFLAGS=["/MT"])
+ else:
+ env.AppendUnique(CCFLAGS=["/MD"])
if env["arch"] == "x86_32":
env["x86_libtheora_opt_vc"] = True
diff --git a/scene/2d/path_2d.cpp b/scene/2d/path_2d.cpp
index b990e47666..c4cb8e7f94 100644
--- a/scene/2d/path_2d.cpp
+++ b/scene/2d/path_2d.cpp
@@ -216,7 +216,7 @@ void PathFollow2D::_update_transform() {
}
if (rotates) {
- Transform2D xform = c->sample_baked_with_rotation(progress, cubic, loop, lookahead);
+ Transform2D xform = c->sample_baked_with_rotation(progress, cubic);
xform.translate_local(v_offset, h_offset);
set_rotation(xform[1].angle());
set_position(xform[2]);
diff --git a/scene/3d/xr_nodes.cpp b/scene/3d/xr_nodes.cpp
index ca7d1dfc1d..05fc73306c 100644
--- a/scene/3d/xr_nodes.cpp
+++ b/scene/3d/xr_nodes.cpp
@@ -627,7 +627,9 @@ void XROrigin3D::set_world_scale(real_t p_world_scale) {
xr_server->set_world_scale(p_world_scale);
}
-void XROrigin3D::set_current(bool p_enabled) {
+void XROrigin3D::_set_current(bool p_enabled, bool p_update_others) {
+ // We run this logic even if current already equals p_enabled as we may have set this previously before we entered our tree.
+ // This is then called a second time on NOTIFICATION_ENTER_TREE where we actually process activating this origin node.
current = p_enabled;
if (!is_inside_tree() || Engine::get_singleton()->is_editor_hint()) {
@@ -638,30 +640,38 @@ void XROrigin3D::set_current(bool p_enabled) {
set_notify_local_transform(current);
set_notify_transform(current);
+ // update XRServer with our current position
if (current) {
- for (int i = 0; i < origin_nodes.size(); i++) {
- if (origin_nodes[i] != this) {
- origin_nodes[i]->set_current(false);
- }
- }
-
- // update XRServer with our current position
XRServer *xr_server = XRServer::get_singleton();
ERR_FAIL_NULL(xr_server);
xr_server->set_world_origin(get_global_transform());
- } else {
- bool found = false;
- // We no longer have a current origin so find the first one we can make current
- for (int i = 0; !found && i < origin_nodes.size(); i++) {
- if (origin_nodes[i] != this) {
- origin_nodes[i]->set_current(true);
- found = true;
+ }
+
+ // Check if we need to update our other origin nodes accordingly
+ if (p_update_others) {
+ if (current) {
+ for (int i = 0; i < origin_nodes.size(); i++) {
+ if (origin_nodes[i] != this && origin_nodes[i]->current) {
+ origin_nodes[i]->_set_current(false, false);
+ }
+ }
+ } else {
+ // We no longer have a current origin so find the first one we can make current
+ for (int i = 0; i < origin_nodes.size(); i++) {
+ if (origin_nodes[i] != this) {
+ origin_nodes[i]->_set_current(true, false);
+ return; // we are done.
+ }
}
}
}
}
+void XROrigin3D::set_current(bool p_enabled) {
+ _set_current(p_enabled, true);
+}
+
bool XROrigin3D::is_current() const {
if (Engine::get_singleton()->is_editor_hint()) {
// return as is
diff --git a/scene/3d/xr_nodes.h b/scene/3d/xr_nodes.h
index 990fb61983..ec8e151a08 100644
--- a/scene/3d/xr_nodes.h
+++ b/scene/3d/xr_nodes.h
@@ -183,6 +183,8 @@ private:
bool current = false;
static Vector<XROrigin3D *> origin_nodes; // all origin nodes in tree
+ void _set_current(bool p_enabled, bool p_update_others);
+
protected:
void _notification(int p_what);
static void _bind_methods();
diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp
index ca1befc135..f7baa7facc 100644
--- a/scene/animation/animation_player.cpp
+++ b/scene/animation/animation_player.cpp
@@ -955,7 +955,7 @@ void AnimationPlayer::_animation_process_animation(AnimationData *p_anim, double
break;
}
- if (player->is_playing() || p_seeked) {
+ if (player->is_playing()) {
player->play(anim_name);
player->seek(at_anim_pos);
nc->animation_playing = true;
diff --git a/scene/debugger/scene_debugger.cpp b/scene/debugger/scene_debugger.cpp
index 599a759b08..35ba49563c 100644
--- a/scene/debugger/scene_debugger.cpp
+++ b/scene/debugger/scene_debugger.cpp
@@ -220,9 +220,31 @@ void SceneDebugger::_save_node(ObjectID id, const String &p_path) {
Node *node = Object::cast_to<Node>(ObjectDB::get_instance(id));
ERR_FAIL_COND(!node);
+ HashMap<const Node *, Node *> duplimap;
+ Node *copy = node->duplicate_from_editor(duplimap);
+
+ // Handle Unique Nodes.
+ for (int i = 0; i < copy->get_child_count(false); i++) {
+ _set_node_owner_recursive(copy->get_child(i, false), copy);
+ }
+ // Root node cannot ever be unique name in its own Scene!
+ copy->set_unique_name_in_owner(false);
+
Ref<PackedScene> ps = memnew(PackedScene);
- ps->pack(node);
+ ps->pack(copy);
ResourceSaver::save(ps, p_path);
+
+ memdelete(copy);
+}
+
+void SceneDebugger::_set_node_owner_recursive(Node *p_node, Node *p_owner) {
+ if (!p_node->get_owner()) {
+ p_node->set_owner(p_owner);
+ }
+
+ for (int i = 0; i < p_node->get_child_count(false); i++) {
+ _set_node_owner_recursive(p_node->get_child(i, false), p_owner);
+ }
}
void SceneDebugger::_send_object_id(ObjectID p_id, int p_max_size) {
diff --git a/scene/debugger/scene_debugger.h b/scene/debugger/scene_debugger.h
index 0428bfcc2e..34891ef0fd 100644
--- a/scene/debugger/scene_debugger.h
+++ b/scene/debugger/scene_debugger.h
@@ -56,6 +56,7 @@ public:
#ifdef DEBUG_ENABLED
private:
static void _save_node(ObjectID id, const String &p_path);
+ static void _set_node_owner_recursive(Node *p_node, Node *p_owner);
static void _set_object_property(ObjectID p_id, const String &p_property, const Variant &p_value);
static void _send_object_id(ObjectID p_id, int p_max_size = 1 << 20);
diff --git a/scene/gui/graph_node.cpp b/scene/gui/graph_node.cpp
index 5df4c066e4..83c789f3d5 100644
--- a/scene/gui/graph_node.cpp
+++ b/scene/gui/graph_node.cpp
@@ -366,38 +366,46 @@ void GraphNode::_notification(int p_what) {
close_rect = Rect2();
}
- for (const KeyValue<int, Slot> &E : slot_info) {
- if (E.key < 0 || E.key >= cache_y.size()) {
- continue;
- }
- if (!slot_info.has(E.key)) {
- continue;
- }
- const Slot &s = slot_info[E.key];
- // Left port.
- if (s.enable_left) {
- Ref<Texture2D> p = port;
- if (s.custom_slot_left.is_valid()) {
- p = s.custom_slot_left;
+ if (get_child_count() > 0) {
+ for (const KeyValue<int, Slot> &E : slot_info) {
+ if (E.key < 0 || E.key >= cache_y.size()) {
+ continue;
}
- p->draw(get_canvas_item(), icofs + Point2(edgeofs, cache_y[E.key]), s.color_left);
- }
- // Right port.
- if (s.enable_right) {
- Ref<Texture2D> p = port;
- if (s.custom_slot_right.is_valid()) {
- p = s.custom_slot_right;
+ if (!slot_info.has(E.key)) {
+ continue;
+ }
+ const Slot &s = slot_info[E.key];
+ // Left port.
+ if (s.enable_left) {
+ Ref<Texture2D> p = port;
+ if (s.custom_slot_left.is_valid()) {
+ p = s.custom_slot_left;
+ }
+ p->draw(get_canvas_item(), icofs + Point2(edgeofs, cache_y[E.key]), s.color_left);
+ }
+ // Right port.
+ if (s.enable_right) {
+ Ref<Texture2D> p = port;
+ if (s.custom_slot_right.is_valid()) {
+ p = s.custom_slot_right;
+ }
+ p->draw(get_canvas_item(), icofs + Point2(get_size().x - edgeofs, cache_y[E.key]), s.color_right);
}
- p->draw(get_canvas_item(), icofs + Point2(get_size().x - edgeofs, cache_y[E.key]), s.color_right);
- }
- // Draw slot stylebox.
- if (s.draw_stylebox) {
- Control *c = Object::cast_to<Control>(get_child(E.key));
- Rect2 c_rect = c->get_rect();
- c_rect.position.x = sb->get_margin(SIDE_LEFT);
- c_rect.size.width = w;
- draw_style_box(sb_slot, c_rect);
+ // Draw slot stylebox.
+ if (s.draw_stylebox) {
+ Control *c = Object::cast_to<Control>(get_child(E.key));
+ if (!c || !c->is_visible_in_tree()) {
+ continue;
+ }
+ if (c->is_set_as_top_level()) {
+ continue;
+ }
+ Rect2 c_rect = c->get_rect();
+ c_rect.position.x = sb->get_margin(SIDE_LEFT);
+ c_rect.size.width = w;
+ draw_style_box(sb_slot, c_rect);
+ }
}
}
diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp
index 8a77c39487..bd39ee3bb3 100644
--- a/scene/gui/line_edit.cpp
+++ b/scene/gui/line_edit.cpp
@@ -768,18 +768,18 @@ void LineEdit::_notification(int p_what) {
case NOTIFICATION_WM_WINDOW_FOCUS_IN: {
window_has_focus = true;
- draw_caret = true;
+ _validate_caret_can_draw();
queue_redraw();
} break;
case NOTIFICATION_WM_WINDOW_FOCUS_OUT: {
window_has_focus = false;
- draw_caret = false;
+ _validate_caret_can_draw();
queue_redraw();
} break;
case NOTIFICATION_INTERNAL_PROCESS: {
- if (caret_blinking) {
+ if (caret_blink_enabled && caret_can_draw) {
caret_blink_timer += get_process_delta_time();
if (caret_blink_timer >= caret_blink_interval) {
@@ -790,10 +790,6 @@ void LineEdit::_notification(int p_what) {
} break;
case NOTIFICATION_DRAW: {
- if ((!has_focus() && !(menu && menu->has_focus()) && !caret_force_displayed) || !window_has_focus) {
- draw_caret = false;
- }
-
int width, height;
bool rtl = is_layout_rtl();
@@ -806,7 +802,6 @@ void LineEdit::_notification(int p_what) {
Ref<StyleBox> style = theme_cache.normal;
if (!is_editable()) {
style = theme_cache.read_only;
- draw_caret = false;
}
Ref<Font> font = theme_cache.font;
@@ -953,7 +948,7 @@ void LineEdit::_notification(int p_what) {
// Draw carets.
ofs.x = x_ofs + scroll_offset;
- if (draw_caret || drag_caret_force_displayed) {
+ if ((caret_can_draw && draw_caret) || drag_caret_force_displayed) {
// Prevent carets from disappearing at theme scales below 1.0 (if the caret width is 1).
const int caret_width = theme_cache.caret_width * MAX(1, theme_cache.base_scale);
@@ -1063,16 +1058,7 @@ void LineEdit::_notification(int p_what) {
} break;
case NOTIFICATION_FOCUS_ENTER: {
- if (!caret_force_displayed) {
- if (caret_blink_enabled) {
- if (!caret_blinking) {
- caret_blinking = true;
- caret_blink_timer = 0.0;
- }
- } else {
- draw_caret = true;
- }
- }
+ _validate_caret_can_draw();
if (select_all_on_focus) {
if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) {
@@ -1093,9 +1079,7 @@ void LineEdit::_notification(int p_what) {
} break;
case NOTIFICATION_FOCUS_EXIT: {
- if (caret_blink_enabled && !caret_force_displayed) {
- caret_blinking = false;
- }
+ _validate_caret_can_draw();
if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) {
DisplayServer::get_singleton()->window_set_ime_position(Point2(), get_viewport()->get_window_id());
@@ -1401,21 +1385,18 @@ bool LineEdit::is_caret_blink_enabled() const {
}
void LineEdit::set_caret_blink_enabled(const bool p_enabled) {
+ if (caret_blink_enabled == p_enabled) {
+ return;
+ }
+
caret_blink_enabled = p_enabled;
set_process_internal(p_enabled);
- if (has_focus() || caret_force_displayed) {
- if (p_enabled) {
- if (!caret_blinking) {
- caret_blinking = true;
- caret_blink_timer = 0.0;
- }
- } else {
- caret_blinking = false;
- }
+ draw_caret = !caret_blink_enabled;
+ if (caret_blink_enabled) {
+ caret_blink_timer = 0.0;
}
-
- draw_caret = true;
+ queue_redraw();
notify_property_list_changed();
}
@@ -1425,8 +1406,13 @@ bool LineEdit::is_caret_force_displayed() const {
}
void LineEdit::set_caret_force_displayed(const bool p_enabled) {
+ if (caret_force_displayed == p_enabled) {
+ return;
+ }
+
caret_force_displayed = p_enabled;
- set_caret_blink_enabled(caret_blink_enabled);
+ _validate_caret_can_draw();
+
queue_redraw();
}
@@ -1442,7 +1428,7 @@ void LineEdit::set_caret_blink_interval(const float p_interval) {
void LineEdit::_reset_caret_blink_timer() {
if (caret_blink_enabled) {
draw_caret = true;
- if (has_focus()) {
+ if (caret_can_draw) {
caret_blink_timer = 0.0;
queue_redraw();
}
@@ -1451,11 +1437,19 @@ void LineEdit::_reset_caret_blink_timer() {
void LineEdit::_toggle_draw_caret() {
draw_caret = !draw_caret;
- if (is_visible_in_tree() && ((has_focus() && window_has_focus) || caret_force_displayed)) {
+ if (is_visible_in_tree() && caret_can_draw) {
queue_redraw();
}
}
+void LineEdit::_validate_caret_can_draw() {
+ if (caret_blink_enabled) {
+ draw_caret = true;
+ caret_blink_timer = 0.0;
+ }
+ caret_can_draw = editable && (window_has_focus || (menu && menu->has_focus())) && (has_focus() || caret_force_displayed);
+}
+
void LineEdit::delete_char() {
if ((text.length() <= 0) || (caret_column == 0)) {
return;
@@ -1846,6 +1840,7 @@ void LineEdit::set_editable(bool p_editable) {
}
editable = p_editable;
+ _validate_caret_can_draw();
update_minimum_size();
queue_redraw();
@@ -2523,6 +2518,8 @@ void LineEdit::_ensure_menu() {
menu->add_child(menu_ctl, false, INTERNAL_MODE_FRONT);
menu->connect("id_pressed", callable_mp(this, &LineEdit::menu_option));
+ menu->connect(SNAME("focus_entered"), callable_mp(this, &LineEdit::_validate_caret_can_draw));
+ menu->connect(SNAME("focus_exited"), callable_mp(this, &LineEdit::_validate_caret_can_draw));
menu_dir->connect("id_pressed", callable_mp(this, &LineEdit::menu_option));
menu_ctl->connect("id_pressed", callable_mp(this, &LineEdit::menu_option));
}
diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h
index e0a079b623..79db9dce21 100644
--- a/scene/gui/line_edit.h
+++ b/scene/gui/line_edit.h
@@ -172,7 +172,7 @@ private:
bool draw_caret = true;
float caret_blink_interval = 0.65;
double caret_blink_timer = 0.0;
- bool caret_blinking = false;
+ bool caret_can_draw = false;
bool pending_select_all_on_focus = false;
bool select_all_on_focus = false;
@@ -225,6 +225,7 @@ private:
void _reset_caret_blink_timer();
void _toggle_draw_caret();
+ void _validate_caret_can_draw();
void clear_internal();
diff --git a/scene/gui/range.cpp b/scene/gui/range.cpp
index e0e4ead55f..00c81c8616 100644
--- a/scene/gui/range.cpp
+++ b/scene/gui/range.cpp
@@ -85,7 +85,7 @@ void Range::set_value(double p_val) {
void Range::set_value_no_signal(double p_val) {
if (shared->step > 0) {
- p_val = Math::round(p_val / shared->step) * shared->step;
+ p_val = Math::round((p_val - shared->min) / shared->step) * shared->step + shared->min;
}
if (_rounded_values) {
diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp
index df41863e74..60d107cce6 100644
--- a/scene/gui/rich_text_label.cpp
+++ b/scene/gui/rich_text_label.cpp
@@ -4528,6 +4528,30 @@ void RichTextLabel::append_text(const String &p_bbcode) {
}
}
+void RichTextLabel::scroll_to_selection() {
+ if (selection.active && selection.from_frame && selection.from_line >= 0 && selection.from_line < (int)selection.from_frame->lines.size()) {
+ // Selected frame paragraph offset.
+ float line_offset = selection.from_frame->lines[selection.from_line].offset.y;
+
+ // Add wrapped line offset.
+ for (int i = 0; i < selection.from_frame->lines[selection.from_line].text_buf->get_line_count(); i++) {
+ Vector2i range = selection.from_frame->lines[selection.from_line].text_buf->get_line_range(i);
+ if (range.x <= selection.from_char && range.y >= selection.from_char) {
+ break;
+ }
+ line_offset += selection.from_frame->lines[selection.from_line].text_buf->get_line_size(i).y + theme_cache.line_separation;
+ }
+
+ // Add nested frame (e.g. table cell) offset.
+ ItemFrame *it = selection.from_frame;
+ while (it->parent_frame != nullptr) {
+ line_offset += it->parent_frame->lines[it->line].offset.y;
+ it = it->parent_frame;
+ }
+ vscroll->set_value(line_offset);
+ }
+}
+
void RichTextLabel::scroll_to_paragraph(int p_paragraph) {
_validate_line_caches();
@@ -4772,7 +4796,7 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p
char_idx = p_search_previous ? selection.from_char - 1 : selection.to_char;
if (!(p_search_previous && char_idx < 0) &&
_search_line(selection.from_frame, selection.from_line, p_string, char_idx, p_search_previous)) {
- scroll_to_line(selection.from_frame->line + selection.from_line);
+ scroll_to_selection();
queue_redraw();
return true;
}
@@ -4797,7 +4821,7 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p
// Search for next element
if (_search_table(parent_table, parent_element, p_string, p_search_previous)) {
- scroll_to_line(selection.from_frame->line + selection.from_line);
+ scroll_to_selection();
queue_redraw();
return true;
}
@@ -4821,7 +4845,7 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p
}
if (_search_line(main, current_line, p_string, char_idx, p_search_previous)) {
- scroll_to_line(current_line);
+ scroll_to_selection();
queue_redraw();
return true;
}
@@ -5309,6 +5333,7 @@ void RichTextLabel::_bind_methods() {
ClassDB::bind_method(D_METHOD("scroll_to_line", "line"), &RichTextLabel::scroll_to_line);
ClassDB::bind_method(D_METHOD("scroll_to_paragraph", "paragraph"), &RichTextLabel::scroll_to_paragraph);
+ ClassDB::bind_method(D_METHOD("scroll_to_selection"), &RichTextLabel::scroll_to_selection);
ClassDB::bind_method(D_METHOD("set_tab_size", "spaces"), &RichTextLabel::set_tab_size);
ClassDB::bind_method(D_METHOD("get_tab_size"), &RichTextLabel::get_tab_size);
diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h
index d30baaa8d3..b00cc3d055 100644
--- a/scene/gui/rich_text_label.h
+++ b/scene/gui/rich_text_label.h
@@ -657,6 +657,8 @@ public:
int get_content_height() const;
int get_content_width() const;
+ void scroll_to_selection();
+
VScrollBar *get_v_scroll_bar() { return vscroll; }
virtual CursorShape get_cursor_shape(const Point2 &p_pos) const override;
diff --git a/scene/resources/curve.cpp b/scene/resources/curve.cpp
index 93c3d4f851..fa7a1f3dbf 100644
--- a/scene/resources/curve.cpp
+++ b/scene/resources/curve.cpp
@@ -341,7 +341,7 @@ real_t Curve::sample_local_nocheck(int p_index, real_t p_local_offset) const {
const Point a = _points[p_index];
const Point b = _points[p_index + 1];
- /* Cubic bezier
+ /* Cubic bézier
*
* ac-----bc
* / \
@@ -774,6 +774,22 @@ void Curve2D::_bake_segment2d(RBMap<real_t, Vector2> &r_bake, real_t p_begin, re
}
}
+void Curve2D::_bake_segment2d_even_length(RBMap<real_t, Vector2> &r_bake, real_t p_begin, real_t p_end, const Vector2 &p_a, const Vector2 &p_out, const Vector2 &p_b, const Vector2 &p_in, int p_depth, int p_max_depth, real_t p_length) const {
+ Vector2 beg = p_a.bezier_interpolate(p_a + p_out, p_b + p_in, p_b, p_begin);
+ Vector2 end = p_a.bezier_interpolate(p_a + p_out, p_b + p_in, p_b, p_end);
+
+ real_t length = beg.distance_to(end);
+
+ if (length > p_length && p_depth < p_max_depth) {
+ real_t mp = (p_begin + p_end) * 0.5;
+ Vector2 mid = p_a.bezier_interpolate(p_a + p_out, p_b + p_in, p_b, mp);
+ r_bake[mp] = mid;
+
+ _bake_segment2d_even_length(r_bake, p_begin, mp, p_a, p_out, p_b, p_in, p_depth + 1, p_max_depth, p_length);
+ _bake_segment2d_even_length(r_bake, mp, p_end, p_a, p_out, p_b, p_in, p_depth + 1, p_max_depth, p_length);
+ }
+}
+
void Curve2D::_bake() const {
if (!baked_cache_dirty) {
return;
@@ -785,94 +801,62 @@ void Curve2D::_bake() const {
if (points.size() == 0) {
baked_point_cache.clear();
baked_dist_cache.clear();
+ baked_forward_vector_cache.clear();
return;
}
if (points.size() == 1) {
baked_point_cache.resize(1);
baked_point_cache.set(0, points[0].position);
-
baked_dist_cache.resize(1);
baked_dist_cache.set(0, 0.0);
+ baked_forward_vector_cache.resize(1);
+ baked_forward_vector_cache.set(0, Vector2(0.0, 0.1));
+
return;
}
- Vector2 position = points[0].position;
- real_t dist = 0.0;
-
- List<Vector2> pointlist;
- List<real_t> distlist;
-
- // Start always from origin.
- pointlist.push_back(position);
- distlist.push_back(0.0);
-
- for (int i = 0; i < points.size() - 1; i++) {
- real_t step = 0.1; // at least 10 substeps ought to be enough?
- real_t p = 0.0;
-
- while (p < 1.0) {
- real_t np = p + step;
- if (np > 1.0) {
- np = 1.0;
- }
-
- Vector2 npp = points[i].position.bezier_interpolate(points[i].position + points[i].out, points[i + 1].position + points[i + 1].in, points[i + 1].position, np);
- real_t d = position.distance_to(npp);
-
- if (d > bake_interval) {
- // OK! between P and NP there _has_ to be Something, let's go searching!
-
- int iterations = 10; //lots of detail!
-
- real_t low = p;
- real_t hi = np;
- real_t mid = low + (hi - low) * 0.5;
-
- for (int j = 0; j < iterations; j++) {
- npp = points[i].position.bezier_interpolate(points[i].position + points[i].out, points[i + 1].position + points[i + 1].in, points[i + 1].position, mid);
- d = position.distance_to(npp);
-
- if (bake_interval < d) {
- hi = mid;
- } else {
- low = mid;
- }
- mid = low + (hi - low) * 0.5;
- }
-
- position = npp;
- p = mid;
- dist += d;
+ // Tesselate curve to (almost) even length segments
+ {
+ Vector<RBMap<real_t, Vector2>> midpoints = _tessellate_even_length(10, bake_interval);
- pointlist.push_back(position);
- distlist.push_back(dist);
- } else {
- p = np;
- }
+ int pc = 1;
+ for (int i = 0; i < points.size() - 1; i++) {
+ pc++;
+ pc += midpoints[i].size();
}
- Vector2 npp = points[i + 1].position;
- real_t d = position.distance_to(npp);
+ baked_point_cache.resize(pc);
+ baked_dist_cache.resize(pc);
+ baked_forward_vector_cache.resize(pc);
- position = npp;
- dist += d;
+ Vector2 *bpw = baked_point_cache.ptrw();
+ Vector2 *bfw = baked_forward_vector_cache.ptrw();
- pointlist.push_back(position);
- distlist.push_back(dist);
- }
-
- baked_max_ofs = dist;
+ // Collect positions and sample tilts and tangents for each baked points.
+ bpw[0] = points[0].position;
+ bfw[0] = points[0].position.bezier_derivative(points[0].position + points[0].out, points[1].position + points[1].in, points[1].position, 0.0).normalized();
+ int pidx = 0;
- baked_point_cache.resize(pointlist.size());
- baked_dist_cache.resize(distlist.size());
+ for (int i = 0; i < points.size() - 1; i++) {
+ for (const KeyValue<real_t, Vector2> &E : midpoints[i]) {
+ pidx++;
+ bpw[pidx] = E.value;
+ bfw[pidx] = points[i].position.bezier_derivative(points[i].position + points[i].out, points[i + 1].position + points[i + 1].in, points[i + 1].position, E.key).normalized();
+ }
- Vector2 *w = baked_point_cache.ptrw();
- real_t *wd = baked_dist_cache.ptrw();
+ pidx++;
+ bpw[pidx] = points[i + 1].position;
+ bfw[pidx] = points[i].position.bezier_derivative(points[i].position + points[i].out, points[i + 1].position + points[i + 1].in, points[i + 1].position, 1.0).normalized();
+ }
- for (int i = 0; i < pointlist.size(); i++) {
- w[i] = pointlist[i];
- wd[i] = distlist[i];
+ // Recalculate the baked distances.
+ real_t *bdw = baked_dist_cache.ptrw();
+ bdw[0] = 0.0;
+ for (int i = 0; i < pc - 1; i++) {
+ bdw[i + 1] = bdw[i] + bpw[i].distance_to(bpw[i + 1]);
+ }
+ baked_max_ofs = bdw[pc - 1];
}
}
@@ -884,27 +868,15 @@ real_t Curve2D::get_baked_length() const {
return baked_max_ofs;
}
-Vector2 Curve2D::sample_baked(real_t p_offset, bool p_cubic) const {
- if (baked_cache_dirty) {
- _bake();
- }
+Curve2D::Interval Curve2D::_find_interval(real_t p_offset) const {
+ Interval interval = {
+ -1,
+ 0.0
+ };
+ ERR_FAIL_COND_V_MSG(baked_cache_dirty, interval, "Backed cache is dirty");
- // Validate: Curve may not have baked points.
int pc = baked_point_cache.size();
- ERR_FAIL_COND_V_MSG(pc == 0, Vector2(), "No points in Curve2D.");
-
- if (pc == 1) {
- return baked_point_cache.get(0);
- }
-
- const Vector2 *r = baked_point_cache.ptr();
-
- if (p_offset < 0) {
- return r[0];
- }
- if (p_offset >= baked_max_ofs) {
- return r[pc - 1];
- }
+ ERR_FAIL_COND_V_MSG(pc < 2, interval, "Less than two points in cache");
int start = 0;
int end = pc;
@@ -924,9 +896,27 @@ Vector2 Curve2D::sample_baked(real_t p_offset, bool p_cubic) const {
real_t offset_end = baked_dist_cache[idx + 1];
real_t idx_interval = offset_end - offset_begin;
- ERR_FAIL_COND_V_MSG(p_offset < offset_begin || p_offset > offset_end, Vector2(), "Couldn't find baked segment.");
+ ERR_FAIL_COND_V_MSG(p_offset < offset_begin || p_offset > offset_end, interval, "Offset out of range.");
- real_t frac = (p_offset - offset_begin) / idx_interval;
+ interval.idx = idx;
+ if (idx_interval < FLT_EPSILON) {
+ interval.frac = 0.5; // For a very short interval, 0.5 is a reasonable choice.
+ ERR_FAIL_V_MSG(interval, "Zero length interval.");
+ }
+
+ interval.frac = (p_offset - offset_begin) / idx_interval;
+ return interval;
+}
+
+Vector2 Curve2D::_sample_baked(Interval p_interval, bool p_cubic) const {
+ // Assuming p_interval is valid.
+ ERR_FAIL_INDEX_V_MSG(p_interval.idx, baked_point_cache.size(), Vector2(), "Invalid interval");
+
+ int idx = p_interval.idx;
+ real_t frac = p_interval.frac;
+
+ const Vector2 *r = baked_point_cache.ptr();
+ int pc = baked_point_cache.size();
if (p_cubic) {
Vector2 pre = idx > 0 ? r[idx - 1] : r[idx];
@@ -937,44 +927,70 @@ Vector2 Curve2D::sample_baked(real_t p_offset, bool p_cubic) const {
}
}
-Transform2D Curve2D::sample_baked_with_rotation(real_t p_offset, bool p_cubic, bool p_loop, real_t p_lookahead) const {
- real_t path_length = get_baked_length(); // Ensure baked.
- ERR_FAIL_COND_V_MSG(path_length == 0, Transform2D(), "Length of Curve2D is 0.");
+Transform2D Curve2D::_sample_posture(Interval p_interval) const {
+ // Assuming that p_interval is valid.
+ ERR_FAIL_INDEX_V_MSG(p_interval.idx, baked_point_cache.size(), Transform2D(), "Invalid interval");
- Vector2 pos = sample_baked(p_offset, p_cubic);
+ int idx = p_interval.idx;
+ real_t frac = p_interval.frac;
- real_t ahead = p_offset + p_lookahead;
+ Vector2 forward_begin = baked_forward_vector_cache[idx];
+ Vector2 forward_end = baked_forward_vector_cache[idx + 1];
- if (p_loop && ahead >= path_length) {
- // If our lookahead will loop, we need to check if the path is closed.
- int point_count = get_point_count();
- if (point_count > 0) {
- Vector2 start_point = get_point_position(0);
- Vector2 end_point = get_point_position(point_count - 1);
- if (start_point == end_point) {
- // Since the path is closed we want to 'smooth off'
- // the corner at the start/end.
- // So we wrap the lookahead back round.
- ahead = Math::fmod(ahead, path_length);
- }
- }
+ // Build frames at both ends of the interval, then interpolate.
+ const Vector2 forward = forward_begin.slerp(forward_end, frac).normalized();
+ const Vector2 side = Vector2(-forward.y, forward.x);
+
+ return Transform2D(forward, side, Vector2(0.0, 0.0));
+}
+
+Vector2 Curve2D::sample_baked(real_t p_offset, bool p_cubic) const {
+ if (baked_cache_dirty) {
+ _bake();
}
- Vector2 ahead_pos = sample_baked(ahead, p_cubic);
+ // Validate: Curve may not have baked points.
+ int pc = baked_point_cache.size();
+ ERR_FAIL_COND_V_MSG(pc == 0, Vector2(), "No points in Curve2D.");
- Vector2 tangent_to_curve;
- if (ahead_pos == pos) {
- // This will happen at the end of non-looping or non-closed paths.
- // We'll try a look behind instead, in order to get a meaningful angle.
- tangent_to_curve =
- (pos - sample_baked(p_offset - p_lookahead, p_cubic)).normalized();
- } else {
- tangent_to_curve = (ahead_pos - pos).normalized();
+ if (pc == 1) {
+ return baked_point_cache[0];
+ }
+
+ p_offset = CLAMP(p_offset, 0.0, get_baked_length()); // PathFollower implement wrapping logic.
+
+ Curve2D::Interval interval = _find_interval(p_offset);
+ return _sample_baked(interval, p_cubic);
+}
+
+Transform2D Curve2D::sample_baked_with_rotation(real_t p_offset, bool p_cubic) const {
+ if (baked_cache_dirty) {
+ _bake();
}
- Vector2 normal_of_curve = -tangent_to_curve.orthogonal();
+ // Validate: Curve may not have baked points.
+ const int point_count = baked_point_cache.size();
+ ERR_FAIL_COND_V_MSG(point_count == 0, Transform2D(), "No points in Curve3D.");
+
+ if (point_count == 1) {
+ Transform2D t;
+ t.set_origin(baked_point_cache.get(0));
+ ERR_FAIL_V_MSG(t, "Only 1 point in Curve2D.");
+ }
+
+ p_offset = CLAMP(p_offset, 0.0, get_baked_length()); // PathFollower implement wrapping logic.
+
+ // 0. Find interval for all sampling steps.
+ Curve2D::Interval interval = _find_interval(p_offset);
+
+ // 1. Sample position.
+ Vector2 pos = _sample_baked(interval, p_cubic);
- return Transform2D(normal_of_curve, tangent_to_curve, pos);
+ // 2. Sample rotation frame.
+ Transform2D frame = _sample_posture(interval);
+ frame.set_origin(pos);
+
+ return frame;
}
PackedVector2Array Curve2D::get_baked_points() const {
@@ -1147,6 +1163,50 @@ PackedVector2Array Curve2D::tessellate(int p_max_stages, real_t p_tolerance) con
return tess;
}
+Vector<RBMap<real_t, Vector2>> Curve2D::_tessellate_even_length(int p_max_stages, real_t p_length) const {
+ Vector<RBMap<real_t, Vector2>> midpoints;
+ ERR_FAIL_COND_V_MSG(points.size() < 2, midpoints, "Curve must have at least 2 control point");
+
+ midpoints.resize(points.size() - 1);
+
+ for (int i = 0; i < points.size() - 1; i++) {
+ _bake_segment2d_even_length(midpoints.write[i], 0, 1, points[i].position, points[i].out, points[i + 1].position, points[i + 1].in, 0, p_max_stages, p_length);
+ }
+ return midpoints;
+}
+
+PackedVector2Array Curve2D::tessellate_even_length(int p_max_stages, real_t p_length) const {
+ PackedVector2Array tess;
+
+ Vector<RBMap<real_t, Vector2>> midpoints = _tessellate_even_length(p_max_stages, p_length);
+ if (midpoints.size() == 0) {
+ return tess;
+ }
+
+ int pc = 1;
+ for (int i = 0; i < points.size() - 1; i++) {
+ pc++;
+ pc += midpoints[i].size();
+ }
+
+ tess.resize(pc);
+ Vector2 *bpw = tess.ptrw();
+ bpw[0] = points[0].position;
+ int pidx = 0;
+
+ for (int i = 0; i < points.size() - 1; i++) {
+ for (const KeyValue<real_t, Vector2> &E : midpoints[i]) {
+ pidx++;
+ bpw[pidx] = E.value;
+ }
+
+ pidx++;
+ bpw[pidx] = points[i + 1].position;
+ }
+
+ return tess;
+}
+
bool Curve2D::_set(const StringName &p_name, const Variant &p_value) {
Vector<String> components = String(p_name).split("/", true, 2);
if (components.size() >= 2 && components[0].begins_with("point_") && components[0].trim_prefix("point_").is_valid_int()) {
@@ -1224,12 +1284,13 @@ void Curve2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_bake_interval"), &Curve2D::get_bake_interval);
ClassDB::bind_method(D_METHOD("get_baked_length"), &Curve2D::get_baked_length);
- ClassDB::bind_method(D_METHOD("sample_baked", "offset", "cubic"), &Curve2D::sample_baked, DEFVAL(false));
- ClassDB::bind_method(D_METHOD("sample_baked_with_rotation", "offset", "cubic", "loop", "lookahead"), &Curve2D::sample_baked_with_rotation, DEFVAL(false), DEFVAL(true), DEFVAL(4.0));
+ ClassDB::bind_method(D_METHOD("sample_baked", "offset", "cubic"), &Curve2D::sample_baked, DEFVAL(0.0), DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("sample_baked_with_rotation", "offset", "cubic"), &Curve2D::sample_baked_with_rotation, DEFVAL(0.0), DEFVAL(false));
ClassDB::bind_method(D_METHOD("get_baked_points"), &Curve2D::get_baked_points);
ClassDB::bind_method(D_METHOD("get_closest_point", "to_point"), &Curve2D::get_closest_point);
ClassDB::bind_method(D_METHOD("get_closest_offset", "to_point"), &Curve2D::get_closest_offset);
ClassDB::bind_method(D_METHOD("tessellate", "max_stages", "tolerance_degrees"), &Curve2D::tessellate, DEFVAL(5), DEFVAL(4));
+ ClassDB::bind_method(D_METHOD("tessellate_even_length", "max_stages", "tolerance_length"), &Curve2D::tessellate_even_length, DEFVAL(5), DEFVAL(20.0));
ClassDB::bind_method(D_METHOD("_get_data"), &Curve2D::_get_data);
ClassDB::bind_method(D_METHOD("_set_data", "data"), &Curve2D::_set_data);
@@ -2133,8 +2194,8 @@ void Curve3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("is_up_vector_enabled"), &Curve3D::is_up_vector_enabled);
ClassDB::bind_method(D_METHOD("get_baked_length"), &Curve3D::get_baked_length);
- ClassDB::bind_method(D_METHOD("sample_baked", "offset", "cubic"), &Curve3D::sample_baked, DEFVAL(false));
- ClassDB::bind_method(D_METHOD("sample_baked_with_rotation", "offset", "cubic", "apply_tilt"), &Curve3D::sample_baked_with_rotation, DEFVAL(false), DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("sample_baked", "offset", "cubic"), &Curve3D::sample_baked, DEFVAL(0.0), DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("sample_baked_with_rotation", "offset", "cubic", "apply_tilt"), &Curve3D::sample_baked_with_rotation, DEFVAL(0.0), DEFVAL(false), DEFVAL(false));
ClassDB::bind_method(D_METHOD("sample_baked_up_vector", "offset", "apply_tilt"), &Curve3D::sample_baked_up_vector, DEFVAL(false));
ClassDB::bind_method(D_METHOD("get_baked_points"), &Curve3D::get_baked_points);
ClassDB::bind_method(D_METHOD("get_baked_tilts"), &Curve3D::get_baked_tilts);
diff --git a/scene/resources/curve.h b/scene/resources/curve.h
index 88c3cf3ae6..ea3ceabb14 100644
--- a/scene/resources/curve.h
+++ b/scene/resources/curve.h
@@ -172,6 +172,7 @@ class Curve2D : public Resource {
mutable bool baked_cache_dirty = false;
mutable PackedVector2Array baked_point_cache;
+ mutable PackedVector2Array baked_forward_vector_cache;
mutable Vector<real_t> baked_dist_cache;
mutable real_t baked_max_ofs = 0.0;
@@ -181,7 +182,16 @@ class Curve2D : public Resource {
real_t bake_interval = 5.0;
+ struct Interval {
+ int idx;
+ real_t frac;
+ };
+ Interval _find_interval(real_t p_offset) const;
+ Vector2 _sample_baked(Interval p_interval, bool p_cubic) const;
+ Transform2D _sample_posture(Interval p_interval) const;
+
void _bake_segment2d(RBMap<real_t, Vector2> &r_bake, real_t p_begin, real_t p_end, const Vector2 &p_a, const Vector2 &p_out, const Vector2 &p_b, const Vector2 &p_in, int p_depth, int p_max_depth, real_t p_tol) const;
+ void _bake_segment2d_even_length(RBMap<real_t, Vector2> &r_bake, real_t p_begin, real_t p_end, const Vector2 &p_a, const Vector2 &p_out, const Vector2 &p_b, const Vector2 &p_in, int p_depth, int p_max_depth, real_t p_length) const;
Dictionary _get_data() const;
void _set_data(const Dictionary &p_data);
@@ -192,6 +202,8 @@ class Curve2D : public Resource {
void _add_point(const Vector2 &p_position, const Vector2 &p_in = Vector2(), const Vector2 &p_out = Vector2(), int p_atpos = -1);
void _remove_point(int p_index);
+ Vector<RBMap<real_t, Vector2>> _tessellate_even_length(int p_max_stages = 5, real_t p_length = 0.2) const;
+
protected:
static void _bind_methods();
@@ -216,12 +228,13 @@ public:
real_t get_baked_length() const;
Vector2 sample_baked(real_t p_offset, bool p_cubic = false) const;
- Transform2D sample_baked_with_rotation(real_t p_offset, bool p_cubic = false, bool p_loop = true, real_t p_lookahead = 4.0) const;
+ Transform2D sample_baked_with_rotation(real_t p_offset, bool p_cubic = false) const;
PackedVector2Array get_baked_points() const; //useful for going through
Vector2 get_closest_point(const Vector2 &p_to_point) const;
real_t get_closest_offset(const Vector2 &p_to_point) const;
PackedVector2Array tessellate(int p_max_stages = 5, real_t p_tolerance = 4) const; //useful for display
+ PackedVector2Array tessellate_even_length(int p_max_stages = 5, real_t p_length = 20.0) const; // Useful for baking.
Curve2D();
};
diff --git a/tests/scene/test_curve.h b/tests/scene/test_curve.h
index 36ec0c0a4d..4afd5b293f 100644
--- a/tests/scene/test_curve.h
+++ b/tests/scene/test_curve.h
@@ -31,6 +31,7 @@
#ifndef TEST_CURVE_H
#define TEST_CURVE_H
+#include "core/math/math_funcs.h"
#include "scene/resources/curve.h"
#include "tests/test_macros.h"
@@ -229,7 +230,7 @@ TEST_CASE("[Curve2D] Linear sampling should return exact value") {
for (int i = 0; i < len; i++) {
Vector2 pos = curve->sample_baked(i);
- CHECK_MESSAGE(pos.x == i, "sample_baked should return exact value");
+ CHECK_MESSAGE(Math::is_equal_approx(pos.x, i), "sample_baked should return exact value");
}
}
@@ -245,7 +246,7 @@ TEST_CASE("[Curve3D] Linear sampling should return exact value") {
for (int i = 0; i < len; i++) {
Vector3 pos = curve->sample_baked(i);
- CHECK_MESSAGE(pos.x == i, "sample_baked should return exact value");
+ CHECK_MESSAGE(Math::is_equal_approx(pos.x, i), "sample_baked should return exact value");
}
}