diff --git a/doc/classes/AnimationNodeOneShot.xml b/doc/classes/AnimationNodeOneShot.xml
index b2a8002d742..28bea47e219 100644
--- a/doc/classes/AnimationNodeOneShot.xml
+++ b/doc/classes/AnimationNodeOneShot.xml
@@ -70,14 +70,14 @@
If [code]true[/code], breaks the loop at the end of the loop cycle for transition, even if the animation is looping.
- Determines how cross-fading between animations is eased. If empty, the transition will be linear.
+ Determines how cross-fading between animations is eased. If empty, the transition will be linear. Should be a unit [Curve].
The fade-in duration. For example, setting this to [code]1.0[/code] for a 5 second length animation will produce a cross-fade that starts at 0 second and ends at 1 second during the animation.
[b]Note:[/b] [AnimationNodeOneShot] transitions the current state after the end of the fading. When [AnimationNodeOutput] is considered as the most upstream, so the [member fadein_time] is scaled depending on the downstream delta. For example, if this value is set to [code]1.0[/code] and a [AnimationNodeTimeScale] with a value of [code]2.0[/code] is chained downstream, the actual processing time will be 0.5 second.
- Determines how cross-fading between animations is eased. If empty, the transition will be linear.
+ Determines how cross-fading between animations is eased. If empty, the transition will be linear. Should be a unit [Curve].
The fade-out duration. For example, setting this to [code]1.0[/code] for a 5 second length animation will produce a cross-fade that starts at 4 second and ends at 5 second during the animation.
diff --git a/doc/classes/AnimationNodeStateMachineTransition.xml b/doc/classes/AnimationNodeStateMachineTransition.xml
index c729eeebbae..d7dffa69162 100644
--- a/doc/classes/AnimationNodeStateMachineTransition.xml
+++ b/doc/classes/AnimationNodeStateMachineTransition.xml
@@ -41,7 +41,7 @@
The transition type.
- Ease curve for better control over cross-fade between this state and the next.
+ Ease curve for better control over cross-fade between this state and the next. Should be a unit [Curve].
The time to cross-fade between this state and the next.
diff --git a/doc/classes/AnimationNodeTransition.xml b/doc/classes/AnimationNodeTransition.xml
index 382166d823e..af80fef73a2 100644
--- a/doc/classes/AnimationNodeTransition.xml
+++ b/doc/classes/AnimationNodeTransition.xml
@@ -96,7 +96,7 @@
The number of enabled input ports for this animation node.
- Determines how cross-fading between animations is eased. If empty, the transition will be linear.
+ Determines how cross-fading between animations is eased. If empty, the transition will be linear. Should be a unit [Curve].
Cross-fading time (in seconds) between each animation connected to the inputs.
diff --git a/doc/classes/CPUParticles2D.xml b/doc/classes/CPUParticles2D.xml
index e8fa13fd0d2..be469080ed1 100644
--- a/doc/classes/CPUParticles2D.xml
+++ b/doc/classes/CPUParticles2D.xml
@@ -57,7 +57,7 @@
- Sets the [Curve] of the parameter specified by [enum Parameter].
+ Sets the [Curve] of the parameter specified by [enum Parameter]. Should be a unit [Curve].
@@ -90,7 +90,7 @@
Number of particles emitted in one emission cycle.
- Each particle's rotation will be animated along this [Curve].
+ Each particle's rotation will be animated along this [Curve]. Should be a unit [Curve].
Maximum initial rotation applied to each particle, in degrees.
@@ -99,7 +99,7 @@
Minimum equivalent of [member angle_max].
- Each particle's angular velocity will vary along this [Curve].
+ Each particle's angular velocity will vary along this [Curve]. Should be a unit [Curve].
Maximum initial angular velocity (rotation speed) applied to each particle in [i]degrees[/i] per second.
@@ -108,7 +108,7 @@
Minimum equivalent of [member angular_velocity_max].
- Each particle's animation offset will vary along this [Curve].
+ Each particle's animation offset will vary along this [Curve]. Should be a unit [Curve].
Maximum animation offset that corresponds to frame index in the texture. [code]0[/code] is the first frame, [code]1[/code] is the last one. See [member CanvasItemMaterial.particles_animation].
@@ -117,7 +117,7 @@
Minimum equivalent of [member anim_offset_max].
- Each particle's animation speed will vary along this [Curve].
+ Each particle's animation speed will vary along this [Curve]. Should be a unit [Curve].
Maximum particle animation speed. Animation speed of [code]1[/code] means that the particles will make full [code]0[/code] to [code]1[/code] offset cycle during lifetime, [code]2[/code] means [code]2[/code] cycles etc.
@@ -136,7 +136,7 @@
Each particle's color will vary along this [Gradient] (multiplied with [member color]).
- Damping will vary along this [Curve].
+ Damping will vary along this [Curve]. Should be a unit [Curve].
The maximum rate at which particles lose velocity. For example value of [code]100[/code] means that the particle will go from [code]100[/code] velocity to [code]0[/code] in [code]1[/code] second.
@@ -184,7 +184,7 @@
Gravity applied to every particle.
- Each particle's hue will vary along this [Curve].
+ Each particle's hue will vary along this [Curve]. Should be a unit [Curve].
Maximum initial hue variation applied to each particle. It will shift the particle color's hue.
@@ -205,7 +205,7 @@
Particle lifetime randomness ratio.
- Each particle's linear acceleration will vary along this [Curve].
+ Each particle's linear acceleration will vary along this [Curve]. Should be a unit [Curve].
Maximum linear acceleration applied to each particle in the direction of motion.
@@ -220,7 +220,7 @@
If [code]true[/code], only one emission cycle occurs. If set [code]true[/code] during a cycle, emission will stop at the cycle's end.
- Each particle's orbital velocity will vary along this [Curve].
+ Each particle's orbital velocity will vary along this [Curve]. Should be a unit [Curve].
Maximum orbital velocity applied to each particle. Makes the particles circle around origin. Specified in number of full rotations around origin per second.
@@ -235,7 +235,7 @@
Particle system starts as if it had already run for this many seconds.
- Each particle's radial acceleration will vary along this [Curve].
+ Each particle's radial acceleration will vary along this [Curve]. Should be a unit [Curve].
Maximum radial acceleration applied to each particle. Makes particle accelerate away from the origin or towards it if negative.
@@ -247,7 +247,7 @@
Emission lifetime randomness ratio.
- Each particle's scale will vary along this [Curve].
+ Each particle's scale will vary along this [Curve]. Should be a unit [Curve].
Maximum initial scale applied to each particle.
@@ -256,11 +256,11 @@
Minimum equivalent of [member scale_amount_max].
- Each particle's horizontal scale will vary along this [Curve].
+ Each particle's horizontal scale will vary along this [Curve]. Should be a unit [Curve].
[member split_scale] must be enabled.
- Each particle's vertical scale will vary along this [Curve].
+ Each particle's vertical scale will vary along this [Curve]. Should be a unit [Curve].
[member split_scale] must be enabled.
@@ -273,7 +273,7 @@
Each particle's initial direction range from [code]+spread[/code] to [code]-spread[/code] degrees.
- Each particle's tangential acceleration will vary along this [Curve].
+ Each particle's tangential acceleration will vary along this [Curve]. Should be a unit [Curve].
Maximum tangential acceleration applied to each particle. Tangential acceleration is perpendicular to the particle's velocity giving the particles a swirling motion.
diff --git a/doc/classes/CPUParticles3D.xml b/doc/classes/CPUParticles3D.xml
index 04ee95457c4..805299556a8 100644
--- a/doc/classes/CPUParticles3D.xml
+++ b/doc/classes/CPUParticles3D.xml
@@ -63,7 +63,7 @@
- Sets the [Curve] of the parameter specified by [enum Parameter].
+ Sets the [Curve] of the parameter specified by [enum Parameter]. Should be a unit [Curve].
@@ -96,7 +96,7 @@
Number of particles emitted in one emission cycle.
- Each particle's rotation will be animated along this [Curve].
+ Each particle's rotation will be animated along this [Curve]. Should be a unit [Curve].
Maximum angle.
@@ -105,7 +105,7 @@
Minimum angle.
- Each particle's angular velocity (rotation speed) will vary along this [Curve] over its lifetime.
+ Each particle's angular velocity (rotation speed) will vary along this [Curve] over its lifetime. Should be a unit [Curve].
Maximum initial angular velocity (rotation speed) applied to each particle in [i]degrees[/i] per second.
@@ -114,7 +114,7 @@
Minimum initial angular velocity (rotation speed) applied to each particle in [i]degrees[/i] per second.
- Each particle's animation offset will vary along this [Curve].
+ Each particle's animation offset will vary along this [Curve]. Should be a unit [Curve].
Maximum animation offset.
@@ -123,7 +123,7 @@
Minimum animation offset.
- Each particle's animation speed will vary along this [Curve].
+ Each particle's animation speed will vary along this [Curve]. Should be a unit [Curve].
Maximum particle animation speed.
@@ -144,7 +144,7 @@
[b]Note:[/b] [member color_ramp] multiplies the particle mesh's vertex colors. To have a visible effect on a [BaseMaterial3D], [member BaseMaterial3D.vertex_color_use_as_albedo] [i]must[/i] be [code]true[/code]. For a [ShaderMaterial], [code]ALBEDO *= COLOR.rgb;[/code] must be inserted in the shader's [code]fragment()[/code] function. Otherwise, [member color_ramp] will have no visible effect.
- Damping will vary along this [Curve].
+ Damping will vary along this [Curve]. Should be a unit [Curve].
Maximum damping.
@@ -212,7 +212,7 @@
Gravity applied to every particle.
- Each particle's hue will vary along this [Curve].
+ Each particle's hue will vary along this [Curve]. Should be a unit [Curve].
Maximum hue variation.
@@ -233,7 +233,7 @@
Particle lifetime randomness ratio.
- Each particle's linear acceleration will vary along this [Curve].
+ Each particle's linear acceleration will vary along this [Curve]. Should be a unit [Curve].
Maximum linear acceleration.
@@ -251,7 +251,7 @@
If [code]true[/code], only one emission cycle occurs. If set [code]true[/code] during a cycle, emission will stop at the cycle's end.
- Each particle's orbital velocity will vary along this [Curve].
+ Each particle's orbital velocity will vary along this [Curve]. Should be a unit [Curve].
Maximum orbit velocity.
@@ -272,7 +272,7 @@
Particle system starts as if it had already run for this many seconds.
- Each particle's radial acceleration will vary along this [Curve].
+ Each particle's radial acceleration will vary along this [Curve]. Should be a unit [Curve].
Maximum radial acceleration.
@@ -284,7 +284,7 @@
Emission lifetime randomness ratio.
- Each particle's scale will vary along this [Curve].
+ Each particle's scale will vary along this [Curve]. Should be a unit [Curve].
Maximum scale.
@@ -311,7 +311,7 @@
Each particle's initial direction range from [code]+spread[/code] to [code]-spread[/code] degrees. Applied to X/Z plane and Y/Z planes.
- Each particle's tangential acceleration will vary along this [Curve].
+ Each particle's tangential acceleration will vary along this [Curve]. Should be a unit [Curve].
Maximum tangent acceleration.
diff --git a/doc/classes/Curve.xml b/doc/classes/Curve.xml
index 25b06f10638..5246e0ba670 100644
--- a/doc/classes/Curve.xml
+++ b/doc/classes/Curve.xml
@@ -4,8 +4,8 @@
A mathematical curve.
- This resource describes a mathematical curve by defining a set of points and tangents at each point. By default, it ranges between [code]0[/code] and [code]1[/code] on the Y axis and positions points relative to the [code]0.5[/code] Y position.
- See also [Gradient] which is designed for color interpolation. See also [Curve2D] and [Curve3D].
+ This resource describes a mathematical curve by defining a set of points and tangents at each point. By default, it ranges between [code]0[/code] and [code]1[/code] on the X and Y axes, but these ranges can be changed.
+ Please note that many resources and nodes assume they are given [i]unit curves[/i]. A unit curve is a curve whose domain (the X axis) is between [code]0[/code] and [code]1[/code]. Some examples of unit curve usage are [member CPUParticles2D.angle_curve] and [member Line2D.width_curve].
@@ -39,6 +39,12 @@
Removes all points from the curve.
+
+
+
+ Returns the difference between [member min_domain] and [member max_domain].
+
+
@@ -74,6 +80,12 @@
Returns the right tangent angle (in degrees) for the point at [param index].
+
+
+
+ Returns the difference between [member min_value] and [member max_value].
+
+
@@ -148,17 +160,28 @@
The number of points to include in the baked (i.e. cached) curve data.
+
+ The maximum domain (x-coordinate) that points can have.
+
- The maximum value the curve can reach.
+ The maximum value (y-coordinate) that points can have. Tangents can cause higher values between points.
+
+
+ The minimum domain (x-coordinate) that points can have.
- The minimum value the curve can reach.
+ The minimum value (y-coordinate) that points can have. Tangents can cause lower values between points.
The number of points describing the curve.
+
+
+ Emitted when [member max_domain] or [member min_domain] is changed.
+
+
Emitted when [member max_value] or [member min_value] is changed.
diff --git a/doc/classes/CurveTexture.xml b/doc/classes/CurveTexture.xml
index 8cb2384da3b..ed4a2f2d357 100644
--- a/doc/classes/CurveTexture.xml
+++ b/doc/classes/CurveTexture.xml
@@ -4,14 +4,14 @@
A 1D texture where pixel brightness corresponds to points on a curve.
- A 1D texture where pixel brightness corresponds to points on a [Curve] resource, either in grayscale or in red. This visual representation simplifies the task of saving curves as image files.
+ A 1D texture where pixel brightness corresponds to points on a unit [Curve] resource, either in grayscale or in red. This visual representation simplifies the task of saving curves as image files.
If you need to store up to 3 curves within a single texture, use [CurveXYZTexture] instead. See also [GradientTexture1D] and [GradientTexture2D].
- The [Curve] that is rendered onto the texture.
+ The [Curve] that is rendered onto the texture. Should be a unit [Curve].
diff --git a/doc/classes/CurveXYZTexture.xml b/doc/classes/CurveXYZTexture.xml
index 8353ed90920..472ce7bb99a 100644
--- a/doc/classes/CurveXYZTexture.xml
+++ b/doc/classes/CurveXYZTexture.xml
@@ -4,20 +4,20 @@
A 1D texture where the red, green, and blue color channels correspond to points on 3 curves.
- A 1D texture where the red, green, and blue color channels correspond to points on 3 [Curve] resources. Compared to using separate [CurveTexture]s, this further simplifies the task of saving curves as image files.
+ A 1D texture where the red, green, and blue color channels correspond to points on 3 unit [Curve] resources. Compared to using separate [CurveTexture]s, this further simplifies the task of saving curves as image files.
If you only need to store one curve within a single texture, use [CurveTexture] instead. See also [GradientTexture1D] and [GradientTexture2D].
- The [Curve] that is rendered onto the texture's red channel.
+ The [Curve] that is rendered onto the texture's red channel. Should be a unit [Curve].
- The [Curve] that is rendered onto the texture's green channel.
+ The [Curve] that is rendered onto the texture's green channel. Should be a unit [Curve].
- The [Curve] that is rendered onto the texture's blue channel.
+ The [Curve] that is rendered onto the texture's blue channel. Should be a unit [Curve].
diff --git a/doc/classes/Line2D.xml b/doc/classes/Line2D.xml
index a553e79746e..825462d21a7 100644
--- a/doc/classes/Line2D.xml
+++ b/doc/classes/Line2D.xml
@@ -101,7 +101,7 @@
The polyline's width.
- The polyline's width curve. The width of the polyline over its length will be equivalent to the value of the width curve over its domain.
+ The polyline's width curve. The width of the polyline over its length will be equivalent to the value of the width curve over its domain. The width curve should be a unit [Curve].
diff --git a/doc/classes/RibbonTrailMesh.xml b/doc/classes/RibbonTrailMesh.xml
index 74523f3c390..113787d3a4e 100644
--- a/doc/classes/RibbonTrailMesh.xml
+++ b/doc/classes/RibbonTrailMesh.xml
@@ -13,7 +13,7 @@
- Determines the size of the ribbon along its length. The size of a particular section segment is obtained by multiplying the baseline [member size] by the value of this curve at the given distance. For values smaller than [code]0[/code], the faces will be inverted.
+ Determines the size of the ribbon along its length. The size of a particular section segment is obtained by multiplying the baseline [member size] by the value of this curve at the given distance. For values smaller than [code]0[/code], the faces will be inverted. Should be a unit [Curve].
The length of a section of the ribbon.
diff --git a/doc/classes/TubeTrailMesh.xml b/doc/classes/TubeTrailMesh.xml
index bf16b3d16af..4408280f42a 100644
--- a/doc/classes/TubeTrailMesh.xml
+++ b/doc/classes/TubeTrailMesh.xml
@@ -19,7 +19,7 @@
If [code]true[/code], generates a cap at the top of the tube. This can be set to [code]false[/code] to speed up generation and rendering when the cap is never seen by the camera.
- Determines the radius of the tube along its length. The radius of a particular section ring is obtained by multiplying the baseline [member radius] by the value of this curve at the given distance. For values smaller than [code]0[/code], the faces will be inverted.
+ Determines the radius of the tube along its length. The radius of a particular section ring is obtained by multiplying the baseline [member radius] by the value of this curve at the given distance. For values smaller than [code]0[/code], the faces will be inverted. Should be a unit [Curve].
The number of sides on the tube. For example, a value of [code]5[/code] means the tube will be pentagonal. Higher values result in a more detailed tube at the cost of performance.
diff --git a/editor/plugins/curve_editor_plugin.cpp b/editor/plugins/curve_editor_plugin.cpp
index 67006af44b4..aaf7c427f0f 100644
--- a/editor/plugins/curve_editor_plugin.cpp
+++ b/editor/plugins/curve_editor_plugin.cpp
@@ -64,6 +64,7 @@ void CurveEdit::set_curve(Ref p_curve) {
if (curve.is_valid()) {
curve->disconnect_changed(callable_mp(this, &CurveEdit::_curve_changed));
curve->disconnect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
+ curve->disconnect(Curve::SIGNAL_DOMAIN_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
}
curve = p_curve;
@@ -71,6 +72,7 @@ void CurveEdit::set_curve(Ref p_curve) {
if (curve.is_valid()) {
curve->connect_changed(callable_mp(this, &CurveEdit::_curve_changed));
curve->connect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
+ curve->connect(Curve::SIGNAL_DOMAIN_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
}
// Note: if you edit a curve, then set another, and try to undo,
@@ -226,10 +228,10 @@ void CurveEdit::gui_input(const Ref &p_event) {
}
} else if (grabbing == GRAB_NONE) {
// Adding a new point. Insert a temporary point for the user to adjust, so it's not in the undo/redo.
- Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(0.0, curve->get_min_value()), Vector2(1.0, curve->get_max_value()));
+ Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(curve->get_min_domain(), curve->get_min_value()), Vector2(curve->get_max_domain(), curve->get_max_value()));
if (snap_enabled || mb->is_command_or_control_pressed()) {
- new_pos.x = Math::snapped(new_pos.x, 1.0 / snap_count);
- new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_range() / snap_count) + curve->get_min_value();
+ new_pos.x = Math::snapped(new_pos.x - curve->get_min_domain(), curve->get_domain_range() / snap_count) + curve->get_min_domain();
+ new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_value_range() / snap_count) + curve->get_min_value();
}
new_pos.x = get_offset_without_collision(selected_index, new_pos.x, mpos.x >= get_view_pos(new_pos).x);
@@ -276,11 +278,11 @@ void CurveEdit::gui_input(const Ref &p_event) {
if (selected_index != -1) {
if (selected_tangent_index == TANGENT_NONE) {
// Drag point.
- Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(0.0, curve->get_min_value()), Vector2(1.0, curve->get_max_value()));
+ Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(curve->get_min_domain(), curve->get_min_value()), Vector2(curve->get_max_domain(), curve->get_max_value()));
if (snap_enabled || mm->is_command_or_control_pressed()) {
- new_pos.x = Math::snapped(new_pos.x, 1.0 / snap_count);
- new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_range() / snap_count) + curve->get_min_value();
+ new_pos.x = Math::snapped(new_pos.x - curve->get_min_domain(), curve->get_domain_range() / snap_count) + curve->get_min_domain();
+ new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_value_range() / snap_count) + curve->get_min_value();
}
// Allow to snap to axes with Shift.
@@ -295,8 +297,8 @@ void CurveEdit::gui_input(const Ref &p_event) {
// Allow to constraint the point between the adjacent two with Alt.
if (mm->is_alt_pressed()) {
- float prev_point_offset = (selected_index > 0) ? (curve->get_point_position(selected_index - 1).x + 0.00001) : 0.0;
- float next_point_offset = (selected_index < curve->get_point_count() - 1) ? (curve->get_point_position(selected_index + 1).x - 0.00001) : 1.0;
+ float prev_point_offset = (selected_index > 0) ? (curve->get_point_position(selected_index - 1).x + 0.00001) : curve->get_min_domain();
+ float next_point_offset = (selected_index < curve->get_point_count() - 1) ? (curve->get_point_position(selected_index + 1).x - 0.00001) : curve->get_max_domain();
new_pos.x = CLAMP(new_pos.x, prev_point_offset, next_point_offset);
}
@@ -357,37 +359,39 @@ void CurveEdit::use_preset(int p_preset_id) {
Array previous_data = curve->get_data();
curve->clear_points();
- float min_value = curve->get_min_value();
- float max_value = curve->get_max_value();
+ const float min_y = curve->get_min_value();
+ const float max_y = curve->get_max_value();
+ const float min_x = curve->get_min_domain();
+ const float max_x = curve->get_max_domain();
switch (p_preset_id) {
case PRESET_CONSTANT:
- curve->add_point(Vector2(0, (min_value + max_value) / 2.0));
- curve->add_point(Vector2(1, (min_value + max_value) / 2.0));
+ curve->add_point(Vector2(min_x, (min_y + max_y) / 2.0));
+ curve->add_point(Vector2(max_x, (min_y + max_y) / 2.0));
curve->set_point_right_mode(0, Curve::TANGENT_LINEAR);
curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
break;
case PRESET_LINEAR:
- curve->add_point(Vector2(0, min_value));
- curve->add_point(Vector2(1, max_value));
+ curve->add_point(Vector2(min_x, min_y));
+ curve->add_point(Vector2(max_x, max_y));
curve->set_point_right_mode(0, Curve::TANGENT_LINEAR);
curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
break;
case PRESET_EASE_IN:
- curve->add_point(Vector2(0, min_value));
- curve->add_point(Vector2(1, max_value), curve->get_range() * 1.4, 0);
+ curve->add_point(Vector2(min_x, min_y));
+ curve->add_point(Vector2(max_x, max_y), curve->get_value_range() / curve->get_domain_range() * 1.4, 0);
break;
case PRESET_EASE_OUT:
- curve->add_point(Vector2(0, min_value), 0, curve->get_range() * 1.4);
- curve->add_point(Vector2(1, max_value));
+ curve->add_point(Vector2(min_x, min_y), 0, curve->get_value_range() / curve->get_domain_range() * 1.4);
+ curve->add_point(Vector2(max_x, max_y));
break;
case PRESET_SMOOTHSTEP:
- curve->add_point(Vector2(0, min_value));
- curve->add_point(Vector2(1, max_value));
+ curve->add_point(Vector2(min_x, min_y));
+ curve->add_point(Vector2(max_x, max_y));
break;
default:
@@ -411,7 +415,7 @@ void CurveEdit::_curve_changed() {
}
}
-int CurveEdit::get_point_at(Vector2 p_pos) const {
+int CurveEdit::get_point_at(const Vector2 &p_pos) const {
if (curve.is_null()) {
return -1;
}
@@ -432,7 +436,7 @@ int CurveEdit::get_point_at(Vector2 p_pos) const {
return closest_idx;
}
-CurveEdit::TangentIndex CurveEdit::get_tangent_at(Vector2 p_pos) const {
+CurveEdit::TangentIndex CurveEdit::get_tangent_at(const Vector2 &p_pos) const {
if (curve.is_null() || selected_index < 0) {
return TANGENT_NONE;
}
@@ -491,7 +495,7 @@ float CurveEdit::get_offset_without_collision(int p_current_index, float p_offse
return safe_offset;
}
-void CurveEdit::add_point(Vector2 p_pos) {
+void CurveEdit::add_point(const Vector2 &p_pos) {
ERR_FAIL_COND(curve.is_null());
// Add a point to get its index, then remove it immediately. Trick to feed the UndoRedo.
@@ -531,7 +535,7 @@ void CurveEdit::remove_point(int p_index) {
undo_redo->commit_action();
}
-void CurveEdit::set_point_position(int p_index, Vector2 p_pos) {
+void CurveEdit::set_point_position(int p_index, const Vector2 &p_pos) {
ERR_FAIL_COND(curve.is_null());
ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
@@ -657,10 +661,12 @@ void CurveEdit::update_view_transform() {
const real_t margin = font->get_height(font_size) + 2 * EDSCALE;
+ float min_x = curve.is_valid() ? curve->get_min_domain() : 0.0;
+ float max_x = curve.is_valid() ? curve->get_max_domain() : 1.0;
float min_y = curve.is_valid() ? curve->get_min_value() : 0.0;
float max_y = curve.is_valid() ? curve->get_max_value() : 1.0;
- const Rect2 world_rect = Rect2(Curve::MIN_X, min_y, Curve::MAX_X, max_y - min_y);
+ const Rect2 world_rect = Rect2(min_x, min_y, max_x - min_x, max_y - min_y);
const Size2 view_margin(margin, margin);
const Size2 view_size = get_size() - view_margin * 2;
const Vector2 scale = view_size / world_rect.size;
@@ -707,71 +713,57 @@ Vector2 CurveEdit::get_tangent_view_pos(int p_index, TangentIndex p_tangent) con
return tangent_view_pos;
}
-Vector2 CurveEdit::get_view_pos(Vector2 p_world_pos) const {
+Vector2 CurveEdit::get_view_pos(const Vector2 &p_world_pos) const {
return _world_to_view.xform(p_world_pos);
}
-Vector2 CurveEdit::get_world_pos(Vector2 p_view_pos) const {
+Vector2 CurveEdit::get_world_pos(const Vector2 &p_view_pos) const {
return _world_to_view.affine_inverse().xform(p_view_pos);
}
// Uses non-baked points, but takes advantage of ordered iteration to be faster.
-template
-static void plot_curve_accurate(const Curve &curve, float step, Vector2 scaling, T plot_func) {
- if (curve.get_point_count() <= 1) {
- // Not enough points to make a curve, so it's just a straight line.
- // The added tiny vectors make the drawn line stay exactly within the bounds in practice.
- float y = curve.sample(0);
- plot_func(Vector2(0, y) * scaling + Vector2(0.5, 0), Vector2(1.f, y) * scaling - Vector2(1.5, 0), true);
+void CurveEdit::plot_curve_accurate(float p_step, const Color &p_line_color, const Color &p_edge_line_color) {
+ const real_t min_x = curve->get_min_domain();
+ const real_t max_x = curve->get_max_domain();
+ if (curve->get_point_count() <= 1) { // Draw single line through entire plot.
+ real_t y = curve->sample(0);
+ draw_line(get_view_pos(Vector2(min_x, y)) + Vector2(0.5, 0), get_view_pos(Vector2(max_x, y)) - Vector2(1.5, 0), p_line_color, LINE_WIDTH, true);
+ return;
+ }
- } else {
- Vector2 first_point = curve.get_point_position(0);
- Vector2 last_point = curve.get_point_position(curve.get_point_count() - 1);
+ Vector2 first_point = curve->get_point_position(0);
+ Vector2 last_point = curve->get_point_position(curve->get_point_count() - 1);
- // Edge lines
- plot_func(Vector2(0, first_point.y) * scaling + Vector2(0.5, 0), first_point * scaling, false);
- plot_func(Vector2(Curve::MAX_X, last_point.y) * scaling - Vector2(1.5, 0), last_point * scaling, false);
+ // Transform pixels-per-step into curve domain. Only works for non-rotated transforms.
+ const float world_step_size = p_step / _world_to_view.get_scale().x;
- // Draw section by section, so that we get maximum precision near points.
- // It's an accurate representation, but slower than using the baked one.
- for (int i = 1; i < curve.get_point_count(); ++i) {
- Vector2 a = curve.get_point_position(i - 1);
- Vector2 b = curve.get_point_position(i);
+ // Edge lines.
+ draw_line(get_view_pos(Vector2(min_x, first_point.y)) + Vector2(0.5, 0), get_view_pos(first_point), p_edge_line_color, LINE_WIDTH, true);
+ draw_line(get_view_pos(last_point), get_view_pos(Vector2(max_x, last_point.y)) - Vector2(1.5, 0), p_edge_line_color, LINE_WIDTH, true);
- Vector2 pos = a;
- Vector2 prev_pos = a;
+ // Draw section by section, so that we get maximum precision near points.
+ // It's an accurate representation, but slower than using the baked one.
+ for (int i = 1; i < curve->get_point_count(); ++i) {
+ Vector2 a = curve->get_point_position(i - 1);
+ Vector2 b = curve->get_point_position(i);
- float scaled_step = step / scaling.x;
- float samples = (b.x - a.x) / scaled_step;
+ Vector2 pos = a;
+ Vector2 prev_pos = a;
- for (int j = 1; j < samples; j++) {
- float x = j * scaled_step;
- pos.x = a.x + x;
- pos.y = curve.sample_local_nocheck(i - 1, x);
- plot_func(prev_pos * scaling, pos * scaling, true);
- prev_pos = pos;
- }
+ float samples = (b.x - a.x) / world_step_size;
- plot_func(prev_pos * scaling, b * scaling, true);
+ for (int j = 1; j < samples; j++) {
+ float x = j * world_step_size;
+ pos.x = a.x + x;
+ pos.y = curve->sample_local_nocheck(i - 1, x);
+ draw_line(get_view_pos(prev_pos), get_view_pos(pos), p_line_color, LINE_WIDTH, true);
+ prev_pos = pos;
}
+
+ draw_line(get_view_pos(prev_pos), get_view_pos(b), p_line_color, LINE_WIDTH, true);
}
}
-struct CanvasItemPlotCurve {
- CanvasItem &ci;
- Color color1;
- Color color2;
-
- CanvasItemPlotCurve(CanvasItem &p_ci, Color p_color1, Color p_color2) :
- ci(p_ci),
- color1(p_color1),
- color2(p_color2) {}
-
- void operator()(Vector2 pos0, Vector2 pos1, bool in_definition) {
- ci.draw_line(pos0, pos1, in_definition ? color1 : color2, 0.5, true);
- }
-};
-
void CurveEdit::_redraw() {
if (curve.is_null()) {
return;
@@ -784,7 +776,7 @@ void CurveEdit::_redraw() {
Vector2 view_size = get_rect().size;
draw_style_box(get_theme_stylebox(SceneStringName(panel), SNAME("Tree")), Rect2(Point2(), view_size));
- // Draw snapping grid, then primary grid.
+ // Draw primary grid.
draw_set_transform_matrix(_world_to_view);
Vector2 min_edge = get_world_pos(Vector2(0, view_size.y));
@@ -794,15 +786,15 @@ void CurveEdit::_redraw() {
const Color grid_color = get_theme_color(SNAME("mono_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.1);
const Vector2i grid_steps = Vector2i(4, 2);
- const Vector2 step_size = Vector2(1, curve->get_range()) / grid_steps;
+ const Vector2 step_size = Vector2(curve->get_domain_range(), curve->get_value_range()) / grid_steps;
draw_line(Vector2(min_edge.x, curve->get_min_value()), Vector2(max_edge.x, curve->get_min_value()), grid_color_primary);
draw_line(Vector2(max_edge.x, curve->get_max_value()), Vector2(min_edge.x, curve->get_max_value()), grid_color_primary);
- draw_line(Vector2(0, min_edge.y), Vector2(0, max_edge.y), grid_color_primary);
- draw_line(Vector2(1, max_edge.y), Vector2(1, min_edge.y), grid_color_primary);
+ draw_line(Vector2(curve->get_min_domain(), min_edge.y), Vector2(curve->get_min_domain(), max_edge.y), grid_color_primary);
+ draw_line(Vector2(curve->get_max_domain(), max_edge.y), Vector2(curve->get_max_domain(), min_edge.y), grid_color_primary);
for (int i = 1; i < grid_steps.x; i++) {
- real_t x = i * step_size.x;
+ real_t x = curve->get_min_domain() + i * step_size.x;
draw_line(Vector2(x, min_edge.y), Vector2(x, max_edge.y), grid_color);
}
@@ -819,30 +811,26 @@ void CurveEdit::_redraw() {
float font_height = font->get_height(font_size);
Color text_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
+ int pad = Math::round(2 * EDSCALE);
+
for (int i = 0; i <= grid_steps.x; ++i) {
- real_t x = i * step_size.x;
- draw_string(font, get_view_pos(Vector2(x - step_size.x / 2, curve->get_min_value())) + Vector2(0, font_height - Math::round(2 * EDSCALE)), String::num(x, 2), HORIZONTAL_ALIGNMENT_CENTER, get_view_pos(Vector2(step_size.x, 0)).x, font_size, text_color);
+ real_t x = curve->get_min_domain() + i * step_size.x;
+ draw_string(font, get_view_pos(Vector2(x, curve->get_min_value())) + Vector2(pad, font_height - pad), String::num(x, 2), HORIZONTAL_ALIGNMENT_CENTER, -1, font_size, text_color);
}
for (int i = 0; i <= grid_steps.y; ++i) {
real_t y = curve->get_min_value() + i * step_size.y;
- draw_string(font, get_view_pos(Vector2(0, y)) + Vector2(2, -2), String::num(y, 2), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
+ draw_string(font, get_view_pos(Vector2(curve->get_min_domain(), y)) + Vector2(pad, -pad), String::num(y, 2), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
}
- // Draw curve.
-
- // An unusual transform so we can offset the curve before scaling it up, allowing the curve to be antialiased.
- // The scaling up ensures that the curve rendering doesn't break when we use a quad line to draw it.
- draw_set_transform_matrix(Transform2D(0, get_view_pos(Vector2(0, 0))));
+ // Draw curve in view coordinates. Curve world-to-view point conversion happens in plot_curve_accurate().
const Color line_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
const Color edge_line_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor)) * Color(1, 1, 1, 0.75);
- CanvasItemPlotCurve plot_func(*this, line_color, edge_line_color);
- plot_curve_accurate(**curve, 2.f, (get_view_pos(Vector2(1, curve->get_max_value())) - get_view_pos(Vector2(0, curve->get_min_value()))) / Vector2(1, curve->get_range()), plot_func);
+ plot_curve_accurate(STEP_SIZE, line_color, edge_line_color);
// Draw points, except for the selected one.
- draw_set_transform_matrix(Transform2D());
bool shift_pressed = Input::get_singleton()->is_key_pressed(Key::SHIFT);
@@ -934,8 +922,8 @@ void CurveEdit::_redraw() {
draw_set_transform_matrix(_world_to_view);
if (Input::get_singleton()->is_key_pressed(Key::ALT) && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {
- float prev_point_offset = (selected_index > 0) ? curve->get_point_position(selected_index - 1).x : 0.0;
- float next_point_offset = (selected_index < curve->get_point_count() - 1) ? curve->get_point_position(selected_index + 1).x : 1.0;
+ float prev_point_offset = (selected_index > 0) ? curve->get_point_position(selected_index - 1).x : curve->get_min_domain();
+ float next_point_offset = (selected_index < curve->get_point_count() - 1) ? curve->get_point_position(selected_index + 1).x : curve->get_max_domain();
draw_line(Vector2(prev_point_offset, curve->get_min_value()), Vector2(prev_point_offset, curve->get_max_value()), Color(point_color, 0.6));
draw_line(Vector2(next_point_offset, curve->get_min_value()), Vector2(next_point_offset, curve->get_max_value()), Color(point_color, 0.6));
@@ -943,7 +931,7 @@ void CurveEdit::_redraw() {
if (shift_pressed && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {
draw_line(Vector2(initial_grab_pos.x, curve->get_min_value()), Vector2(initial_grab_pos.x, curve->get_max_value()), get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor)).darkened(0.4));
- draw_line(Vector2(0, initial_grab_pos.y), Vector2(1, initial_grab_pos.y), get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)).darkened(0.4));
+ draw_line(Vector2(curve->get_min_domain(), initial_grab_pos.y), Vector2(curve->get_max_domain(), initial_grab_pos.y), get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)).darkened(0.4));
}
}
@@ -1079,15 +1067,15 @@ Ref CurvePreviewGenerator::generate(const Ref &p_from, cons
Color line_color = EditorInterface::get_singleton()->get_editor_theme()->get_color(SceneStringName(font_color), EditorStringName(Editor));
// Set the first pixel of the thumbnail.
- float v = (curve->sample_baked(0) - curve->get_min_value()) / curve->get_range();
+ float v = (curve->sample_baked(curve->get_min_domain()) - curve->get_min_value()) / curve->get_value_range();
int y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);
im.set_pixel(0, y, line_color);
// Plot a line towards the next point.
int prev_y = y;
for (int x = 1; x < im.get_width(); ++x) {
- float t = static_cast(x) / im.get_width();
- v = (curve->sample_baked(t) - curve->get_min_value()) / curve->get_range();
+ float t = static_cast(x) / im.get_width() * curve->get_domain_range() + curve->get_min_domain();
+ v = (curve->sample_baked(t) - curve->get_min_value()) / curve->get_value_range();
y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);
Vector points = Geometry2D::bresenham_line(Point2i(x - 1, prev_y), Point2i(x, y));
diff --git a/editor/plugins/curve_editor_plugin.h b/editor/plugins/curve_editor_plugin.h
index c844f420296..8fc87ee04e3 100644
--- a/editor/plugins/curve_editor_plugin.h
+++ b/editor/plugins/curve_editor_plugin.h
@@ -78,14 +78,14 @@ private:
virtual void gui_input(const Ref &p_event) override;
void _curve_changed();
- int get_point_at(Vector2 p_pos) const;
- TangentIndex get_tangent_at(Vector2 p_pos) const;
+ int get_point_at(const Vector2 &p_pos) const;
+ TangentIndex get_tangent_at(const Vector2 &p_pos) const;
float get_offset_without_collision(int p_current_index, float p_offset, bool p_prioritize_right = true);
- void add_point(Vector2 p_pos);
+ void add_point(const Vector2 &p_pos);
void remove_point(int p_index);
- void set_point_position(int p_index, Vector2 p_pos);
+ void set_point_position(int p_index, const Vector2 &p_pos);
void set_point_tangents(int p_index, float p_left, float p_right);
void set_point_left_tangent(int p_index, float p_tangent);
@@ -94,17 +94,20 @@ private:
void update_view_transform();
+ void plot_curve_accurate(float p_step, const Color &p_line_color, const Color &p_edge_line_color);
+
void set_selected_index(int p_index);
- void set_selected_tangent_index(TangentIndex p_tangent);
Vector2 get_tangent_view_pos(int p_index, TangentIndex p_tangent) const;
- Vector2 get_view_pos(Vector2 p_world_pos) const;
- Vector2 get_world_pos(Vector2 p_view_pos) const;
+ Vector2 get_view_pos(const Vector2 &p_world_pos) const;
+ Vector2 get_world_pos(const Vector2 &p_view_pos) const;
void _redraw();
private:
const float ASPECT_RATIO = 6.f / 13.f;
+ const float LINE_WIDTH = 0.5f;
+ const int STEP_SIZE = 2; // Number of pixels between plot points.
Transform2D _world_to_view;
@@ -136,9 +139,9 @@ private:
};
GrabMode grabbing = GRAB_NONE;
Vector2 initial_grab_pos;
- int initial_grab_index;
- float initial_grab_left_tangent;
- float initial_grab_right_tangent;
+ int initial_grab_index = -1;
+ float initial_grab_left_tangent = 0;
+ float initial_grab_right_tangent = 0;
bool snap_enabled = false;
int snap_count = 10;
diff --git a/scene/resources/curve.cpp b/scene/resources/curve.cpp
index 91d3757590b..eb95b7a37a2 100644
--- a/scene/resources/curve.cpp
+++ b/scene/resources/curve.cpp
@@ -33,6 +33,7 @@
#include "core/math/math_funcs.h"
const char *Curve::SIGNAL_RANGE_CHANGED = "range_changed";
+const char *Curve::SIGNAL_DOMAIN_CHANGED = "domain_changed";
Curve::Curve() {
}
@@ -56,14 +57,11 @@ void Curve::set_point_count(int p_count) {
}
int Curve::_add_point(Vector2 p_position, real_t p_left_tangent, real_t p_right_tangent, TangentMode p_left_mode, TangentMode p_right_mode) {
- // Add a point and preserve order
+ // Add a point and preserve order.
- // Curve bounds is in 0..1
- if (p_position.x > MAX_X) {
- p_position.x = MAX_X;
- } else if (p_position.x < MIN_X) {
- p_position.x = MIN_X;
- }
+ // Points must remain within the given value and domain ranges.
+ p_position.x = CLAMP(p_position.x, _min_domain, _max_domain);
+ p_position.y = CLAMP(p_position.y, _min_value, _max_value);
int ret = -1;
@@ -88,11 +86,11 @@ int Curve::_add_point(Vector2 p_position, real_t p_left_tangent, real_t p_right_
int i = get_index(p_position.x);
if (i == 0 && p_position.x < _points[0].position.x) {
- // Insert before anything else
+ // Insert before anything else.
_points.insert(0, Point(p_position, p_left_tangent, p_right_tangent, p_left_mode, p_right_mode));
ret = 0;
} else {
- // Insert between i and i+1
+ // Insert between i and i+1.
++i;
_points.insert(i, Point(p_position, p_left_tangent, p_right_tangent, p_left_mode, p_right_mode));
ret = i;
@@ -121,7 +119,7 @@ int Curve::add_point_no_update(Vector2 p_position, real_t p_left_tangent, real_t
}
int Curve::get_index(real_t p_offset) const {
- // Lower-bound float binary search
+ // Lower-bound float binary search.
int imin = 0;
int imax = _points.size() - 1;
@@ -134,16 +132,14 @@ int Curve::get_index(real_t p_offset) const {
if (a < p_offset && b < p_offset) {
imin = m;
-
} else if (a > p_offset) {
imax = m;
-
} else {
return m;
}
}
- // Will happen if the offset is out of bounds
+ // Will happen if the offset is out of bounds.
if (p_offset > _points[imax].position.x) {
return imax;
}
@@ -305,30 +301,80 @@ void Curve::update_auto_tangents(int p_index) {
}
}
+#define MIN_X_RANGE 0.01
#define MIN_Y_RANGE 0.01
-void Curve::set_min_value(real_t p_min) {
- if (_minmax_set_once & 0b11 && p_min > _max_value - MIN_Y_RANGE) {
- _min_value = _max_value - MIN_Y_RANGE;
- } else {
- _minmax_set_once |= 0b10; // first bit is "min set"
- _min_value = p_min;
+Array Curve::get_limits() const {
+ Array output;
+ output.resize(4);
+
+ output[0] = _min_value;
+ output[1] = _max_value;
+ output[2] = _min_domain;
+ output[3] = _max_domain;
+
+ return output;
+}
+
+void Curve::set_limits(const Array &p_input) {
+ if (p_input.size() != 4) {
+ WARN_PRINT_ED(vformat(R"(Could not find Curve limit values when deserializing "%s". Resetting limits to default values.)", this->get_path()));
+ _min_value = 0;
+ _max_value = 1;
+ _min_domain = 0;
+ _max_domain = 1;
+ return;
}
- // Note: min and max are indicative values,
- // it's still possible that existing points are out of range at this point.
+
+ // Do not use setters because we don't want to enforce their logical constraints during deserialization.
+ _min_value = p_input[0];
+ _max_value = p_input[1];
+ _min_domain = p_input[2];
+ _max_domain = p_input[3];
+}
+
+void Curve::set_min_value(real_t p_min) {
+ _min_value = MIN(p_min, _max_value - MIN_Y_RANGE);
+
+ for (const Point &p : _points) {
+ _min_value = MIN(_min_value, p.position.y);
+ }
+
emit_signal(SNAME(SIGNAL_RANGE_CHANGED));
}
void Curve::set_max_value(real_t p_max) {
- if (_minmax_set_once & 0b11 && p_max < _min_value + MIN_Y_RANGE) {
- _max_value = _min_value + MIN_Y_RANGE;
- } else {
- _minmax_set_once |= 0b01; // second bit is "max set"
- _max_value = p_max;
+ _max_value = MAX(p_max, _min_value + MIN_Y_RANGE);
+
+ for (const Point &p : _points) {
+ _max_value = MAX(_max_value, p.position.y);
}
+
emit_signal(SNAME(SIGNAL_RANGE_CHANGED));
}
+void Curve::set_min_domain(real_t p_min) {
+ _min_domain = MIN(p_min, _max_domain - MIN_X_RANGE);
+
+ if (_points.size() > 0 && _min_domain > _points[0].position.x) {
+ _min_domain = _points[0].position.x;
+ }
+
+ mark_dirty();
+ emit_signal(SNAME(SIGNAL_DOMAIN_CHANGED));
+}
+
+void Curve::set_max_domain(real_t p_max) {
+ _max_domain = MAX(p_max, _min_domain + MIN_X_RANGE);
+
+ if (_points.size() > 0 && _max_domain < _points[_points.size() - 1].position.x) {
+ _max_domain = _points[_points.size() - 1].position.x;
+ }
+
+ mark_dirty();
+ emit_signal(SNAME(SIGNAL_DOMAIN_CHANGED));
+}
+
real_t Curve::sample(real_t p_offset) const {
if (_points.size() == 0) {
return 0;
@@ -370,7 +416,7 @@ real_t Curve::sample_local_nocheck(int p_index, real_t p_local_offset) const {
* d1 == d2 == d3 == d / 3
*/
- // Control points are chosen at equal distances
+ // Control points are chosen at equal distances.
real_t d = b.position.x - a.position.x;
if (Math::is_zero_approx(d)) {
return b.position.y;
@@ -458,7 +504,7 @@ void Curve::bake() {
_baked_cache.resize(_bake_resolution);
for (int i = 1; i < _bake_resolution - 1; ++i) {
- real_t x = i / static_cast(_bake_resolution - 1);
+ real_t x = get_domain_range() * i / static_cast(_bake_resolution - 1) + _min_domain;
real_t y = sample(x);
_baked_cache.write[i] = y;
}
@@ -483,11 +529,11 @@ real_t Curve::sample_baked(real_t p_offset) const {
ERR_FAIL_COND_V_MSG(!Math::is_finite(p_offset), 0, "Offset is non-finite");
if (_baked_cache_dirty) {
- // Last-second bake if not done already
+ // Last-second bake if not done already.
const_cast(this)->bake();
}
- // Special cases if the cache is too small
+ // Special cases if the cache is too small.
if (_baked_cache.size() == 0) {
if (_points.size() == 0) {
return 0;
@@ -497,8 +543,8 @@ real_t Curve::sample_baked(real_t p_offset) const {
return _baked_cache[0];
}
- // Get interpolation index
- real_t fi = p_offset * (_baked_cache.size() - 1);
+ // Get interpolation index.
+ real_t fi = (p_offset - _min_domain) / get_domain_range() * (_baked_cache.size() - 1);
int i = Math::floor(fi);
if (i < 0) {
i = 0;
@@ -508,7 +554,7 @@ real_t Curve::sample_baked(real_t p_offset) const {
fi = 0;
}
- // Sample
+ // Sample.
if (i + 1 < _baked_cache.size()) {
real_t t = fi - i;
return Math::lerp(_baked_cache[i], _baked_cache[i + 1], t);
@@ -631,6 +677,14 @@ void Curve::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_min_value", "min"), &Curve::set_min_value);
ClassDB::bind_method(D_METHOD("get_max_value"), &Curve::get_max_value);
ClassDB::bind_method(D_METHOD("set_max_value", "max"), &Curve::set_max_value);
+ ClassDB::bind_method(D_METHOD("get_value_range"), &Curve::get_value_range);
+ ClassDB::bind_method(D_METHOD("get_min_domain"), &Curve::get_min_domain);
+ ClassDB::bind_method(D_METHOD("set_min_domain", "min"), &Curve::set_min_domain);
+ ClassDB::bind_method(D_METHOD("get_max_domain"), &Curve::get_max_domain);
+ ClassDB::bind_method(D_METHOD("set_max_domain", "max"), &Curve::set_max_domain);
+ ClassDB::bind_method(D_METHOD("get_domain_range"), &Curve::get_domain_range);
+ ClassDB::bind_method(D_METHOD("_get_limits"), &Curve::get_limits);
+ ClassDB::bind_method(D_METHOD("_set_limits", "data"), &Curve::set_limits);
ClassDB::bind_method(D_METHOD("clean_dupes"), &Curve::clean_dupes);
ClassDB::bind_method(D_METHOD("bake"), &Curve::bake);
ClassDB::bind_method(D_METHOD("get_bake_resolution"), &Curve::get_bake_resolution);
@@ -638,13 +692,17 @@ void Curve::_bind_methods() {
ClassDB::bind_method(D_METHOD("_get_data"), &Curve::get_data);
ClassDB::bind_method(D_METHOD("_set_data", "data"), &Curve::set_data);
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "min_value", PROPERTY_HINT_RANGE, "-1024,1024,0.01"), "set_min_value", "get_min_value");
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_value", PROPERTY_HINT_RANGE, "-1024,1024,0.01"), "set_max_value", "get_max_value");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "min_domain", PROPERTY_HINT_RANGE, "-1024,1024,0.01,or_greater,or_less", PROPERTY_USAGE_EDITOR), "set_min_domain", "get_min_domain");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_domain", PROPERTY_HINT_RANGE, "-1024,1024,0.01,or_greater,or_less", PROPERTY_USAGE_EDITOR), "set_max_domain", "get_max_domain");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "min_value", PROPERTY_HINT_RANGE, "-1024,1024,0.01,or_greater,or_less", PROPERTY_USAGE_EDITOR), "set_min_value", "get_min_value");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_value", PROPERTY_HINT_RANGE, "-1024,1024,0.01,or_greater,or_less", PROPERTY_USAGE_EDITOR), "set_max_value", "get_max_value");
+ ADD_PROPERTY(PropertyInfo(Variant::NIL, "_limits", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_limits", "_get_limits");
ADD_PROPERTY(PropertyInfo(Variant::INT, "bake_resolution", PROPERTY_HINT_RANGE, "1,1000,1"), "set_bake_resolution", "get_bake_resolution");
ADD_PROPERTY(PropertyInfo(Variant::INT, "_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_data", "_get_data");
ADD_ARRAY_COUNT("Points", "point_count", "set_point_count", "get_point_count", "point_");
ADD_SIGNAL(MethodInfo(SIGNAL_RANGE_CHANGED));
+ ADD_SIGNAL(MethodInfo(SIGNAL_DOMAIN_CHANGED));
BIND_ENUM_CONSTANT(TANGENT_FREE);
BIND_ENUM_CONSTANT(TANGENT_LINEAR);
@@ -855,7 +913,7 @@ void Curve2D::_bake() const {
return;
}
- // Tessellate curve to (almost) even length segments
+ // Tessellate curve to (almost) even length segments.
{
Vector> midpoints = _tessellate_even_length(10, bake_interval);
@@ -1614,7 +1672,7 @@ void Curve3D::_bake() const {
return;
}
- // Step 1: Tessellate curve to (almost) even length segments
+ // Step 1: Tessellate curve to (almost) even length segments.
{
Vector> midpoints = _tessellate_even_length(10, bake_interval);
@@ -1689,7 +1747,7 @@ void Curve3D::_bake() const {
return;
}
- // Step 2: Calculate the up vectors and the whole local reference frame
+ // Step 2: Calculate the up vectors and the whole local reference frame.
//
// See Dougan, Carl. "The parallel transport frame." Game Programming Gems 2 (2001): 215-219.
// for an example discussing about why not the Frenet frame.
@@ -1725,7 +1783,7 @@ void Curve3D::_bake() const {
Basis rotate;
rotate.rotate_to_align(-frame_prev.get_column(2), forward);
frame = rotate * frame_prev;
- frame.orthonormalize(); // guard against float error accumulation
+ frame.orthonormalize(); // Guard against float error accumulation.
up_write[idx] = frame.get_column(1);
frame_prev = frame;
@@ -1986,7 +2044,7 @@ real_t Curve3D::sample_baked_tilt(real_t p_offset) const {
return baked_tilt_cache.get(0);
}
- p_offset = CLAMP(p_offset, 0.0, get_baked_length()); // PathFollower implement wrapping logic
+ p_offset = CLAMP(p_offset, 0.0, get_baked_length()); // PathFollower implement wrapping logic.
Curve3D::Interval interval = _find_interval(p_offset);
return _sample_baked_tilt(interval);
diff --git a/scene/resources/curve.h b/scene/resources/curve.h
index 154d91e23b8..03416ed487c 100644
--- a/scene/resources/curve.h
+++ b/scene/resources/curve.h
@@ -38,10 +38,8 @@ class Curve : public Resource {
GDCLASS(Curve, Resource);
public:
- static const int MIN_X = 0.f;
- static const int MAX_X = 1.f;
-
static const char *SIGNAL_RANGE_CHANGED;
+ static const char *SIGNAL_DOMAIN_CHANGED;
enum TangentMode {
TANGENT_FREE = 0,
@@ -101,11 +99,18 @@ public:
real_t get_min_value() const { return _min_value; }
void set_min_value(real_t p_min);
-
real_t get_max_value() const { return _max_value; }
void set_max_value(real_t p_max);
+ real_t get_value_range() const { return _max_value - _min_value; }
- real_t get_range() const { return _max_value - _min_value; }
+ real_t get_min_domain() const { return _min_domain; }
+ void set_min_domain(real_t p_min);
+ real_t get_max_domain() const { return _max_domain; }
+ void set_max_domain(real_t p_max);
+ real_t get_domain_range() const { return _max_domain - _min_domain; }
+
+ Array get_limits() const;
+ void set_limits(const Array &p_input);
real_t sample(real_t p_offset) const;
real_t sample_local_nocheck(int p_index, real_t p_local_offset) const;
@@ -156,7 +161,8 @@ private:
int _bake_resolution = 100;
real_t _min_value = 0.0;
real_t _max_value = 1.0;
- int _minmax_set_once = 0b00; // Encodes whether min and max have been set a first time, first bit for min and second for max.
+ real_t _min_domain = 0.0;
+ real_t _max_domain = 1.0;
};
VARIANT_ENUM_CAST(Curve::TangentMode)
diff --git a/tests/scene/test_curve.h b/tests/scene/test_curve.h
index d67550f9f72..4c179cc59a6 100644
--- a/tests/scene/test_curve.h
+++ b/tests/scene/test_curve.h
@@ -55,7 +55,7 @@ TEST_CASE("[Curve] Default curve") {
"Default curve should return the expected value at offset 1.0.");
}
-TEST_CASE("[Curve] Custom curve with free tangents") {
+TEST_CASE("[Curve] Custom unit curve with free tangents") {
Ref curve = memnew(Curve);
// "Sawtooth" curve with an open ending towards the 1.0 offset.
curve->add_point(Vector2(0, 0));
@@ -136,7 +136,90 @@ TEST_CASE("[Curve] Custom curve with free tangents") {
"Custom free curve should return the expected baked value at offset 0.6 after clearing all points.");
}
-TEST_CASE("[Curve] Custom curve with linear tangents") {
+TEST_CASE("[Curve] Custom non-unit curve with free tangents") {
+ Ref curve = memnew(Curve);
+ curve->set_min_domain(-100.0);
+ curve->set_max_domain(100.0);
+ // "Sawtooth" curve with an open ending towards the 100 offset.
+ curve->add_point(Vector2(-100, 0));
+ curve->add_point(Vector2(-50, 1));
+ curve->add_point(Vector2(0, 0));
+ curve->add_point(Vector2(50, 1));
+ curve->set_bake_resolution(11);
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_left_tangent(0)),
+ "get_point_left_tangent() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(0)),
+ "get_point_right_tangent() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(0) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_left_mode() should return the expected value for point index 0.");
+ CHECK_MESSAGE(
+ curve->get_point_right_mode(0) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_right_mode() should return the expected value for point index 0.");
+
+ CHECK_MESSAGE(
+ curve->get_point_count() == 4,
+ "Custom free curve should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample(-200)),
+ "Custom free curve should return the expected value at offset -200.");
+ CHECK_MESSAGE(
+ curve->sample(0.1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.352),
+ "Custom free curve should return the expected value at offset equivalent to a unit curve's 0.1.");
+ CHECK_MESSAGE(
+ curve->sample(0.4 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.352),
+ "Custom free curve should return the expected value at offset equivalent to a unit curve's 0.4.");
+ CHECK_MESSAGE(
+ curve->sample(0.7 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.896),
+ "Custom free curve should return the expected value at offset equivalent to a unit curve's 0.7.");
+ CHECK_MESSAGE(
+ curve->sample(1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(1),
+ "Custom free curve should return the expected value at offset equivalent to a unit curve's 1.");
+ CHECK_MESSAGE(
+ curve->sample(2 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(1),
+ "Custom free curve should return the expected value at offset equivalent to a unit curve's 2.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample_baked(-200)),
+ "Custom free curve should return the expected baked value at offset equivalent to a unit curve's -0.1.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.352),
+ "Custom free curve should return the expected baked value at offset equivalent to a unit curve's 0.1.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.4 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.352),
+ "Custom free curve should return the expected baked value at offset equivalent to a unit curve's 0.4.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.7 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.896),
+ "Custom free curve should return the expected baked value at offset equivalent to a unit curve's 0.7.");
+ CHECK_MESSAGE(
+ curve->sample_baked(1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(1),
+ "Custom free curve should return the expected baked value at offset equivalent to a unit curve's 1.");
+ CHECK_MESSAGE(
+ curve->sample_baked(2 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(1),
+ "Custom free curve should return the expected baked value at offset equivalent to a unit curve's 2.");
+
+ curve->remove_point(1);
+ CHECK_MESSAGE(
+ curve->sample(0.1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(0),
+ "Custom free curve should return the expected value at offset equivalent to a unit curve's 0.1 after removing point at index 1.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(0),
+ "Custom free curve should return the expected baked value at offset equivalent to a unit curve's 0.1 after removing point at index 1.");
+
+ curve->clear_points();
+ CHECK_MESSAGE(
+ curve->sample(0.6 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(0),
+ "Custom free curve should return the expected value at offset 0.6 after clearing all points.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.6 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(0),
+ "Custom free curve should return the expected baked value at offset 0.6 after clearing all points.");
+}
+
+TEST_CASE("[Curve] Custom unit curve with linear tangents") {
Ref curve = memnew(Curve);
// "Sawtooth" curve with an open ending towards the 1.0 offset.
curve->add_point(Vector2(0, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
@@ -219,6 +302,91 @@ TEST_CASE("[Curve] Custom curve with linear tangents") {
"Custom free curve should return the expected baked value at offset 0.7 after removing point at invalid index 10.");
}
+TEST_CASE("[Curve] Custom non-unit curve with linear tangents") {
+ Ref curve = memnew(Curve);
+ curve->set_min_domain(-100.0);
+ curve->set_max_domain(100.0);
+ // "Sawtooth" curve with an open ending towards the 100 offset.
+ curve->add_point(Vector2(-100, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(-50, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(0, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+ curve->add_point(Vector2(50, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR);
+
+ CHECK_MESSAGE(
+ curve->get_point_left_tangent(3) == doctest::Approx(1.f / 50),
+ "get_point_left_tangent() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(3)),
+ "get_point_right_tangent() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(3) == Curve::TangentMode::TANGENT_LINEAR,
+ "get_point_left_mode() should return the expected value for point index 3.");
+ CHECK_MESSAGE(
+ curve->get_point_right_mode(3) == Curve::TangentMode::TANGENT_LINEAR,
+ "get_point_right_mode() should return the expected value for point index 3.");
+
+ ERR_PRINT_OFF;
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->get_point_right_tangent(300)),
+ "get_point_right_tangent() should return the expected value for invalid point index 300.");
+ CHECK_MESSAGE(
+ curve->get_point_left_mode(-12345) == Curve::TangentMode::TANGENT_FREE,
+ "get_point_left_mode() should return the expected value for invalid point index -12345.");
+ ERR_PRINT_ON;
+
+ CHECK_MESSAGE(
+ curve->get_point_count() == 4,
+ "Custom linear unit curve should contain the expected number of points.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample(-0.1 * curve->get_domain_range() + curve->get_min_domain())),
+ "Custom linear curve should return the expected value at offset equivalent to a unit curve's -0.1.");
+ CHECK_MESSAGE(
+ curve->sample(0.1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.4),
+ "Custom linear curve should return the expected value at offset equivalent to a unit curve's 0.1.");
+ CHECK_MESSAGE(
+ curve->sample(0.4 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.4),
+ "Custom linear curve should return the expected value at offset equivalent to a unit curve's 0.4.");
+ CHECK_MESSAGE(
+ curve->sample(0.7 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.8),
+ "Custom linear curve should return the expected value at offset equivalent to a unit curve's 0.7.");
+ CHECK_MESSAGE(
+ curve->sample(1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(1),
+ "Custom linear curve should return the expected value at offset equivalent to a unit curve's 1.0.");
+ CHECK_MESSAGE(
+ curve->sample(2 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(1),
+ "Custom linear curve should return the expected value at offset equivalent to a unit curve's 2.0.");
+
+ CHECK_MESSAGE(
+ Math::is_zero_approx(curve->sample_baked(-0.1 * curve->get_domain_range() + curve->get_min_domain())),
+ "Custom linear curve should return the expected baked value at offset equivalent to a unit curve's -0.1.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.4),
+ "Custom linear curve should return the expected baked value at offset equivalent to a unit curve's 0.1.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.4 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.4),
+ "Custom linear curve should return the expected baked value at offset equivalent to a unit curve's 0.4.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.7 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.8),
+ "Custom linear curve should return the expected baked value at offset equivalent to a unit curve's 0.7.");
+ CHECK_MESSAGE(
+ curve->sample_baked(1 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(1),
+ "Custom linear curve should return the expected baked value at offset equivalent to a unit curve's 1.0.");
+ CHECK_MESSAGE(
+ curve->sample_baked(2 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx(1),
+ "Custom linear curve should return the expected baked value at offset equivalent to a unit curve's 2.0.");
+
+ ERR_PRINT_OFF;
+ curve->remove_point(10);
+ ERR_PRINT_ON;
+ CHECK_MESSAGE(
+ curve->sample(0.7 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.8),
+ "Custom free curve should return the expected value at offset equivalent to a unit curve's 0.7 after removing point at invalid index 10.");
+ CHECK_MESSAGE(
+ curve->sample_baked(0.7 * curve->get_domain_range() + curve->get_min_domain()) == doctest::Approx((real_t)0.8),
+ "Custom free curve should return the expected baked value at offset equivalent to a unit curve's 0.7 after removing point at invalid index 10.");
+}
+
TEST_CASE("[Curve] Straight line offset test") {
Ref curve = memnew(Curve);
curve->add_point(Vector2(0, 0));