Browse Source

Extend `Curve` to allow for arbitrary domains

ocean (they/them) 2 years ago
parent
commit
6c65213487

+ 2 - 2
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.
 			If [code]true[/code], breaks the loop at the end of the loop cycle for transition, even if the animation is looping.
 		</member>
 		</member>
 		<member name="fadein_curve" type="Curve" setter="set_fadein_curve" getter="get_fadein_curve">
 		<member name="fadein_curve" type="Curve" setter="set_fadein_curve" getter="get_fadein_curve">
-			Determines how cross-fading between animations is eased. If empty, the transition will be linear.
+			Determines how cross-fading between animations is eased. If empty, the transition will be linear. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="fadein_time" type="float" setter="set_fadein_time" getter="get_fadein_time" default="0.0">
 		<member name="fadein_time" type="float" setter="set_fadein_time" getter="get_fadein_time" default="0.0">
 			The fade-in duration. For example, setting this to [code]1.0[/code] for a 5 second length animation will produce a cross-fade that starts at 0 second and ends at 1 second during the animation.
 			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.
 			[b]Note:[/b] [AnimationNodeOneShot] transitions the current state after the end of the fading. When [AnimationNodeOutput] is considered as the most upstream, so the [member fadein_time] is scaled depending on the downstream delta. For example, if this value is set to [code]1.0[/code] and a [AnimationNodeTimeScale] with a value of [code]2.0[/code] is chained downstream, the actual processing time will be 0.5 second.
 		</member>
 		</member>
 		<member name="fadeout_curve" type="Curve" setter="set_fadeout_curve" getter="get_fadeout_curve">
 		<member name="fadeout_curve" type="Curve" setter="set_fadeout_curve" getter="get_fadeout_curve">
-			Determines how cross-fading between animations is eased. If empty, the transition will be linear.
+			Determines how cross-fading between animations is eased. If empty, the transition will be linear. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="fadeout_time" type="float" setter="set_fadeout_time" getter="get_fadeout_time" default="0.0">
 		<member name="fadeout_time" type="float" setter="set_fadeout_time" getter="get_fadeout_time" default="0.0">
 			The fade-out duration. For example, setting this to [code]1.0[/code] for a 5 second length animation will produce a cross-fade that starts at 4 second and ends at 5 second during the animation.
 			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.

+ 1 - 1
doc/classes/AnimationNodeStateMachineTransition.xml

@@ -41,7 +41,7 @@
 			The transition type.
 			The transition type.
 		</member>
 		</member>
 		<member name="xfade_curve" type="Curve" setter="set_xfade_curve" getter="get_xfade_curve">
 		<member name="xfade_curve" type="Curve" setter="set_xfade_curve" getter="get_xfade_curve">
-			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].
 		</member>
 		</member>
 		<member name="xfade_time" type="float" setter="set_xfade_time" getter="get_xfade_time" default="0.0">
 		<member name="xfade_time" type="float" setter="set_xfade_time" getter="get_xfade_time" default="0.0">
 			The time to cross-fade between this state and the next.
 			The time to cross-fade between this state and the next.

+ 1 - 1
doc/classes/AnimationNodeTransition.xml

@@ -96,7 +96,7 @@
 			The number of enabled input ports for this animation node.
 			The number of enabled input ports for this animation node.
 		</member>
 		</member>
 		<member name="xfade_curve" type="Curve" setter="set_xfade_curve" getter="get_xfade_curve">
 		<member name="xfade_curve" type="Curve" setter="set_xfade_curve" getter="get_xfade_curve">
-			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].
 		</member>
 		</member>
 		<member name="xfade_time" type="float" setter="set_xfade_time" getter="get_xfade_time" default="0.0">
 		<member name="xfade_time" type="float" setter="set_xfade_time" getter="get_xfade_time" default="0.0">
 			Cross-fading time (in seconds) between each animation connected to the inputs.
 			Cross-fading time (in seconds) between each animation connected to the inputs.

+ 14 - 14
doc/classes/CPUParticles2D.xml

@@ -57,7 +57,7 @@
 			<param index="0" name="param" type="int" enum="CPUParticles2D.Parameter" />
 			<param index="0" name="param" type="int" enum="CPUParticles2D.Parameter" />
 			<param index="1" name="curve" type="Curve" />
 			<param index="1" name="curve" type="Curve" />
 			<description>
 			<description>
-				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].
 			</description>
 			</description>
 		</method>
 		</method>
 		<method name="set_param_max">
 		<method name="set_param_max">
@@ -90,7 +90,7 @@
 			Number of particles emitted in one emission cycle.
 			Number of particles emitted in one emission cycle.
 		</member>
 		</member>
 		<member name="angle_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="angle_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="angle_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="angle_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum initial rotation applied to each particle, in degrees.
 			Maximum initial rotation applied to each particle, in degrees.
@@ -99,7 +99,7 @@
 			Minimum equivalent of [member angle_max].
 			Minimum equivalent of [member angle_max].
 		</member>
 		</member>
 		<member name="angular_velocity_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="angular_velocity_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="angular_velocity_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="angular_velocity_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum initial angular velocity (rotation speed) applied to each particle in [i]degrees[/i] per second.
 			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].
 			Minimum equivalent of [member angular_velocity_max].
 		</member>
 		</member>
 		<member name="anim_offset_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="anim_offset_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="anim_offset_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="anim_offset_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			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].
 			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].
 			Minimum equivalent of [member anim_offset_max].
 		</member>
 		</member>
 		<member name="anim_speed_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="anim_speed_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="anim_speed_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="anim_speed_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			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.
 			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]).
 			Each particle's color will vary along this [Gradient] (multiplied with [member color]).
 		</member>
 		</member>
 		<member name="damping_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="damping_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			Damping will vary along this [Curve].
+			Damping will vary along this [Curve]. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="damping_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="damping_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			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.
 			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.
 			Gravity applied to every particle.
 		</member>
 		</member>
 		<member name="hue_variation_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="hue_variation_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			Each particle's hue will vary along this [Curve].
+			Each particle's hue will vary along this [Curve]. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="hue_variation_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="hue_variation_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum initial hue variation applied to each particle. It will shift the particle color's hue.
 			Maximum initial hue variation applied to each particle. It will shift the particle color's hue.
@@ -205,7 +205,7 @@
 			Particle lifetime randomness ratio.
 			Particle lifetime randomness ratio.
 		</member>
 		</member>
 		<member name="linear_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="linear_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="linear_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="linear_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum linear acceleration applied to each particle in the direction of motion.
 			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.
 			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.
 		</member>
 		</member>
 		<member name="orbit_velocity_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="orbit_velocity_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="orbit_velocity_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="orbit_velocity_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum orbital velocity applied to each particle. Makes the particles circle around origin. Specified in number of full rotations around origin per second.
 			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.
 			Particle system starts as if it had already run for this many seconds.
 		</member>
 		</member>
 		<member name="radial_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="radial_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="radial_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="radial_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum radial acceleration applied to each particle. Makes particle accelerate away from the origin or towards it if negative.
 			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.
 			Emission lifetime randomness ratio.
 		</member>
 		</member>
 		<member name="scale_amount_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="scale_amount_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			Each particle's scale will vary along this [Curve].
+			Each particle's scale will vary along this [Curve]. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="scale_amount_max" type="float" setter="set_param_max" getter="get_param_max" default="1.0">
 		<member name="scale_amount_max" type="float" setter="set_param_max" getter="get_param_max" default="1.0">
 			Maximum initial scale applied to each particle.
 			Maximum initial scale applied to each particle.
@@ -256,11 +256,11 @@
 			Minimum equivalent of [member scale_amount_max].
 			Minimum equivalent of [member scale_amount_max].
 		</member>
 		</member>
 		<member name="scale_curve_x" type="Curve" setter="set_scale_curve_x" getter="get_scale_curve_x">
 		<member name="scale_curve_x" type="Curve" setter="set_scale_curve_x" getter="get_scale_curve_x">
-			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.
 			[member split_scale] must be enabled.
 		</member>
 		</member>
 		<member name="scale_curve_y" type="Curve" setter="set_scale_curve_y" getter="get_scale_curve_y">
 		<member name="scale_curve_y" type="Curve" setter="set_scale_curve_y" getter="get_scale_curve_y">
-			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.
 			[member split_scale] must be enabled.
 		</member>
 		</member>
 		<member name="speed_scale" type="float" setter="set_speed_scale" getter="get_speed_scale" default="1.0">
 		<member name="speed_scale" type="float" setter="set_speed_scale" getter="get_speed_scale" default="1.0">
@@ -273,7 +273,7 @@
 			Each particle's initial direction range from [code]+spread[/code] to [code]-spread[/code] degrees.
 			Each particle's initial direction range from [code]+spread[/code] to [code]-spread[/code] degrees.
 		</member>
 		</member>
 		<member name="tangential_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="tangential_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="tangential_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="tangential_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum tangential acceleration applied to each particle. Tangential acceleration is perpendicular to the particle's velocity giving the particles a swirling motion.
 			Maximum tangential acceleration applied to each particle. Tangential acceleration is perpendicular to the particle's velocity giving the particles a swirling motion.

+ 12 - 12
doc/classes/CPUParticles3D.xml

@@ -63,7 +63,7 @@
 			<param index="0" name="param" type="int" enum="CPUParticles3D.Parameter" />
 			<param index="0" name="param" type="int" enum="CPUParticles3D.Parameter" />
 			<param index="1" name="curve" type="Curve" />
 			<param index="1" name="curve" type="Curve" />
 			<description>
 			<description>
-				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].
 			</description>
 			</description>
 		</method>
 		</method>
 		<method name="set_param_max">
 		<method name="set_param_max">
@@ -96,7 +96,7 @@
 			Number of particles emitted in one emission cycle.
 			Number of particles emitted in one emission cycle.
 		</member>
 		</member>
 		<member name="angle_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="angle_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="angle_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="angle_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum angle.
 			Maximum angle.
@@ -105,7 +105,7 @@
 			Minimum angle.
 			Minimum angle.
 		</member>
 		</member>
 		<member name="angular_velocity_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="angular_velocity_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="angular_velocity_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="angular_velocity_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum initial angular velocity (rotation speed) applied to each particle in [i]degrees[/i] per second.
 			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.
 			Minimum initial angular velocity (rotation speed) applied to each particle in [i]degrees[/i] per second.
 		</member>
 		</member>
 		<member name="anim_offset_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="anim_offset_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="anim_offset_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="anim_offset_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum animation offset.
 			Maximum animation offset.
@@ -123,7 +123,7 @@
 			Minimum animation offset.
 			Minimum animation offset.
 		</member>
 		</member>
 		<member name="anim_speed_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="anim_speed_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="anim_speed_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="anim_speed_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum particle animation speed.
 			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.
 			[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.
 		</member>
 		</member>
 		<member name="damping_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="damping_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			Damping will vary along this [Curve].
+			Damping will vary along this [Curve]. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="damping_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="damping_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum damping.
 			Maximum damping.
@@ -212,7 +212,7 @@
 			Gravity applied to every particle.
 			Gravity applied to every particle.
 		</member>
 		</member>
 		<member name="hue_variation_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="hue_variation_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			Each particle's hue will vary along this [Curve].
+			Each particle's hue will vary along this [Curve]. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="hue_variation_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="hue_variation_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum hue variation.
 			Maximum hue variation.
@@ -233,7 +233,7 @@
 			Particle lifetime randomness ratio.
 			Particle lifetime randomness ratio.
 		</member>
 		</member>
 		<member name="linear_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="linear_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="linear_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="linear_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum linear acceleration.
 			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.
 			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.
 		</member>
 		</member>
 		<member name="orbit_velocity_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="orbit_velocity_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="orbit_velocity_max" type="float" setter="set_param_max" getter="get_param_max">
 		<member name="orbit_velocity_max" type="float" setter="set_param_max" getter="get_param_max">
 			Maximum orbit velocity.
 			Maximum orbit velocity.
@@ -272,7 +272,7 @@
 			Particle system starts as if it had already run for this many seconds.
 			Particle system starts as if it had already run for this many seconds.
 		</member>
 		</member>
 		<member name="radial_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="radial_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="radial_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="radial_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum radial acceleration.
 			Maximum radial acceleration.
@@ -284,7 +284,7 @@
 			Emission lifetime randomness ratio.
 			Emission lifetime randomness ratio.
 		</member>
 		</member>
 		<member name="scale_amount_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="scale_amount_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			Each particle's scale will vary along this [Curve].
+			Each particle's scale will vary along this [Curve]. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="scale_amount_max" type="float" setter="set_param_max" getter="get_param_max" default="1.0">
 		<member name="scale_amount_max" type="float" setter="set_param_max" getter="get_param_max" default="1.0">
 			Maximum scale.
 			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 initial direction range from [code]+spread[/code] to [code]-spread[/code] degrees. Applied to X/Z plane and Y/Z planes.
 		</member>
 		</member>
 		<member name="tangential_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
 		<member name="tangential_accel_curve" type="Curve" setter="set_param_curve" getter="get_param_curve">
-			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].
 		</member>
 		</member>
 		<member name="tangential_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 		<member name="tangential_accel_max" type="float" setter="set_param_max" getter="get_param_max" default="0.0">
 			Maximum tangent acceleration.
 			Maximum tangent acceleration.

+ 27 - 4
doc/classes/Curve.xml

@@ -4,8 +4,8 @@
 		A mathematical curve.
 		A mathematical curve.
 	</brief_description>
 	</brief_description>
 	<description>
 	<description>
-		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].
 	</description>
 	</description>
 	<tutorials>
 	<tutorials>
 	</tutorials>
 	</tutorials>
@@ -39,6 +39,12 @@
 				Removes all points from the curve.
 				Removes all points from the curve.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="get_domain_range" qualifiers="const">
+			<return type="float" />
+			<description>
+				Returns the difference between [member min_domain] and [member max_domain].
+			</description>
+		</method>
 		<method name="get_point_left_mode" qualifiers="const">
 		<method name="get_point_left_mode" qualifiers="const">
 			<return type="int" enum="Curve.TangentMode" />
 			<return type="int" enum="Curve.TangentMode" />
 			<param index="0" name="index" type="int" />
 			<param index="0" name="index" type="int" />
@@ -74,6 +80,12 @@
 				Returns the right tangent angle (in degrees) for the point at [param index].
 				Returns the right tangent angle (in degrees) for the point at [param index].
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="get_value_range" qualifiers="const">
+			<return type="float" />
+			<description>
+				Returns the difference between [member min_value] and [member max_value].
+			</description>
+		</method>
 		<method name="remove_point">
 		<method name="remove_point">
 			<return type="void" />
 			<return type="void" />
 			<param index="0" name="index" type="int" />
 			<param index="0" name="index" type="int" />
@@ -148,17 +160,28 @@
 		<member name="bake_resolution" type="int" setter="set_bake_resolution" getter="get_bake_resolution" default="100">
 		<member name="bake_resolution" type="int" setter="set_bake_resolution" getter="get_bake_resolution" default="100">
 			The number of points to include in the baked (i.e. cached) curve data.
 			The number of points to include in the baked (i.e. cached) curve data.
 		</member>
 		</member>
+		<member name="max_domain" type="float" setter="set_max_domain" getter="get_max_domain" default="1.0">
+			The maximum domain (x-coordinate) that points can have.
+		</member>
 		<member name="max_value" type="float" setter="set_max_value" getter="get_max_value" default="1.0">
 		<member name="max_value" type="float" setter="set_max_value" getter="get_max_value" default="1.0">
-			The maximum value the curve can reach.
+			The maximum value (y-coordinate) that points can have. Tangents can cause higher values between points.
+		</member>
+		<member name="min_domain" type="float" setter="set_min_domain" getter="get_min_domain" default="0.0">
+			The minimum domain (x-coordinate) that points can have.
 		</member>
 		</member>
 		<member name="min_value" type="float" setter="set_min_value" getter="get_min_value" default="0.0">
 		<member name="min_value" type="float" setter="set_min_value" getter="get_min_value" default="0.0">
-			The minimum value the curve can reach.
+			The minimum value (y-coordinate) that points can have. Tangents can cause lower values between points.
 		</member>
 		</member>
 		<member name="point_count" type="int" setter="set_point_count" getter="get_point_count" default="0">
 		<member name="point_count" type="int" setter="set_point_count" getter="get_point_count" default="0">
 			The number of points describing the curve.
 			The number of points describing the curve.
 		</member>
 		</member>
 	</members>
 	</members>
 	<signals>
 	<signals>
+		<signal name="domain_changed">
+			<description>
+				Emitted when [member max_domain] or [member min_domain] is changed.
+			</description>
+		</signal>
 		<signal name="range_changed">
 		<signal name="range_changed">
 			<description>
 			<description>
 				Emitted when [member max_value] or [member min_value] is changed.
 				Emitted when [member max_value] or [member min_value] is changed.

+ 2 - 2
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.
 	</brief_description>
 	</brief_description>
 	<description>
 	<description>
-		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].
 		If you need to store up to 3 curves within a single texture, use [CurveXYZTexture] instead. See also [GradientTexture1D] and [GradientTexture2D].
 	</description>
 	</description>
 	<tutorials>
 	<tutorials>
 	</tutorials>
 	</tutorials>
 	<members>
 	<members>
 		<member name="curve" type="Curve" setter="set_curve" getter="get_curve">
 		<member name="curve" type="Curve" setter="set_curve" getter="get_curve">
-			The [Curve] that is rendered onto the texture.
+			The [Curve] that is rendered onto the texture. Should be a unit [Curve].
 		</member>
 		</member>
 		<member name="resource_local_to_scene" type="bool" setter="set_local_to_scene" getter="is_local_to_scene" overrides="Resource" default="false" />
 		<member name="resource_local_to_scene" type="bool" setter="set_local_to_scene" getter="is_local_to_scene" overrides="Resource" default="false" />
 		<member name="texture_mode" type="int" setter="set_texture_mode" getter="get_texture_mode" enum="CurveTexture.TextureMode" default="0">
 		<member name="texture_mode" type="int" setter="set_texture_mode" getter="get_texture_mode" enum="CurveTexture.TextureMode" default="0">

+ 4 - 4
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 curves.
 	</brief_description>
 	</brief_description>
 	<description>
 	<description>
-		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].
 		If you only need to store one curve within a single texture, use [CurveTexture] instead. See also [GradientTexture1D] and [GradientTexture2D].
 	</description>
 	</description>
 	<tutorials>
 	<tutorials>
 	</tutorials>
 	</tutorials>
 	<members>
 	<members>
 		<member name="curve_x" type="Curve" setter="set_curve_x" getter="get_curve_x">
 		<member name="curve_x" type="Curve" setter="set_curve_x" getter="get_curve_x">
-			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].
 		</member>
 		</member>
 		<member name="curve_y" type="Curve" setter="set_curve_y" getter="get_curve_y">
 		<member name="curve_y" type="Curve" setter="set_curve_y" getter="get_curve_y">
-			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].
 		</member>
 		</member>
 		<member name="curve_z" type="Curve" setter="set_curve_z" getter="get_curve_z">
 		<member name="curve_z" type="Curve" setter="set_curve_z" getter="get_curve_z">
-			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].
 		</member>
 		</member>
 		<member name="resource_local_to_scene" type="bool" setter="set_local_to_scene" getter="is_local_to_scene" overrides="Resource" default="false" />
 		<member name="resource_local_to_scene" type="bool" setter="set_local_to_scene" getter="is_local_to_scene" overrides="Resource" default="false" />
 		<member name="width" type="int" setter="set_width" getter="get_width" default="256">
 		<member name="width" type="int" setter="set_width" getter="get_width" default="256">

+ 1 - 1
doc/classes/Line2D.xml

@@ -101,7 +101,7 @@
 			The polyline's width.
 			The polyline's width.
 		</member>
 		</member>
 		<member name="width_curve" type="Curve" setter="set_curve" getter="get_curve">
 		<member name="width_curve" type="Curve" setter="set_curve" getter="get_curve">
-			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].
 		</member>
 		</member>
 	</members>
 	</members>
 	<constants>
 	<constants>

+ 1 - 1
doc/classes/RibbonTrailMesh.xml

@@ -13,7 +13,7 @@
 	</tutorials>
 	</tutorials>
 	<members>
 	<members>
 		<member name="curve" type="Curve" setter="set_curve" getter="get_curve">
 		<member name="curve" type="Curve" setter="set_curve" getter="get_curve">
-			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].
 		</member>
 		</member>
 		<member name="section_length" type="float" setter="set_section_length" getter="get_section_length" default="0.2">
 		<member name="section_length" type="float" setter="set_section_length" getter="get_section_length" default="0.2">
 			The length of a section of the ribbon.
 			The length of a section of the ribbon.

+ 1 - 1
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.
 			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.
 		</member>
 		</member>
 		<member name="curve" type="Curve" setter="set_curve" getter="get_curve">
 		<member name="curve" type="Curve" setter="set_curve" getter="get_curve">
-			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].
 		</member>
 		</member>
 		<member name="radial_steps" type="int" setter="set_radial_steps" getter="get_radial_steps" default="8">
 		<member name="radial_steps" type="int" setter="set_radial_steps" getter="get_radial_steps" default="8">
 			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.
 			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.

+ 86 - 98
editor/plugins/curve_editor_plugin.cpp

@@ -64,6 +64,7 @@ void CurveEdit::set_curve(Ref<Curve> p_curve) {
 	if (curve.is_valid()) {
 	if (curve.is_valid()) {
 		curve->disconnect_changed(callable_mp(this, &CurveEdit::_curve_changed));
 		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_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
+		curve->disconnect(Curve::SIGNAL_DOMAIN_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
 	}
 	}
 
 
 	curve = p_curve;
 	curve = p_curve;
@@ -71,6 +72,7 @@ void CurveEdit::set_curve(Ref<Curve> p_curve) {
 	if (curve.is_valid()) {
 	if (curve.is_valid()) {
 		curve->connect_changed(callable_mp(this, &CurveEdit::_curve_changed));
 		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_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,
 	// Note: if you edit a curve, then set another, and try to undo,
@@ -226,10 +228,10 @@ void CurveEdit::gui_input(const Ref<InputEvent> &p_event) {
 				}
 				}
 			} else if (grabbing == GRAB_NONE) {
 			} 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.
 				// 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()) {
 				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);
 				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<InputEvent> &p_event) {
 			if (selected_index != -1) {
 			if (selected_index != -1) {
 				if (selected_tangent_index == TANGENT_NONE) {
 				if (selected_tangent_index == TANGENT_NONE) {
 					// Drag point.
 					// 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()) {
 					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.
 					// Allow to snap to axes with Shift.
@@ -295,8 +297,8 @@ void CurveEdit::gui_input(const Ref<InputEvent> &p_event) {
 
 
 					// Allow to constraint the point between the adjacent two with Alt.
 					// Allow to constraint the point between the adjacent two with Alt.
 					if (mm->is_alt_pressed()) {
 					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);
 						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();
 	Array previous_data = curve->get_data();
 	curve->clear_points();
 	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) {
 	switch (p_preset_id) {
 		case PRESET_CONSTANT:
 		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_right_mode(0, Curve::TANGENT_LINEAR);
 			curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
 			curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
 			break;
 			break;
 
 
 		case PRESET_LINEAR:
 		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_right_mode(0, Curve::TANGENT_LINEAR);
 			curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
 			curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
 			break;
 			break;
 
 
 		case PRESET_EASE_IN:
 		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;
 			break;
 
 
 		case PRESET_EASE_OUT:
 		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;
 			break;
 
 
 		case PRESET_SMOOTHSTEP:
 		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;
 			break;
 
 
 		default:
 		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()) {
 	if (curve.is_null()) {
 		return -1;
 		return -1;
 	}
 	}
@@ -432,7 +436,7 @@ int CurveEdit::get_point_at(Vector2 p_pos) const {
 	return closest_idx;
 	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) {
 	if (curve.is_null() || selected_index < 0) {
 		return TANGENT_NONE;
 		return TANGENT_NONE;
 	}
 	}
@@ -491,7 +495,7 @@ float CurveEdit::get_offset_without_collision(int p_current_index, float p_offse
 	return safe_offset;
 	return safe_offset;
 }
 }
 
 
-void CurveEdit::add_point(Vector2 p_pos) {
+void CurveEdit::add_point(const Vector2 &p_pos) {
 	ERR_FAIL_COND(curve.is_null());
 	ERR_FAIL_COND(curve.is_null());
 
 
 	// Add a point to get its index, then remove it immediately. Trick to feed the UndoRedo.
 	// 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();
 	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_COND(curve.is_null());
 	ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
 	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;
 	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 min_y = curve.is_valid() ? curve->get_min_value() : 0.0;
 	float max_y = curve.is_valid() ? curve->get_max_value() : 1.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_margin(margin, margin);
 	const Size2 view_size = get_size() - view_margin * 2;
 	const Size2 view_size = get_size() - view_margin * 2;
 	const Vector2 scale = view_size / world_rect.size;
 	const Vector2 scale = view_size / world_rect.size;
@@ -707,70 +713,56 @@ Vector2 CurveEdit::get_tangent_view_pos(int p_index, TangentIndex p_tangent) con
 	return tangent_view_pos;
 	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);
 	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);
 	return _world_to_view.affine_inverse().xform(p_view_pos);
 }
 }
 
 
 // Uses non-baked points, but takes advantage of ordered iteration to be faster.
 // Uses non-baked points, but takes advantage of ordered iteration to be faster.
-template <typename T>
-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);
-
-		// 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);
-
-		// 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);
-
-			Vector2 pos = a;
-			Vector2 prev_pos = a;
-
-			float scaled_step = step / scaling.x;
-			float samples = (b.x - a.x) / scaled_step;
-
-			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;
-			}
+	Vector2 first_point = curve->get_point_position(0);
+	Vector2 last_point = curve->get_point_position(curve->get_point_count() - 1);
 
 
-			plot_func(prev_pos * scaling, b * scaling, true);
-		}
-	}
-}
+	// 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;
+
+	// 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);
 
 
-struct CanvasItemPlotCurve {
-	CanvasItem &ci;
-	Color color1;
-	Color color2;
+	// 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);
 
 
-	CanvasItemPlotCurve(CanvasItem &p_ci, Color p_color1, Color p_color2) :
-			ci(p_ci),
-			color1(p_color1),
-			color2(p_color2) {}
+		Vector2 pos = a;
+		Vector2 prev_pos = a;
 
 
-	void operator()(Vector2 pos0, Vector2 pos1, bool in_definition) {
-		ci.draw_line(pos0, pos1, in_definition ? color1 : color2, 0.5, true);
+		float samples = (b.x - a.x) / world_step_size;
+
+		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);
 	}
 	}
-};
+}
 
 
 void CurveEdit::_redraw() {
 void CurveEdit::_redraw() {
 	if (curve.is_null()) {
 	if (curve.is_null()) {
@@ -784,7 +776,7 @@ void CurveEdit::_redraw() {
 	Vector2 view_size = get_rect().size;
 	Vector2 view_size = get_rect().size;
 	draw_style_box(get_theme_stylebox(SceneStringName(panel), SNAME("Tree")), Rect2(Point2(), view_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);
 	draw_set_transform_matrix(_world_to_view);
 
 
 	Vector2 min_edge = get_world_pos(Vector2(0, view_size.y));
 	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 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 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(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(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++) {
 	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);
 		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);
 	float font_height = font->get_height(font_size);
 	Color text_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
 	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) {
 	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) {
 	for (int i = 0; i <= grid_steps.y; ++i) {
 		real_t y = curve->get_min_value() + i * step_size.y;
 		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 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);
 	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 points, except for the selected one.
-	draw_set_transform_matrix(Transform2D());
 
 
 	bool shift_pressed = Input::get_singleton()->is_key_pressed(Key::SHIFT);
 	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);
 	draw_set_transform_matrix(_world_to_view);
 
 
 	if (Input::get_singleton()->is_key_pressed(Key::ALT) && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {
 	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(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));
 		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) {
 	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(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<Texture2D> CurvePreviewGenerator::generate(const Ref<Resource> &p_from, cons
 	Color line_color = EditorInterface::get_singleton()->get_editor_theme()->get_color(SceneStringName(font_color), EditorStringName(Editor));
 	Color line_color = EditorInterface::get_singleton()->get_editor_theme()->get_color(SceneStringName(font_color), EditorStringName(Editor));
 
 
 	// Set the first pixel of the thumbnail.
 	// 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);
 	int y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);
 	im.set_pixel(0, y, line_color);
 	im.set_pixel(0, y, line_color);
 
 
 	// Plot a line towards the next point.
 	// Plot a line towards the next point.
 	int prev_y = y;
 	int prev_y = y;
 	for (int x = 1; x < im.get_width(); ++x) {
 	for (int x = 1; x < im.get_width(); ++x) {
-		float t = static_cast<float>(x) / im.get_width();
-		v = (curve->sample_baked(t) - curve->get_min_value()) / curve->get_range();
+		float t = static_cast<float>(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);
 		y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);
 
 
 		Vector<Point2i> points = Geometry2D::bresenham_line(Point2i(x - 1, prev_y), Point2i(x, y));
 		Vector<Point2i> points = Geometry2D::bresenham_line(Point2i(x - 1, prev_y), Point2i(x, y));

+ 13 - 10
editor/plugins/curve_editor_plugin.h

@@ -78,14 +78,14 @@ private:
 	virtual void gui_input(const Ref<InputEvent> &p_event) override;
 	virtual void gui_input(const Ref<InputEvent> &p_event) override;
 	void _curve_changed();
 	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);
 	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 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_tangents(int p_index, float p_left, float p_right);
 	void set_point_left_tangent(int p_index, float p_tangent);
 	void set_point_left_tangent(int p_index, float p_tangent);
@@ -94,17 +94,20 @@ private:
 
 
 	void update_view_transform();
 	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_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_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();
 	void _redraw();
 
 
 private:
 private:
 	const float ASPECT_RATIO = 6.f / 13.f;
 	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;
 	Transform2D _world_to_view;
 
 
@@ -136,9 +139,9 @@ private:
 	};
 	};
 	GrabMode grabbing = GRAB_NONE;
 	GrabMode grabbing = GRAB_NONE;
 	Vector2 initial_grab_pos;
 	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;
 	bool snap_enabled = false;
 	int snap_count = 10;
 	int snap_count = 10;

+ 97 - 39
scene/resources/curve.cpp

@@ -33,6 +33,7 @@
 #include "core/math/math_funcs.h"
 #include "core/math/math_funcs.h"
 
 
 const char *Curve::SIGNAL_RANGE_CHANGED = "range_changed";
 const char *Curve::SIGNAL_RANGE_CHANGED = "range_changed";
+const char *Curve::SIGNAL_DOMAIN_CHANGED = "domain_changed";
 
 
 Curve::Curve() {
 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) {
 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;
 	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);
 		int i = get_index(p_position.x);
 
 
 		if (i == 0 && p_position.x < _points[0].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));
 			_points.insert(0, Point(p_position, p_left_tangent, p_right_tangent, p_left_mode, p_right_mode));
 			ret = 0;
 			ret = 0;
 		} else {
 		} else {
-			// Insert between i and i+1
+			// Insert between i and i+1.
 			++i;
 			++i;
 			_points.insert(i, Point(p_position, p_left_tangent, p_right_tangent, p_left_mode, p_right_mode));
 			_points.insert(i, Point(p_position, p_left_tangent, p_right_tangent, p_left_mode, p_right_mode));
 			ret = i;
 			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 {
 int Curve::get_index(real_t p_offset) const {
-	// Lower-bound float binary search
+	// Lower-bound float binary search.
 
 
 	int imin = 0;
 	int imin = 0;
 	int imax = _points.size() - 1;
 	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) {
 		if (a < p_offset && b < p_offset) {
 			imin = m;
 			imin = m;
-
 		} else if (a > p_offset) {
 		} else if (a > p_offset) {
 			imax = m;
 			imax = m;
-
 		} else {
 		} else {
 			return m;
 			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) {
 	if (p_offset > _points[imax].position.x) {
 		return imax;
 		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
 #define MIN_Y_RANGE 0.01
 
 
+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;
+	}
+
+	// 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) {
 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;
+	_min_value = MIN(p_min, _max_value - MIN_Y_RANGE);
+
+	for (const Point &p : _points) {
+		_min_value = MIN(_min_value, p.position.y);
 	}
 	}
-	// Note: min and max are indicative values,
-	// it's still possible that existing points are out of range at this point.
+
 	emit_signal(SNAME(SIGNAL_RANGE_CHANGED));
 	emit_signal(SNAME(SIGNAL_RANGE_CHANGED));
 }
 }
 
 
 void Curve::set_max_value(real_t p_max) {
 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));
 	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 {
 real_t Curve::sample(real_t p_offset) const {
 	if (_points.size() == 0) {
 	if (_points.size() == 0) {
 		return 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
 	 * 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;
 	real_t d = b.position.x - a.position.x;
 	if (Math::is_zero_approx(d)) {
 	if (Math::is_zero_approx(d)) {
 		return b.position.y;
 		return b.position.y;
@@ -458,7 +504,7 @@ void Curve::bake() {
 	_baked_cache.resize(_bake_resolution);
 	_baked_cache.resize(_bake_resolution);
 
 
 	for (int i = 1; i < _bake_resolution - 1; ++i) {
 	for (int i = 1; i < _bake_resolution - 1; ++i) {
-		real_t x = i / static_cast<real_t>(_bake_resolution - 1);
+		real_t x = get_domain_range() * i / static_cast<real_t>(_bake_resolution - 1) + _min_domain;
 		real_t y = sample(x);
 		real_t y = sample(x);
 		_baked_cache.write[i] = y;
 		_baked_cache.write[i] = y;
 	}
 	}
@@ -480,11 +526,11 @@ void Curve::set_bake_resolution(int p_resolution) {
 
 
 real_t Curve::sample_baked(real_t p_offset) const {
 real_t Curve::sample_baked(real_t p_offset) const {
 	if (_baked_cache_dirty) {
 	if (_baked_cache_dirty) {
-		// Last-second bake if not done already
+		// Last-second bake if not done already.
 		const_cast<Curve *>(this)->bake();
 		const_cast<Curve *>(this)->bake();
 	}
 	}
 
 
-	// Special cases if the cache is too small
+	// Special cases if the cache is too small.
 	if (_baked_cache.size() == 0) {
 	if (_baked_cache.size() == 0) {
 		if (_points.size() == 0) {
 		if (_points.size() == 0) {
 			return 0;
 			return 0;
@@ -494,8 +540,8 @@ real_t Curve::sample_baked(real_t p_offset) const {
 		return _baked_cache[0];
 		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);
 	int i = Math::floor(fi);
 	if (i < 0) {
 	if (i < 0) {
 		i = 0;
 		i = 0;
@@ -505,7 +551,7 @@ real_t Curve::sample_baked(real_t p_offset) const {
 		fi = 0;
 		fi = 0;
 	}
 	}
 
 
-	// Sample
+	// Sample.
 	if (i + 1 < _baked_cache.size()) {
 	if (i + 1 < _baked_cache.size()) {
 		real_t t = fi - i;
 		real_t t = fi - i;
 		return Math::lerp(_baked_cache[i], _baked_cache[i + 1], t);
 		return Math::lerp(_baked_cache[i], _baked_cache[i + 1], t);
@@ -628,6 +674,14 @@ void Curve::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_min_value", "min"), &Curve::set_min_value);
 	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("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("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("clean_dupes"), &Curve::clean_dupes);
 	ClassDB::bind_method(D_METHOD("bake"), &Curve::bake);
 	ClassDB::bind_method(D_METHOD("bake"), &Curve::bake);
 	ClassDB::bind_method(D_METHOD("get_bake_resolution"), &Curve::get_bake_resolution);
 	ClassDB::bind_method(D_METHOD("get_bake_resolution"), &Curve::get_bake_resolution);
@@ -635,13 +689,17 @@ void Curve::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_get_data"), &Curve::get_data);
 	ClassDB::bind_method(D_METHOD("_get_data"), &Curve::get_data);
 	ClassDB::bind_method(D_METHOD("_set_data", "data"), &Curve::set_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, "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_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_ARRAY_COUNT("Points", "point_count", "set_point_count", "get_point_count", "point_");
 
 
 	ADD_SIGNAL(MethodInfo(SIGNAL_RANGE_CHANGED));
 	ADD_SIGNAL(MethodInfo(SIGNAL_RANGE_CHANGED));
+	ADD_SIGNAL(MethodInfo(SIGNAL_DOMAIN_CHANGED));
 
 
 	BIND_ENUM_CONSTANT(TANGENT_FREE);
 	BIND_ENUM_CONSTANT(TANGENT_FREE);
 	BIND_ENUM_CONSTANT(TANGENT_LINEAR);
 	BIND_ENUM_CONSTANT(TANGENT_LINEAR);
@@ -852,7 +910,7 @@ void Curve2D::_bake() const {
 		return;
 		return;
 	}
 	}
 
 
-	// Tessellate curve to (almost) even length segments
+	// Tessellate curve to (almost) even length segments.
 	{
 	{
 		Vector<RBMap<real_t, Vector2>> midpoints = _tessellate_even_length(10, bake_interval);
 		Vector<RBMap<real_t, Vector2>> midpoints = _tessellate_even_length(10, bake_interval);
 
 
@@ -1592,7 +1650,7 @@ void Curve3D::_bake() const {
 		return;
 		return;
 	}
 	}
 
 
-	// Step 1: Tessellate curve to (almost) even length segments
+	// Step 1: Tessellate curve to (almost) even length segments.
 	{
 	{
 		Vector<RBMap<real_t, Vector3>> midpoints = _tessellate_even_length(10, bake_interval);
 		Vector<RBMap<real_t, Vector3>> midpoints = _tessellate_even_length(10, bake_interval);
 
 
@@ -1653,7 +1711,7 @@ void Curve3D::_bake() const {
 		return;
 		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.
 	// See Dougan, Carl. "The parallel transport frame." Game Programming Gems 2 (2001): 215-219.
 	// for an example discussing about why not the Frenet frame.
 	// for an example discussing about why not the Frenet frame.
@@ -1689,7 +1747,7 @@ void Curve3D::_bake() const {
 			Basis rotate;
 			Basis rotate;
 			rotate.rotate_to_align(-frame_prev.get_column(2), forward);
 			rotate.rotate_to_align(-frame_prev.get_column(2), forward);
 			frame = rotate * frame_prev;
 			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);
 			up_write[idx] = frame.get_column(1);
 			frame_prev = frame;
 			frame_prev = frame;
@@ -1941,7 +1999,7 @@ real_t Curve3D::sample_baked_tilt(real_t p_offset) const {
 		return baked_tilt_cache.get(0);
 		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);
 	Curve3D::Interval interval = _find_interval(p_offset);
 	return _sample_baked_tilt(interval);
 	return _sample_baked_tilt(interval);

+ 12 - 6
scene/resources/curve.h

@@ -38,10 +38,8 @@ class Curve : public Resource {
 	GDCLASS(Curve, Resource);
 	GDCLASS(Curve, Resource);
 
 
 public:
 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_RANGE_CHANGED;
+	static const char *SIGNAL_DOMAIN_CHANGED;
 
 
 	enum TangentMode {
 	enum TangentMode {
 		TANGENT_FREE = 0,
 		TANGENT_FREE = 0,
@@ -101,11 +99,18 @@ public:
 
 
 	real_t get_min_value() const { return _min_value; }
 	real_t get_min_value() const { return _min_value; }
 	void set_min_value(real_t p_min);
 	void set_min_value(real_t p_min);
-
 	real_t get_max_value() const { return _max_value; }
 	real_t get_max_value() const { return _max_value; }
 	void set_max_value(real_t p_max);
 	void set_max_value(real_t p_max);
+	real_t get_value_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; }
 
 
-	real_t get_range() const { return _max_value - _min_value; }
+	Array get_limits() const;
+	void set_limits(const Array &p_input);
 
 
 	real_t sample(real_t p_offset) const;
 	real_t sample(real_t p_offset) const;
 	real_t sample_local_nocheck(int p_index, real_t p_local_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;
 	int _bake_resolution = 100;
 	real_t _min_value = 0.0;
 	real_t _min_value = 0.0;
 	real_t _max_value = 1.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)
 VARIANT_ENUM_CAST(Curve::TangentMode)

+ 170 - 2
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.");
 			"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> curve = memnew(Curve);
 	Ref<Curve> curve = memnew(Curve);
 	// "Sawtooth" curve with an open ending towards the 1.0 offset.
 	// "Sawtooth" curve with an open ending towards the 1.0 offset.
 	curve->add_point(Vector2(0, 0));
 	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.");
 			"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> 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> curve = memnew(Curve);
 	Ref<Curve> curve = memnew(Curve);
 	// "Sawtooth" curve with an open ending towards the 1.0 offset.
 	// "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);
 	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.");
 			"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> 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") {
 TEST_CASE("[Curve] Straight line offset test") {
 	Ref<Curve> curve = memnew(Curve);
 	Ref<Curve> curve = memnew(Curve);
 	curve->add_point(Vector2(0, 0));
 	curve->add_point(Vector2(0, 0));