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));